Portal
Documentation: all sections

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:

  1. Nothing is invented. A source that is not configured or cannot be reached yields UNAVAILABLE for 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.
  2. 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.
  3. Payment cannot move a status. A check costs the same whatever it finds; a DISCREPANCY costs exactly what a VERIFIED costs. 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

StatusWhat it means
VERIFIEDThe listed checks ran by the listed methods and agreed, inside the freshness window.
PARTIALLY_VERIFIEDSome claims verified, at least one did not. Read the per-claim statuses; this headline is not a summary of the weakest one.
UNVERIFIEDNothing independent supports this. Usually an issuer's own statement, recorded as a statement.
STALEChecked, but outside its freshness window. Shown labelled as old — never re-presented as current.
DISCREPANCYTwo sources that should agree do not, beyond the stated tolerance.
UNAVAILABLEA 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

FormExample
EVM tokenevm:1:0xdac17f958d2ee523a2206206994597c13d831ec7
Solana mintsolana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
Sable Vault assetvault:va_0123456789abcdef
Bare address0xdac17f… 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
      }'
FieldMeaning
assetThe identifier, in one of the forms above. Required.
chain_idRequired only when asset is a bare 0x address.
expected_supplyAn issuer-declared supply in raw base units, reconciled against the chain.
tolerance_bpsReconciliation tolerance, 010000. Defaults to 0 — exact.
freshness_secsHow long this record stays current, 60 s to 30 days.
vault_asset_idLink a Sable Vault asset so the issuer's declared facts join the record. Issuer-only.

What gets checked

ClaimHow
identityeth_getCode (is there a contract at all?) plus symbol(), name(), decimals(). An address with no code is a DISCREPANCY, not a pass.
supplytotalSupply(), 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_controlowner() and the EIP-1967 admin storage slot.
upgradeabilityThe EIP-1967 implementation slot: is this a proxy, and to what?
pause_statepaused(), where the contract exposes it.
token_authoritiesSolana: mintAuthority and freezeAuthority — live, or revoked.
supply_reconciliationExact integer comparison of a declared supply against the chain.
issuer, declared_backing, yield, maturityRead from a linked Vault entry. Always UNVERIFIED: they are the issuer's own word.
custodyAnswered 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:

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 STALEthe 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

EventWhen
attestation.verifiedThe headline became VERIFIED — first time, or a recovery. An unchanged verified record is silent.
attestation.discrepancyA claim disagrees with what it was reconciled against.
attestation.staleThe record this check replaced had already passed its freshness window.
attestation.contract_changedContract state moved: an implementation upgrade, an ownership change, a pause flipping, an authority appearing.
attestation.source_changedThe 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: