Skip to main content
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:
The primary type is exactly:
Two field rules matter for verifiers:
  • costUsd is an unsigned integer in 1e-4 USD units: 18420 represents $1.8420. The gateway computes that exact value before signing. The receipt never carries a floating-point number.
  • settledAt is Unix seconds from the immutable terminal outcome. The vector fixtures use 1700000000 only as a stable synthetic timestamp (the same value localnet pins as its genesis time; see Network Separation).
What V1 omits: payer, payment identifier, payment scheme, transaction hash, direct wallet address, prompt, completion, and the raw provider request ID. Payment provenance lives in the credit and settlement records (see x402 Stage A and x402 Stage B). The provider request ID appears only as a privacy-preserving 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. What GET /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 lowercase bytes32. 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 positive uint16, and it is a separate field from receiptSchemaVersion, which is fixed at 1 for V1.
  • signerAddress: the lowercase address the signature must recover to.
Verification is a registry lookup keyed first by 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 no verifyingContract 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
Capitalization is part of the preimage. 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:
  1. receipt: the EIP-712 signed UsageReceiptV1, verified against the versioned signer registry.
  2. receiptRoot: the ordered Merkle root over one builder subject’s receipts in one epoch. Leaves are keccak256(abi.encode(leafDomain, receiptDigest, subjectWallet, subjectEpoch)) under leafDomain = keccak256("TOKSCALE_RECEIPT_LEAF_V1"); interior nodes are keccak256(abi.encode(nodeDomain, left, right)) under nodeDomain = 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.
  3. 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.
  4. Usage Root: the EAS attestation carrying that receiptRoot on GIWA.
A holder presents the receipt payload and its Merkle path; the verifier recomputes every step client-side.

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
The receipt vector is verified by the Rust and TypeScript runners; the attestation vector suite (which includes the receipt digest) adds Solidity as the third implementation:
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.
usage-receipt-v1.json contains a development-only Anvil private key. It is fixed public test data, not an operational secret, and must never appear in runtime configuration.

Next steps