Documentation: all sections

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:

  1. asks the custodian how much of the real asset it holds,
  2. reads the token supply off the chain itself,
  3. compares the two against your declared backing ratio,
  4. 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.

"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

VERIFIEDMeansHoldings covered the outstanding supply at the declared ratio, within tolerance, at that moment.
DISCREPANCYMeansThey did not. The shortfall is published.
UNAVAILABLEMeansA 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:

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.

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.

pob.verifiedFires whenThe status became VERIFIED (first check, or a recovery).
pob.discrepancyFires whenHoldings did not cover the supply. This is the page-me event.
pob.unavailableFires 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

POSTPath/v1/pob/subjectsAuthSession (Member+)
GETPath/v1/pob/subjectsAuthSession (Viewer+)
GETPath/v1/pob/subjects/:idAuthSession (Viewer+)
GETPath/v1/pob/subjects/:id/checksAuthSession (Viewer+)
POSTPath/v1/pob/subjects/:id/checkAuthSession (Member+)
PATCHPath/v1/pob/subjects/:idAuthSession (Admin+)
POSTPath/v1/pob/subjects/:id/retireAuthSession (Admin+)
GETPath/v1/pob/public/:slugAuthPublic
GETPath/v1/pob/public/:slug/checksAuthPublic
GETPath/v1/pob/directoryAuthPublic

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.