Proof
Receipts
Signed, privacy-preserving records of every billed operation: payload fields, hashing, signatures and anchoring.
Overview
A receipt is a signed statement by the platform about one billed operation: what ran, on which model or runtime, how many tokens it used, what it cost, how long it took, and commitments to its input and output. Receipts are designed to be published. They let you, your customers or an auditor confirm usage and cost without trusting the platform's dashboards and without revealing any content.
- The payload is serialized as canonical JSON and hashed with keccak256.
- The hash is signed with an EIP-191 personal_sign signature, verifiable with standard Ethereum tooling.
- Batches of receipt hashes can be anchored on Robinhood Chain as Merkle roots. Beta
When receipts are issued
| Outcome | Receipt | status |
|---|---|---|
| Inference completed (streaming or not) | Issued | succeeded |
| Stream failed or client disconnected after output was delivered | Issued for the delivered portion | failed |
| Inference failed before any output | None; reservation released | — |
| Sandbox execution completed | Issued | succeeded |
| Sandbox execution hit its timeout | Issued | timeout |
| Sandbox execution reported failure | Issued | failed |
| Sandbox node unreachable or rejected the request | None; reservation released | — |
| Rejected by authentication, validation, policy, rate limit, budget or balance | None; not billed | — |
In short: an operation that is charged has a receipt, and an operation without a receipt was not charged.
Retrieving receipts
- Every billed response includes a
receiptsummary (id,hash,signature,signer,url) and thex-receipt-idheader. - The public proof page at
https://www.hushcompute.xyz/proofs/{receipt_id}shows the payload and re-verifies it on every view. Public JSON is described in Receipt Verification. - The authenticated endpoint below returns the full receipt for receipts in your project.
Requires the receipts scope. Receipts belonging to other projects return not_found (HTTP 404).
curl https://www.hushcompute.xyz/v1/receipts/rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ \
-H "Authorization: Bearer $HUSH_API_KEY"{
"object": "receipt",
"id": "rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ",
"receipt_hash": "0x6f0d…a3c9",
"signature": "0x9b1f…c07e1b",
"signer_address": "0x3A4c…91De",
"payload": {
"version": "1",
"receipt_id": "rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ",
"request_id": "req_01K58ZQ4W5V8E2R7PKD3N6A0TG",
"project_id_hash": "0xc41e…0b72",
"operation": "inference.chat",
"status": "succeeded",
"model": "claude-sonnet",
"provider": "anthropic",
"runtime": null,
"input_tokens": 480,
"output_tokens": 310,
"customer_cost": "4466",
"currency": "USD",
"cost_unit": "micro_usd",
"latency_ms": 2130,
"input_fingerprint": "0x2d9a…e514",
"output_fingerprint": "0x87b3…4c1f",
"privacy_tier": "standard",
"started_at": "2026-09-14T12:00:00.000Z",
"completed_at": "2026-09-14T12:00:02.130Z",
"run": null
},
"anchor_batch_id": null,
"url": "https://www.hushcompute.xyz/proofs/rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ"
}| Field | Type | Description |
|---|---|---|
| id | string | Receipt ID (rcpt_…). |
| receipt_hash | string | keccak256 of the canonical payload. |
| signature | string | 65-byte EIP-191 signature over receipt_hash, 0x-hex. |
| signer_address | string | Ethereum address of the receipt signer. |
| payload | object | The signed payload. See Payload fields. |
| anchor_batch_id | string | null | The anchoring batch this receipt was assigned to, or null. |
| url | string | Public proof page. |
Payload fields
Payload version "1" has exactly these 21 keys. Every key is always present, with null where it does not apply, so the canonical form is stable.
| Field | Type | Description |
|---|---|---|
| version | string | Payload schema version. Always "1". |
| receipt_id | string | Receipt ID, rcpt_ followed by 26 Crockford base32 characters. |
| request_id | string | The inference request ID (req_…) or sandbox execution ID (sbxr_…). |
| project_id_hash | bytes32 | keccak256("project:" + project_id). Links receipts from one project without revealing the project ID. |
| operation | string | inference.chat, inference.responses or sandbox.execute. |
| status | string | succeeded, failed, or timeout (sandbox only). |
| model | string | null | Gateway model ID. null for sandbox executions. |
| provider | string | null | Upstream provider, or sandbox-node for sandbox executions. |
| runtime | string | null | python or node for sandbox executions; otherwise null. |
| input_tokens | integer | Billed input tokens. 0 for sandbox executions. |
| output_tokens | integer | Billed output tokens. 0 for sandbox executions. |
| customer_cost | string | Amount charged, integer µUSD as a decimal string. |
| currency | string | Always "USD". |
| cost_unit | string | Always "micro_usd". |
| latency_ms | integer | Inference: request start to completion. Sandbox: execution duration. |
| input_fingerprint | bytes32 | sha256(salt ‖ canonical input). See Fingerprints. |
| output_fingerprint | bytes32 | sha256(salt ‖ canonical output). |
| privacy_tier | string | standard. private_routing and confidential are reserved. |
| started_at | string | ISO 8601 UTC with milliseconds, e.g. 2026-09-14T12:00:00.000Z. |
| completed_at | string | ISO 8601 UTC with milliseconds. |
| run | object | null | For agent runs: run_id, sequence and previous_receipt_hash. null otherwise. See Run chains. |
Canonical JSON
Hashes are computed over a canonical serialization, a strict subset of RFC 8785 (JCS) that is easy to reproduce in any language:
- Object keys are sorted by UTF-16 code units (plain ASCII order for receipt keys).
- No insignificant whitespace.
- Strings are escaped exactly as JavaScript's
JSON.stringifydoes: non-ASCII characters are written literally, not as\uescapes. - Numbers must be safe integers (magnitude at most 253 − 1). Monetary values are decimal strings.
- Floats,
NaN,Infinityand undefined values are rejected, not coerced, so two implementations cannot silently disagree.
{"completed_at":"2026-09-14T12:00:02.130Z","cost_unit":"micro_usd","currency":"USD","customer_cost":"4466","input_fingerprint":"0x2d9a…","input_tokens":480,"latency_ms":2130,"model":"claude-sonnet","operation":"inference.chat","output_fingerprint":"0x87b3…","output_tokens":310,"privacy_tier":"standard","project_id_hash":"0xc41e…","provider":"anthropic","receipt_id":"rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ","request_id":"req_01K58ZQ4W5V8E2R7PKD3N6A0TG","run":null,"runtime":null,"started_at":"2026-09-14T12:00:00.000Z","status":"succeeded","version":"1"}Hashing and signing
canonical = canonical_json(payload) // UTF-8 string
receipt_hash = keccak256(utf8_bytes(canonical)) // 32 bytes, 0x-hex lowercase
signed_digest = keccak256("\x19Ethereum Signed Message:\n32" ‖ receipt_hash)
signature = secp256k1_sign(signed_digest) // 65 bytes: r ‖ s ‖ vThe signature is EIP-191 personal_sign over the raw 32 bytes of the receipt hash, not over its hex string. Any Ethereum library can recover the signer: viem recoverMessageAddress({ message: { raw: hash }, signature }), ethers verifyMessage(getBytes(hash), signature), or Python eth_account with encode_defunct(primitive=hash_bytes).
Fingerprints
Receipts commit to content without containing it. Each operation gets a fresh 32-byte random salt, and the fingerprints are:
// Chat Completions and Responses
input = canonical_json({
"messages": [ normalized messages, in order ],
"tools": [ { "name": …, "description": … } ] // [] when no tools
})
output = canonical_json({
"content": generated text, // "" when the model only called tools
"tool_calls": [ { "id": …, "name": …, "arguments": … } ]
})
// Sandbox executions
input = canonical_json({ "runtime": "python" | "node", "code": source })
output = canonical_json({ "stdout": …, "stderr": …, "exit_code": integer | null })
fingerprint = sha256(salt_bytes ‖ utf8_bytes(content)) // salt: 32 random bytes per operationNormalized messages are those the gateway sent upstream: system and developer become {"role":"system","content":…}; user messages are {"role":"user","content":…}; assistant messages are {"role":"assistant","content":…,"tool_calls":[…]} with an always-present (possibly empty) tool_calls array; tool messages are {"role":"tool","content":…,"tool_call_id":…}. Content part arrays are joined into one string. For the Responses API, instructions is the first system message.
Run chains
Operations performed by a hosted agent carry a run object:
run_id: the agent run (run_…).sequence: the step position within the run, starting at 0. A tool call that fails before producing a receipt consumes a sequence number, so gaps are possible.previous_receipt_hash: the receipt hash of the run's previous receipted operation, ornullfor the first.
Because each receipt's signed payload includes its predecessor's hash, receipts cannot be removed or reordered without breaking the chain. The run record also stores a chain head that commits to every receipt in order:
head_0 = 0x0000…0000 (32 zero bytes)
head_n = keccak256(head_(n-1) ‖ receipt_hash_n)Anchoring Beta
Anchoring publishes a commitment to many receipts on Robinhood Chain in one transaction:
- Up to 1,000 receipts not yet in a batch are collected in creation order.
- A Merkle tree is built over their hashes. Leaves are
keccak256(0x00 ‖ receipt_hash); internal nodes arekeccak256(0x01 ‖ min(a, b) ‖ max(a, b)). An odd node at any level is promoted unchanged, never duplicated. - The root is submitted with
ReceiptAnchor.anchor(bytes32 root, uint256 count), which emitsReceiptBatchAnchored(batchId, root, count, timestamp, anchorer)and recordsanchoredAt(root). A root can be anchored only once. - The batch moves through
built→submitted→anchoredonce the transaction has the configured confirmations. Failed submissions are markedfailedand retried.
Domain-separated leaves and nodes prevent an internal node from being passed off as a receipt, and sorted pairs mean proofs need no left/right flags.
What receipts never contain
- Prompts, messages, system prompts or agent instructions.
- Model outputs, tool arguments or tool results.
- Sandbox source code, stdin, stdout or stderr.
- API keys, provider credentials, or raw project and organization IDs.
The stored payload is exactly what is signed, so it is safe to publish in full.
Signer trust
A valid signature proves that the holder of the signer key produced the receipt. Verifiers must also confirm that the signer is the platform's published receipt signer. The public proof page checks this against the platform's trusted signer set.
Development deployments
Local deployments without a configured signer key sign receipts with a fixed, publicly known development key so receipts stay verifiable across restarts. Such receipts carry no trust.