Proof of Backing
When a company turns something real — shares, gold, property — into a token, you are trusting that the real thing is sitting somewhere. Today that trust rests on an attestation letter published a few times a year. Between two letters, nobody can check anything.
The gap is structural rather than criminal: audits are periodic and the world is continuous. An honest issuer is in exactly the same position as a dishonest one — they have nothing to show.
Proof of Backing closes the interval. On a schedule you set, Sable:
- asks the custodian how much of the real asset it holds,
- reads the token supply off the chain itself,
- compares the two against your declared backing ratio,
- signs the result and appends it to a hash chain that is anchored publicly.
Anyone can read the result. Anyone can check the signature. Nobody — including you, the issuer paying for it — can change a published result or make one disappear.
Why both sides are read by Sable
This is the one thing that separates Proof of Backing from Sable Attestation, which also reads a chain.
There, the backing figure is the issuer's own declaration. It enters the
record as UNVERIFIED, honestly, because nobody independent confirmed it.
Here, Sable reads the custodian itself. The two sides of the comparison come from two different parties, and neither of them is the party being checked.
What a check proves
Sable independently read two sources at the stated time and reports what each said: the named custodian's reported holdings, and the token supply read from the chain.
That sentence is not marketing copy. It is the trust_model field, and it is
inside every signed payload — so a reader who never opens this page still
meets the limits when they verify the receipt.
What a check does not prove
This section matters more than the one above it.
- It is not an audit. Sable is not an auditor and does not certify that an asset is sound, solvent, or a good investment.
- It does not prove the custodian is honest. A custodian that misreports to
Sable produces a
VERIFIEDcheck over a false figure. What the product removes is the blind interval, not the need for a trustworthy custodian. - It does not prove the assets are unencumbered or segregated. It proves a quantity was reported, not that it is free of claims.
- Sable does not hold or custody anything. The vault is the custodian's.
- A listing is not an endorsement. A subject whose last check was a
DISCREPANCYappears in the public directory exactly like one that passed.
"Continuously" means continuously checked
Not continuously guaranteed. If a source cannot be read, the honest output is
UNAVAILABLE — and no figure at all is published for that check.
This is the single most important behaviour in the system, so it is worth saying plainly what the alternative looks like. The common way a reserve dashboard lies, without anyone intending it to, is this: the custodian times out, the page keeps showing last hour's number, and "we could not check" is rendered identically to "the backing is fine."
Sable cannot do that. The function that decides a verdict takes both readings
as arguments and has no access to storage — there is no code path by which a
previous reading can reach a current answer. A missing figure is null in the
API, an em dash on the page, and an absent field in the signed receipt. Never a
zero, and never last time's number.
The three states
| Status | Means |
|---|---|
VERIFIED | MeansHoldings covered the outstanding supply at the declared ratio, within tolerance, at that moment. |
DISCREPANCY | MeansThey did not. The shortfall is published. |
UNAVAILABLE | MeansA source could not be read, so no figure is published. Distinct from a shortfall on purpose. |
UNAVAILABLE and DISCREPANCY are separate webhook events for the same
reason: "we could not check" and "the backing is short" are different facts and
must never arrive as the same alert.
Tolerance is asymmetric
tolerance_bps bounds a shortfall only. Holding more of the asset than
the supply requires is never a failure — the question is whether the asset is
there, and more of it is not "not there."
A tolerance of 0, the default, means the custodian must cover the supply
exactly or better. This is deliberately not the usual two-sided band, and the
API says so in the response rather than leaving you to assume.
Two rounding rules follow from the same principle, and both run against the issuer:
- the requirement ceils — rounding the obligation down would let an issuer sit a fraction short and still read as fully backed;
- coverage floors — the published percentage never overstates how backed the token is.
Nobody can pay to change a result
Every check carries a position (seq), the previous check's hash
(prev_hash), and a hash that folds both.
- Editing a published figure breaks that check's own hash.
- Deleting an inconvenient check breaks the link of the one that followed it, so the hole is detected rather than silently closing.
- Re-signing a doctored check does not help either: its successor still points at the original hash, so the break just moves one link along.
There is no endpoint — at any role, for the issuer who pays or for an operator
— that edits or deletes a published check. POST /v1/pob/subjects/:id/retire
stops future checks and withdraws none of the past ones; the public page
keeps serving the history and says the record is retired, because "this record
stopped being maintained on this date" is information a holder needs and a
deletion would destroy.
The backing rule itself is immutable for the same reason. units_per_token and
tolerance_bps cannot be changed after the subject exists, because every
published check was judged under them, and silently re-basing the rule would
retroactively change what an already-signed verdict meant. To change the terms,
register a new subject — the old chain stays exactly as it was.
Registering a subject
curl -X POST https://api.buildsable.com/v1/pob/subjects \
-H "authorization: Bearer $SABLE_SESSION" \
-H 'content-type: application/json' \
-d '{
"slug": "acme-tokenized-aapl",
"name": "Acme Tokenized AAPL",
"asset": "evm:1:0xYourTokenAddress",
"token_decimals": 18,
"unit_label": "AAPL shares",
"custodian_name": "Acme Custody Ltd",
"custodian_url": "https://custody.example.com/holdings",
"custodian_auth": "Bearer …",
"custodian_pointer": "/data/total_shares",
"custodian_decimals": 2,
"units_per_token": "1",
"tolerance_bps": 0,
"interval_secs": 3600
}'
The custodian endpoint must be a public HTTPS URL returning JSON. Sable GETs
it and reads one figure at custodian_pointer, an RFC 6901 JSON pointer.
The endpoint and its credential are AES-GCM-sealed at rest and are never returned by any endpoint, to anyone — including you. A credential is write-only once set; to change it, send a new one. The URL is validated against private, loopback and link-local ranges at registration and again at every dial, and the client follows no redirects.
The custodian's response body is never stored at all. Only its sha256 and the single figure parsed out of it are kept, so a reader can prove two checks saw the same reading without Sable keeping the reading.
Reading the record
The record is public. A backing proof only the issuer can read is a private reassurance, not a proof.
curl https://api.buildsable.com/v1/pob/public/acme-tokenized-aapl
{
"latest": {
"seq": 412,
"status": "VERIFIED",
"checked_at": "2026-09-20T16:00:00Z",
"custodian": { "ref": "Acme Custody Ltd", "ok": true, "units": "412000", "sha256": "e5f3…" },
"chain": { "ref": "evm:1:0x…", "ok": true, "token_supply": "412000", "sha256": "c963…" },
"required_units": "412000",
"coverage_bps": 10000,
"coverage_pct": "100.00%",
"prev_hash": "af48…",
"hash": "a6ee…",
"canonical": "sable-pob/1|af48…|pob_…|412|2026-09-20T16:00:00Z|VERIFIED|412000000000|…",
"receipt": "eyJ2Ijox…",
"signature": "0x8bc8…"
}
}
Human-readable at /backing/acme-tokenized-aapl, with the
signature, the hash and the chain linkage all re-checked in your browser.
Checking it yourself
Two independent checks, neither of which trusts Sable's word.
1. The hash. Every check publishes the exact canonical line its hash was
computed over, so you never have to guess the layout:
printf '%s' "$CANONICAL" | shasum -a 256
# expect the check's `hash`
2. The signature, through the public endpoint — no account needed:
curl -s https://api.buildsable.com/v1/receipts/verify \
-H 'content-type: application/json' \
-d "{\"receipt\":\"$RECEIPT\",\"signature\":\"$SIGNATURE\"}"
Walk the whole chain from /v1/pob/public/:slug/checks, folding each check's
prev_hash into the next. Every check ever taken is there, including the failed
ones and the ones that could not be taken.
Check hashes are also batched into a public anchor
(sable-pob:v1:<root>) on the same pass as every other Sable stream, where a
deployment has anchoring configured — so the record is timestamped by something
Sable does not control. Every check anchors, whatever it found: a stream that
anchored only healthy checks would be a public record with the bad news
filtered out.
Alerts
Webhook events fire on a status transition, not on every check — a subject checked hourly and healthy would otherwise deliver 24 identical webhooks a day and train you to ignore them. The first check always announces itself.
| Event | Fires when |
|---|---|
pob.verified | Fires whenThe status became VERIFIED (first check, or a recovery). |
pob.discrepancy | Fires whenHoldings did not cover the supply. This is the page-me event. |
pob.unavailable | Fires whenA source could not be read, so no figure was published. |
Payloads are content-free: ids, the public labels, the status and the coverage figure that is already on the public page. Never the custodian endpoint or its credential.
Endpoints
| Method | Path | Auth |
|---|---|---|
POST | Path/v1/pob/subjects | AuthSession (Member+) |
GET | Path/v1/pob/subjects | AuthSession (Viewer+) |
GET | Path/v1/pob/subjects/:id | AuthSession (Viewer+) |
GET | Path/v1/pob/subjects/:id/checks | AuthSession (Viewer+) |
POST | Path/v1/pob/subjects/:id/check | AuthSession (Member+) |
PATCH | Path/v1/pob/subjects/:id | AuthSession (Admin+) |
POST | Path/v1/pob/subjects/:id/retire | AuthSession (Admin+) |
GET | Path/v1/pob/public/:slug | AuthPublic |
GET | Path/v1/pob/public/:slug/checks | AuthPublic |
GET | Path/v1/pob/directory | AuthPublic |
PATCH accepts operational fields only: the custodian endpoint, its
credential, the pointer, the name and the interval. The backing rule and the
asset are not among them.