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

OutcomeReceiptstatus
Inference completed (streaming or not)Issuedsucceeded
Stream failed or client disconnected after output was deliveredIssued for the delivered portionfailed
Inference failed before any outputNone; reservation released
Sandbox execution completedIssuedsucceeded
Sandbox execution hit its timeoutIssuedtimeout
Sandbox execution reported failureIssuedfailed
Sandbox node unreachable or rejected the requestNone; reservation released
Rejected by authentication, validation, policy, rate limit, budget or balanceNone; 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 receipt summary (id, hash, signature, signer, url) and the x-receipt-id header.
  • 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.
GET/v1/receipts/{receipt_id}

Requires the receipts scope. Receipts belonging to other projects return not_found (HTTP 404).

Shell
curl https://www.hushcompute.xyz/v1/receipts/rcpt_01K58ZQ4W6N3T2B9XRC7D1M5HJ \
  -H "Authorization: Bearer $HUSH_API_KEY"
200 OK
{
  "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"
}
FieldTypeDescription
idstringReceipt ID (rcpt_…).
receipt_hashstringkeccak256 of the canonical payload.
signaturestring65-byte EIP-191 signature over receipt_hash, 0x-hex.
signer_addressstringEthereum address of the receipt signer.
payloadobjectThe signed payload. See Payload fields.
anchor_batch_idstring | nullThe anchoring batch this receipt was assigned to, or null.
urlstringPublic 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.

FieldTypeDescription
versionstringPayload schema version. Always "1".
receipt_idstringReceipt ID, rcpt_ followed by 26 Crockford base32 characters.
request_idstringThe inference request ID (req_…) or sandbox execution ID (sbxr_…).
project_id_hashbytes32keccak256("project:" + project_id). Links receipts from one project without revealing the project ID.
operationstringinference.chat, inference.responses or sandbox.execute.
statusstringsucceeded, failed, or timeout (sandbox only).
modelstring | nullGateway model ID. null for sandbox executions.
providerstring | nullUpstream provider, or sandbox-node for sandbox executions.
runtimestring | nullpython or node for sandbox executions; otherwise null.
input_tokensintegerBilled input tokens. 0 for sandbox executions.
output_tokensintegerBilled output tokens. 0 for sandbox executions.
customer_coststringAmount charged, integer µUSD as a decimal string.
currencystringAlways "USD".
cost_unitstringAlways "micro_usd".
latency_msintegerInference: request start to completion. Sandbox: execution duration.
input_fingerprintbytes32sha256(salt ‖ canonical input). See Fingerprints.
output_fingerprintbytes32sha256(salt ‖ canonical output).
privacy_tierstringstandard. private_routing and confidential are reserved.
started_atstringISO 8601 UTC with milliseconds, e.g. 2026-09-14T12:00:00.000Z.
completed_atstringISO 8601 UTC with milliseconds.
runobject | nullFor 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.stringify does: non-ASCII characters are written literally, not as \u escapes.
  • Numbers must be safe integers (magnitude at most 253 − 1). Monetary values are decimal strings.
  • Floats, NaN, Infinity and undefined values are rejected, not coerced, so two implementations cannot silently disagree.
Canonical form of the payload above (fingerprints abbreviated)
{"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

Text
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 ‖ v

The 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:

Commitments
// 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 operation

Normalized 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, or null for 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:

Chain head
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:

  1. Up to 1,000 receipts not yet in a batch are collected in creation order.
  2. A Merkle tree is built over their hashes. Leaves are keccak256(0x00 ‖ receipt_hash); internal nodes are keccak256(0x01 ‖ min(a, b) ‖ max(a, b)). An odd node at any level is promoted unchanged, never duplicated.
  3. The root is submitted with ReceiptAnchor.anchor(bytes32 root, uint256 count), which emits ReceiptBatchAnchored(batchId, root, count, timestamp, anchorer) and records anchoredAt(root). A root can be anchored only once.
  4. The batch moves through builtsubmitted anchored once the transaction has the configured confirmations. Failed submissions are marked failed and 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.