Proof of Solvency methodology v1
Every day Rocky publishes a cryptographic commitment proving that custodied platform assets ≥ total user liabilities, and lets every user verify, without trusting the platform, that their own balance is fully included. This page is the complete specification of that mechanism. Once published, the methodology is versioned: any change increments the version and keeps the history.
Status (2026-09-08): live in production.
solvency-proverruns daily at 00:30 UTC and the first report (liabilities_only) was published on 2026-09-08;GET /v1/solvency/latestreturns it without login. The custody (asset) side is not yet included, see §4.
- Public report page:
/solvencyon the exchange (the Transparency entry in the top navigation is temporarily hidden; the page and route remain and can be opened directly) - Public API:
GET /v1/solvency/latest(report, no login) andGET /v1/solvency/proof/me(your own proof,Authorization: Bearer <session_token>) - Reference implementation (byte-for-byte what runs in production): rocky-backend
crates/solvency-merkle(tree and hashes) andservices/solvency-prover(snapshot and report)
1. Snapshot
Every day at 00:30 UTC (SNAPSHOT_UTC of solvency-prover, default 00:30) a single-transaction (REPEATABLE READ) consistent snapshot of the internal ledger is taken, together with the event-stream high-water mark (the id of the newest event in ledger.events), which pins the snapshot to a definite point in event history.
The per-contract prices used for unrealized PnL are read at the snapshot instant from the exchange index-price stream (oracle.index_price.>, published by oracle-poller) and written into the report verbatim (mark_prices), so anyone can recompute. If the price for any contract with open positions is missing or stale, the snapshot fails rather than publishing with an old price.
2. Liabilities
Liabilities = Σ net equity per user per asset, made up as follows:
| Item | Treatment |
|---|---|
Perp margin balances (ledger.accounts) | available + locked, counted in full |
Spot balances (ledger.spot_balances) | free + locked, counted in full |
Unrealized PnL (ledger.positions) | folded into the margin asset at the snapshot price. Omitting it would systematically understate liabilities |
| Realized PnL, funding | already reflected in balances; not double-counted |
In-flight withdrawals (pending / submitted) | counted as liabilities (assets have not left custody, the user has not received them) |
| In-flight deposits | excluded on both sides to keep the inequality symmetric |
Insurance fund (ledger.insurance_fund) | not a user liability; disclosed separately (insurance_fund), including any shortfall when negative |
House accounts (Rocky's own market-making and taker fleet, i.e. API keys on the house rate tier) | excluded from liabilities (platform equity); the number excluded and their totals are disclosed (house_excluded, house_totals). External market makers are ordinary users and stay in the liability set. |
| Negative-equity accounts | clamped to 0; the negative total is disclosed separately as bad debt (bad_debt) |
| Zero balances | omitted from the leaf; shortfalls appear only through bad debt |
3. Assets
Assets = the actual on-chain holdings of the exchange custody party on the Canton ledger, per asset, read at the same instant as liabilities. On-chain contracts belonging to in-flight deposits are excluded as well.
The custody verification module has not shipped yet: today the report's assets field is null and status is liabilities_only. Once it ships, status becomes complete, or degraded if the asset side could not be read.
4. Commitment structure: Merkle Sum Tree
- Leaf:
SHA-256("rocky-solvency-leaf-v1" ‖ salt ‖ SHA-256(user_id) ‖ canonical(balances))saltis a per-user 32-byte salt:HMAC-SHA256(master_salt, user_id). The master salt is theSOLVENCY_MASTER_SALTsetting ofsolvency-prover(hex-encoded, at least 16 bytes, rotated by operations, not derived per day) and is revealed only to you, inside your own proof;canonical(balances)is the list ofASSET:amountpairs sorted bytewise by asset name, amounts fixed at 18 decimals, joined by|, e.g.CC:12.000000000000000000|CUSD:3.500000000000000000.
- Internal node:
SHA-256("rocky-solvency-node-v1" ‖ left_hash ‖ right_hash ‖ canonical(sums)), wheresumsis the per-asset sum of the two subtrees. The root's sums are the published liability totals, so a verifier can confirm the published totals really aggregate every leaf; the platform cannot leave a user out. - An odd node is promoted to the next level unchanged (never duplicated, which rules out double counting); any sum overflow is an error; non-negative leaf amounts are the structural safety precondition (see the clamp in §2).
5. API
5.1 GET /v1/solvency/latest
Returns the report for the most recent snapshot:
| Field | Description |
|---|---|
report_id, snapshot_at | Report id and snapshot time (UTC) |
event_high_water | Event-stream high-water mark at the snapshot |
root_hash | Merkle Sum Tree root hash (hex) |
liabilities | The root's sums: total liabilities per asset |
assets | Custody party on-chain holdings; null until custody verification ships |
mark_prices | Per-contract prices used by the snapshot |
insurance_fund, bad_debt, house_excluded, house_totals | Disclosures from §2 |
user_count | Number of leaves |
status | liabilities_only / complete / degraded |
Returns 404 while no report exists.
5.2 GET /v1/solvency/proof/me
Returns the inclusion proof of the signed-in user in the latest report:
| Field | Description |
|---|---|
report_id, snapshot_at, root_hash | The report the proof belongs to |
leaf.user_id, leaf.leaf_index | Your leaf position |
leaf.balances | Your net equity per asset at the snapshot |
leaf.salt | Your derived salt (hex) |
leaf.leaf_hash | The leaf hash computed by the server (hex) |
path[] | Sibling nodes from leaf to root: sibling_hash, sibling_sums, sibling_on_left |
6. Verify it yourself
Sign in, open /solvency and click Verify my balance. The browser performs three steps locally (see src/shared/lib/solvency/verify.ts in the frontend; nothing computed is sent to the server):
- Recompute your leaf hash from the disclosed salt and balances and compare it with the leaf hash the server returned;
- Combine along the sibling path level by level and compare with the published Merkle root;
- If the public report and the proof belong to the same snapshot, compare the liability totals derived from the path with the published totals.
All three passing means your balance is included faithfully and counted in the published totals. Any tampering (altering your balance, omitting you, overstating the totals) makes at least one step fail.
7. Known limits
- The asset side is the custody party's on-chain holdings and excludes off-chain assets (Rocky is fully on-chain today, so this does not apply); until the custody verification module ships the report covers the liability side only.
- Unrealized PnL depends on the index prices the platform publishes;
oracle-pollersupports Chainlink Data Streams as a price source (PRICE_SOURCE=chainlink), and once enabled the prices themselves become third-party verifiable. - Snapshots are daily; nothing is committed in between, so intraday coverage swings in extreme markets are not captured by that day's report.
- "No user was omitted" relies on users spot-checking their own proofs (the classic PoR limitation); the verification entry stays on the page to raise the spot-check rate.
8. Versions
| Version | Date | Change |
|---|---|---|
| v1 | 2026-08 | Initial release |