The AI agent economy is scaling fast. Agents pay for APIs, buy data from other agents, settle trades, and manage resources autonomously. But running these agents on public blockchains introduces a critical flaw: surveillance.
When every on-chain agent payment is public, an agent’s entire financial graph becomes visible. Anyone can see who an agent paid, how much, and when. This reveals the agent’s strategy, its vendor partnerships, and its vulnerabilities.
For AI agents to function effectively in a competitive economy, they need the digital equivalent of cash.
That’s why I built SNAP (Shield Network Agent Payments) — a privacy protocol purpose-built for AI agent-to-agent payments on Solana. This is a deep dive into how it works, the architecture choices I made, and the challenges of putting zero-knowledge proofs on Solana.
The Problem: Payment Graphs Leak Strategy
Consider an autonomous trading agent. It sources price data from Agent A, executes trades through Agent B, and pays for compute from Agent C. On a public blockchain, any observer can reconstruct this entire supply chain just by watching the payment graph.
A competitor doesn’t need to hack your agent. They just need a block explorer.
This isn’t hypothetical. MEV bots already exploit transaction visibility on Solana and Ethereum. As agents become larger economic actors, payment graph analysis becomes the next attack vector.
The Approach: Commitment-Nullifier Scheme
To break the link between sender and receiver, SNAP uses a commitment-nullifier scheme powered by Groth16 zero-knowledge proofs.
Instead of Agent A sending SOL directly to Agent B, the transaction goes through a shielded pool:
-
Deposit: Agent A deposits a fixed denomination (e.g., 0.1 SOL) into the pool alongside a cryptographic commitment —
Poseidon(secret, nullifier). - Transfer: Agent A sends a “secret note” (the commitment pre-image) to Agent B through any private channel.
- Withdraw: Agent B generates a ZK proof demonstrating they possess a valid note for some commitment in the pool, without revealing which commitment.
The key is the nullifier — a unique hash derived from the secret note. When Agent B withdraws, the on-chain program records the nullifier hash to prevent double-spending. Because the commitment and nullifier are Poseidon hashes, observers cannot link the nullifier back to the 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)"
An observer sees deposits going in and withdrawals going out, but cannot connect any specific withdrawal to any specific deposit.
Architecture
The system has four components: 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 the user’s funds and a 32-byte commitment. Inserts the commitment into the pool’s Merkle tree. -
withdraw_zk— Takes a Groth16 proof, nullifier hash, recipient, and Merkle root. Verifies the proof on-chain using BN254 pairing operations and transfers funds to the recipient. -
withdraw_zk_relayed— Same verification, but the relayer submits the transaction and takes a 0.25% fee from the withdrawal amount.
Solana’s native alt_bn128 precompiles make Groth16 verification possible directly on-chain. The challenge was fitting the pairing operations within Solana’s 1.4M compute unit limit per transaction — this required careful optimization of the verifier code.
ZK Circuit (circom/Groth16)
The withdrawal circuit (withdraw_20.circom) proves:
- The prover knows a
secretand anullifier -
Poseidon(secret, nullifier)equals a commitment that exists in the Merkle tree -
Poseidon(nullifier)equals the publicly submittednullifierHash - The Merkle path is valid for the given
root - The proof is bound to a specific
recipientaddress (preventing front-running)
The circuit uses a depth-20 Merkle tree, supporting over 1 million deposits per pool (1,048,576 leaves). Poseidon is the hash function throughout — it’s ZK-friendly (low constraint count) and collision-resistant.
On-Chain State: Commitment Pages
Storing a depth-20 Merkle tree on Solana is non-trivial. A naive approach would require a single account holding all 1M+ commitments, which exceeds Solana’s 10MB account size limit.
SNAP uses CommitmentPage accounts — paginated storage where each page holds a slice of the tree’s leaves. When a deposit occurs, the commitment is inserted into the current page. For verification, the SDK reconstructs the Merkle path client-side from the commitment pages and passes the path as proof inputs.
NullifierRecord PDAs track spent nullifiers. Each nullifier maps to a PDA derived from [pool_address, nullifier_hash]. The program checks if the PDA exists (already spent) before allowing a withdrawal.
The Relayer: Solving the Gas Problem
Even with ZK proofs breaking the payment link, a privacy leak remains: gas fees.
If Agent B withdraws to a fresh wallet, how does it pay the Solana transaction fee? If it funds the wallet from an existing account, the privacy is compromised — an observer can link the funding transaction to the withdrawal.
The SNAP Relayer solves this. It’s an Express service that:
- Receives a withdrawal request (ZK proof + parameters) from the agent
- Verifies the proof off-chain (fast sanity check)
- Builds and submits the Solana transaction, paying the gas fee
- Deducts a 0.25% protocol fee from the withdrawal amount
This means agents can withdraw to completely fresh, unfunded wallets with zero on-chain footprint connecting the recipient to any 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: 5 Lines to Private Payments
A privacy protocol that requires a PhD to use is a protocol nobody uses. The SDK (snap-solana-sdk) wraps the entire 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, proof generation (WASM-based snarkjs), and transaction building. The developer never touches circom constraints or BN254 math.
Agent Framework Integrations
Privacy should plug into whatever agent framework you’re already using:
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 any MCP-compatible AI coding assistant can execute private payments as part of its tool set.
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 are a privacy feature, not a limitation. When every deposit is the same size, deposits become indistinguishable — the anonymity set is the entire pool.
What I Learned Building This
ZK artifact management is harder than ZK math. Bundling WASM files, zkeys, and verification keys into an npm package that works in Node.js environments required more engineering than the circuit itself. Agents run in server environments, not browsers — the artifact loading pipeline had to work with require(), not fetch().
Agents need API-first privacy. Agents don’t click buttons in web wallets. They execute scripts. Reducing the integration surface to 5 lines of code was harder than building the smart contract, but it’s what makes the protocol actually usable.
Solana’s compute limits are tight but workable. Groth16 verification on BN254 fits within the 1.4M compute unit budget, but just barely. Every unnecessary operation in the on-chain verifier had to go.
The relayer is the most underrated component. Without gas abstraction, ZK proofs alone don’t provide 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 ceremony — Expanding beyond the current single-contributor setup
- Larger denomination pools — As the protocol hardens
- More framework integrations — ElizaOS, Coinbase AgentKit, and others in progress
SNAP is fully open source. If you’re building AI agents on Solana and want private payments:
- GitHub: github.com/agentzeny/snap-public
-
SDK:
npm install snap-solana-sdk -
LangChain tools:
npm install snap-langchain-tools - Website: agentzeny.ai
Your agent’s payment graph is a map of your business.