Signed Inference Receipts
Opt into a signed, offline-verifiable receipt on any inference call: exact request and response hashes, the routed model and provider, the upstream verification tier, and a signing key committed into hardware attestation.
Every response can carry a signed receipt.
Add one header to any chat or responses call and TrustedRouter returns a signed receipt: a compact proof that this exact request produced this exact response, which model and provider served it, and how the upstream was verified. The signing key is minted inside the attested enclave and committed into its hardware attestation, so a receipt chains to the same Confidential Space evidence as the live gateway.
Receipts are opt-in per request and never stored server-side. Managed prepaid inference with a signed receipt uses a 12% total TrustedRouter service fee instead of the standard 5.5%. BYOK token pricing is unchanged. Verification is offline: signature, hashes, and the attestation chain all check without calling TrustedRouter again.
A receipt is not a confidentiality proof. It proves integrity and origin of the bytes; session privacy remains the job of the attested TLS channel documented on the trust page.
Compact JWS in a header
The response carries x-inference-receipt: a three-part JWS over the exact request and response body bytes. The attestation document is pinned by hash and fetched separately.
Self-contained final chunk
The last data event before [DONE] is a chat.completion.chunk with an inference_receipt object — a flattened JWS with the full attestation embedded in its protected header. Truncated streams never get a receipt.
Quickstart
Send the header with true, or with a fresh nonce of 1–88 characters from A–Z a–z 0–9 _ -. The nonce is echoed inside the signed claims, which is what makes a receipt provably fresh to you.
curl https://api.trustedrouter.com/v1/chat/completions \
-H "Authorization: Bearer $TRUSTEDROUTER_API_KEY" \
-H "Content-Type: application/json" \
-H "x-inference-receipt: $(openssl rand -hex 16)" \
-d '{"model":"trustedrouter/auto","messages":[{"role":"user","content":"Hello"}]}' \
-D - -o response.json
The x-inference-receipt response header holds the compact receipt. Requests without the header keep standard 5.5% pricing and are byte-identical to today. Receipt-enabled managed prepaid requests use the 12% total service fee. Malformed opt-in values are rejected with 400.
Verify a receipt
All six SDKs verify receipts offline, and the reference verifier ships in the open quill-cloud-proxy repository as tools/verify-attestation.py --verify-receipt.
Streaming, fully self-contained
from trustedrouter.receipts import verify_receipt
claims = verify_receipt(
receipt, # the inference_receipt object
request_body=body_bytes, # exact bytes you sent
response_stream=wire_bytes, # exact SSE bytes you received
expected_nonce=my_nonce,
max_age_seconds=300,
)
print(claims.model.selected, claims.upstream.tier)
Compact, with the pinned attestation
import { verifyReceipt } from "@lore-hex/trusted-router/receipts";
const claims = await verifyReceipt(compactReceipt, {
requestBody, responseBody,
expectedNonce: myNonce,
maxAgeSeconds: 300,
attestation, // bytes whose sha256 equals the att_sha256 claim
});
Go, Rust, Swift, and Java expose the same verification with the same checks and the same frozen cross-SDK test vectors. Verification always covers: the Ed25519 signature; rv, iat freshness, and your nonce; the request and response hashes over exact bytes; the receipt event's position in a stream; and the attestation chain with the signing key's commitment checked by set membership.
What the claims say
The payload is inference-receipt/1. Hashes are unpadded base64url SHA-256.
| Claim | Meaning |
|---|---|
rv | Receipt version, 1 |
iss | The issuing plane's canonical https origin |
iat | Signing time, Unix seconds; verifiers allow at most 60 s of future skew |
jti | The response id (chatcmpl-… or resp_…) |
gen | Generation id — non-streaming only, since streams settle after [DONE] |
nonce | Your opt-in value, echoed verbatim when it was not true |
route | chat.completions or responses |
req | sha256 over the exact request body bytes |
resp | sha256 over exact response bytes: of = body, or the stream domains sse-data-v1 / sse-events-v1 with an events count |
model | requested, selected, provider, endpoint — the router's actual selection |
upstream | The verification tier below |
att_sha256 | Compact receipts only: SHA-256 of the exact key-binding attestation document |
Upstream tiers name a mechanism, never a privacy property
The tier records how the upstream that served this request was verified. It makes no claim about what the upstream retains.
| Tier | Meaning | Extra claims |
|---|---|---|
tee-verified | The upstream's own TEE attestation was verified before your bytes were sent, under a named policy such as chutes-tdx-nvidia-e2e-v1 or tinfoil-snp-dual-source-v1 | policy, verified_at, verification_expires_at; verifiers require verified_at ≤ iat < verification_expires_at |
tls-webpki | The upstream was reached over ordinary WebPKI TLS | — |
A failed attested candidate never lends its tier to the fallback that actually serves the response — the tier always describes the connection your bytes used.
The attestation chain
Each enclave instance mints a fresh Ed25519 keypair at boot and commits SHA-256("inference-receipt-key-v1" ‖ 0x00 ‖ pubkey) into its hardware attestation. Verifiers check that commitment by set membership among the attestation's committed values — never by position.
| Endpoint | Serves |
|---|---|
GET /receipt-attestation | This instance's raw key-binding attestation document |
GET /receipt-key | This instance's {kid, jwk, att, att_kind} envelope — match a compact receipt's kid directly |
GET /.well-known/inference-receipt-keys | The append-only key log: every observed signing key with its attestation, so compact receipts stay verifiable after the instance that signed them is gone |
Streaming receipts embed the attestation, so nothing extra is fetched and they verify forever from the captured bytes alone. For compact receipts, keys are per-instance and routing is DNS-based, so fetch deterministically: resolve the gateway hostname's A records and ask each instance directly (pin Host/SNI to the hostname while connecting by IP) for its /receipt-key until the kid matches — polling the hostname is a lottery that can miss instances behind connection reuse. The key log covers keys after their instance is gone.
Archive the attestation with the receipt. If a receipt matters to you — an audit trail, a dispute, an eval run — store the matching attestation document (and for streams, the captured wire bytes) next to it at verification time. That bundle verifies offline forever with no dependency on TrustedRouter, the signing instance, or the key log being reachable.
What a receipt proves — and deliberately does not
Integrity and origin
The exact request produced the exact response; the named model, provider, and endpoint served it; the signing key lives in a measured, debug-disabled Confidential Space workload; with your nonce, that the receipt was minted for your request and is not a replay.
Confidentiality or identity
A relay that forwarded your request holds a valid receipt and read everything — session privacy is the attested-TLS flow on the trust page, not the receipt. Receipts carry no requester identity, so sharing one cannot deanonymize you.