senddy

Create and manage private stablecoin wallets using Senddy's zero-knowledge protocol on Base. Use when building payment agents, bots, server-side apps, or any system that needs private USDC transfers. Covers @senddy/node for headless agents and @senddy/client for browser apps.

Safety Notice

This listing is from the official public ClawHub registry. Review SKILL.md and referenced scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "senddy" with this command: npx skills add mattt21/senddy

Senddy Private Wallet

Build private stablecoin wallets with zero-knowledge proofs on Base. Senddy lets agents and apps hold, transfer, and withdraw USDC privately — no public on-chain linkage between deposits, transfers, and withdrawals.

Quick Start (Headless Agent)

5 steps to a working private wallet:

npm install @senddy/node
import { createSenddyAgent, toUSDC } from '@senddy/node';
import { randomBytes } from 'node:crypto';

// 1. Generate a seed (store this securely — it controls the wallet)
const seed = randomBytes(32);

// 2. Create the agent (only seed + apiKey required)
const agent = createSenddyAgent({
  seed,
  apiKey: process.env.SENDDY_API_KEY!,
});

// 3. Initialize (derives keys, loads WASM prover, first sync)
await agent.init();

// 4. Get your receive address
console.log('Address:', agent.getReceiveAddress()); // senddy1...

// 5. Check balance, transfer, withdraw
const balance = await agent.getBalance();
await agent.transfer('senddy1...recipient', toUSDC('5.00'));
await agent.withdraw('0xPublicAddress...', toUSDC('10.00'));

Set SENDDY_API_KEY in your environment. Get one at https://senddy.com.

Configuration

Minimal Config (recommended)

Only seed and apiKey are required. Everything else defaults to the canonical Base mainnet deployment:

createSenddyAgent({
  seed: Uint8Array,        // 32-byte secret (REQUIRED)
  apiKey: string,          // 'sk_live_...' (REQUIRED)
})

Full Config (overrides)

createSenddyAgent({
  seed: Uint8Array,
  apiKey: string,
  apiUrl: string,          // default: 'https://senddy.com/api/v1'
  chainId: number,         // default: 8453 (Base)
  rpcUrl: string,          // default: 'https://mainnet.base.org'
  pool: '0x...',           // default: canonical pool
  usdc: '0x...',           // default: canonical USDC
  permit2: '0x...',        // default: canonical Permit2
  subgraphUrl: string,     // default: canonical subgraph
  attestorUrl: string,     // override: bypass API gateway for attestor
  relayerUrl: string,      // override: bypass API gateway for relayer
  context: string,         // default: 'main' (for multi-agent from one seed)
  debug: boolean,          // default: false
})

What the API Key Gates

The apiKey authenticates all requests through the Senddy API gateway:

  • Attestor — ZK proof verification (TEE-based, off-chain)
  • Relayer — Gas-sponsored transaction submission (you don't pay gas)
  • Usernames — Resolve senddy1... addresses to human-readable names
  • Merkle tree — Proof generation helper endpoints

Operations

Balance

const balance = await agent.getBalance();
// { shares: bigint, estimatedUSDC: bigint, noteCount: number }

estimatedUSDC is in 6-decimal USDC units. shares are 18-decimal internal units.

Transfer

// Simple transfer
const result = await agent.transfer('senddy1...', toUSDC('25.00'));
// { txHash, shares, nullifierCount, circuit: 'spend' | 'spend9' }

// With memo (max 31 ASCII chars)
await agent.transfer('senddy1...', toUSDC('5.00'), { memo: 'Payment' });

// Anonymous (hide sender identity)
await agent.transfer('senddy1...', toUSDC('5.00'), { anonymous: true });

Auto-escalation: tries spend circuit (3 inputs), escalates to spend9 (9 inputs), and auto-consolidates if neither suffices.

Withdraw

Withdraw to a public Ethereum address (USDC leaves the privacy pool):

const result = await agent.withdraw('0x...', toUSDC('50.00'));
// { txHash, shares, to, circuit }

Sync

State is synced automatically on init(). For long-running agents:

// Manual sync
const result = await agent.sync();
// { newNotes, newSpent, unspentCount, durationMs }

Consolidation

When notes fragment (many small UTXOs), consolidate them:

const result = await agent.consolidate({ noteThreshold: 16 });
// { txHash, notesConsolidated, totalShares, needsMore }

Receive Address

const address = agent.getReceiveAddress(); // 'senddy1qw508d6q...'

Share this address to receive private transfers. It's derived from your viewing public key and is deterministic for a given seed + context.

Transaction History

const txs = await agent.getTransactions({ limit: 50 });
// Array<{ id, type, shares, estimatedUSDC, counterparty, memo, timestamp, status }>

Events

agent.on('balanceChange', (balance) => { /* ... */ });
agent.on('sync', (result) => { /* ... */ });
agent.on('noteStrategy', (event) => { /* escalation/consolidation info */ });
agent.on('error', (err) => { /* ... */ });

Multiple Agents from One Seed

Use the context parameter to derive different wallets from the same seed:

const treasury = createSenddyAgent({ seed, apiKey, context: 'treasury' });
const payroll  = createSenddyAgent({ seed, apiKey, context: 'payroll' });
const tips     = createSenddyAgent({ seed, apiKey, context: 'tips' });

Each context produces different keys and a different receive address.

Amounts

Always use toUSDC() to convert human-readable amounts:

import { toUSDC } from '@senddy/node';

toUSDC('1.00')     // 1_000_000n
toUSDC('100')      // 100_000_000n
toUSDC('0.01')     // 10_000n
toUSDC(50)         // 50_000_000n

Raw amounts are in USDC's 6-decimal format (bigint).

Address Validation

import { isValidSenddyAddress } from '@senddy/node';

isValidSenddyAddress('senddy1qw508d6q...');  // true
isValidSenddyAddress('0x...');                // false

Contract Addresses

import { SHARED_CONTRACTS, V3_CONTRACTS } from '@senddy/node';

SHARED_CONTRACTS.USDC     // '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'
SHARED_CONTRACTS.Permit2  // '0x000000000022D473030F116dDEE9F6B43aC78BA3'
V3_CONTRACTS.Pool         // '0x0b4e0C18e4005363A10a93cb30e0a11A88bee648'

Cleanup

agent.destroy(); // Zeros key material and cleans up resources

Always call destroy() when done (especially in short-lived processes).

CRITICAL: Run as a Persistent Process

Do NOT create a new agent and call init() on every request. The init() call takes 5-15 seconds because it compiles WASM, loads the SRS into memory, and syncs the full state from the subgraph. Re-initializing on every request will make the agent unusably slow.

Instead, run the agent as a long-lived background process that initializes once and handles requests over a local HTTP API or Unix socket:

// senddy-daemon.ts — run once, stays alive forever
import { createSenddyAgent, toUSDC, isValidSenddyAddress } from '@senddy/node';
import { createServer } from 'node:http';

const agent = createSenddyAgent({
  seed: Buffer.from(process.env.AGENT_SEED_HEX!, 'hex'),
  apiKey: process.env.SENDDY_API_KEY!,
});

await agent.init();
console.log(`Agent ready: ${agent.getReceiveAddress()}`);

// Periodic sync to stay up-to-date
setInterval(() => agent.sync().catch(console.error), 30_000);

// Simple JSON-RPC over HTTP
const server = createServer(async (req, res) => {
  if (req.method !== 'POST') { res.writeHead(405); res.end(); return; }

  const chunks: Buffer[] = [];
  for await (const chunk of req) chunks.push(chunk as Buffer);
  const { method, params } = JSON.parse(Buffer.concat(chunks).toString());

  try {
    let result: any;
    switch (method) {
      case 'getBalance':
        result = await agent.getBalance();
        result = { ...result, shares: result.shares.toString(), estimatedUSDC: result.estimatedUSDC.toString() };
        break;
      case 'getAddress':
        result = { address: agent.getReceiveAddress() };
        break;
      case 'transfer':
        result = await agent.transfer(params.to, toUSDC(params.amount), params.opts);
        result = { ...result, shares: result.shares.toString() };
        break;
      case 'withdraw':
        result = await agent.withdraw(params.to, toUSDC(params.amount));
        result = { ...result, shares: result.shares.toString() };
        break;
      case 'sync':
        result = await agent.sync();
        break;
      case 'getTransactions':
        result = await agent.getTransactions(params);
        break;
      default:
        res.writeHead(400);
        res.end(JSON.stringify({ error: `Unknown method: ${method}` }));
        return;
    }
    res.writeHead(200, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: true, result }));
  } catch (err: any) {
    res.writeHead(500, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ ok: false, error: err.message }));
  }
});

const PORT = parseInt(process.env.SENDDY_DAEMON_PORT ?? '0');
server.listen(PORT, '127.0.0.1', () => {
  const addr = server.address() as import('node:net').AddressInfo;
  console.log(`Senddy daemon on :${addr.port}`);
});

Start the daemon once. It picks a free port automatically (or set SENDDY_DAEMON_PORT to pin one). Read the port from stdout and use it for all subsequent requests:

AGENT_SEED_HEX="your64charhex..." SENDDY_API_KEY="sk_live_..." \
  npx tsx senddy-daemon.ts
# Output: "Senddy daemon on :18790"  (port varies)

Then query it instantly from any client:

# Check balance (use the port printed at startup)
curl -s -X POST http://127.0.0.1:18790 \
  -d '{"method":"getBalance"}' | jq

# Transfer
curl -s -X POST http://127.0.0.1:18790 \
  -d '{"method":"transfer","params":{"to":"senddy1...","amount":"5.00"}}' | jq

For a complete daemon example with process management, see examples.md.

Gotchas

  • No deposits: Agents can't deposit directly. Fund them by sending a private transfer from a funded wallet (browser app or another agent).
  • In-memory storage: Notes are lost on process restart. The agent re-syncs from the subgraph on init(), so this is safe — just costs a few seconds.
  • First init downloads SRS: The first init() downloads a ~16 MB structured reference string (cached to ~/.bb-crs/ for subsequent runs).
  • WASM compilation: Even with cached SRS, init() takes 5-15s to compile the WASM prover. Always run the agent persistently, not per-request.
  • Shares vs USDC: Internal values are in 18-decimal shares. Use balance.estimatedUSDC and toUSDC() for human-readable amounts.

Additional Resources

  • For full type signatures and advanced composition, see reference.md
  • For copy-paste usage examples, see examples.md

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Web3

PredictClash

Predict Clash - join prediction rounds on crypto prices and stock indices for PP rewards. Server assigns unpredicted questions, you analyze and submit. Use w...

Registry SourceRecently Updated
Web3

Crypto Holdings Monitor

加密货币持仓监控工具。支持多钱包地址监控、实时价格查询、持仓统计。

Registry SourceRecently Updated
Web3

OpenClaw News Watcher

Monitors CoinDesk or PANews for new crypto articles, summarizes them, and sends updates to Telegram without API keys or login.

Registry SourceRecently Updated