Guides
- Quickstart
- Authentication and keys
- Key protection
- Models, aliases and fallbacks
- Chat completions
- Streaming
- Structured outputs and tools
- Logprobs
- Vision
- Responses, Claude and Ollama
- Embeddings, rerank, audio, images
- Files and batches
- LoRA adapters
- Project tags and costs
- Prefix caching
- Headers, rate limits and spend caps
- Errors
- Privacy and retention
- Verifiable receipts
- Pricing
- Billing
- Status
- SDKs
- Integrations
- Run a node
- Architecture
- Changelog
API reference
Guide
Verifiable receipts
A signed record of what you sent, what came back, and where it ran. Hashes and counts only, never your prompt or the answer.
What a receipt is
Every receipt holds two statements, each signed with ed25519 over its exact bytes:
The node statement, signed by the serving engine with the same key it signs its model attestation with. It says: I received a request with this hash, produced a response with this hash, running this model with this weight hash, and counted these tokens.
The gateway statement, a countersignature with the platform key. It says: the caller sent a body with this hash and was given a body with this hash; I routed it to this node, run by this operator in this region, with this attestation; I sent the node these bytes and got these back; and the node statement does or does not match all of that.
Neither statement names your organization or your key, so a receipt can be shown to a third party, for example to settle a dispute, without revealing who you are.
Asking for one
Send Silicon-Receipt: 1 on any inference request, or turn on Signed receipt on every response for a key in the dashboard (or receipts: true on POST /v1/keys and PATCH /v1/keys/{id}). The header takes 1, true, yes or on, and with the key setting on, 0, false, no or off skips it for one request. Batch requests never carry receipts.
curl -sD headers.txt https://api.siliconnetwork.io/v1/chat/completions \
-H "Authorization: Bearer $SILICON_API_KEY" \
-H "Content-Type: application/json" \
-H "Silicon-Receipt: 1" \
--data-binary @request.json -o response.json
grep -i '^silicon-receipt:' headers.txtWhere it arrives
On a successful, complete response from an inference surface (chat completions, Responses, Claude Messages, embeddings, rerank, transcription, speech audio, and images) the receipt is the Silicon-Receipt response header, base64url encoded JSON. The body is untouched.
On a Server-Sent Events stream it is one extra event at the very end, after data: [DONE], where OpenAI clients have already stopped reading:
data: [DONE]
event: silicon.receipt
data: {"v":1,"node":{...},"gateway":{...}}The stream's response_sha256 covers every byte before that event, keep-alive comments included, so hash the raw bytes rather than the parsed chunks. Error responses, newline-delimited JSON streams (the Ollama surface) and raw PCM audio streams carry no receipt. The gateway removes any receipt event a node tries to send on its own, so the only one you see is the one the gateway countersigned.
What is in it
The delivered object is {v: 1, node, gateway}. Each statement is an envelope {alg: "ed25519", payload, signature} where payload is the base64url of the statement JSON and the signature covers those bytes as carried, so nothing has to be re-serialized to check it. The node envelope adds pubkey; the gateway envelope adds key_id. Either can be null: gateway when the platform has no signing key configured, node when the engine returned no statement or one for a different request id (the gateway's countersigned check then says why).
| v, kind | Format version 1 and the statement kind, silicon.receipt.node. |
| request_id | The gateway's request id, the same value as the Silicon-Request-Id header. |
| node_id | The node that produced the answer. |
| model, model_hash | The engine model id and the sha256 of its weight manifest: the hash the node attested to, or one it measured locally when it has not attested that model, or null when it cannot tell. |
| request_sha256 | sha256 of the request body exactly as the node received it. |
| response_sha256 | sha256 of the response body exactly as the node produced it (for a stream, every byte before the receipt event). |
| prompt_tokens, completion_tokens | The node's own token counts. |
| stream, path, issued_at | Whether it streamed, which surface, and when the node signed. |
| v, kind, request_id, path, stream | Format version 1, silicon.receipt.gateway, the request id, the surface, and whether it streamed. |
| request_sha256 | sha256 of the body you sent, before the gateway rewrote anything (aliases, the output-token cap, stream usage). |
| response_sha256 | sha256 of the body you were given, before the receipt event. |
| upstream_request_sha256, upstream_response_sha256 | What the gateway sent the node and got back. The node's hashes must equal these. |
| route | target (node or default_engine), node_id, operator_org_id, region, and attestation: state attested (model hash, key fingerprint, egress posture, time), pinned (key fingerprint), or none. |
| node_receipt | The gateway's own check of the node statement: present, sha256, signature_valid, hashes_match, request_id_match, node_id_match, key_matches_attestation, model_hash_matches_attestation, and problems. verified is true only when every applicable check passed. |
| model, requested_model, usage, issued_at | The model that served, the name you asked for, the token counts (on a stream, the usage that was billed; null when none was reported), and when the gateway signed. |
| variant, context_tokens | Present only when a quantization variant served the request, or it was routed in the long-context tier. |
Verifying
Both SDKs ship verify_receipt. It checks the gateway signature against the published key, the node signature, that the gateway countersigned this exact node statement, that the node signed with the key it attested with, that the two agree on the hashes, and, when you pass them, that the hashes are of the bytes you actually sent and received. It does not throw on a bad receipt; read valid (no check failed and at least one statement verified) and the list of checks. A receipt can be valid without being tied to your request, so when you use it as proof of an exchange, pass both bodies (or their SHA-256 hashes) and check bound, which is true only when the receipt is valid and both matched.
import httpx
from siliconnetwork import fetch_receipt_keys, verify_receipt
body = open("request.json", "rb").read()
resp = httpx.post(
"https://api.siliconnetwork.io/v1/chat/completions",
content=body,
headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json",
"Silicon-Receipt": "1"},
)
keys = fetch_receipt_keys() # cache this; it changes only on rotation
result = verify_receipt(
resp.headers["silicon-receipt"], keys,
request_body=body, response_body=resp.content,
)
print(result.valid, result.bound, result.errors)import { fetchReceiptKeys, splitStreamReceipt, verifyReceipt } from "@siliconnetwork/sdk";
const keys = await fetchReceiptKeys();
const res = await fetch("https://api.siliconnetwork.io/v1/chat/completions", {
method: "POST",
headers: { Authorization: `Bearer ${key}`, "Content-Type": "application/json", "Silicon-Receipt": "1" },
body: requestBody,
});
// a stream: read the raw bytes, then split off the receipt event
const raw = new Uint8Array(await res.arrayBuffer());
const { body, receipt } = splitStreamReceipt(raw);
const result = await verifyReceipt(receipt!, keys, { requestBody, responseBody: body });
console.log(result.valid, result.bound, result.errors);The checks are format, gateway_signature, gateway_key_window, node_signature, node_bound, node_key_attested, node_hashes, request_id, node_verified_by_gateway, request_body and response_body; each is ok, failed, or not applicable. The gateway signature is required by default (require_gateway=False, or requireGateway: false, accepts a node-only receipt) and the node statement is not (require_node, requireNode). Python needs the [receipts] extra and raises ImportError without it; TypeScript needs Ed25519 in WebCrypto, which means Node 20 or later.
With the SDK clients
The TypeScript client puts the raw header on meta.receipt. The Python client puts the decoded receipt there and, on a non-streaming call, adds meta.request_sha256 and meta.response_sha256 of the exact bytes, which verify_receipt takes in place of the bodies. The Python client takes request headers through default_headers (or headers= on client.request); the TypeScript client also per call. Both clients' stream iterators skip the receipt event, so verify a stream from its raw bytes as above.
From the command line
pip install "siliconnetwork[receipts]"
# a JSON response: the header value, plus the bytes you sent and got
silicon-verify-receipt "$(grep -i '^silicon-receipt:' headers.txt | cut -d' ' -f2 | tr -d '\r')" \
--request request.json --response response.json
# a stream saved raw (curl -N ... > stream.sse)
silicon-verify-receipt --stream stream.sse --request request.jsonEach check prints as ok, FAIL or -- (not applicable), then VALID or INVALID; the exit code is 0 when the receipt is valid. --json prints the result with bound, which the text output leaves out. --keys reads a saved copy of the key list instead of fetching it, --base-url fetches it from another deployment, and --allow-unsigned-gateway accepts a receipt without a gateway statement.
Keys and rotation
GET https://api.siliconnetwork.io/v1/receipts/keys (also at /.well-known/silicon-receipt-keys.json, public, cacheable for five minutes) lists the platform keys. When the key rotates, the old one stays listed as retired with not_after, the time it stopped signing.
{
"object": "list",
"keys": [
{"key_id": "rk_...", "alg": "ed25519", "public_key": "<base64>", "status": "active", "not_after": null},
{"key_id": "rk_...", "alg": "ed25519", "public_key": "<base64>", "status": "retired",
"not_after": "2026-10-01T00:00:00+00:00"}
]
}The verifiers check a receipt signed by a key that has a not_after against that window (gateway_key_window): a receipt issued at or before it still verifies, one issued later fails, and the times are compared as instants, not text. A retired key without not_after is never published and fails if you supply one. The window rests on the signed issued_at, so it protects against a retired key being used by mistake, not against someone who stole it and backdates a statement.
Node keys are not listed there. Each node statement carries its own public key, and the gateway statement says whether it matches the key fingerprint the node registered when it attested its model. Pin the fingerprints you care about if you do not want to rely on the gateway for that.
What a receipt proves, and what it does not
This is software attestation. It is useful, and it is not a hardware guarantee. Read this section before relying on it.
It does prove
That the platform received a request whose hash you can match against the bytes you sent, delivered a response whose hash you can match against the bytes you received, and routed it to a named node, operator and region with a stated attestation. The platform cannot later deny any of that, because it signed it.
That the operator's node signed a claim about what it received and produced, with the key it attested its model weights with. The operator cannot later deny making that claim, and a node statement cannot be moved onto another request: it is bound to the request id, the exact bytes and the countersignature.
That nothing between the platform and you changed the body, if your hash matches.
It does not prove
That the node ran the model it names. The operator controls the machine, the engine and the key. A modified engine can hash a manifest of the right weights and answer with something else, and sign that. The model hash covers the weight manifest (file names, sizes and the index), not every byte of the weights. What catches a node that lies is separate: sampled re-execution of live requests on a trusted engine, and escrowed earnings that a failed check forfeits.
Anything about the hardware. There is no TEE or secure enclave behind the node key today, and nothing in a receipt says what chip ran the request. A hardware root of trust is a later tier.
That the node forgot your prompt. A receipt is about integrity, not retention. The serving node processes your prompt and, today, keeps it in its event log; see Privacy.
That the platform is honest. You trust the platform key. The node statement lets you hold the gateway to what the node said, but node keys are self-generated, so a platform that colluded with an operator could make both statements agree. Token counts are the node's; times are each signer's clock.
Anything about the default engine's attestation. When no network node can serve a request, the platform's own engine does. It does not attest. Where the platform has pinned that engine's key, its statement is checked against it and the gateway statement says default_engine and pinned; otherwise the attestation state is none and the node key is not checked.
Privacy
Receipts contain hashes and counts, never prompt or completion text, and the gateway does not store them: you get the only copy. They work with X-Silicon-No-Retention: true unchanged.
A hash still identifies its input to anyone who can guess it. If you share a receipt for a short or predictable prompt (a yes or no question, a form letter), whoever holds it can confirm a guess at the text. Keep receipts as private as you would keep the request id.