Docs

How privum works

A private pool for ETH on Ethereum, Arbitrum and Robinhood Chain, with a dollar pool and, on Ethereum and Arbitrum, a staked pool beside it. Everything here is open source, and nothing has an owner.

How it works

One public step in, then notes.

privum is a private pool for ETH on Ethereum, Arbitrum and Robinhood Chain, with a dollar pool beside it on each (USDC; USDG on Robinhood) and, on Ethereum and Arbitrum, a staked pool of wstETH. You put funds in once, from a public account. Inside, they are notes: amounts only your keys can spend, which nobody else can link to you or to each other. From notes you send, swap, stake, bridge and withdraw, each with a proof made on your device.

How funds moveFunds enter the private pool once, from a public account; inside it they are notes, which move to another wallet, the other pool, another chain, or out to an address.PRIVATE POOL · EACH CHAINSHIELDSENDSWAPBRIDGEWITHDRAWYour accountpublicNotesonly you can spendAnother walleta note of theirsThe other poolETH ⇄ dollars, a noteAnother chaina note thereAny addresspublic
One public step in (shielding), then notes. Everything after it is a proof made on your device and relayed; only a withdrawal shows where funds went.

The pools are forked from Tacit's EVM pool, its pool and verifier byte for byte the same as Tacit's live deployment. No contract has an owner, a pause or an upgrade.

Your wallet

Your private wallet is a recovery phrase: 12 or 24 words (BIP-39), made in your browser. It isn't an Ethereum account and has no public address. Write it down: it is the only backup, and nobody, us included, can restore it for you. Or let your browser wallet make it: it signs one message, and the phrase comes from that signature, so the same account signing again restores the wallet on any device.

Keys from one phraseThe recovery phrase gives an identity key on your device, and from it the spending key, the viewing key and every receive box.DERIVESRecovery phrase12 or 24 wordsIdentity keynever leaves the deviceSpending keyproves a note is yoursViewing keyfinds your notes on-chainReceive boxesone-time, any chain
Everything comes from the phrase, on your device. Restore it anywhere and the same notes and boxes are found again on-chain.
  • Payment address (bp1…): what another private wallet sends to.
  • Public payment address (st:eth:…): what any wallet can pay, a fresh one-time address each time (below).
  • Receive boxes: one-time addresses on any chain, for anyone sending from a public account or an exchange, and where swaps, bridges, refunds and payouts arrive. What lands in one waits there until you sweep it into a note (the relayer's flat fee, at most 0.25%).
  • Private accounts: ordinary addresses, one per app, for apps that want a wallet (below).

Relayers and OHTTP

After the first deposit, you never send a transaction yourself: a relayer does, and is paid a flat fee out of the spend. The fee, the amounts and where funds go are bound into your proof, so a relayer can only send it as it is, or refuse.

Who sees what when you sendA request goes from your device, sealed, through an OHTTP relay that sees your IP but not the request, to a relayer that sees the request but not your IP, which sends the transaction.SEALEDNO IPTXYour deviceproves, sealsOHTTP relayrun by someone elseRelayerpays the gasChainsees your IPnot the requestsees the requestnot your IPsees the relayernot you
Nobody sees both who you are and what you asked. The relay is run by someone other than the relayers, and relayers keep no logs.

Every request, reads included, goes through an Oblivious HTTP relay (RFC 9458). Relayers connect to it from anywhere and list themselves in its directory, each entry signed by its relayer's key; your wallet picks among them weighted by their last week of work, so the relay can't steer you to its own.

No relayer sees all of what your wallet does. Only the relayer sending your transactions gets them; every read goes to another relayer on the chain, picked at random each time, and the balances of your own receive boxes and accounts are asked among decoys: addresses just seen on-chain, each asked the same way. A network can list several relays run by different people: relayers connect to all of them, and each request goes through one at random. “Delay requests” adds a random wait before each one.

Swaps

A swap moves between the ETH pool and the dollar or staked pool on one chain, note to note. One proof spends your ETH note into the Swapper contract, which takes the fee, trades on a DEX and delivers into a receive box of yours in the other pool.

A private swapAn ETH note is spent into the Swapper, which pays its fee to the relayer and ops and trades the rest on Uniswap; the dollars land in a receive box of the dollar pool and are swept into a dollar note.ETH POOLDOLLAR POOL1 PROOFFEEETHUSDCSWEEPETH noteSwappertakes the feeIts relayer · ops60% · 40%UniswapReceive boxyoursUSDC note
ETH → USDC: one proof, one transaction. The output never touches an address: it lands in a receive box of yours in the dollar pool and becomes a note.

On-chain, it shows as a swap between two pools: not who made it. You can also deliver to an address instead (public from there on). On Ethereum, relayers send privately, not through the public mempool.

Staking

On Ethereum and Arbitrum, a third pool holds wstETH, Lido's staked ETH. A swap from ETH into it is a stake: the wstETH lands as a note in the staked pool, and earns as wstETH's price rises against ETH. Swap back whenever you like; there is no lockup.

  • A flat 0.05%, on the ETH side, in place of the swap fee: about a week of the yield.
  • Lido's risks come with it: wstETH can trade below ETH, and on Arbitrum it is a bridged token.

Bridging

ETH crosses chains in batches: many people's joins of the same size, settled together through Across when the epoch ends. Your amount is split into the source chain's sizes (0.01, 0.1, 1 and 10 ETH on the L2s; 0.1, 1 and 10 on Ethereum; 5 tops Robinhood) and joined in one withdrawal.

Bridging in batches0.12 ETH splits into pieces of the batch sizes, 0.1 plus 0.01 plus 0.01, each joining the batch of its size with other people's joins; at the end of the epoch a keeper sends the batches through Across, and each piece arrives in its own receive box on the other chain.BATCHES · HEREOTHER CHAINACROSS0.12 ETHyour notes0.1 ETH0.01 ETH0.01 ETH0.1 batchyou + others0.01 batchyou ×2 + othersReceive boxReceive boxReceive box
Any amount crosses as pieces of fixed sizes, joined in one withdrawal for one relayer fee. A batch is many people's joins of the same size, so on the other side nothing ties a piece to you, or the pieces to each other.

Each piece pays a join fee of 0.1% on top: 0.02% to the relayer that submits it, 0.02% to the keeper that settles its batch, 0.01% to ops, and up to 0.05% for its Across fee, whatever that leaves going to ops. A batch short of its minimum when the epoch ends waits a few more epochs, then refunds its pieces into a receive box on the source chain, Across part included; so does a full batch nobody settles within a day.

Relaying, joining and settling for yourself gets your own fee back, never more: at least ops' part and the Across fee are lost, so making up joins never pays.

ZEC

Swap ETH to ZEC and back, without a public address on the ETH side.

Swaps with ZcashETH to ZEC: an ETH note is spent into the Swapper, which takes the fee and deposits the rest with MAYAChain, which pays the shielded address or refunds into a receive box. ZEC to ETH: a Zcash wallet pays a one-time deposit address from 1Click; NEAR Intents sends ETH to a receive box, swept into a note.ETH → ZEC · MAYACHAINZECREFUNDETH noteSwapperswap feeMAYAChainmemo: u1…, min ZECu1… addressshieldedReceive boxif it can'tZEC → ETH · NEAR INTENTSETHZcash walletanyDeposit addressone-time, 1ClickNEAR IntentsswapsReceive boxswept into a note
Out: one proof, the swap fee, and MAYAChain pays your shielded address; if it can't meet the least amount, the ETH comes back to a receive box as a note. In: pay a one-time address from any Zcash wallet; the ETH arrives in a receive box.
  • To ZEC goes through MAYAChain, from Ethereum or Arbitrum: the only route that pays straight into a shielded u1… address. A transparent address works too, but the ZEC is public there.
  • From ZEC goes through NEAR Intents onto any of the three chains. Pay the exact amount within the hour; give your own Zcash address for refunds.
  • A relayer places the order, so the exchanges only ever hear from its server, never from you. Your wallet checks what a relayer passes on: 1Click's quotes must carry its signature and be for exactly the swap you asked for; MAYAChain's vault is read through two relayers, which must agree.

Other chains

Pay out from your ETH notes to almost any asset on any chain NEAR Intents serves (Bitcoin, Solana, Tron, XRP, dollars on many chains), and pay in from any of them into an ETH note. Without an address, a payout to a privum chain's ETH or dollar lands in a receive box of yours there, so it stays private.

Swaps with other chainsOut: an ETH note is spent into the Swapper, which takes the fee and pays a one-time NEAR Intents deposit address; NEAR Intents pays the asset to the address on its own chain, or refunds into a receive box. In: any wallet on another chain pays a one-time deposit address there; the ETH lands in a receive box here.ETH → ANY ASSET · NEAR INTENTSBTC · SOLREFUNDETH noteSwapperswap feeNEAR Intentsone-time addressAddresson its chainReceive boxif it can'tANY ASSET → ETH · NEAR INTENTSETHAny walletany chainDeposit addressone-time, its chainNEAR IntentsswapsReceive boxhere
Out: one proof, the swap fee, and NEAR Intents pays the address on the other chain; if it can't, the ETH comes back to a receive box as a note. In: pay a one-time address on the asset's own chain from any wallet; the ETH arrives in a receive box here.
  • The exchange only hears from a relayer's server, never from you; on the ETH side, only a pool withdrawal to a one-time address, or ETH arriving at a fresh one.
  • Every quote must carry NEAR Intents' signature and be for exactly the swap you asked for, recipient and refund address included, so a relayer can't redirect it.

Spreading out

Amounts and timing are what most often link a withdrawal back to a deposit. Split a payout across many addresses, or a swap into several, and the app runs each part as its own proof at a random time over the window you pick, in uneven amounts.

A payout spread over timeOne private balance of 1.2 ETH becomes five withdrawals of different sizes to five fresh addresses, at random times over six hours, each its own proof.NOW+2 H+4 H+6 H0.310x…a10.190x…7f0.270x…c30.220x…090.210x…e4Private balance1.20 ETH
One balance, five fresh addresses: five separate withdrawals, uneven amounts, random times across the window. On-chain they are five unrelated withdrawals among everyone else's.

A proof is made against the pool as it is when it runs, so nothing is signed ahead: the steps run while the app is open, and any that were missed run as soon as it is open again.

Private accounts

An address for apps that want a wallet, tied to nothing of yours.

Some apps need an ordinary account: one that holds ETH, signs messages and pays. For each one, your wallet derives a private account from your phrase. Your notes fund it by a relayed withdrawal, so on-chain the pool paid a fresh address and nothing says who asked. When you are done, it sends what it holds, less gas, back into a receive box of yours, swept into a note.

Private accounts for appsYour notes fund a private account by a relayed withdrawal; the account pays the app; what is left goes back to a receive box of yours and is swept into a note.PRIVATE POOLRELAYEDPAYSWHAT'S LEFTYour notesPrivate accountone per app, from your phraseThe appzkAPI, IMD, any siteReceive boxswept into a note
One account per app, from your phrase. The pool pays it, so on-chain nothing ties it to you or to your other accounts; what it doesn't spend comes back as a note.
  • One per app: the zkAPI account, the IMD account and a general one are different addresses, so nothing ties them to each other.
  • Restored with the phrase: the same phrase gives the same accounts on any device.
  • Sent privately: the account signs on your device; its transactions go through a relayer, by the private route on Ethereum, so no RPC provider sees its IP.

Private AI

Pay for AI models without your requests being tied to you. zkAPI (by Open Anonymity, with the Ethereum Foundation) holds ETH in a vault on Ethereum as a private note, and proves each request is paid without saying which note pays. In privum, the deposit comes from your zkAPI private account, so not even the deposit is your wallet's.

Private AI through zkAPIThe zkAPI account deposits ETH into zkAPI's vault as a private note; each AI request proves it is paid without saying which note; a short-lived key goes to the model provider with the prompt.DEPOSITPROOFPROMPTzkAPI accountthe pool paid itzkAPI vaulta private notezkAPIa short keyModelOpenRoutersees a depositnot whosesees a paid requestnot which notesees the promptnot who paid
zkAPI learns that some deposit paid, never which; the provider sees the prompt and a key, never who paid. The deposit itself comes from your private account, not your wallet.
  • Add credit in Apps → Private AI: the app funds the account from your notes and deposits with zkAPI.
  • Get a key: a short-lived OpenRouter key with a spending cap, for any OpenAI-compatible app. Only what you use is charged.
  • Withdraw what's left into a receive box, then a note. Credit expires: what isn't withdrawn by then goes to zkAPI's treasury, and the app counts down to it.
  • The model still sees your prompts, and zkAPI sees your IP unless you use Tor or a VPN. What neither can learn is who paid.

IMD

IMD is a swarm of agent workers that write sourced reports, answer oracle questions and launch contracts, paid 0.5 IMD a job on Ethereum. In privum, your IMD private account pays.

  • Buying IMD: the account swaps its ETH through the Swapper (the usual fee) along WETH → USDC → IMD on Uniswap, and approves Permit2 for IMD once.
  • Paying a job: the account signs IMD's x402 payment and its approval of the quote; IMD's server pays the gas. The token that reads your orders back comes from the account's key, so it restores with your phrase.
  • IMD sees the account and the job, never your wallet.

Public payment address

One address to share; a fresh one for every payment.

Your public payment address is a stealth meta-address (ERC-5564, scheme 1) from your phrase: st:eth:0x…. Post it anywhere. A wallet that supports stealth addresses pays a fresh one-time address computed from it, and announces the payment on the standard Announcer contract. Your wallet finds it with your viewing key, moves it into a receive box and sweeps it into a note.

Being paid at a public addressAny wallet pays a fresh one-time address computed from your public payment address and announces it; your viewing key finds the announcement; the app moves the payment into a receive box and sweeps it into a note.PAYSANNOUNCESMOVEDYOUR KEY FINDSAny walletERC-5564One-time addressfresh for every paymentAnnouncerpublic, for everyone'sReceive boxswept into a note
Each payment goes to its own one-time address that only your keys can find and spend. The app finds it from the standard announcement, moves it into a receive box and sweeps it into a note.
  • Nothing to link: every payment has its own address; none points to another, or to you.
  • Publish it, if you like (ERC-6538), under a public wallet's address, so wallets can look it up from that address. That links the public address to the payment address, never to what is paid to it.
  • ETH only, on chains where the Announcer is deployed.

Disclosure reports

Show what's yours, and only that.

For taxes or an exchange, download a report of your history in one pool, from Wallet → Activity → Report: every shield, receive, sweep, send and withdrawal, rebuilt from the chain with your keys. It comes as CSV for a spreadsheet and as JSON anyone can check.

  • Every note it lists is disclosed by its opening (value, key, randomness), so a checker can recompute its leaf and find it among that transaction's outputs.
  • Every spend, by its nullifier key, so a checker can recompute the nullifier the transaction published.
  • It says nothing about anyone else's notes. Whoever you give it to can link these notes to each other and to you; nobody else can.
import { verifyReport } from '@privum-lite/client'

const problems = await verifyReport(report, rpc)   // [] when every row checks out

Earn

Anyone can run the relayers and keepers that make this work, and be paid for each job, on the spot. One click in Earn runs them in the open tab; one command runs them on a server. No ports to open: they connect out to the OHTTP relay.

How operators are paidEvery job pays the operator that did it, in the same transaction: a relayed swap pays its relayer 60% of the swap fee; every relayed action pays its relayer's flat fee; a relayed batch join pays its relayer 0.02% of the join; and the keeper that settles a batch is paid 0.02% of every join in it.60% OF THE FEEFLAT FEE0.02%0.02%, SETTLEDA swap0.50% → 0.20% feeAny relayed actionthe relayer's own feeA batch join0.1% join feeRelayerpaid on the spotKeeperpaid on the spot
Nothing to claim and no pool to wait on: the pay is in the transaction itself. Relaying or settling for yourself gets your own fee back, never more, so made-up work never pays.

Wallets pick relayers by their work over the last week and, if they have one, their bond: ETH or the chain's dollar locked in OperatorBonds, never taken, back 14 days after asking. A bond is optional; relayers without work or a bond still share a tenth of picks. A bond can come straight from a private note, so it doesn't point to a known wallet.

Referrals

Share your link from Earn. Swaps made through it pay you 30% of their fee, out of ops' share (the relayer's is unchanged), straight into a receive box of yours, the same address on every chain. Your code is that box's address, so nothing public points to you.

A wallet that came through a link names the code in each of its swaps, which makes those swaps linkable to each other (not to the wallet). It can drop the code in the swap settings.

Fees

The swap feeThe swap fee is 0.50% of the first 1 ETH, 0.35% of the next 9, 0.25% of the next 90 and 0.20% above 100 ETH, charged like tax brackets; it is paid out on the spot: 60% to the relayer that submits the swap, 40% to ops.0.1%0.2%0.3%0.4%0.5%0.50%First 1 ETH0.35%Next 90.25%Next 900.20%Above 100WHERE IT GOESIts relayer · 60%Ops · 40%
Charged like tax brackets: each part of a swap pays its own bracket's rate, so splitting a swap never makes it cheaper (0.50% at 1 ETH, 0.37% at 10, 0.26% at 100). It's paid out on the spot, on the chain of the swap.
WhatFeeGoes to
Swap (ETH ⇄ dollar, ETH → ZEC)0.50% → 0.20%, on the ETH side60% its relayer, 40% ops
Staking (ETH ⇄ wstETH)A flat 0.05%, on the ETH side60% its relayer, 40% ops
Send, withdraw, batch join, swapA flat relayer fee per action; on Ethereum, its gas at today's price plus 30% when that's moreThe relayer
Sweep a receive boxA flat fee, at most 0.25% of the boxThe relayer
Bridge (each batch join)0.1%: 0.02% relayer, 0.02% keeper, 0.01% ops, up to 0.05% AcrossThe operators who did the work; Across; ops
Payouts to other chainsThe swap fee, then the exchange's own, in its quoteIts relayer and ops; NEAR Intents
ZEC, deposits from other chainsThe exchange's own fee, in its quoteMAYAChain or NEAR Intents
Private AI, IMDTheir own prices; IMD bought through the Swapper pays the swap feezkAPI, IMD; its relayer and ops
Through a referral link30% of the swap fee, out of ops' shareThe referrer, as a note

The client

Everything the app does is in @privum-lite/client: proofs on the device, every request through the OHTTP relay. A wallet or a bot can use it directly.

import { createClient, identityFromPhrase } from '@privum-lite/client'
import { ohttpFetch } from '@privum-lite/ohttp'

const client = await createClient({
  identityKey: identityFromPhrase(phrase),
  chain: { chainId: 42161, pool, router, swapper, batchManager },
  relayer: 'https://<route>.relayer.tunnel',
  relayerFetch: ohttpFetch(routes),
})
await client.payout({ amount: 10n ** 17n, destinationAsset: 'nep141:btc.omft.near', recipient: 'bc1q…' })
await client.joinBatch({ destChainId: 4663, amount: 2n * 10n ** 17n })

const acct = client.account('my-app')            // a private account: fund, provider(), drain
const { found } = await client.stealth.scan(from) // payments to the public payment address
const report = await client.report()              // a disclosure report

Kohaku plugin

@privum-lite/kohaku makes the pools a plugin for Kohaku, the Ethereum Foundation's privacy kit for wallets, beside Railgun and Privacy Pools. Its keys come from the wallet's keystore, its state from the wallet's storage; every request goes through OHTTP, not the wallet's RPC.

import { createPrivumPlugin } from '@privum-lite/kohaku'

const privum = await createPrivumPlugin(host, { chain, relayer, relayerFetch })
const op = await privum.prepareShield({ asset: { __type: 'native' }, amount })  // txns for the wallet to send
await privum.balance()                                                          // sweeps arrived shields
await privum.broadcastPrivateOperation(await privum.prepareTransfer(asset, 'bp1…'))
await privum.broadcastPrivateOperation(await privum.prepareUnshield(asset, '0x…'))

Contracts

ShieldedPool, TransactVerifier
Tacit's pool and verifier, unchanged. One per asset on each chain: ETH, the dollar and, on Ethereum and Arbitrum, wstETH.
PoolRouter
Withdraw-and-call intents and receive boxes, one per pool.
BatchManager, DropFactory
Batches, their settlement through Across, and delivery into receive boxes.
Swapper
Swaps between the pools and out to other chains; staking at 0.05%; pays the fee on the spot.
OperatorBonds
Optional operator bonds: locked ETH or dollars, each operator's in a vault of its own.
OperatorDistributor
The operator pot for the token's fees, paid daily by relayers' work.

No owner, pause or upgrade on any of them. The code is open source.