How it works
Private payroll
Pay a whole team in one run. Each person sees their own line, the public sees a pool interaction, and you can still prove the total to your accountant.
- Input
- UMBR addresses + amounts
- Public sees
- Pool interactions only
- Proof for books
- Payroll total
- Networks
- Testnets today
Why payroll needs privacy#
A public stablecoin payroll is a published salary sheet. Anyone who knows your company wallet can list every payment, match addresses to people over time and work out who earns what, when you hire and when someone leaves. Recipients are exposed too: once their address is known, their savings and spending are readable by coworkers, landlords and strangers.
UMBR pays each person from a private balance inside the shielded pool. Amounts and the link between your balance and each recipient stay off the public record. Each person learns their own payment. Your accountant can still get the one number they need through a proof, without seeing a single salary.
Anatomy of a run#
A run is one list of payments in one asset, sent together from your private balance.
Fund the private balance
Shield enough of the asset (for example USDG on Robinhood Chain testnet) to cover the whole list plus a margin. The shield deposit itself is public.Load the list
Paste or upload a CSV, or type lines by hand. The app checks every address and amount and shows the total before anything is signed.Make the proofs on your device
Each payment is a private transfer inside the pool. Your browser builds a zero-knowledge proof for each one; your keys never leave the device.Submit
Proofs go to the pool contract, through the relayer if you use one. Large lists are split into several transactions.Recipients sync
Each recipient's wallet scans the pool, finds the note encrypted to them and shows the payment with its label if you added one.Keep the run record
The run is saved locally with its id, so you can review it and prove its total later.
Run your first payroll#
- Open Shield and move test USDG or pathUSD into your private balance. Testnet funds come from the network's public faucet; the testnet guide walks through it.
- Ask each recipient for their UMBR address. They find it in the app under Receive; it starts with
umbr1q. - Open Payroll, pick the asset and paste your list or upload a CSV.
- Review the total, the line count and any warnings. Fix flagged lines before continuing.
- Confirm. Keep the tab open while the proofs are made; a long list can take a few minutes.
- When the run finishes, check each line's status in the run view.
Try it without funds
/app/payroll?demo=1 on a local build to walk through the screens with a pretend balance. Nothing is signed or sent in demo mode.List format#
A list is plain CSV with the columns address,amount,label. The header row is optional; the label column is optional on every line.
address,amount,label
umbr1qexample7x2k9...ana,4200.00,Ana - design
umbr1qexample3m8p0...ben,3850.00,Ben - backend
umbr1qexample9t4w1...chi,1200.50,"Chi - contract, March"
umbr1qexample5q7r2...dev,600- address is an UMBR address (
umbr1q...). Public0xaddresses are rejected, because paying one would put the payment on the public record. - amount is a plain decimal in the run's asset, with a dot as the separator and no currency sign or thousands commas. USDG and pathUSD use 6 decimals; more precision is rejected rather than rounded.
- label is free text that travels encrypted to the recipient as the payment memo. If it contains a comma, wrap it in double quotes. Long labels are cut to the memo limit, and the app warns you.
- Blank lines are skipped. Zero and negative amounts are rejected.
- The same address can appear twice (for example salary and expenses). The app flags it so you can confirm it is intended.
The last line has no label, which is fine. The app shows the parsed list before you confirm, so a column that shifted because of an unquoted comma is caught there.
The same list works from code:
const lines = umbr.payroll.parseCsv(csvText);
const run = await umbr.payroll.run({ asset: "USDG", lines });
// run.id -> keep it; you need it to prove the total later
// run.txHashes -> one or more pool transactions
// run.lines -> each line with its own statusWho sees what#
| Who | Sees | Does not see |
|---|---|---|
| You | Every line, the total, statuses and the run history on your device | Nothing is hidden from you |
| Each recipient | Their own amount, asset, label and settlement time | Other lines, the total, the headcount, your balance |
| Public explorer | Pool transactions at certain times, their gas payer and fee | Amounts, recipients, the number of people paid, that it was payroll |
| Accountant with a proof | The run total, the number of payments, the asset and the settlement window | Any single amount, any address or name, your balance |
Recipients can still talk to each other, and anyone a recipient shows their wallet to can see that recipient's line. Privacy on chain does not stop people sharing what they know.
Scheduled runs#
A schedule repeats a list on a calendar, such as monthly on the 25th. You set the asset, the lines, the interval and the start date. On each date the app prepares the run and asks you to confirm it.
- Proofs need your keys, and your keys stay on your device. A schedule therefore cannot pay while your app is closed. It reminds you and prepares the run; you approve it.
- From code,
umbr.payroll.schedulestores the plan; a process you run (with its own key from a secret store) is what executes it. There is no UMBR server holding keys to do it for you. - Edit or cancel a schedule at any time. Past runs are not affected.
- If the private balance is short on the date, the run waits and tells you how much to shield.
Prove the total#
Open a finished run and choose Prove total. You get a payroll-total proof stating the exact total and the number of payments, for an audience you name and an expiry of 1, 7 or 30 days. The reader checks it at umbr.trade/verify and learns nothing about individual lines. The rules behind it are in Proofs: payroll total.
The proof needs the run record, which lives on the device that sent the run. Back up your keys and run history before you clear the browser.
The relayer#
Without a relayer, the wallet that submits each pool transaction pays its gas, and that wallet appears on the explorer next to every payroll transaction. That is a pattern worth hiding: the same address touching the pool on the 25th of every month says a lot.
With the relayer on, it submits the proofs and pays the gas, and a small fee comes out of the private balance. The destination and the fee are bound inside each proof, so the relayer cannot redirect funds or take more. It can refuse or delay a submission, and it sees your IP address and timing. Turn it off in Settings if you prefer to submit yourself.
If something goes wrong#
Partial runs
A large run is split into several transactions, and each settles on its own. If one fails (a dropped connection, a relayer refusal, a pool root that moved), the lines in the other transactions are already paid. The run view marks every line as paid, pending or failed.
Retry
Choose Retry failed lines. The app rebuilds proofs only for lines that did not settle. A paid line is never sent twice, because its notes are already spent and the pool rejects a reused nullifier.
Nothing is lost
A failed transaction spends nothing. The notes it tried to use remain yours and show up again after a sync. If the tab closed mid-run, reopen Payroll; the saved run picks up where it stopped.
Testnet