Asset attestation
Sable Attestation never says "trust us". It says: here is what we checked, how we checked it, when we checked it, and what the evidence shows.
You give it an asset identifier. It reads the sources it can reach, decides a status for each claim, and stores a signed, versioned record carrying the sha256 of every byte it read. Anyone can re-run the calls and check the signature without a Sable account.
What "verified" means here, and what it does not
This is the most important section on the page, so it is first.
VERIFIED means: Sable performed the listed checks, by the listed methods, at
the listed time, and they agreed. Nothing more.
It does not mean the asset is sound, solvent, legally issued, correctly described, or a good idea. Sable is not an auditor. This is not an audit, not a rating, and never investment advice. Every record carries that sentence in its own signed payload, so it cannot be separated from the claim it qualifies.
Three rules follow from that, and they are enforced in code, not by convention:
- Nothing is invented. A source that is not configured or cannot be reached
yields
UNAVAILABLEfor the claims that depend on it. There is no fallback value, no last-known figure presented as current, and no default market-price vendor assumed into existence. - An issuer's word is never a check. Anything an issuer declared — backing
value, yield, maturity — is recorded as
UNVERIFIED, with the issuer named as the source. A declared backing figure is never proof of reserves. - Payment cannot move a status. A check costs the same whatever it finds; a
DISCREPANCYcosts exactly what aVERIFIEDcosts. Reading a record, its history and its evidence is free, so nobody ever has a financial reason to leave a discrepancy unread.
The six statuses
| Status | What it means |
|---|---|
VERIFIED | The listed checks ran by the listed methods and agreed, inside the freshness window. |
PARTIALLY_VERIFIED | Some claims verified, at least one did not. Read the per-claim statuses; this headline is not a summary of the weakest one. |
UNVERIFIED | Nothing independent supports this. Usually an issuer's own statement, recorded as a statement. |
STALE | Checked, but outside its freshness window. Shown labelled as old — never re-presented as current. |
DISCREPANCY | Two sources that should agree do not, beyond the stated tolerance. |
UNAVAILABLE | A required source was unreachable or is not configured. No figure is published in this state. |
A DISCREPANCY in any claim becomes the headline status of the whole record,
whatever else passed. That is deliberate: a disagreement is the one thing that
must never be averaged away.
Identifiers
| Form | Example |
|---|---|
| EVM token | evm:1:0xdac17f958d2ee523a2206206994597c13d831ec7 |
| Solana mint | solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v |
| Sable Vault asset | vault:va_0123456789abcdef |
| Bare address | 0xdac17f… together with chain_id |
ISIN, CUSIP and FIGI identifiers are refused, with an explanation. Sable has
no data source for them on this deployment, and answering with a page of
UNAVAILABLE claims after charging for the check would be worse than an honest
error.
On-chain checks run against the RPCs the operator configured for payments. An
asset on a chain this deployment has no RPC for is not refused — every on-chain
claim comes back UNAVAILABLE, naming the missing chain, because "we cannot
check this here" is itself something you need to know.
Running a check
curl -s https://api.buildsable.com/v1/attestations/check \
-H "authorization: Bearer $SABLE_KEY" \
-H 'content-type: application/json' \
-d '{
"asset": "evm:1:0xdac17f958d2ee523a2206206994597c13d831ec7",
"expected_supply": "88306226684053103",
"tolerance_bps": 0
}'
| Field | Meaning |
|---|---|
asset | The identifier, in one of the forms above. Required. |
chain_id | Required only when asset is a bare 0x address. |
expected_supply | An issuer-declared supply in raw base units, reconciled against the chain. |
tolerance_bps | Reconciliation tolerance, 0–10000. Defaults to 0 — exact. |
freshness_secs | How long this record stays current, 60 s to 30 days. |
vault_asset_id | Link a Sable Vault asset so the issuer's declared facts join the record. Issuer-only. |
What gets checked
| Claim | How |
|---|---|
identity | eth_getCode (is there a contract at all?) plus symbol(), name(), decimals(). An address with no code is a DISCREPANCY, not a pass. |
supply | totalSupply(), decoded as an exact uint256 — a supply too large for any Rust primitive is still exact, because it is carried as a decimal string. |
admin_control | owner() and the EIP-1967 admin storage slot. |
upgradeability | The EIP-1967 implementation slot: is this a proxy, and to what? |
pause_state | paused(), where the contract exposes it. |
token_authorities | Solana: mintAuthority and freezeAuthority — live, or revoked. |
supply_reconciliation | Exact integer comparison of a declared supply against the chain. |
issuer, declared_backing, yield, maturity | Read from a linked Vault entry. Always UNVERIFIED: they are the issuer's own word. |
custody | Answered on every record, and the honest answer today is UNAVAILABLE — no custodian feed is configured. |
Every claim carries its own limitations list saying what it does not
establish. Some of those are the point:
- A null
owner()means the standard getter is absent, not that the contract is ownerless — privileged roles can live in access-control mappings this check does not enumerate. - An empty EIP-1967 slot means this is not an EIP-1967 proxy. It does not mean the contract is immutable.
- The absence of a
paused()getter is not evidence that transfers cannot be halted.
Reconciliation, and why rounding cannot hide anything
Where a declared figure can be compared against an independent one, the comparison is exact integer cross-multiplication:
|reported − expected| × 10000 ≤ |expected| × tolerance_bps
No division, so no rounding can widen the tolerance. A tolerance of 0 means
exactly equal — a difference of one unit against a quintillion is a
DISCREPANCY, which is the whole point. The deviation shown on the record is a
display value — the one figure here that division produces — and it is rounded
up, so a displayed deviation is never smaller than the real one.
An expected of zero against a non-zero reported is a DISCREPANCY at every
tolerance: a percentage of zero is undefined, and "we expected none and found
some" is a disagreement, not a pass.
Freshness
Every claim carries verified_at, a window, and the stale_after timestamp
derived from them. Past that timestamp a read reports STALE — the record is
not rewritten, it is labelled. status is what is true now; status_at_check
is what was true when the check ran. Both are returned.
A DISCREPANCY does not decay into STALE as it ages. Ageing never downgrades a
disagreement into something milder.
Evidence
curl -s "https://api.buildsable.com/v1/attestations/$ASSET_ID/evidence" \
-H "authorization: Bearer $SABLE_KEY"
Each entry names the source, when it was read, and the sha256 of the exact bytes
that came back. Pass ?include_body=true to get the bytes themselves.
The bytes are stored encrypted at rest (AES-GCM, the same envelope discipline as the rest of Sable) and are never logged. They are opened only to answer the owner's own read. The store is content-addressed, so two records that read the same chain state share one entry — which is what makes "is this the same reading you showed me before?" answerable by hash equality.
Versioning
Records are versioned, never overwritten. A re-check writes a new row and marks the old one superseded; there is no update path that changes a status, a claim, or an evidence reference after the fact.
curl -s "https://api.buildsable.com/v1/attestations/$ASSET_ID/history" \
-H "authorization: Bearer $SABLE_KEY"
An old record is returned exactly as it was signed, so "what did Sable say last Tuesday, and does that record still verify?" always has an answer.
The public view
GET /v1/assets/{assetId}/verification
No account, no key, no attribution. It serves the newest record whose every source is publicly re-readable on-chain, and shows neither who ran the check nor how many times.
A record that draws on an issuer declaration or sealed Vault data has no public view at all and returns 404. That is decided when the record is written, not by stripping fields on the way out.
The rendered version lives at /attest/{assetId} — it verifies the signature
live on load and prints the exact RPC call behind every claim.
Webhooks
| Event | When |
|---|---|
attestation.verified | The headline became VERIFIED — first time, or a recovery. An unchanged verified record is silent. |
attestation.discrepancy | A claim disagrees with what it was reconciled against. |
attestation.stale | The record this check replaced had already passed its freshness window. |
attestation.contract_changed | Contract state moved: an implementation upgrade, an ownership change, a pause flipping, an authority appearing. |
attestation.source_changed | The set of sources behind a claim moved — a feed appeared, or stopped answering. |
Payloads are content-free: ids, the status and the version. The claims and their evidence stay behind your own authenticated read.
Verifying a record
The record is signed with the same key that signs every Sable receipt, so it verifies through the endpoint you already use:
curl -s https://api.buildsable.com/v1/receipts/verify \
-H 'content-type: application/json' \
-d '{"receipt":"<record>","signature":"<signature>"}'
Then check the recovered address against the gateway's published signing key at
GET /v1/receipts/pubkey. And re-read the sources: every claim names the exact
call behind it, so you can run them against any node and compare.
Not built
Stated plainly, so nobody assumes otherwise:
- Continuous monitoring. Checks run when you ask for one. Nothing re-checks an asset on a schedule and bills you for it in the background.
- Market prices and NAV. The price source is configuration-gated and absent
by default; where it is unconfigured, the
navclaim isUNAVAILABLEand says so. Sable ships no default price vendor. - Custody verification. No custodian feed is configured anywhere yet, so
custodyisUNAVAILABLEon every record. - Securities identifiers. No ISIN/CUSIP/FIGI data source.
- Mint and burn event history. Not scanned in this version.