Skip to content
ZKJEV

Docs

How it works.

ZKJEV is two things bolted together with care: a shielded pool on Robinhood Chain that swaps on the way out, and a decision layer on Jev, a model that answers typed questions in probabilities and never sees an address. 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 time it keeps the conversation on someone’s server, usually with your address in it.

ZKJEV 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 only judgement calls that matter for privacy, when to withdraw and through which venue, are made by Jev from a state that holds counts and ages and nothing else. 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.
  • TypeSafe (Jev) receives a state of numbers: the pool’s deposit count, a note’s age in hours, candidate venues with their liquidity, the size of a bundle. It never receives an address, a note, a transaction hash or a sentence. The route publishes the exact request it sends, and the app can show it before you withdraw.
  • ZKJEV the site has no analytics and no database. The app reads prices from DexScreener in your browser. Two routes run on the server: the decide route, which forwards a state of counts to Jev 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 · Jev decides

The maths hides which note a withdrawal spends. It does not hide when you withdraw or how the crowd looks at that moment, and those are the two things an observer uses. ZKJEV puts that judgement in Jev, a System One model from TypeSafe AI, for three reasons.

  • It does not write. Jev is not a chat model. It takes a state and typed questions and returns one answer per question: a noul (the probability a statement is true), a choice (one option from a set you define, with a probability per option), or a score (a position on a scale you define). There is no prose to leak into, no prompt to inject into, no transcript to read.
  • It is fast and cheap enough to ask every time. Most calls return in about a hundred milliseconds and input costs $0.042 per million tokens, so the app asks before every withdrawal and every bundle instead of once a day.
  • It sees numbers, not you. The state is the pool’s deposit and unspent counts, the note’s age in hours, candidate venues with liquidity and fee, whether a relayer is on, and the size of a bundle. No address, no note, no hash. The request is returned with every answer so the claim can be checked.

The decision schema

Five questions, asked through POST /api/decide. The first three are always asked; the last two when they apply.

QuestionTypeAnswers
withdraw_nownoulProbability that a withdrawal sent now blends into the crowd and is not linkable by timing.
crowd_riskscorenegligible · low · moderate · high, with the distribution and a confidence.
waitchoicenow · one_hour · six_hours · a_day.
venuechoicev4 · v3 · none (plain ETH), from each venue’s liquidity and fee. Asked when there is a token out.
spreadchoiceback_to_back · thirty_minutes · two_hours · six_hours. Asked for a bundle.

The app shows the answers in the withdraw and bundle panels, with a toggle that prints the literal request. The MCP server stops a withdrawal when withdraw_now is under 0.5 unless told to force. Nothing is enforced by the contract, on purpose: the pool cannot know your reasons, and a rule on chain would leak them.

Without a key. Jev is in early access. Until a key lands on this deployment the route answers the same five questions from the rules the app shipped with (ten deposits, six hours, deepest venue) in exactly Jev’s shapes, and every response says source: "local". The stats page shows which model answered. Nothing is labelled Jev that Jev did not answer.

Any token, by name. Ask for a ticker the app has never heard of and the plan route finds it through DexScreener’s index of this chain, checks the address on chain (an ERC-20 whose symbol matches) and finds a venue (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.

ZKJEV is a customer of TypeSafe’s API. Nothing here is endorsed by TypeSafe AI, and Jev’s own claims are documented at docs.typesafe.ai.

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 app fetches quotes, cuts the amount into fixed notes, and asks Jev for a venue. Nothing about you is in the question.
  • 3. Shield. Your wallet deposits each note with its commitment. This is the first and last time your wallet touches the system.
  • 4. Decide, then relay. For each note, Jev is asked whether now is the moment; then 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 tokenOut to the fresh address. The chain holds two facts it cannot join.

The app on this site runs all five steps: Plan quotes, Deposit sends from your wallet, Withdraw asks Jev, makes the proof in your tab and hands it to the relayer or to your wallet; Bundle spreads several notes over a window Jev picks.

Fees

ItemRateGoes to
Pool swap fee0.05% – 0.30%Uniswap liquidity providers, by pool tier
Relayer fee0.10%The relayer, for gas and the risk of a failed submit
Protocol feenoneNothing accrues to ZKJEV at launch

Status

This mirrors the block on the home page and is updated first.

PieceState
Site, design, docsBuilt
Live quotes from chain 4663Built
Circuit (Groth16, Poseidon, 20 levels)Built
Pool contractsWritten, 26 tests on real bytecode
Browser proverBuilt
RelayerLive
Decide route (Jev)Local answers until the key lands; the stats page shows which
Trusted setupPublic ceremony open; pools run on the round-one key
Pools on chainLive, 3 denominations
$ZKJEVLaunching on pons

The contracts, as deployed

ContractAddress on chain 4663
Poseidon hasher0x915543d93b868b53a02c9cd9b445b3b4459aa92b
Groth16 verifier0x5a8c968663c5c0fb0f5d97c64d2ef7203b0d5422
$ZKJEV token
Pool 0.01 ETH0xf8ade805a67f572947eae239dc6f848eae3edb7c
Pool 0.1 ETH0x23a7c8382fa5c2c95f7622baf68b8f62244142c1
Pool 1 ETH0xb41c395b2ad4490f03751ccc73a1938e21262732

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

Filesha256
withdraw.wasm70520a15ac349eec5f277790a3a3378545ecda0575a5a4b4d30eeb218e95d9c2
withdraw_final.zkey30573a9749af148501b99d7bd3345f58d427f38a5dc4859af47b07c1e8942568
verification_key.json38c466e7d6eab404aa9cc57e0e6bc68db7f2a724b2d996086aafee7eed9e14ed

What the tests run

  • The Uniswap v4 PoolManager is the runtime bytecode of the singleton at 0x8366…0951, and the v3 factory the bytecode of 0x1f7d…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.
CallGas
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) gets seven 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.

ToolWhat it does
poolsPools on chain, notes deposited, ETH held, relayer status, token address.
quoteHow an amount splits into fixed notes, what it becomes after the fee, the minimum the proof binds.
planA sentence to a structured intent (local parser, DexScreener for unknown tickers), then a quote.
depositA fresh note saved to ~/.zkjev/notes.json and the unsigned deposit transaction for your wallet.
notesLocal notes with their on-chain state: not deposited, unspent, spent.
withdrawAsks Jev, rebuilds the tree, makes the Groth16 proof locally, hands it to the relayer or returns the unsigned withdraw transaction. Stops on a low verdict unless forced.

One file, no repository needed. Download it and point your client at it:

curl -O https://www.zkjev.com/mcp/zkjev-mcp.mjs

# Claude Code
claude mcp add zkjev -- node /path/to/zkjev-mcp.mjs

# Cursor / any MCP client: command "node", args ["/path/to/zkjev-mcp.mjs"]

Needs Node 20 or newer. On first withdrawal it fetches the proving key and wasm (7 MB) from this site into ~/.zkjev and keeps them.

The skill

For agents that read skills rather than tools: www.zkjev.com/skill.md is a self-contained file. 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.zkjev.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 ZKJEV_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.
  • Jev: the decide route is wired to TypeSafe’s API and answers locally until the key lands. Next: a per-pool history of verdicts on the stats page, and Jev choosing the relayer when there is more than one.
  • Public ceremony for the zkey, then a verifier redeploy, before any launch.
  • Timing defence: done in the advice layer, now as Jev’s verdict. The app and the MCP server read the crowd (deposits and unspent notes in the pool) and the note’s age, ask Jev, and stop on a low withdraw_now 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-20, when ZKVENICE became ZKJEV. Same pools, same circuit, a different model.