Tag: AI agents

  • SNAP: Private Agent Payments on Solana with Zero‑Knowledge Proofs

    SNAP: Private Agent Payments on Solana with Zero‑Knowledge Proofs

    AI agents are starting to act like businesses. They pay for APIs, buy data, settle trades, and manage compute on their own. Put those payments on a public chain, though, and a hard problem shows up: surveillance.

    When every payment is public, an agent’s financial graph is exposed. Who it pays. How much. When. That reveals strategy, vendors, and weak points. In a competitive market, that’s costly.

    Agents need the digital equivalent of cash. That’s why I built SNAP (Shield Network Agent Payments), a privacy protocol for agent-to-agent payments on Solana. Here’s how it works, why the architecture looks the way it does, and what it took to bring zero-knowledge proofs to Solana in practice.

    The problem: payment graphs leak strategy

    Picture a trading agent. It buys price data from Agent A, routes trades through Agent B, and pays for compute from Agent C. On a public blockchain, anyone can rebuild that supply chain just by watching payments.

    No hack required. A block explorer is enough.

    We’ve seen this pattern already. MEV bots exploit transaction visibility on Solana and Ethereum. As agents grow into larger economic actors, payment graph analysis becomes the next attack surface.

    The approach: a commitment–nullifier scheme

    SNAP breaks the link between sender and receiver using a commitment–nullifier scheme with Groth16 zero-knowledge proofs. Instead of sending funds directly, agents move value through a shielded pool with fixed denominations (e.g., 0.1 SOL).

    1. Deposit: Agent A deposits a fixed amount with a commitment Poseidon(secret, nullifier).
    2. Transfer: Agent A shares the “secret note” (the commitment preimage) with Agent B over any private channel.
    3. Withdraw: Agent B generates a ZK proof that it holds a valid note for some commitment in the pool—without revealing which one.

    The nullifier prevents double-spends. On withdrawal, the program records nullifierHash = Poseidon(nullifier). Because both commitment and nullifier use Poseidon, observers cannot link a nullifier back to its original commitment.

    Deposit:  commitment = Poseidon(secret, nullifier)  →  stored in Merkle tree
    Withdraw: nullifierHash = Poseidon(nullifier)        →  checked against nullifier set
              proof verifies: "I know (secret, nullifier) such that
                               Poseidon(secret, nullifier) is in the tree
                               AND nullifierHash = Poseidon(nullifier)"

    From the outside, you see deposits in and withdrawals out, but you can’t connect a specific withdrawal to a specific deposit.

    Architecture

    Four pieces make this work: the Solana program, ZK circuits, on-chain Merkle state, and an off-chain relayer.

    Solana program (Rust/Anchor)

    The on-chain program exposes three core instructions:

    • deposit — Takes user funds and a 32-byte commitment. Inserts the commitment into the pool’s Merkle tree.
    • withdraw_zk — Accepts a Groth16 proof, nullifier hash, recipient, and Merkle root. Verifies on-chain using BN254 pairing operations and transfers funds to the recipient.
    • withdraw_zk_relayed — Same verification, but submitted by a relayer that takes a 0.25% fee from the withdrawal amount.

    Solana’s native alt_bn128 precompiles make Groth16 verification possible directly on-chain. The hard part was fitting the pairing operations into Solana’s 1.4M compute unit limit per transaction. That required careful verifier optimization.

    ZK circuit (circom/Groth16)

    The withdrawal circuit (withdraw_20.circom) proves:

    • The prover knows a secret and a nullifier.
    • Poseidon(secret, nullifier) equals a commitment in the Merkle tree.
    • Poseidon(nullifier) equals the public nullifierHash.
    • The Merkle path is valid for the given root.
    • The proof is bound to a specific recipient (prevents front‑running).

    It uses a depth‑20 Merkle tree (1,048,576 leaves). Poseidon is the hash function throughout—ZK‑friendly and collision‑resistant.

    On‑chain state: commitment pages

    Storing a depth‑20 tree on Solana isn’t trivial. A single account can’t hold 1M+ commitments due to the ~10MB account size limit.

    SNAP uses CommitmentPage accounts—paginated storage where each page holds a slice of leaves. On deposit, the commitment goes into the current page. For withdrawals, the SDK reconstructs the Merkle path client‑side from these pages and passes it to the prover.

    NullifierRecord PDAs track spent nullifiers. Each nullifier maps to a PDA derived from [pool_address, nullifier_hash]. The program checks if that PDA exists (already spent) before allowing a withdrawal.

    The relayer: solving the gas problem

    ZK proofs hide links, but gas fees can still leak identity. If Agent B withdraws to a fresh wallet, how does that wallet pay the fee without revealing a connection?

    The SNAP Relayer handles it. It:

    1. Receives a withdrawal request (ZK proof + parameters).
    2. Verifies the proof off‑chain as a quick check.
    3. Builds and submits the Solana transaction, paying the fee.
    4. Deducts a 0.25% protocol fee from the withdrawal amount.

    This lets agents withdraw to brand‑new, unfunded wallets with no on‑chain link back to prior activity.

    // Agent B withdraws via relayer — no gas needed
    const result = await snap.withdrawViaRelayer(
      pool,
      note,
      freshRecipientWallet,
      "https://relayer.agentzeny.ai"
    );
    // result: { txSignature, fee, recipientReceived }

    SDK: private payments in five lines

    Privacy that’s hard to use won’t be used. The snap-solana-sdk wraps the full flow:

    import { Connection, Keypair, PublicKey } from "@solana/web3.js";
    import { SNAPClient } from "snap-solana-sdk";
    const connection = new Connection("https://your-rpc-url.com");
    const sender = Keypair.generate();
    const pool = new PublicKey("B8SyffZKt8LABKogWjH9rZcjY5PV2hyYRCbTxxbcrpFf");
    // Agent A deposits
    const snap = new SNAPClient(connection, sender);
    const note = await snap.deposit(pool, 0.1);
    const serialized = SNAPClient.serializeNote(note);
    // Send `serialized` to Agent B through any private channel
    // Agent B withdraws
    const snapB = new SNAPClient(connection, recipient);
    const tx = await snapB.withdraw(
      pool,
      SNAPClient.deserializeNote(serialized),
      recipient
    );

    The SDK handles commitment generation, Merkle path reconstruction, WASM‑based proof generation (snarkjs), and transaction building. No circom constraints or BN254 math for the developer.

    Agent framework integrations

    Privacy should fit the tools you already use.

    Solana Agent Kit

    import { SolanaAgentKit } from "solana-agent-kit";
    // SNAP plugin auto-registers snap_deposit, snap_withdraw, snap_withdraw_private
    const agent = new SolanaAgentKit(wallet, rpcUrl, {});

    LangChain / LangGraph

    npm install snap-langchain-tools @langchain/core
    import { createSNAPTools } from "snap-langchain-tools";
    import { createReactAgent } from "@langchain/langgraph/prebuilt";
    const tools = createSNAPTools(connection, wallet);
    // Returns: [snap_list_pools, snap_deposit, snap_withdraw, snap_estimate_fee]
    const agent = createReactAgent({ llm, tools });
    const result = await agent.invoke({
      messages: [{ role: "user", content: "Deposit 0.1 SOL into the SNAP pool" }],
    });

    MCP server (Claude Code, Cursor, etc.)

    SNAP also ships as an MCP server so MCP‑compatible coding assistants can execute private payments as tools.

    Mainnet pools

    SNAP is live on Solana mainnet with three pools:

    Pool Address Denomination
    SOL B8SyffZKt8LABKogWjH9rZcjY5PV2hyYRCbTxxbcrpFf 0.1 SOL
    USDC 5LeuHrPBgHNhgbCy996MEjcsBk5gNHhVj6AiuuCHZ8od 1 USDC
    USDC ECuHf8kgiWfmL3Q6id4WGBQWvuukhzqvF5vsxuPAKZBv 10 USDC

    Program ID: 9uePoqdgaXpqFLQM2ED1GGQrwSEiqe3r6tW1AfsnrrbS

    Fixed denominations improve privacy. When every deposit is the same size, deposits blend together. The anonymity set is the entire pool.

    What I learned building this

    • ZK artifact management is harder than ZK math. Packaging WASM files, zkeys, and verification keys for Node.js took more engineering than the circuit. Agents run in servers, not browsers—so the loader had to work with require(), not fetch().
    • Agents need API‑first privacy. Agents don’t click buttons. They run scripts. Compressing the integration down to five lines mattered more than the smart contract work.
    • Solana’s compute limits are tight but workable. Groth16 on BN254 fits within ~1.4M compute units, but just barely. Every extra operation in the verifier had to go.
    • The relayer is underrated. Without gas abstraction, ZK alone doesn’t give full privacy. The relayer closes the last gap.

    What’s next

    • Security audit — Engaging a ZK/Solana audit firm for the program and circuits.
    • Multi‑party trusted setup — Moving beyond a single‑contributor setup.
    • Larger denomination pools — As the protocol hardens.
    • More integrations — ElizaOS, Coinbase AgentKit, and others in progress.

    SNAP is open source. If you’re building AI agents on Solana and want private payments:

    Your agent’s payment graph is a map of your business.

    Reference: View article