A private balance is a set of notes only you can open. Every token is a note asset in one shared tree, so every deposit adds to the same anonymity set.
In the app
/private is the Private item in the nav, and the green Deposit action in the top bar goes there.
On a first visit it offers Set up keys or Restore from backup. See Keys and backup. After that it shows:
- Your private balance in USD, and the set size: every note in the tree.
- Holdings by asset, with bin shares, vault shares and position notes.
- Deposit, send to a
zkd1address, and withdraw. A withdrawal offersUse a fresh addressand a linkability reading that says hard, somewhat easy or easy to link, with reasons. - Sealed orders with claim and refund, and a randomised suggested claim time.
- Activity, your private address, backup download, viewing key, disclosure and
Lock this tab.
Swap, pools, launches, Earn, perps and Portfolio have private tickets of their own. Using the app lists them.
What a note is
commitment = Poseidon(amount, assetId, ownerBlind)
ownerBlind = Poseidon(pk, blinding)
pk = Poseidon(sk)
nullifier = Poseidon(commitment, leafIndex, nk)
assetId = keccak256(token, tokenId) >> 8
Stablecoins, stock tokens, launch tokens, ERC-4626 shares, LP vault shares, DLMM bin shares (tokenId is the bin) and perp positions (tokenId is a random share id) all move through one circuit. Private LP, private yield and private perps are actions on a note, not separate systems.
Three ways a note moves
deposit pulls the tokens, measures what arrived and computes the commitment on chain. It needs no proof, and a note can never claim more than arrived, so a fee-on-transfer token simply makes a smaller note. depositETH wraps ETH to WETH. Bin shares and positions cannot be deposited: they enter through act.
transact covers private transfer and withdrawal. It is a join-split with 4 inputs and 3 outputs. The proof binds the recipient, the relayer, the fee, the ciphertexts and a deadline, and a relayer must be the address that sends the transaction. A private transfer shows a spend and the relayer's fee, and no amount and no recipient. A fee paid in the note's own token shows that token, which is why a fee in private WETH comes first.
act spends a note into an adapter you choose, bound in the proof. The adapter runs the venue call and returns tokens to custody, and the pool measures each output and writes a new note. There is no adapter allowlist: an adapter only receives what a valid proof from the note's owner sends it. The one exception is the sealed lane, which the pool refuses as an adapter, because its route belongs to its own settlement. And an action that leaves the spent token on its adapter reverts, since an adapter holds nothing between calls. Immediate lane covers this.
The client refuses a withdrawal or a full-tree exit to one of the pool's own contracts.
One proof moves one asset, and it can pay its relayer from a second one, private WETH. If an amount needs more notes than fit, a self-transfer merges them first: one more proof and one more transaction.
The tree
One incremental Poseidon tree of depth 26 holds every note. A proof can reference any of the latest 65,536 tree states, so a proof built a moment ago still verifies after other people's transactions land.
Spending a note publishes its nullifier. Nothing on chain links a nullifier to its commitment, and a nullifier spends once.
Deposits and action outputs stop when the tree is three quarters full, which leaves room for change notes. Every open sealed order holds back two more leaves for its claim or refund, and nothing else can use them. exitFullTree withdraws when the tree has no room left for a full join-split beside those. treeFull() says when that point is reached, and openOrders() counts the open orders.
Your keys
Your wallet signs one fixed typed-data request, and your keys derive from that signature. It costs nothing and sends no transaction. Sign it only on the ZKDepth app: a signature of this request is your keys, for good. There is no way to rotate them short of moving your notes to a new balance. The app forgets them on Lock this tab, on a reload and when you switch wallets, and the wallet asks again next time. See Keys and backup.
Custody
AssetCustody is one contract per token, created by anyone through CREATE2 on first use. It tracks what is owed per asset and measures its balance on every move. A hostile token or adapter can only reach what its own notes hold.
Yield on a private balance
ERC-4626 shares can sit in a note like any other token. Deposit a note into a vault through VaultAdapter, and the shares come back as a note that keeps earning. LP vaults and DLMM vaults go in and out this way. The perps vault takes private deposits, and its shares leave through a plain withdrawal to a fresh address, because its exit takes two steps and a delay. See Immediate lane.
What the owner can do
The pool is immutable. Its owner can stop deposit and nothing else: spending, withdrawals and actions stay open whatever the owner does. A change ships as a new pool, and people move across themselves.
The setup
Proofs are Groth16 on BN254, and the verifiers are set immutably at deploy. Phase 1 uses a public Powers of Tau file. Phase 2 takes three contributions across two separate machines, run by the owner, and a public drand beacon finalises each circuit. Its round is named ahead of the last contribution, so nobody contributing can have seen it. The transcript and hashes are published. The deploy refuses a development verifier on chain 4663.
The app loads the wasm and the proving key only for the circuit an action needs, caches them in the browser and checks them against a manifest's SHA-256. A deployment whose manifest has development keys refuses to run private lanes anywhere but a local chain.
contracts/src/shielded/ShieldedPool.solthe tree, nullifiers, deposit, transact and actcontracts/src/shielded/AssetCustody.solone clone per tokencircuits/src/transaction.circomthe join-split