Verifiable receipts
Every chat completion and embedding comes with a signed receipt: a small, metadata-only attestation that Sable served the request, on which machine, in which region, under which tier, signed with secp256k1 so anyone can verify it without trusting us. The receipt proves that inference happened and how; it never carries the prompt or completion.
What you get back
Non-streaming POST /v1/chat/completions and POST /v1/embeddings
responses add these headers:
x-sable-receipt: base64url of the canonical JSON receipt payloadx-sable-receipt-sig:0xsecp256k1 signature over the payload, using the EIP-191 (personal_sign) schemex-sable-receipt-signer: the0xaddress that signed itx-sable-node: the machine that served the request (today:gateway)x-sable-region: the region this deployment declares it runs in, orunspecifiedif it declares none
The x-sable-receipt* headers are CORS-exposed, so a browser client can read
them from a cross-origin response. You don't need a server-side proxy
just to collect receipts.
curl -i https://api.buildsable.com/v1/chat/completions \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "sable-llama-3.3-70b",
"messages": [{"role":"user","content":"hi"}]
}'
# ... response headers ...
# x-sable-node: gateway
# x-sable-region: unspecified
# x-sable-receipt: eyJ2IjoxLCJyZXF1ZXN0X2lkIjoi...
# x-sable-receipt-sig: 0x4f8c...1b
# x-sable-receipt-signer: 0xA1b2...9FThe receipt payload
Base64url-decode x-sable-receipt and you get canonical JSON. It is
metadata only: note content_fingerprint, a SHA-256 over the sealed
payload, not the content itself.
{
"v": 1,
"request_id": "req_8f3c...",
"content_fingerprint": "9b1c4e...",
"model": "sable-llama-3.3-70b",
"privacy_tier": "standard",
"node": "gateway",
"region": "unspecified",
"prompt_tokens": 12,
"completion_tokens": 48,
"total_tokens": 60,
"cost_micro_usd": 540,
"logging": "metadata-only",
"issued_at": "2026-05-30T12:00:00Z"
}
The fingerprint lets you bind a receipt to a specific request you made
(hash your own sealed envelope and compare) without ever exposing the
plaintext to the receipt itself. cost_micro_usd is the metered cost in
millionths of a dollar. See Cost & metering.
The logging claim
Every receipt carries logging: "metadata-only". It is a signed, per-request
claim that for this specific call Sable persisted no prompt, no completion, and
no submitted code, only the metadata that appears on the receipt itself. Where
most services state their retention posture in a policy page, this is the
privacy contract restated as a verifiable artifact: a claim
you can pin to one request and check the signature on, not a promise you have
to take on trust.
Read it for what it is. This is Sable's signed claim about its own
logging, not a hardware proof that the host could not have retained the
plaintext. The confidential tier is the rung where that
guarantee is hardware-backed; on every other tier logging: "metadata-only" is
the gateway attesting, under its own signature, to the contract it operates
under.
engine: what actually served
Some catalog ids are names for an engine rather than the engine itself. The
flagship lane (sable, sable-fast, sable-max) most of
all. Whenever the upstream engine that served the request differs from the
public model id, the receipt carries an engine field naming it. On flagship
receipts it is always present, because a flagship id always resolves to a
disclosed engine, and because the flagship ids carry an
ordered failover chain, the receipt is
how you see which link in the chain ran:
{
"model": "sable",
"engine": "anthropic/claude-opus-5"
}
Billing prices the engine that ran, so engine is also the field that
explains the metered cost when a failover served the request. This is the
honesty mechanism that keeps flagship routing from being a black box: the
signed receipt names the engine, every time.
scrubbed: the outbound scrub ran
A request that opted into outbound scrubbing with
sable_scrub: true gets "scrubbed": true on its receipt. The field is
present only when the scrub ran; it means the redaction filter executed on the
outbound prompt before it left for the vendor. Nothing more. It does not
change what is stored (nothing is, scrubbed or not: §3 is unchanged; only the
redaction count is logged) and it is not a claim that the content was
anonymized. See the scrubbing page for exactly what the
filter catches and what it doesn't.
What node and region mean
node names the machine that served the request, and it only ever names
something that exists. One gateway process serves inference today, so an
inference receipt reads "node": "gateway". A sandbox run
served by an enrolled fleet node is recorded against that node's real id;
one served by the deployment's configured backend is recorded against the
backend that executed the code: sandbox:http-runner or
sandbox:local-docker.
GET /v1/nodes lists the same set from the same source: the gateway itself,
one tee:<name> entry per configured confidential backend carrying its
live attestation state, and every enrolled fleet node with a status computed
from its heartbeat, never stored optimism. Each entry has a synthetic flag:
true means the entry is derived from this deployment's configuration
rather than from a separately enrolled machine; enrolled nodes carry
synthetic: false.
Receipts issued before August 2026 name one of four seeded ids
(node-fra-01, node-nyc-02, node-sin-01, node-lhr-03) describing
SGX/SEV/NVIDIA-CC hardware in four cities. That hardware never existed. Those
rows are retired and excluded from GET /v1/nodes, but the ids stay
resolvable, because old receipts name them and a receipt is a permanent claim.
Per-machine node ids are real now for fleet-served sandbox runs; inference
still serves from the gateway process.
region is the region this deployment declares it runs in (one of
eu-central, eu-west, us-east, us-west, ap-southeast), or the literal
string unspecified when the operator has declared none. It is not your
sable_region pin echoed back: a pin is honored only when it resolves to the
region the gateway actually runs in, and refused with 400 otherwise. See
Region pinning.
A confidential-tier receipt still reads "node": "gateway". That is the
machine you called. The enclave that ran the model is identified in the
attestation block below, which is the part that carries a hardware
guarantee.
Confidential-tier attestation
A request on the confidential tier (today, the
sable-confidential-* models)
adds an attestation block (also metadata only) proving it ran inside an
attested Intel TDX enclave. Standard-tier receipts omit it entirely.
{
"attestation": {
"platform": "tdx",
"measurement": "3b6dd335…",
"mrtd": "f06dfda6…",
"tcb_status": "UpToDate",
"verification": "tee-attested",
"response_bound": true,
"signer": "0x79a5061e…"
}
}
The gateway verifies the enclave's TDX quote against a pinned measurement
before routing, and verifies a per-response signature from the key bound into
that quote after, so verification: "tee-attested" with response_bound: true
means this specific response came from a correctly-measured enclave the host
couldn't see into. The public GET /v1/attestation returns the live verified
status, and the SDK's verifyAttestation() checks it as a pre-flight before you
send anything.
Streaming
A streamed chat completion can't set trailing headers, so the receipt arrives as the final SSE event after the stream ends:
event: sable.receipt
data: {"receipt":"eyJ2Ijox...","signature":"0x4f8c...","signer":"0xA1b2...9F"}
Read it the same way you'd read the headers: base64url-decode receipt for
the payload, verify signature against signer.
Re-fetching a receipt
Receipts are stored (metadata only, like everything else), so losing the
response headers no longer means losing the proof. GET /v1/receipts/:id
(session-authed, Bearer sess_…) returns the stored receipt and its signature.
The :id is either the receipt's own request_id or the usage-event id
shown in the dashboard history. The prompt and completion are not part of the
receipt and were never stored, so a re-fetched receipt carries exactly what the
original headers did.
Verifying a receipt
Because receipts use standard secp256k1 + EIP-191, any Ethereum signature
library verifies them. Recover the signer over the decoded payload
string (atob(receipt)) and check it equals x-sable-receipt-signer.
import { verifyMessage } from "viem";
// receipt = x-sable-receipt, signature = x-sable-receipt-sig,
// signer = x-sable-receipt-signer
async function verifyReceipt(
receipt: string,
signature: `0x${string}`,
signer: `0x${string}`,
): Promise<boolean> {
const payload = atob(receipt); // base64url -> canonical JSON string
return verifyMessage({ address: signer, message: payload, signature });
}
// ethers works too:
// ethers.verifyMessage(payload, signature) === signerVerify endpoints
Two public endpoints (no auth) help you verify without wiring up crypto yourself:
GET /v1/receipts/pubkey→{ "scheme": "secp256k1-eip191", "signer_address": "0x…", "verify": "…" }. Pinsigner_addressfor your deployment.POST /v1/receipts/verifywith body{ receipt, signature }→{ "valid": true, "signer_address": "0x…", "recovered_address": "0x…", "payload": { … } }.validistrueonly when the recovered address matches the deployment signer.
The signing key is derived from the deployment's master key, so the signer
address is stable per deployment: fetch it once from
/v1/receipts/pubkey, pin it, and reject any receipt that recovers to a
different address.
operator: who served it (receipt v:3)
When a run executes on an enrolled fleet node that counter-signs its work, the
sandbox receipt carries an operator block:
"operator": { "node_id": "node-…", "output_fp": "…", "verified": true }
The serving node holds its own ed25519 key and signs a fingerprint of
(input, output) for every run. The gateway verifies that signature against
the key it has on record for that node (recorded on first sight, then pinned;
a silent key change is refused, not attributed) and checks that the signed
input fingerprint is its own hash of the code you submitted. verified: true
means both held.
Read this honestly: operator attribution proves which machine served the
run: the honest label is operator-attributed, not a proof that the work
itself was performed correctly (that is the confidential/TEE tier). It is the
accountability rung: it says who to hold responsible. A run on a node that
doesn't sign, or whose signature doesn't verify, simply omits the
block, and the receipt stays the earlier v:2 shape.
Verifiable context
An agent doing retrieval can declare the context it was given, and have the
receipt prove those inputs. Pass sable_context on a chat or /v1/messages
request: an array of strings, or objects { text, label? } where the label is a
citation.
curl https://api.buildsable.com/v1/chat/completions \
-H "authorization: Bearer $SABLE_KEY" \
-H 'content-type: application/json' \
-d '{
"model": "sable",
"messages": [{"role":"user","content":"Summarize the policy."}],
"sable_context": [
{"text":"Refunds are issued within 30 days.","label":"policy.pdf#p3"},
{"text":"Enterprise plans renew annually."}
]
}'The gateway fingerprints each item in-frame and stamps a context block on the
receipt. Nothing of the text is stored (§3): only sha256 fingerprints, byte
lengths, and any labels.
"context": {
"count": 2,
"root": "9f2c…",
"items": [
{ "label": "policy.pdf#p3", "fp": "3b1e…", "len": 33 },
{ "fp": "a07d…", "len": 27 }
]
}
root is sha256( sha256(item_0) ++ sha256(item_1) ++ … ) over the items in
order, so it is sensitive to both content and ordering. Anyone later holding the
same documents recomputes each item's sha256 (its prefix must match fp)
and the root (it must match root), and so proves those exact inputs, in that
order, fed this exact signed call. It is the input-side twin of the output
fingerprint: proof of the context, not just the completion.
Read it honestly: this proves what context was declared, not that the model attended to it and not that the documents are true. It closes the "did the agent get the context it claims" gap, which is the one a third party can check.
Action attestation
When a chat completion emits tool calls, the signed receipt gains an actions
block recording the calls the model asked for. Like everything else on the
receipt it is metadata only: tool names, argument-hash prefixes, and byte
lengths, never the argument text itself.
"actions": {
"count": 2,
"root": "7c4a…",
"calls": [
{ "tool": "search_docs", "args_fp": "3b1e…", "args_len": 48 },
{ "tool": "send_email", "args_fp": "a07d…", "args_len": 132 }
]
}
Each call's fingerprint is sha256("name|arguments"). The root is the
sha256 of the full per-call hashes concatenated in emission order, so it is
sensitive to both the calls and their ordering. Anyone later holding the same
argument payloads recomputes each call's sha256 (its prefix must match
args_fp) and the root (it must match root), and so proves the model emitted
exactly those tool calls, in that order, on this exact signed call.
It works on buffered and streamed responses identically. The block is omitted entirely when a response carries no tool calls, so tool-free receipts are byte-for-byte unchanged. Past 64 calls the block is omitted rather than truncated, so a partial list can never be mistaken for a complete one. Only tool names, hash prefixes, and byte lengths reach the receipt (§3): the argument text is never stored or logged.
Read it honestly: this proves what tool calls the model emitted through
Sable, not that the caller executed them and not what they returned. Together
with context (the inputs) and content_fingerprint (the output), the receipt
now covers inputs, output, and requested actions.
Agent runs: chain a whole session
Tag any billable request with sable_run_id (1 to 64 chars of [A-Za-z0-9._:-],
accepted on chat, embeddings, /v1/messages, and sandboxes) and its signed
receipt joins a per-run hash chain:
chain(n) = sha256( chain(n-1) ‖ sha256(receipt(n)) )
One head hash then proves the entire session: every model call and sandbox execution, in order, across kinds. Rewriting or omitting any step breaks every later link.
curl https://api.buildsable.com/v1/chat/completions \
-H "authorization: Bearer $SABLE_API_KEY" \
-H 'content-type: application/json' \
-d '{
"model": "sable",
"sable_run_id": "my-agent-42",
"messages": [{"role": "user", "content": "…"}]
}'Session-authed: GET /v1/runs lists your runs; GET /v1/runs/{run_id}
returns the chained receipts (each independently signed and verifiable);
POST /v1/runs/{run_id}/proof mints a run proof (a signed statement
carrying the head hash, receipt count, total cost, and time span) which
verifies through the same public POST /v1/receipts/verify as everything
else. Run heads are batch-anchored to Solana (memo format
sable-runs:v1:<root>) when the deployment's anchor account is funded;
until a batch finalizes, a run honestly reports no anchor. The proof's
embedded trust_model states exactly what is proven: attested by the
gateway over its hash chain, publicly anchored when an anchor exists, not
zero-knowledge.
Sharing a receipt
Receipts are metadata-only by construction, so an owner can safely publish
one: POST /v1/receipts/{id}/share (session-authed) makes it fetchable at
GET /v1/receipts/shared/{request_id} and viewable at /r/{request_id} on
the site: model, cost, fingerprint, and chain position, never content.
DELETE /v1/receipts/{id}/share unshares it. Nothing is public unless you
shared it, and unsharing takes effect immediately.