Confidential token distributions: payroll on-chain, salaries off the record.
Send tokens to your whole team, cap table or community in one transaction,
with every amount encrypted. Public enough to audit. Private enough to be humane.
Live demo · Audit a live treasury (no wallet needed) · TokenOps SDK · ERC-7984 · Sepolia
Confidential disperse and airdrop flows for real orgs: team payroll, investor allocations, community rewards. Send configures a batch, recipients decrypt only their own allocation, auditors verify the rest.
Every ordinary token transfer publishes who received how much, forever. That makes on-chain payroll, investor allocations and grant programs a privacy disaster: one Etherscan link exposes the whole org chart, the cap table, the reward tiers. So teams retreat to spreadsheets and bank wires, and the chain loses its most obvious payments use case.
Envelope is a web app for confidential batch distributions built on ERC-7984 confidential tokens and the TokenOps SDK. Amounts are encrypted in the sender's browser with FHE before they ever touch the chain, and travel as ciphertext handles that only the right people can open.
One batch transaction. Three honest views:
| View | Persona | Sees | Never sees |
|---|---|---|---|
| Send | Founder / ops | Templates, recipient list, live preflight checklist | — |
| Inbox | Recipient | Every envelope addressed to them, decrypted for their eyes only via an EIP-712 signature | Anyone else's amount |
| Treasury | Finance / auditor | Distribution count, program breakdown, recipient counts, on-chain proofs | Any individual amount |
Distributions start from a template that matches how orgs actually pay people:
- Team payroll: monthly salaries; period metadata, e.g.
Payroll · July 2026 - Investor allocation: SAFT tranches; round metadata, e.g.
Allocation · Seed - Community rewards: grants and campaigns, e.g.
Rewards · Testnet Season 3
The template label is deliberately public (that's the auditability half of the story); the amounts are deliberately not (that's the confidentiality half).
sender browser Sepolia recipient browser
────────────── ─────── ─────────────────
amounts (plaintext)
│ FHE encrypt (WASM, in-browser)
▼
ciphertext handles + ZK input proof
│
▼
DisperseConfidential.disperseConfidentialTokenDirect()
│ one tx, N confidential ERC-7984 transfers
│ emits DirectDistribution(recipients, encrypted handles)
▼
EnvelopeRegistry.logDistribution()
public metadata notary: template, label,
recipient count, zero amount data
read events addressed to me
│ EIP-712 signature
▼
userDecrypt → my amount, my eyes only
- Encryption / decryption:
@zama-fhe/sdkv3 relayer; plaintext never leaves the browser. - Batch transfer: TokenOps
fhe-dispersesingleton (0x710d…DBb4on Sepolia), direct mode: onesetOperatorapproval, then one tx per distribution. - Metadata:
EnvelopeRegistry.sol, a 23-line event-only notary. It holds no funds and knows no amounts; it exists so recipients and auditors can reconstruct the timeline purely from logs. - Token: CTTT, the ERC-7984 confidential test token (open faucet mint built into the app).
git clone <this repo> envelope && cd envelope/web
npm install
npm run devOpen http://localhost:3000, connect a Sepolia wallet, and use the built-in “Mint 100,000 test CTTT” helper on the review screen. You'll need a little Sepolia ETH for gas (faucet).
No environment needed: the deployed registry and a Sepolia RPC that serves eth_getLogs are the defaults. Optional overrides in web/.env.local:
NEXT_PUBLIC_RPC_URL=https://sepolia.drpc.org # must allow eth_getLogs (publicnode paywalls it)
NEXT_PUBLIC_REGISTRY_ADDRESS=0x… # your own EnvelopeRegistry deployment
NEXT_PUBLIC_REGISTRY_DEPLOY_BLOCK=… # its deploy block (getLogs scan start)cd contracts
forge test
forge script script/Deploy.s.sol --rpc-url $RPC_URL --private-key $PRIVATE_KEY --broadcastscripts/ contains the Node E2E proofs used during development (Node ≥ 22):
cd scripts && npm install
node gate-a.mjs # faucet mint → operator approval → preflight → confidential disperse
node gate-b.mjs # EIP-712 user decryption of an encrypted balance/amountLive demo: envelope.unitynodes.com
| Contract | Address |
|---|---|
| EnvelopeRegistry | 0xe2ce78FAF1C3b8b8faec6ea00053720b3D450789 |
| DisperseConfidential (TokenOps) | 0x710dD9885Cc9986EfD234E7719483147a6d8DBb4 |
| CTTT (ERC-7984) | 0x258F9D60dc023870e4E3109c894D834D5377361a |
Example distributions on Sepolia: community rewards · team payroll. Amounts in the event logs are FHE ciphertext handles.
The registry + disperse events give an auditor, per organization: when each batch went out, under which program, to how many wallets, with a transaction proof for each. The amounts exist on-chain only as FHE ciphertext handles under the recipients' access control. There is no admin key, no backdoor decrypt, no “trust us”. The privacy boundary is the protocol's ACL, not this app.
Metadata is the audit half; amounts are what we seal. We are precise about where the privacy line falls, because on a confidential-token bounty that line is the whole product.
- Amounts are the secret. On-chain they exist only as FHE ciphertext under each recipient's ACL. Nobody can decrypt someone else's payout: not the sender, not this app.
- Recipient addresses are public by protocol, not by our choice. ERC-7984 confidential transfers move ciphertext to a known
toaddress, so the address is on-chain in plaintext. OpenZeppelin's audit of the TokenOps confidential airdrop states this outright: recipient addresses are not hidden, "this is a property of the underlying asset, which is an ERC-7984 Confidential Token." Any project claiming it hides recipient lists on a disperse flow is misleading you. Envelope seals what can be sealed (the amounts) and tells you exactly where the boundary is. - Metadata is deliberately public. Sender, recipient set, count, template type, label and timing are plaintext events. That's a feature, not a leak: it's what lets a finance team verify payouts happened on schedule without seeing a single figure. Anything shown in Treasury is already readable on Etherscan.
- The label is public forever. It should name the program, not the counterparty:
Payroll · July 2026is fine;Seed round · Acme Venturesis not. The send flow warns about this where the label is composed, and the label is optional. - Metadata can deanonymize small groups. “Team payroll, 2 recipients, monthly” plus timing correlation can suggest whose wallets these are even with sealed amounts. That is an EVM-level property, not something an app layer can patch, so we say it plainly instead of pretending otherwise.
Next.js 14 · Tailwind v4 · wagmi 3 + viem · @tokenops/sdk (fhe-disperse, testnet-faucet) · @zama-fhe/sdk + @zama-fhe/react-sdk v3 · Foundry
MIT