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.
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.
- 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.
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.
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.
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.
- 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.
- 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 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.
- 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.
- 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.
- 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 outEarn
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.
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
| What | Fee | Goes to |
|---|---|---|
| Swap (ETH ⇄ dollar, ETH → ZEC) | 0.50% → 0.20%, on the ETH side | 60% its relayer, 40% ops |
| Staking (ETH ⇄ wstETH) | A flat 0.05%, on the ETH side | 60% its relayer, 40% ops |
| Send, withdraw, batch join, swap | A flat relayer fee per action; on Ethereum, its gas at today's price plus 30% when that's more | The relayer |
| Sweep a receive box | A flat fee, at most 0.25% of the box | The relayer |
| Bridge (each batch join) | 0.1%: 0.02% relayer, 0.02% keeper, 0.01% ops, up to 0.05% Across | The operators who did the work; Across; ops |
| Payouts to other chains | The swap fee, then the exchange's own, in its quote | Its relayer and ops; NEAR Intents |
| ZEC, deposits from other chains | The exchange's own fee, in its quote | MAYAChain or NEAR Intents |
| Private AI, IMD | Their own prices; IMD bought through the Swapper pays the swap fee | zkAPI, IMD; its relayer and ops |
| Through a referral link | 30% of the swap fee, out of ops' share | The 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 reportKohaku 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.