V1 Mainnet · Compliance-friendly by design
Viewing keys
Privacy by default, transparency by choice. A viewing key lets someone you choose — an auditor, an exchange, an accountant — see your private activity. It never lets them spend.
| Key | Holds | They can see | Spend? |
|---|---|---|---|
| Incoming viewing key | incomingSecret, userCommitment | Notes you received, every epoch — not what you spent or who you paid | No |
| Epoch viewing key | ivk + ovk of one epoch, nk, userCommitment | Everything, but only for that viewing epoch | No |
| Full viewing key | incomingSecret, outgoingSecret, nk, userCommitment | Everything — received, spent, who you paid, your balance — every epoch | No |
| Payment receipt | No key: one note’s opening (token, amount, receiver uc, r) | One payment — anyone rebuilds its commitment and finds it on-chain | No |
The protocol supports all four; the app shares the full viewing key.
How a viewing key reads the pool
In the viewer’s browser
the same public data for everyone
- 1Downloads every NoteCreated, NullifierSpent, OutgoingRecord and Swapped event. Everyone downloads everything, so nothing reveals whose key is being used.
- 2Derives ivk and ovk for every epoch, from 0 to the current one.
- 3Tries to decrypt every note with each ivk. A swap’s bought note takes its amount from the Swapped event.
- 4Keeps a note only if Poseidon(4, token, amount, Poseidon(3, userCommitment, r)) equals the commitment on-chain — anyone can encrypt something to you, but only real notes rebuild. Zero-value dummies are skipped.
- 5Computes each note’s nullifier with nk: if it appears in NullifierSpent, the note is spent — and the viewer sees when.
- 6Opens the outgoing records with ovk: who was paid, how much, and the fee.
- 7Result: the balance of every token and the full history — masked, received, sent, swapped, unmasked.
The full viewing key, as a string
menoid-vk1:<base64url(json)>.<checksum>
json = { kind: "full", address, network, keyIndex,
userCommitment, incomingSecret, outgoingSecret, nullifierKey }
checksum = first 4 bytes of keccak256(payload) // catches a mistyped keyIn the app
- Share yours: click your address to open your account, then Copy viewing key.
- Read someone’s: at the bottom of the account box, View someone’s private activity, then paste their key. It is decrypted in your browser and never sent anywhere — and the app checks the key really matches their on-chain registration.
Why it can’t spend
Spending needs a proof, and a proof needs sk — the circuit rebuilds spendPk and your userCommitment from it. A viewing key holds nk = Poseidon(2, sk): enough to recognise spent notes, but Poseidon can’t be run backwards to get sk.
Sharing can’t be undone
Whoever holds your full viewing key can read everything it covers — including future activity under the same key index. To stop that, move to a new key index with rotateKeys: a new identity they can’t see. updateViewingKey only changes the epoch: it shuts out holders of an epoch key, not holders of a full or an incoming key, which cover every epoch.
