Portal
Documentation: all sections

Sable Notary

Every other receipt this gateway mints describes work that crossed it: an inference it proxied, a sandbox it scheduled, a batch line it executed. That makes the proof layer available only to people who move their compute here — and most people never will. A training run sits on a cluster that took a year to get budget for. A nightly ETL job is load-bearing and nobody is rewriting it to get a receipt.

The Notary detaches the proof from the compute. Your own process hashes what went in and what came out and registers the two digests. Sable signs the registration with the same receipt key that signs an inference receipt, stores it in the same table — so GET /v1/receipts/{id} and the public POST /v1/receipts/verify serve it with no new verification machinery — and queues its hash for the public anchor stream.

The Notary is built and tested. It is not on the production deployment yet, so the calls below describe a surface you cannot reach at api.buildsable.com today.

What a notary receipt proves

This account registered these exact fingerprints at this time.

Honesty box

Proven: that this Sable account registered these digests at the signed notarized_at, and — once anchored — that the hash of that signed statement was published in a public memo.

Not proven: that the computation happened. That it was performed correctly. That the fingerprints describe what you say they describe — an input_sha256 labelled training-run might be a photograph. Sable did not run the work, did not observe it, and cannot check any part of it. The digests are supplied by the account and taken on trust.

This is not verified computation. That is worth more and this is not it.

The asymmetry is the whole product, and it cuts one way only: a notary receipt cannot make a false claim true, but it makes a later claim falsifiable. You cannot backdate a digest you did not register, you cannot quietly change an output whose hash you published, and you cannot retroactively pick which of five runs you meant.

Sable attests to the registration. The account makes the claim. Those are different acts by different parties, and the receipt keeps them apart — the limit rides inside the signature as trust_model, so a third party verifying the receipt in a year reads the claim and its bounds in the same breath.

Content-free by construction

The request accepts digests, never payloads. There is no column in notary_receipts that could hold a dataset, a model, a prompt or an output, and nothing here is sealed because there is nothing to seal. The Notary adds no plaintext surface to the privacy contract.

Two caller-controlled fields are readable at rest: label and the string values inside attributes. Both are hard capped, both are never logged — the success path emits ids, the digests, a byte count and a price, never the map — and both are labels rather than payload. A caller who puts content in a label has put content in a label; the schema cannot prevent that, so this says so plainly rather than implying a guarantee the shape does not give.

Quickstart

IN=$(shasum -a 256 dataset.parquet | cut -d' ' -f1)
OUT=$(shasum -a 256 model.safetensors | cut -d' ' -f1)

curl -s https://api.buildsable.com/v1/notary/receipts \
-H "Authorization: Bearer $SABLE_API_KEY" \
-H "Content-Type: application/json" \
-d "{
  \"input_sha256\": \"$IN\",
  \"output_sha256\": \"$OUT\",
  \"kind\": \"training-run\",
  \"label\": \"nightly ranker, seed 7\",
  \"attributes\": {\"epochs\": 3, \"gpu_hours\": 41.5, \"commit\": \"a1b2c3d\"}
}"

# {
#   "id": "nt_9f3c…",
#   "kind": "training-run",
#   "input_sha256": "…", "output_sha256": "…",
#   "hash": "…",                       <- sha256 of the canonical signed JSON
#   "receipt": "eyJ2IjoxLCJraW5kIjoibm90YXJ5Iiw…",
#   "signature": "0x…", "signer": "0x…",
#   "usage_event_id": "…", "cost_micro_usd": 1000,
#   "notarized_at": "2026-09-13T12:00:00Z",
#   "anchor": null,                    <- until the anchor pass runs
#   "trust_model": "A notary receipt proves that this Sable account registered…"
# }

Fields

FieldRequiredNotes
input_sha256yes64 lowercase hex characters. No 0x, no whitespace, no separators — refusals name the likely mistake rather than saying “invalid”.
output_sha256yesSame.
kindyesA short grouping label: training-run, etl-job, build. [a-z0-9._-], ≤ 64 characters. It lands in usage_events.resource as notary:<kind>, where it becomes a grouping key in spend exports — which is why it is a charset, not prose.
labelnoHuman caption, ≤ 120 characters, no control characters. Readable at rest. Not content.
attributesnoA flat map of content-free scalars — counts, durations, version strings. ≤ 16 entries, keys ≤ 64 characters, string values ≤ 120, whole map ≤ 1 KiB canonical. Nesting and null are refused. Readable at rest. Not content.

Digests are case-strict on purpose. Every common producer already emits lowercase (sha256sum, shasum -a 256, Python's hexdigest(), openssl dgst), and strictness buys a real property: one digest has exactly one canonical signed form, so two parties who notarize the same bytes produce byte-identical payloads and can compare receipts directly instead of normalising first. Silently lowercasing would give one digest two signatures over two different byte strings.

Attributes are stored and signed in sorted key order. Iteration order that varied run to run would sign different byte strings for the same map.

Verify it yourself

Three checks, in increasing strength, none of which trusts this site.

  1. The digests. Recompute sha256 over your own input and output bytes and compare with input_sha256 / output_sha256. This is the step Sable is not in.
  2. The signature. POST /v1/receipts/verify with {receipt, signature}. The recovered address must equal GET /v1/receipts/pubkey. The decoded receipt is the canonical signed JSON.
  3. The anchor. When anchor is present, look up the published signature and confirm the memo reads sable-notary:v1:<root>, where root folds this receipt's hash — itself sha256 of that canonical JSON. So the public anchor commits to the entire statement, both digests, the instant and the trust model, and not merely to one fingerprint you supplied.

Until the anchor pass runs, the proof rests on Sable's signature alone, and the response says "anchor": null rather than inventing one.

Billing

One ordinary metered usage event, kind: "notary", unit receipts, one receipt per request. It goes through the same gate every billed request goes through, which is why credit holds, per-key spend caps and the circuit breaker all apply with no new enforcement code. The usage row and the notary row are written in one transaction; the debit is idempotent on the event id.

The default price is $0.001 per receipt, operator-configurable, so a job notarizing every nightly run is not punished for it.

A retry bills twice

There is no Idempotency-Key here, and the table is deliberately not unique on (account, input_sha256, output_sha256) — re-notarizing the same pair later is a legitimate act (a reproducible build proving it still reproduces a year on), so the natural key is not a duplicate key.

The consequence is small and stated rather than hidden: a client that retries a request whose response it never saw mints a second receipt and pays twice. Nothing executes twice, because nothing executes at all — the cost is one price of one receipt.

Endpoints

MethodPathAuthNotes
POST/v1/notary/receiptssk-sable_ keyRegister a pair of digests. Returns the signed receipt.
GET/v1/notary/receiptssk-sable_ keyYour registrations. ?limit= (≤ 500, default 100), ?kind=.
GET/v1/notary/receipts/{id}sk-sable_ keyOne registration, with anchor state.
POST/v1/receipts/verifypublicThe ordinary verifier. A notary receipt needs nothing special.

Key-authed rather than session-authed, because the caller is an external process on a cluster somewhere, not a browser.

Every notarization also fires the ordinary receipt_minted webhook, so a notary receipt lands in the same audit stream as everything else you meter.