Docs
How it works.
ZKVENICE is two things bolted together with care: a shielded pool on Robinhood Chain that swaps on the way out, and an agent on Venice that plans the swap and keeps no record of it. This page explains each, what each one can and cannot hide, and which parts exist today.
Overview
A normal swap is one transaction from your wallet: anyone can see what you held, what you bought, and when. A normal AI assistant that helps you do it keeps the conversation on someone’s server, usually with your address in it.
ZKVENICE separates the two problems. On chain, your wallet deposits into a pool; later, a relayer withdraws from the pool and swaps, delivering to an address that never touched your wallet. Off chain, the planning happens on Venice, whose inference does not persist prompts or answers. The result is that no single party, and no combination of the public ones, can pair you with the trade.
Threat model
Who learns what, in the design:
- A chain observer sees a deposit from your wallet into the pool and, at some later time, a withdraw-and-swap submitted by a relayer to some fresh address. The amounts are fixed denominations, so size cannot pair them. Timing can, if the pool is thin: the app shows the crowd and the note’s age before you withdraw, and warns when either is small.
- The relayer receives a proof and a nullifier. It learns that some valid note is being spent and where the output goes. It cannot tell which deposit the note came from.
- Venice receives the intent text for the seconds it takes to answer, inside a TEE whose attestation the route verifies, or on a zero-retention model if no TEE model is available. The prompt never contains your address.
- ZKVENICE the site has no analytics and no database. The app reads prices from DexScreener in your browser. Two routes run on the server: the planner, which forwards the sentence you typed to Venice and nothing else, and the relayer, which receives a finished proof and submits it. Neither is told your address; the relayer sees the recipient, as the chain will.
- Your ISP or a network observer sees that you visited this site and DexScreener. Use the network protection you already use.
What this does not hide: that you used the pool at all (the deposit is public), and, against a patient adversary, timing correlation when the pool has few participants.
Layer 1 · the pool
The pool is a standard commitment-and-nullifier design, the family every serious shielded pool descends from. There is no novelty in the cryptography, on purpose; the novelty is that the exit is a swap.
Deposit
The client picks a random secret and nullifier, computes the commitment C = H(secret ‖ nullifier), and calls deposit(C) with exactly one fixed denomination of the deposit token. The contract inserts C as a leaf in an incremental Merkle tree and emits the new root. Nothing about the sender is stored beyond what the chain records for every transaction.
Withdraw and swap
To spend a note, the client generates a zero-knowledge proof that it knows a secret and nullifier whose commitment is in the tree at a recent root, and reveals the hash N = H(nullifier) so the note cannot be spent twice. The proof also binds the recipient, the swap parameters, and the relayer fee, so a relayer cannot redirect the output.
function withdraw( uint256[2] a, uint256[2][2] b, uint256[2] c, // Groth16 proof bytes32 root, bytes32 nullifierHash, address payable recipient, address payable relayer, uint256 fee, Exit calldata exit // venue, tokenOut, minOut, fee tier ) external;
The contract verifies the proof, marks N spent, and routes denomination - feethrough Uniswap on Robinhood Chain: a v4 pool whose other side is native ETH (the singleton at 0x8366…0951), or a v3 WETH pool from the factory at 0x1f7d…2EfA (where USDG lives), sending tokenOut to recipient and fee in ETH to the relayer. With tokenOut = 0 it is a plain ETH withdrawal. One transaction, submitted and paid by whoever holds the proof.
Fixed denominations
Notes are 0.01, 0.1 and 1 ETH, one pool each. A swap for 0.4 ETH becomes four 0.1 notes spent over time. This is a cost, not a detail: variable amounts are the easiest way to pair a withdrawal with a deposit, and the app makes the cut visible before you sign anything.
Layer 2 · the agent
The agent is a small program that turns an intent into a plan: parse the sentence, fetch quotes, choose a route, cut the amount into notes, and hand the client the exact calls to make. It runs its language model calls on Venice for three reasons that no other provider offers together.
- Hardware isolation, attested. The planner picks its model at runtime from Venice’s catalogue, TEE models first: the sentence is processed inside a hardware enclave, and the route fetches the enclave’s remote attestation with a fresh 32-byte nonce before answering. The app shows “TEE · attested” when that check passed. Zero-retention models are the fallback. End-to-end encryption from the browser is the next step; it needs the request to leave from your tab, not from a server.
- No policy layer. The open-weight models are served without a moderation layer on top. An agent that runs a multi-step task about “private” swaps does not get refused halfway, and there is no review queue reading the transcript.
- The agent pays for itself. Venice accepts x402: a request signed by a wallet and paid from a USDC balance, no API key and no account. The planner holds its own wallet on Base, signs every call with it, and tops its balance up from the wallet when it runs low. Nothing on Venice’s side resolves to a person. An API key stays as the fallback so a payment hiccup never takes the planner down. DIEM (staked VVV, a dollar of inference a day) can fund the same balance.
Any token, by name. Ask for a ticker the app has never heard of and the planner finds it: first through DexScreener’s index of this chain, then, if no index has caught it yet, through Venice web search. Either way the address is checked on chain (it must be an ERC-20 whose symbol matches) and a venue is found (a Uniswap v3 WETH pool, or a v4 pool whose key is read back from the PoolManager’s own Initialize log). Only then can a proof bind it.
The integration surface is Venice’s OpenAI-compatible API, which is also exposed as an MCP server and as one-line skills for agent frameworks. ZKVENICE uses the API directly; the planner is a few hundred lines and it is the least interesting part of the system, which is how it should be.
The agent is a customer of Venice. Nothing here is endorsed by Venice AI, and Venice’s own claims are documented at venice.ai/agents.
A swap, end to end
- 1. Intent. You type “0.4 ETH to USDG, best route, private” or fill the form; the form is what the sentence parses into.
- 2. Plan. The agent fetches quotes, picks the route, cuts the amount into notes, and returns the plan. Venice keeps nothing.
- 3. Shield. Your wallet deposits each note with its commitment. This is the first and last time your wallet touches the system.
- 4. Relay. For each note, your browser generates a proof and sends it to a relayer with the nullifier hash, the recipient and the swap parameters.
- 5. Swap and deliver. The relayer submits; the pool verifies, swaps, and delivers
tokenOutto the fresh address. The chain holds two facts it cannot join.
The app on this site runs all five steps once the pools are deployed: Plan calls the agent, Deposit sends from your wallet, Withdraw makes the proof in your tab and hands it to the relayer or to your wallet.
Fees
| Item | Rate | Goes to |
|---|---|---|
| Pool swap fee | 0.05% – 0.30% | Uniswap liquidity providers, by pool tier |
| Relayer fee | 0.10% | The relayer, for gas and the risk of a failed submit |
| Protocol fee | none | Nothing accrues to ZKVENICE at launch |
Status
This mirrors the block on the home page and is updated first.
| Piece | State |
|---|---|
| Site, design, docs | Built |
| Live quotes from chain 4663 | Built |
| Circuit (Groth16, Poseidon, 20 levels) | Built |
| Pool contracts | Written, 26 tests on real bytecode |
| Browser prover | Built |
| Relayer | Live |
| Agent on Venice | Live |
| Trusted setup | Development ceremony only |
| Pools on chain | Live, 3 denominations |
| $ZKVENICE | Live · 0x9287ece030f3c3ef13505d721648e90108872310 |
The contracts, as deployed
| Contract | Address on chain 4663 |
|---|---|
| Poseidon hasher | 0x915543d93b868b53a02c9cd9b445b3b4459aa92b |
| Groth16 verifier | 0x5a8c968663c5c0fb0f5d97c64d2ef7203b0d5422 |
| $ZKVENICE token | 0x9287ece030f3c3ef13505d721648e90108872310 |
| Pool 0.01 ETH | 0xf8ade805a67f572947eae239dc6f848eae3edb7c |
| Pool 0.1 ETH | 0x23a7c8382fa5c2c95f7622baf68b8f62244142c1 |
| Pool 1 ETH | 0xb41c395b2ad4490f03751ccc73a1938e21262732 |
Three contracts, no owner, no upgrade path. MerkleTreeWithHistory is Tornado’s tree with Poseidon and a constructor that computes its own zero values. Groth16Verifier is what snarkjs exported for the circuit. ZKVenicePool is one fixed denomination of native ETH; three are deployed at 0.01, 0.1 and 1 ETH.
function deposit(bytes32 commitment) external payable; // msg.value == denomination
function getLeaves(uint256 from, uint256 count) view returns (uint256[]); // rebuild the tree with eth_call, no logs
struct Exit { Venue venue; address tokenOut; uint256 minOut; uint24 fee; int24 tickSpacing; address hooks; }
function withdraw(
uint256[2] a, uint256[2][2] b, uint256[2] c, // the Groth16 proof
bytes32 root, bytes32 nullifierHash,
address payable recipient, address payable relayer, uint256 fee,
Exit exit // None = plain ETH, V4 = native pool, V3 = WETH pool
) external;The public signals of the proof are root, nullifierHash, recipient, relayer, fee, tokenOut, minOut, so a relayer can choose the venue and the fee tier but cannot change where the money goes, what it is paid, what it becomes, or the floor on how much. The tree is rebuilt by the client from getLeavesbecause the public RPCs of this chain do not serve logs reliably.
What the circuit proves, and where to read it
The circuit is 80 lines: withdraw.circom, with the two circomlib pieces it uses (poseidon, mux1). It proves four things about private inputs it never reveals: that Poseidon(nullifier, secret) is a leaf of the tree with the given root, walking 20 hashes up a Merkle path; that nullifierHash = Poseidon(nullifier); that the leaf’s position is consistent with the path; and, by squaring them, that the recipient, relayer, fee, token and minimum in the proof are the ones the verifier is given. 11,317 constraints. The compiled wasm, the proving key and the verification key are the files the app loads: withdraw.wasm, withdraw_final.zkey, verification_key.json. Compile the source yourself and the wasm matches.
| File | sha256 |
|---|---|
| withdraw.wasm | 70520a15ac349eec5f277790a3a3378545ecda0575a5a4b4d30eeb218e95d9c2 |
| withdraw_final.zkey | 30573a9749af148501b99d7bd3345f58d427f38a5dc4859af47b07c1e8942568 |
| verification_key.json | 38c466e7d6eab404aa9cc57e0e6bc68db7f2a724b2d996086aafee7eed9e14ed |
What the tests run
- The Uniswap v4 PoolManager is the runtime bytecode of the singleton at
0x8366…0951, and the v3 factory the bytecode of0x1f7d…2EfA, both pulled from chain 4663 and executed in-process on an EVM at the Cancun fork. - Every proof in the suite is a real Groth16 proof from the same wasm and zkey the browser uses; about two seconds each in Node.
- Covered: zero values and empty root match the client; three deposits produce the client’s root; wrong value and duplicate commitment revert; relayed withdraw-and-swap through a v4 native pool; fee paid; nullifier spent; double spend; the same proof with the recipient, fee, minOut or token changed is rejected; unknown root; minOut too high; plain ETH exit with no relayer; a proof against an older root still verifies after a new deposit; relayed withdraw-and-swap through a genuine v3 pool created by the real factory; a fee tier with no pool reverts.
| Call | Gas |
|---|---|
| deposit | ≈ 735,000 |
| withdraw as ETH | ≈ 265,000 |
| withdraw and swap (v4) | ≈ 411,000 |
| withdraw and swap (v3) | ≈ 405,000 |
The trusted setup, honestly
Groth16 needs a circuit-specific ceremony. The key the current pools use has a single contribution, made on a laptop over a locally generated powers-of-tau. Whoever ran it could, in principle, forge proofs against these pools. Acceptable for small amounts on a new chain; not for real volume.
The public ceremony is open: anyone contributes from their browser, one at a time, and every contribution lands on a public transcript with the file and its hash. The r1cs and the powers of tau are published next to it, so snarkjs zkey verify checks the whole chain offline; this site does not ask to be trusted for that. When the round closes, a verifier is deployed from the final key and new pools use it. If one contributor was honest, that key is safe.
For agents: the MCP server
The same flow the app has, as a Model Context Protocol server that runs on your machine over stdio. Any agent that speaks MCP (Claude Code, Cursor, Venice Agents) gets six tools, and the boundary is the same as in the browser: notes and proofs never leave the process. The agent sees calldata and hashes, nothing else.
| Tool | What it does |
|---|---|
| pools | Pools on chain, notes deposited, ETH held, relayer status, token address. |
| quote | How an amount splits into fixed notes, what it becomes after the fee, the minimum the proof binds. |
| plan | A sentence to a structured intent through the Venice planner, then a quote. Only the sentence is sent. |
| deposit | A fresh note saved to ~/.zkvenice/notes.json and the unsigned deposit transaction for your wallet. |
| notes | Local notes with their on-chain state: not deposited, unspent, spent. |
| withdraw | Rebuilds the tree, makes the Groth16 proof locally, hands it to the relayer or returns the unsigned withdraw transaction. |
One file, no repository needed. Download it and point your client at it:
curl -O https://www.zkvenice.com/mcp/zkvenice-mcp.mjs # Claude Code claude mcp add zkvenice -- node /path/to/zkvenice-mcp.mjs # Cursor / any MCP client: command "node", args ["/path/to/zkvenice-mcp.mjs"]
Needs Node 20 or newer. On first withdrawal it fetches the proving key and wasm (7 MB) from this site into ~/.zkvenice and keeps them.
The skill
For agents that read skills rather than tools: zkvenice.com/skill.md is a self-contained file in the same format Venice uses for its own API. It carries the rules that matter (a note is a secret, the recipient must be fresh, what the proof binds), the MCP tools, the HTTP routes and the contract addresses. Load it with your runtime’s skill installer, for example npx skills add https://www.zkvenice.com, or paste the URL into the agent.
The server never holds a private key. deposit and withdraw without the relayer return transactions for a wallet you control to sign; with the relayer, the proof goes to /api/relay exactly as the browser does. Set ZKVENICE_SITE to point it at another deployment.
Roadmap
- Relayer: one to start, run by us, with a 0.1% floor; then an open set with a fee market.
- Agent: the planner already calls Venice when a key is present. Next: the agent holds DIEM and pays for itself, and the prompt is published so the claim “your address is never in it” can be checked against the code.
- Public ceremony for the zkey, then a verifier redeploy, before any launch.
- Timing defence: done in the advice layer. The app and the MCP server read the crowd (deposits and unspent notes in the pool) and the note’s age, and warn below 10 deposits or six hours; the MCP tool stops unless told to force. Not enforced by the contract, on purpose. And the app’s Bundle tab spends several notes to one address at random moments inside a window you choose: all proofs are made up front in your tab, the relayer sends each one when its moment comes.
Last updated 2026-09-11, after the pools went on chain.