SPUR-IC receipt format Spec v1
SPUR-IC ("Inference Contract") is the over-the-wire receipt protocol used between a DAIT-chain tenant and a host running an x/state_channel session. HTTP framing patterns from x402 and RFC 8555 (ACME), payloads domain-specific to verifiable compute under TEE attestation.
The goal: every inference response carries enough cryptographic evidence that the tenant can independently (a) confirm the host ran the agreed-upon model inside an attested TEE, (b) bill against channel escrow without trusting the host, and (c) build a court-admissible receipt chain that can be replayed on-chain in a dispute.
1. Transport
SPUR-IC rides on top of any HTTP/1.1, HTTP/2, HTTP/3, or WebSocket transport. TLS is REQUIRED in production but is NOT a substitute for the SPUR-IC signature; the host's tee_pubkey is the trust anchor, not the TLS leaf.
For server-streamed inference (SSE, chunked JSON, WebSocket frames) the final framing message MUST contain the headers below. Intermediate chunks MAY repeat them but only the last is authoritative.
2. Required headers
| Header | Value |
|---|---|
X-DAIT-Version | 1 |
X-DAIT-Channel-Id | 64-char lowercase hex of the 32-byte channel ID |
X-DAIT-Receipt | base64url (no padding) of the canonical-encoded Receipt |
X-DAIT-State | base64url of the latest ChannelState, co-signed by the user if turn >= 1 |
Content-Type | application/json or text/event-stream |
Optional headers
| Header | Value |
|---|---|
X-DAIT-Tee-Quote-Type | nras-jwt | tdx | sev-snp |
X-DAIT-Model-Id | echoes ChannelState.model_id for log readability |
X-DAIT-Call-Seq | decimal string of Receipt.call_seq |
X-DAIT-Settle-Hint | cooperative | dispute-pending |
3. Receipt schema
interface Receipt {
channel_id: Uint8Array; // 32 bytes
call_seq: number; // monotonic per channel, starts at 1
request_hash: Uint8Array; // 32 bytes; sha256(canonical(prompt || params || nonce))
response_hash: Uint8Array; // 32 bytes; sha256(canonical(model_output))
model_hash: Uint8Array; // 32 bytes; identifier of weights actually loaded
tokens_in: number;
tokens_out: number;
price_uDAIT: bigint; // total cost of THIS call in uDAIT
tee_quote: Uint8Array; // raw bytes of the attestation quote
tee_pubkey: Uint8Array; // 33-byte secp256k1 compressed pubkey bound in REPORTDATA
timestamp: number; // unix milliseconds
sig_host: Uint8Array; // 64 bytes; secp256k1 over canonical_receipt_minus_sig
}
request_hash and response_hash MUST be computed over the canonical JSON form of the application-level payload. The host's adapter is responsible for stable JSON canonicalization (RFC 8785 / JCS) prior to hashing.
4. ChannelState schema
interface ChannelState {
channel_id: Uint8Array; // 32 bytes, matches Receipt.channel_id
host_addr: string; // bech32 dait1...
user_addr: string; // bech32 dait1...
model_id: string; // free-form, lower-kebab; e.g. "qwen2.5-32b-instruct"
max_calls: number;
deadline: number; // unix milliseconds
escrow_amount: bigint; // uDAIT locked at channel open
spent_amount: bigint; // sum of Receipt.price_uDAIT seen so far
call_count: number; // length of receipts merkle tree leaves
receipts_merkle_root: Uint8Array; // 32 bytes; sha256-merkle of canonical receipts
turn_num: number; // 0 = initial state at open; ++ each cosign
sig_user: Uint8Array; // 64 bytes (empty until user cosigns turn)
sig_host: Uint8Array; // 64 bytes
}
A state with sig_user.len === 0 is provisional. The user MUST counter-sign before the state can be presented in a dispute.
5. Canonical encoding
Two equivalent encodings MUST be supported by every implementation:
- Wire-canonical: deterministic protobuf. Field ordering by tag number ascending, no unknown fields, no default-value fields emitted, varints in shortest form. Schemas live in
dait-chain/proto/state_channel/v1/. - Diagnostic-canonical: JCS (RFC 8785) JSON. Used for logs and signed payload hashing of the application-level request / response.
Implementations SHOULD use protobuf for X-DAIT-Receipt / X-DAIT-State header values. JCS is permitted only for non-header diagnostic copies.
6. Signature scheme
- Curve: secp256k1 (matches Cosmos SDK + EVM)
- Hash: sha256
- Encoding: 64-byte raw
r || s(no recovery byte; pubkey is given intee_pubkeyforReceipt, recoverable from bech32 addr forChannelState)
Domain separation:
Receipts: sig_host = secp256k1.sign(sha256("DAIT-RCPT-v1\x00" || canonical_receipt_minus_sig))
User cosign: sig_user = secp256k1.sign(sha256("DAIT-STATE-v1\x00" || canonical_state_minus_sigs))
Host on state: sig_host = secp256k1.sign(sha256("DAIT-STATE-v1\x00" || canonical_state_minus_sigs))
The leading domain tag is mandatory: it prevents any cross-protocol re-binding between Receipts and ChannelStates.
7. Verification rules (client-side)
For every inbound response a tenant MUST:
- Parse
X-DAIT-Receipt, decode protobuf. - Reject if
channel_iddoes not match the open channel. - Reject if
call_seqis not exactlylast_seen + 1. - Verify
sig_hostagainsttee_pubkey. - Verify the TEE quote (
tee_quote) chains to a root the tenant trusts AND that the quote's report-data field hashes totee_pubkey. - Recompute
request_hashfrom the request the tenant just sent; reject on mismatch. - Recompute
response_hashfrom the body bytes; reject on mismatch. - Update local
ChannelState: bumpcall_count, addprice_uDAITtospent_amount, append receipt hash to the merkle tree, incrementturn_num, sign, shipX-DAIT-Stateback on the next request.
Any failed step is a non-recoverable fault. The tenant SHOULD checkpoint the last good state, persist the offending receipt, and initiate a dispute via MsgChallengeChannel.
8. Versioning
X-DAIT-Version is the wire version. Major bumps when canonical encoding or signature domain tags change. Implementations MUST refuse versions they do not understand rather than silently proceeding.
9. Reference implementations
- TypeScript:
dait-sdk/packages/core/src/receipt.ts - Python:
dait-sdk/python/dait_sdk/types.py
Both expose a verifyReceipt(headers, requestBytes, responseBytes, channelState) function that returns either an updated ChannelState or a typed error.
See also
- x/state_channel for the on-chain side
- x/tee_attest for what makes
tee_quotetrustworthy - TypeScript SDK + Python SDK for usage