# MAKO GUARD Receipt Spec — v1.0.0

> **FROZEN 1.0.0 — 2026-06-11.** This version is immutable: any change
> requires a new version. The SHA-256 of this document's final text is
> recorded in gates.md; an edit to a frozen version is a violation, not a
> revision. (Public publication is a separate step — Phase 1.4.) Receipts
> emitted before the freeze carry `receipt_spec_version: 1.0.0-draft` in
> their signed body and remain valid; the field structure is identical.

One receipt per payment-bearing request to a MAKO x402 route. Receipts are
the actuarial table: they anchor claims to settlements, settlements to
payers, and failures to evidence. Implementation: `services/guard_receipt.py`;
storage: `receipts` table (`services/guard_db.py`); emission:
`api/x402_server.py` receipt middleware.

## Field reference

| Field | Type | Source | Notes |
|---|---|---|---|
| `receipt_id` | string | computed | `0x` + SHA-256 of the canonical receipt body (sorted-key JSON, excluding `receipt_id`, `mako_signature`, `signer`) |
| `receipt_spec_version` | string | constant | `1.0.0` |
| `taxonomy_version` | string | constant | taxonomy in force at emission (`1.0.0-draft`) |
| `timestamp_request_ms` | uint64 | middleware | epoch ms when the request entered the receipt middleware |
| `timestamp_response_ms` | uint64 | middleware | epoch ms when the response (incl. settlement) completed |
| `endpoint_id` | string | route table | e.g. `GET /api/pulse/score` |
| `endpoint_url` | string | request | full request URL |
| `declared_schema_hash` | string | route declarations | `0x` + SHA-256 of the route's declared response schema; `""` when the route declares none (then MALFORMED_RESPONSE is not claimable per its `claimable_condition`) |
| `declared_sla` | string | route declarations | canonical JSON of the declared SLA (e.g. `{"max_latency_ms":5000}`); `""` when undeclared (then LATENCY_BREACH is not claimable, amendment d) |
| `request_hash` | string | middleware | `0x` + SHA-256 over canonical request: `METHOD\npath\nsorted-query\nbody-bytes` |
| `response_hash` | string | middleware | `0x` + SHA-256 of response body bytes; `""` if no body was produced |
| `http_status` | uint32 | middleware | status returned to the buyer |
| `quote_amount_atomic` | uint256 | payment requirements | quoted price in USDC atomic units (6 dp) |
| `settled_amount_atomic` | uint256 | settlement capture | effective settled amount; `0` when settlement failed |
| `settlement_tx_hash` | string | settlement capture | on-chain tx hash; `""` when settlement failed |
| `settlement_success` | bool | settlement capture | `false` is a SETTLEMENT_FAILURE event candidate, not a missing receipt |
| `settlement_error_reason` | string | settlement capture | `""` on success |
| `network` | string | payment requirements | CAIP-2, e.g. `eip155:8453` |
| `payer_address` | string | settlement capture | from SettleResponse.payer, falling back to the EIP-3009 authorization `from` |
| `payment_payload_sha256` | string | middleware | correlates receipt ↔ verified payment payload |
| `pulse_score_at_call` | string | probe DB | stringified int 0–100 from `score_endpoint_from_probes` (self-score of the MAKO endpoint), `""` when unavailable; 60s cache |
| `traffic_source` | string | request headers | `external` \| `self-call` \| `synthetic` (honest-data rule) |
| `synthetic` | bool | derived | `traffic_source != "external"`; synthetic rows are excluded from public stats |
| `mako_signature` | string | signer | EIP-712 signature (below) |
| `signer` | string | signer | recovered/expected signer address |
| `signature_error` | string \| null | builder | **Storage-only — never signed, excluded from `receipt_id`.** `null` on every signed receipt. When the signer is misconfigured the builder's error message is recorded here and the row is stored unsigned (`mako_signature=""`, `signer=""`); reconciliation counts any such row as a failed day. A non-null `signature_error` row is evidence of a pipeline fault, never a valid receipt |

## Signature

EIP-712 typed data, signed with the **dedicated GUARD signing key**
(`MAKO_GUARD_SIGNING_KEY`) — separate from the treasury/receiver wallet and
from the legacy route-response key (`MAKO_RECEIPT_PRIVATE_KEY`). Rotation:
[key-rotation.md](key-rotation.md).

- Domain: `{name: "MAKO GUARD Receipt", version: "1", chainId: 8453}` (Base
  mainnet). **Multichain:** a receipt for any other network requires a new
  spec version with its own domain — `chainId` is part of the signature
  domain by design; one chain, one spec version.
- Primary type `Receipt`, **verbatim and normative** — this field order and
  these types ARE the signature contract; verification requires no repo
  access beyond this document:

```json
{
  "EIP712Domain": [
    {"name": "name", "type": "string"},
    {"name": "version", "type": "string"},
    {"name": "chainId", "type": "uint256"}
  ],
  "Receipt": [
    {"name": "receiptId", "type": "string"},
    {"name": "receiptSpecVersion", "type": "string"},
    {"name": "taxonomyVersion", "type": "string"},
    {"name": "timestampRequestMs", "type": "uint64"},
    {"name": "timestampResponseMs", "type": "uint64"},
    {"name": "endpointId", "type": "string"},
    {"name": "endpointUrl", "type": "string"},
    {"name": "declaredSchemaHash", "type": "string"},
    {"name": "declaredSla", "type": "string"},
    {"name": "requestHash", "type": "string"},
    {"name": "responseHash", "type": "string"},
    {"name": "httpStatus", "type": "uint32"},
    {"name": "quoteAmountAtomic", "type": "uint256"},
    {"name": "settledAmountAtomic", "type": "uint256"},
    {"name": "settlementTxHash", "type": "string"},
    {"name": "settlementSuccess", "type": "bool"},
    {"name": "settlementErrorReason", "type": "string"},
    {"name": "network", "type": "string"},
    {"name": "payerAddress", "type": "string"},
    {"name": "paymentPayloadSha256", "type": "string"},
    {"name": "pulseScoreAtCall", "type": "string"},
    {"name": "trafficSource", "type": "string"},
    {"name": "synthetic", "type": "bool"}
  ]
}
```

  Message keys are the camelCase forms above; storage keys are their
  snake_case equivalents. Absent string values sign as `""`, absent amounts
  and timestamps as `0`, absent booleans as `false`. **Excluded from the
  signed message:** `created_at` (storage metadata) and `signature_error`
  (storage-only fault marker). **Excluded from the `receipt_id` content
  hash:** `receipt_id` itself, `mako_signature`, `signer`, `signature_error`,
  `created_at` — the id is SHA-256 over the canonical sorted-key JSON of all
  remaining fields.

### Verification rule (normative)

A receipt is valid if and only if ALL of:

1. `receipt_id` recomputes from the receipt body per the exclusion rule above;
2. `mako_signature` recovers (EIP-712, domain and types above) to `signer`;
3. `signer` appears in the **published append-only GUARD signer registry**
   ([key-rotation.md](key-rotation.md)) with a validity window covering the
   receipt's `timestamp_response_ms`. Response time is the anchor because
   signing occurs at emission, immediately after the response completes —
   `timestamp_response_ms` is the signing-time anchor.

Rule 3 is what makes rotation safe: a leaked-then-rotated key cannot forge
receipts dated outside its window. Registry windows are contiguous by
construction (each key's close is the next key's open), so every honestly
emitted receipt falls in exactly one window.

Signing is mandatory for GUARD receipts: the builder raises rather than
emitting an unsigned receipt. The emission layer records a builder failure as
a `signature_error` receipt row (loud in reconciliation), never silently
unsigned-but-stored-as-signed.

## Storage & integrity

- Receipts are stored off-chain (`receipts` table). No per-receipt on-chain
  writes. (Optional daily Merkle anchor is a Phase 1.4+ nice-to-have.)
- `receipt_id` is recomputable from the stored body; any row whose recomputed
  id differs has been tampered with.
- Synthetic/self-call receipts carry `synthetic=true` and are excluded from
  every public stat.

## Versioning

`receipt_spec_version` follows the taxonomy convention: drafts are mutable;
any post-freeze change is a new version. **At freeze**, the version string
flips `1.0.0-draft` → `1.0.0` everywhere (this document, `RECEIPT_SPEC_VERSION`
in code, and the value signed into receipts), and the SHA-256 of this
document's final text is recorded in gates.md — any later edit to a frozen
version is a violation, not a revision. **Freeze is a hard stop pending
Chris's review.**
