🎁 Get the FREE AI Skills Starter Guide β€” Subscribe β†’
BytesAgainBytesAgain
πŸ¦€ ClawHub

Senddy

by @mattt21

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...

Versionv0.1.1
Downloads1,154
TERMINAL
clawhub install senddy

πŸ“– About This Skill


name: senddy description: 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. metadata: {"openclaw":{"requires":{"env":["SENDDY_API_KEY","AGENT_SEED_HEX"]},"primaryEnv":"SENDDY_API_KEY","emoji":"πŸ›‘οΈ","homepage":"https://senddy.com"}}

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
  • βš™οΈ 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