# GAIP receipt, version 1 (gaip-receipt-v1)

A GAIP receipt is GAIP's signed, dated record of what it observed. This page says what is in one, how it is
signed, how to check it yourself without trusting GAIP's servers, and what a receipt does not claim.

Verifiers (MIT licence, no dependencies): [Python](/receipt-verifier/gaip_receipt_verify.py) ·
[JavaScript module](/receipt-verifier/gaip-receipt-verify.mjs) · [test vectors](/receipt-verifier/test-vectors.json).

## 1. The signature object

Every signed GAIP output carries a `signature` object:

| Field | Meaning |
|---|---|
| `alg` | Always `Ed25519` (RFC 8032). |
| `kid` | Key id: the first 16 lower-case hex characters of SHA-256 over the raw 32-byte public key. |
| `sig` | The 64-byte signature, base64url without padding. |
| `purpose` | Which digest was signed, for example `gaip.second_look.offer_sha256.v1` or `gaip.record_sha256.v1`. The full list is in the key set's `purposes`. |
| `signed_digest` | The signed SHA-256 digest, 64 lower-case hex characters. |
| `signed_message` | `<purpose>:<signed_digest>`, for convenience. |
| `keys_url` | Where the public keys are published. |

## 2. The signed message

The signed bytes are the ASCII text `<purpose>:<digest>`, for example
`gaip.second_look.offer_sha256.v1:7628fd8a…`. The purpose prefix stops a signature over one kind of digest being
reused as another.

## 3. Canonical form (how a digest is computed)

Where a digest covers a JSON body, the body is serialised as JSON with object keys sorted, no whitespace
(`,` and `:` separators), and every character outside printable ASCII (space to `~`) escaped as `\uXXXX`
(lower-case hex; UTF-16 surrogate pairs above U+FFFF). The digest is SHA-256 over those bytes. Second Look receipts
carry amounts as strings and counts as integers, so number formatting does not affect them.

**Second Look "offer seen" receipts** (`evidence_class` `OFFER_SEEN`): the digest (`offer_sha256`) covers the
whole receipt except `receipt_id`, `observation_id`, `offer_sha256`, `signature`, `signature_status` and, since
10 October 2026, `handle_hash` and `handle_hash_retention`. A verifier rebuilds it from the receipt, so any change
to what the receipt says breaks the check. `handle_hash` is the keyed hash (HMAC under GAIP's signing key, never the
handle) of the continuity handle that presented the check; it sits beside the signed body and GAIP drops it 90 days
after the receipt date (`handle_hash_retention` then says when), so the same receipt verifies before and after.
`mandate_sha256`, the keyed hash of an agent's own mandate reference, is inside the signed body.

The public read `GET /v1/free/receipts/{receipt_id}` (since 11 October 2026) withholds references to a party
(`handle_hash`, `mandate_sha256`, continuity ids and proof digests) and lists what it withheld under
`public_view.withheld`; the stored row is unchanged. A receipt bound to a mandate therefore verifies from the copy
its holder received (the whole receipt), or through `GET /v1/free/receipts/{receipt_id}/verify`, which checks the
stored row; a digest rebuilt from the public view of such a receipt will not match, and `public_view.withheld` says
why. A receipt with no party reference verifies from the public view as before.

Other purposes sign a digest GAIP already publishes (for example a History Spine `record_sha256`); a verifier
given only the signature object checks the signature over `signed_digest`.

## 4. Keys

Public keys are published at `https://www.gaipagents.com/.well-known/gaip-receipt-keys.json` (JWKS-like, RFC 8037
`OKP` / `Ed25519` members: `kid`, `kty`, `crv`, `x`, `alg`, `use`, `status`, `created_at`; a retired key also has
`retired_at` and `retired_reason`).

* `ACTIVE`: the key signing today.
* `RETIRED` with `retired_reason` `ROTATED`: replaced by a newer key on `retired_at`. Retired keys stay published, so
  older receipts still verify.
* `RETIRED` with `retired_reason` `COMPROMISED`: listed for transparency only. Do not trust its signatures.

Save the key set once and verification works offline. A receipt whose `kid` is not in the key set does not verify.
Keep the copy you saved: a key set saved before a rotation still verifies every signature made before it, and the
newest key set lists every key GAIP has signed with. A copy of the key set is also saved weekly in the Internet
Archive.

**During a rotation.** GAIP signs a receipt when it is read, over a digest it already published, so the same receipt
read after a rotation carries a signature by the new key, and one read before it carries the old key's. Both verify
against the newest key set (unless the old key is `COMPROMISED`). For a few minutes after a rotation some answers may
still be signed by the previous key; they verify for as long as that key is `RETIRED` with `retired_reason`
`ROTATED`.

## 5. Checking a receipt

1. Take the `signature` object (or a bare signature object).
2. For an `OFFER_SEEN` receipt, rebuild the digest from the receipt (section 3) and require it to equal
   `offer_sha256` and `signed_digest`.
3. Find the key whose `kid` equals `signature.kid`; refuse a `COMPROMISED` key; check the `kid` matches the key.
4. Check the Ed25519 signature over `<purpose>:<digest>`.

The verifiers return `valid` and one `reason`: `SIGNATURE_VALID`, `RECEIPT_CHANGED_SINCE_SIGNING`, `UNKNOWN_KEY`,
`KEY_COMPROMISED`, `KEY_ID_MISMATCH`, `SIGNATURE_DOES_NOT_MATCH`, `NO_SIGNED_DIGEST`, `BAD_ENCODING`,
`UNSIGNED_OR_UNKNOWN_ALGORITHM` or `NOT_A_RECEIPT`.

## 6. What a receipt does not claim

A valid signature shows that GAIP's published key signed that digest. It records what GAIP observed at the stated
time. It does **not** prove that the recorded event is true or complete, anyone's identity, ownership, value or
outcome, or that a purchase was made. It is not a certification, an audit opinion, or legal, financial or insurance
advice, and it is not a qualified electronic signature, seal or timestamp. Corrections: https://www.gaipagents.com/corrections.
