UsageReceiptV1 is the narrow signed statement the gateway issues after it observes provider usage and determines the immutable pricing snapshot that applies to that request. It is not an x402 payment receipt, not a wallet attestation, and not an on-chain transaction. It is a gateway assertion: the operator signs “we saw this usage, at this price, for this request,” and anyone holding the payload can verify the signature with no Tokscale account and no RPC call.
Canonical typed data
The EIP-712 domain is exactly:costUsdis an unsigned integer in 1e-4 USD units:18420represents$1.8420. The gateway computes that exact value before signing. The receipt never carries a floating-point number.settledAtis Unix seconds from the immutable terminal outcome. The vector fixtures use1700000000only as a stable synthetic timestamp (the same value localnet pins as its genesis time; see Network Separation).
bytes32 commitment. A builder’s wallet is bound outside the receipt, in the attestation leaf (see Receipt Attestation).
The issued envelope
The typed data above is what gets signed. WhatGET /v1/receipts/:request_id returns is the issued envelope, which carries four fields beyond the signed ones:
digest: the EIP-712 hash of the typed data, as lowercasebytes32. A verifier recomputes it and rejects the envelope if it does not match.signature: 65 bytes of ECDSA. It must be canonical low-S, and a high-S or malformed value is rejected before any recovery is attempted.signerVersion: the registered key generation that issued the receipt. It is a positiveuint16, and it is a separate field fromreceiptSchemaVersion, which is fixed at1for V1.signerAddress: the lowercase address the signature must recover to.
signerVersion and then by signerAddress, so the issuer key can rotate without touching the frozen V1 schema. The published vector fixture’s Anvil signer sits at signerVersion 2, which is exactly why the production registry is versioned and excludes it.
The contract-less domain
There is intentionally noverifyingContract in the domain. V1 is an off-chain gateway assertion, not a contract call. A verifying contract would change the EIP-712 domain separator and therefore the digest of every retained signature, so contract verification belongs in a separately versioned UsageReceiptV2, never in a V1 edit.
The evolution rule is strict: field order, Solidity types, field semantics, domain name, domain version, and chain ID are fixed for V1. Any new field, changed meaning, payment binding, verifying contract, or altered decimal scale creates UsageReceiptV2 with a new domain version and a new persisted receipt schema version. A verifier selects the exact type from the persisted version and never reinterprets a V1 signature as V2. V1’s ECDSA recovery helper covers EOA signers only; a future contract-wallet signer requires an explicit ERC-1271 path.
The V1 wire identifiers
Three strings are hashed into every receipt and every proof built from it:- EIP-712 domain name:
Tokscale Gateway - Merkle leaf domain:
TOKSCALE_RECEIPT_LEAF_V1 - Merkle node domain:
TOKSCALE_RECEIPT_NODE_V1
Tokscale Gateway is not interchangeable with any other casing of the same words, and the two merkle separators are upper-case in full. A verifier that normalizes case derives a different digest and rejects valid receipts.
These replaced an earlier set of identifiers in a deliberate hard cutover rather than a versioned migration. Because the domain name is an EIP-712 input, every receipt signed under the previous name now derives a different digest and no longer validates. Those receipts were not migrated and cannot be re-signed, and receiptSchemaVersion did not change, so nothing inside a receipt distinguishes the two generations: the earlier evidence was reset, not dual-verified. Any further change to these three strings is the same breaking event and must ship as a separately versioned schema instead.
The two-layer design: why receipts are cheap
Per-request on-chain writes would scale cost linearly with traffic (1M requests per day is hundreds of dollars of gas daily even on an L2) and would put a chain write inside the request path. Tokscale splits evidence into two layers:
The signature proves issuance and integrity immediately; the epoch root proves non-retroactive tampering after anchoring. 1M or 100M requests. Same on-chain cost.
The provability chain
Any single receipt is provable down a four-step chain:- receipt: the EIP-712 signed
UsageReceiptV1, verified against the versioned signer registry. - receiptRoot: the ordered Merkle root over one builder subject’s receipts in one epoch. Leaves are
keccak256(abi.encode(leafDomain, receiptDigest, subjectWallet, subjectEpoch))underleafDomain = keccak256("TOKSCALE_RECEIPT_LEAF_V1"); interior nodes arekeccak256(abi.encode(nodeDomain, left, right))undernodeDomain = keccak256("TOKSCALE_RECEIPT_NODE_V1"). Ordering is ascending by raw RFC 4122 UUID bytes, pairs are never sorted, and an odd terminal node is promoted unchanged. - leaf: the wallet-bound membership leaf commits the receipt digest to one builder wallet and one subject epoch, so a receipt cannot move between passports.
- Usage Root: the EAS attestation carrying that
receiptRooton GIWA.
One vector set, three languages
The published fixtures lock the field order, domain, digest, ECDSA signature, and recovered signer as fixed public test data. Three independent implementations must reproduce identical bytes:- Rust (Alloy):
cargo test -p giwa-onchain --test receipt_vectors - TypeScript (viem): the web vector runner over the same JSON
- Solidity (forge):
ReceiptAttestationV1Test, which recomputes the receipt digest, leaf, root, and summary commitment on-chain
receipt-vectors:verify exits non-zero on a digest or signature mismatch between the Rust and TypeScript runners. The leaf, root, and summary-commitment checks, and the Solidity implementation, belong to attestation-vectors:verify.
Next steps
- To verify a receipt end to end yourself, see Verifying a Receipt.
- For the EAS attestation that carries the
receiptRooton-chain, see Receipt Attestation. - For the worker that signs receipts and the one that publishes roots, see The Gateway and Its Workers.
- For why the domain’s chain ID cannot tell localnet from Sepolia, see Network Separation.