Documentation: all sections

Saxeo 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. Saxeo 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 live on the production gateway: POST /v1/notary/receipts is mounted and answers today. It is a billed route like any other — it takes an sk-sable_ key, debits the per-receipt price this deployment sets, and refuses when the balance will not cover it.

What a notary receipt proves

This account registered these exact fingerprints at this time.

Honesty box

Proven: that this Saxeo 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. Saxeo 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.

Saxeo 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://www.saxeonetwork.tech/__api/v1/notary/receipts \
-H "Authorization: Bearer $SAXEO_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 Saxeo account registered…"
# }

Fields

input_sha256RequiredyesNotes64 lowercase hex characters. No 0x, no whitespace, no separators — refusals name the likely mistake rather than saying “invalid”.
output_sha256RequiredyesNotesSame.
kindRequiredyesNotesA 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.
labelRequirednoNotesHuman caption, ≤ 120 characters, no control characters. Readable at rest. Not content.
attributesRequirednoNotesA flat map of content-free scalars — counts, durations, version strings. ≤ 16 entries, keys [a-z0-9._-] and ≤ 64 characters, string values ≤ 120, whole map ≤ 1 KiB canonical. Nesting and null are refused. So are reserved key names, with a 400: v, id, kind, type, and anything ending in _id — see below. 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.

Reserved attribute names

v, id, kind, type, and any key ending in _id are refused with a 400 naming the rule and suggesting your-<key> instead. They are how Saxeo states identity and statement type in its own signed payloads, and an attribute a caller chose must never be mistakable for one.

The reason is specific rather than tidy-minded. One key signs every statement this gateway makes, and a signature proves Saxeo said something, never that Saxeo said this kind of thing. contracts/src/SaxeoEscrow — pay-on-proof settlement — used to release funds against any signed payload that contained the bytes "request_id":"<the job id>" anywhere in it, and this endpoint signs a caller-chosen map. Notarizing a hash with attributes: {"request_id": "<somebody else's job id>"} bought a validly signed document carrying those exact bytes.

The real fix is in the contract, and it is positional: it now parses the payload as one JSON object and reads top-level members only, so anything inside attributes is structurally invisible however it is named, and it pins the statement type so a notary receipt cannot stand in for an inference one. This rule is the second lock, not the first — it refuses the confusion, on the reasoning that a signed document should not be able to look like it is asserting something the account merely supplied.

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 Saxeo 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 Saxeo'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

POSTPath/v1/notary/receiptsAuthsk-sable_ keyNotesRegister a pair of digests. Returns the signed receipt.
GETPath/v1/notary/receiptsAuthsk-sable_ keyNotesYour registrations. ?limit= (≤ 500, default 100), ?kind=.
GETPath/v1/notary/receipts/{id}Authsk-sable_ keyNotesOne registration, with anchor state.
POSTPath/v1/receipts/verifyAuthpublicNotesThe 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.