How a Proof of Reserves Audit Works
See how bi-temporal corrections work
Clone Formance Ledger on GitHub, run it locally, and post a transaction to see how bi-temporal corrections work.
See how bi-temporal corrections work
Clone Formance Ledger on GitHub, run it locally, and post a transaction to see how bi-temporal corrections work.
Stablecoin issuers, custodians, exchanges, and investing platforms that hold customer money in one shared bank account, called an omnibus account, must commission proof of reserves audits to show that the money on hand covers what they owe customers at a single point in time.
The engagement letter lands and asks for every customer balance as of midnight Coordinated Universal Time (UTC) on the last day of the month. You open the live ledger, and eleven customer accounts are negative, fee and holding accounts carry balances, deposits are pending confirmation, and withdrawals are queued. No one from the financial team has written down which of those belong in the total owed to customers.
This guide walks through what a proof of reserves audit actually is, how to pick the right auditor firm, the five steps the audit runs through, the ledger properties that make those steps possible, and a checklist to run before the audit.
A proof of reserves audit is a point-in-time check that shows an asset custodian’s digital asset holdings and cash reserves cover what customers are owed.
In practice, most audits only confirm that certain assets existed at a single moment. The audits rarely check the other side of the ledger, including whether every customer balance was counted, whether the assets were borrowed, or whether they were still there the next day.
An audit shows that assets were present, but it doesn't show that customers are covered.
Selecting a proof of reserves auditor means picking one of three firms: registered public accounting firms, independent CPAs applying the AICPA rulebook, or specialist firms with on-chain tooling, then screening the chosen firm against four decision criteria.
| Tier | When required | What it proves |
| Registered public accounting firm | Regulator names the tier (e.g., payment stablecoin issuers) | Monthly reserve report examined under PCAOB-registered standards with CEO/CFO certification |
| Independent CPA applying AICPA standards | Regulator allows any qualified CPA (most non-bank custodians and exchanges) | Point-in-time that reserves matched declared liabilities at the cut-off |
| Specialist firm with on-chain tooling | Exchanges and non-issuer custodians outside the registered tier | Cryptographic customer-balance proofs that let each customer verify inclusion in the total |
Once you've identified which tier applies, evaluate four decision criteria when selecting the firm within that tier:
Whichever firm you choose, it audits what it's given. A programmable core ledger like Formance can rebuild the same customer-balance list on demand, which supports how a proof of reserves audit is run.
A proof of reserves audit runs in five steps: freeze the liability snapshot, commit to the liability set, prove control of reserves, reconcile internal ledger balances, and compare reserve totals to liabilities and issue the report.
Lock the customer-balance list by pinning every balance to a ledger timestamp. "As of midnight UTC" means every ledger entry effective at or before that instant and none after it, no matter when the export runs or when late entries land.
Append-only immutability ledgers let you rebuild the same snapshot on demand. If entries can be edited or deleted, the snapshot the firm inspected and the one you rebuild next week diverge. Neither can be called the record.
The freeze also catches gaming around review dates the firm announced in advance. If a platform knows when the check is coming, it can borrow money, park it in the account just long enough to look good, then send it back after.
Only random, surprise checks give a true picture of what the platform holds on a normal day.
Take the frozen list of customer balances and run it through a Merkle sum tree. A Merkle sum tree pairs and sums balances up to a single root that holds the customer total plus a fingerprint of the whole list. Think of it as a tournament bracket where each parent adds its children's balances.
The bracket keeps going up until you reach one line at the top that holds the total owed to all customers, plus a unique code that acts like a fingerprint for the whole list. The firm publishes that fingerprint and the total. Any customer can then check that their own balance was counted by following the short path from their line up to the top and making sure the numbers match.
The total still has to be double-checked because the first version of this design had a hole. A platform that runs multiple customer accounts can build a proof that only shows one balance while hiding the rest.
Funds can also sneak in a fake account with a negative balance to make the total look smaller than it really is, and no real customer would notice.
Prove you control the money by signing a message with the private key of each reserve wallet at or near the cut-off, and by getting balance confirmations straight from the bank or custodian holding the cash. The firm goes to the bank or custodian directly instead of trusting a statement the platform exported itself.
A valid signature proves someone with the key signed at that moment. But copies of a key produce the same signature, so a signature cannot prove the key is held in only one place.
A signature also says nothing about hidden claims on the balance, like loans taken out against the wallet or funds borrowed for a day and returned. A bank confirmation proves money sat in an account under the platform's name, but not whose money it really is.
Match every internal reserve account to a specific custodian sub-account or bank account so the two balances line up at cut-off once in-transit items clear. Balance-level account reconciliation totals your reserve accounts by asset and compares that total to the confirmed outside balance for the same asset.
A dated in-transit item with the matching bank or custodian record attached explains every missing or miscalculated figure.
For each asset, compare the Merkle root total to the signed and confirmed reserves, then issue a report on whether reserves met or exceeded what was owed to customers.
The report lists every check the firm ran and shows the root value, the reserve figures, and the ratio between them. If reserves fall short, the report shows the shortfall per asset and the ratio drops below one, signaling that a counterparty or regulator should step in.
The report does not confirm that it included every customer account, and it does not confirm that reserves are free of hidden claims.
The scope you set in Step 1 is used as-is, and whether Step 1 holds up depends on the ledger properties behind it.
A ledger needs five properties in place well before the firm starts the proof of reserves audit. It has to enforce cut-offs with bi-temporality, commit reserve and supply moves atomically, attribute every omnibus unit to an owner, classify non-customer accounts, and keep an append-only, hash-chained log.
You can't enforce these ledger properties at the last minute, because each one shapes how you record transactions from the start.
Bi-temporality tracks two times for every entry: when it was recorded in the ledger, and when it actually took effect in the real world.
A correction posted three days later with an effective date inside the reporting period does not rewrite the snapshot the firm already inspected, because the bi-temporal query pins recorded time and effective time.
With one time axis only, you choose between a snapshot that changes on every re-run and one that drops late-arriving facts.
Reserve growth, supply growth, and customer credit have to move in a single all-or-nothing transaction, so the entries cannot be applied separately. Any ledger that supports all-or-nothing multi-entry transactions meets the requirement.
The example below uses the Numscript transaction language, Formance's purpose-built language for money, as one implementation.
It models a USDC mint backed by a matching cash deposit across four accounts: the external cash boundary, the platform reserve, a circulating-supply tracker, and the receiving customer wallet.
// STABLECOIN_MINT
send [USD/2 250000000] (
source = @counterparties:banks:reserveCustodian allowing unbounded overdraft
destination = @platform:reserves:main
)
send [USDC/6 2500000000000] (
source = @world
destination = @supply:usdc:circulating
)
send [USDC/6 2500000000000] (
source = @world
destination = @customers:001:wallet
)The reserve, the supply count, and the customer's balance all update together, or nothing changes. The ledger can never show a customer with money that isn't backed by a matching reserve.
At any point in time, a single check confirms customer balances add up to the total supply, which equals the cash held in reserve.
Attribution gives every unit in the omnibus account a named owner in the ledger. Without this, the omnibus account is just one big pool of money with no way to tie a specific balance back to a specific customer, which is exactly what the per-customer breakdown that the audit needs to reconstruct.
Negative customer balances are recorded as money owed to the platform in a separate account and never subtracted from the customer total. Keeping them separate prevents a customer overdraft from reducing the platform's reported total liability.
Assets Coloring, a native Formance feature, handles the reserve side by tagging units of the same asset by which custodian or restriction they belong to. A USD balance at one bank and a restricted USD balance at another stay separate in the snapshot without needing a new asset type.
Classify every non-customer account in writing before the firm asks, because the audit will follow whatever scope you handed over.
Fee, holding, and inter-company accounts are excluded from customer liabilities and included in total liabilities. The account-classification policy belongs in the documentation shared with the firm before the audit starts.
If a fee account gets lumped in with customer balances by mistake, the total owed to customers looks bigger than it is and the reserve ratio comes out wrong.
Classification lives inside the ledger. Step 4 of the audit procedure handles the outside matching to bank and custodian statements.
An append-only, hash-chained log lets the firm rebuild the exact ledger state it inspected.
Fix mistakes by adding a new offsetting entry, never by editing the old one. Each entry carries its own hash plus the previous entry's hash, so any silent change breaks the chain and shows up the next time the firm re-runs the log.
If a firm asks in March why the January total moved, the answer is a specific offsetting entry with its own dates and hash.
With all five ledger properties in place, the audit itself comes down to a short pre-arrival checklist.
Complete these six actions before the firm starts the proof of reserves audit:
On Formance, the reserve-and-supply entries commit atomically. The immutable, bi-temporal log makes point-in-time snapshots reproducible against the record itself. A customer-balance snapshot becomes a query over the system of record at an effective timestamp. That query runs against the same log the firm will inspect.