Build
Selective disclosure
Prove one fact about your private money to one reader, for a limited time, without opening the rest of your history.
- Proof kinds
- 4
- Made
- On the caller's machine
- Expiry
- 1, 7 or 30 days
- Checked at
- /verify or verifyProof
Why it matters#
Private money still has to answer questions. A landlord wants proof of funds. A supplier wants to see that an invoice was paid. An accountant needs this month's payroll total. On a public chain, answering any of these usually means sharing an address, and an address shows every balance and every payment it has ever made.
Selective disclosure answers the question that was asked and nothing else. The reader gets one statement they can check for themselves, and your history stays where it was.
What a disclosure says#
Every disclosure carries the same parts, and the reader sees all of them:
| Part | Example | Notes |
|---|---|---|
| Claim | balance-at-least | One of exact balance, at least, payment, payroll total |
| Statement | Holds at least 6,000.00 USDG | The single fact being proved |
| Audience | Northwind Leasing | Free text you choose: a name, a company, an email |
| Expiry | 7 days after creation | 1, 7 or 30 days |
| Network | Robinhood Chain testnet | The pool the proof was checked against |
It does not contain your public wallet, your UMBR address, your other balances or any other payment. For what each kind shows and hides, see Proofs.
How it works#
You pick a claim
Choose the kind of proof, the amount or floor, the audience and the expiry.The SDK selects notes locally
It reads your synced notes and picks the ones that support the claim. They never leave the machine.A zero-knowledge proof is made
The proof shows the notes exist in the pool's Merkle tree and satisfy the claim. Audience and expiry are inputs, so they cannot be edited afterwards.You share the result
A link or a JSON file. Nothing is posted on chain and no UMBR server stores it.The reader verifies
The proof is checked against the published verification key, and its Merkle root is checked against roots the pool has recorded.
Create a disclosure#
Sync first so the proof uses your current notes, then call umbr.prove with the claim you need:
import { createUmbrClient, networks } from "@umbr/sdk";
const umbr = createUmbrClient({ network: networks.robinhoodTestnet, wallet });
await umbr.notes.sync(); // prove from up-to-date notes
// Proof of funds for a lease application.
const funds = await umbr.prove({
claim: "balance-at-least",
asset: "USDG",
amount: "6000.00",
audience: "Northwind Leasing",
expiresIn: "7d",
});
// A single payment, revealing only a floor.
const paid = await umbr.prove({
claim: "payment",
payment: paymentId,
reveal: "at-least",
amount: "300.00",
audience: "billing@vendor.example",
expiresIn: "30d",
});
// The total of one payroll run, for the books.
const total = await umbr.prove({
claim: "payroll-total",
run: runId,
audience: "accounting@yourco.example",
expiresIn: "30d",
});An at-least proof can draw on up to four notes in the same asset. If your balance is spread across more, send to yourself to merge notes first, or prove a lower floor. A payroll-total proof works only for runs you sent.
Share it#
prove returns a Proof in three forms of the same thing. Use link for people, json for systems, and text when you need one line you can paste anywhere.
The file format
A disclosure is a JSON object whose format field is umbr.disclosure/1. It names the network (chain id) and the pool, the kind of claim (funds, balance, payment or payroll), the asset and amount (plus a count for payroll), the audience, issue and expiry times, the public inputs and a Groth16 proof on BN254. The one-line form is the same JSON, base64url-encoded, behind the prefix umbrproof1:. The SDK and the verify page accept either.
// A link: easiest for a person. Opens umbr.trade/verify with the proof in it.
await sendEmail({ to: "leasing@northwind.example", body: funds.link });
// JSON: easiest for a system. Attach it or post it to their API.
await fetch("https://intake.example/proofs", {
method: "POST",
headers: { "content-type": "application/json" },
body: funds.json,
});Choose the audience and expiry with care
- Make the audience specific. "Northwind Leasing, unit 4B application" says more than "landlord" if the proof ever turns up somewhere else.
- Use the shortest expiry that fits. A proof is a snapshot; a balance can change the next minute. One day is right for most proof-of-funds checks; thirty days suits an audit window.
- Treat a proof like a signed letter. Anyone you forward it to can read it.
Why both fields matter is covered in Made for one person.
Verify a disclosure#
A reader needs no account and no wallet. A person can paste the link or the file at umbr.trade/verify. A system can call verifyProof, which is exported on its own so you do not need a client:
import { verifyProof } from "@umbr/sdk";
export async function acceptProofOfFunds(input: string, minimum: number) {
const r = await verifyProof(input); // a link or the JSON
if (!r.valid) return { ok: false, reason: "invalid proof" };
if (r.expired) return { ok: false, reason: "proof expired" };
if (r.claim !== "balance-at-least") return { ok: false, reason: "wrong kind of proof" };
if (r.audience !== "Northwind Leasing") return { ok: false, reason: "made for someone else" };
return { ok: true, statement: r.statement, expiresAt: r.expiresAt };
}What the verify page checks today
Check every field you rely on: validity, expiry, the kind of claim and the audience. An invalid proof returns valid: false rather than throwing. For a payroll total, the reader also gets the number of payments; see Payroll total for what the checker rejects so a total cannot be padded.
Guarantees and limits#
What a valid proof does guarantee
- The statement was true for notes in the pool when the proof was made.
- The audience and expiry are the ones chosen by the person who made it.
- Nothing beyond the statement is revealed by the proof itself.
What it does not
- It does not say the funds are still there. Expiry bounds how stale a proof can be, nothing more.
- Audience is a label, not access control. A forwarded proof still verifies; it just shows who it was for.
- A payment proof shows a payment happened inside the pool, not what it was for.
- Soundness rests on the circuits and their setup. UMBR runs on testnets today; read the threat model and the production gate.
Revealing more than you meant