> ## Documentation Index
> Fetch the complete documentation index at: https://docs.usebacked.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Verify a receipt yourself

> Recompute the terms hash and check a proof bundle without trusting Backed — the full verification story, including the hash-covered field list per schema version.

Every cleared job produces a record any party can verify independently:
the terms hash pinned by the payment, content-hashed delivery proofs, the
on-chain settlement and payout transactions, and a signed proof bundle
tying them together. This page is the complete recipe. Don't trust our
dashboard — verify the chain.

## The terms hash

`termsHash = keccak256(canonical JSON of the hash-covered receipt fields)`

Canonicalization follows RFC 8785 (JCS) restricted to the types the
receipt actually uses: strings, booleans, null, safe integers, arrays,
objects. Object keys sort by UTF-16 code units; amounts are fixed
6-decimal strings; timestamps are ISO-8601 UTC with milliseconds; floats
never appear. `GET /clearing/receipts/{id}` serves the exact canonical
object under `receipt` — recompute over it and compare with the published
`termsHash`.

### Hash-covered fields, by schema version

The hash-covered set is **versioned with `schemaVersion`**. Runtime
fields — everything about the buyer, states, and post-offer timestamps —
are excluded by construction: the canonical object simply does not
contain them. In particular the canonical form has **no `buyer` key at
all** (not a `buyer` object with null fields).

**Schema `1.0`:**

```
receiptId · schemaVersion · createdAt · offerExpiresAt
seller { backedProfileId, displayName, payoutAddress }
deliverable · acceptanceCriteria[] { id, check }
deliveryProof { type } · price { amount, asset, chain }
deadline ({ type: "duration", hours } | { type: "absolute", at })
reviewWindowHours · escrow { payTo, scheme } 
fee { bps, fixed, payer, chargedOn } · verdictRules
```

**Schema `1.1`** adds, all hash-covered:

```
acceptanceCriteria[].evidence { acceptedKinds[], minClass }
evidenceFloor · releaseFlow
cure { maxRounds, reviewWindowHours, disputeClockHours, sellerFlagWindowHours }
expectedPayer            (only present when the offer is buyer-locked)
specProvenance { contentHash, ref? }   (only present on bespoke jobs)
```

Optional fields that are unset are **absent keys**, never nulls. A
verifier should hash the served canonical object as-is rather than
reconstructing it from this list; the list is what lets you audit that
nothing runtime-dependent leaked in.

## The proof bundle

`GET /clearing/receipts/{id}/proof` returns a self-contained bundle:

1. **`receipt` + `termsHash`** — recompute as above.
2. **`payment`** — look up `txHash` on Base: a USDC transfer of exactly
   `amount` from `from` to the receipt's `escrow.payTo`.
3. **`payment.authorization`** — present when Backed settled the payment
   as the x402 facilitator (null for direct transfers observed by the
   watcher). Verify it independently: recover the signer of the EIP-3009
   `TransferWithAuthorization` typed data (USDC's domain on the receipt's
   chain) and check it equals `from`; check `to`, `value`, and that the
   nonce is consumed on-chain (`authorizationState(from, nonce)`).
   Publishing the authorization is safe — a consumed nonce can never be
   replayed.
4. **`deliveries`** — for hash proofs, hash the delivered content and
   compare; for url proofs, the recorded snapshot status and content hash
   are Backed's witness record of what the URL served at post time.
5. **`resolution.payoutTxs`** — each leg's transaction on Base.
6. **`bundleHash`** — keccak256 over the canonical JSON of everything
   above (the attestation itself is excluded).
7. **`attestation`** — Backed's EIP-191 signature over `bundleHash`,
   with the signing address echoed as `attestation.signer`. Check the
   address against the published attestation address. If `attestation`
   is `null`, the bundle is served unsigned (the attestation key was not
   configured); treat it as unattested data and rely on the on-chain
   checks above, which need no signature from us.

## Reference verifier

An independent reimplementation (shares no code with our backend) lives
in the public tooling; the same \~40 lines work anywhere:

```ts theme={null}
function canonicalize(value) {
  if (value === null) return 'null';
  if (typeof value === 'string') return JSON.stringify(value);
  if (typeof value === 'boolean') return value ? 'true' : 'false';
  if (typeof value === 'number') {
    if (!Number.isSafeInteger(value)) throw new Error('float in canonical form');
    return String(value);
  }
  if (Array.isArray(value)) return `[${value.map((v) => canonicalize(v ?? null)).join(',')}]`;
  const entries = Object.entries(value)
    .filter(([, v]) => v !== undefined)
    .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
  return `{${entries.map(([k, v]) => `${JSON.stringify(k)}:${canonicalize(v)}`).join(',')}}`;
}
// termsHash = '0x' + hex(keccak256(utf8(canonicalize(receipt))))
```

Because the canonical object is served verbatim and the hash-covered set
is versioned inside it (`schemaVersion`), this verifier needs no changes
across receipt schema versions.
