# Receipt Verification API — api.whitemagic.dev

Stateless verification of `continuity-receipt` bundles and anchors, revocation
distribution, and signed verification receipts. No accounts required for the
free beta keys; no content logging.

Integration guide:
[VERIFY_IN_5_MIN.md](https://github.com/lbailey94/continuity-receipt/blob/main/VERIFY_IN_5_MIN.md)
— install, offline verify, hosted verdict, signed receipt, offline check.

## Endpoints

| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/health` | keyless | liveness (`{"ok": true}`) |
| GET | `/info` | keyless | service metadata, specs, receipt issuer |
| GET | `/openapi.json` | keyless | OpenAPI 3.1 contract |
| POST | `/verify` | keyed or x402 | bundle → verdict; `?require_anchor=1`; `?receipt=1` adds a signed verification receipt; `?cache=1` opts into the digest cache; `{"bundles": [...]}` batch (≤50) |
| POST | `/verify-receipt` | keyed or x402 | verification receipt → validity (shape, consistency, signature; optional digest and revocation checks) |
| POST | `/verify-anchor` | keyed or x402 | OpenTimestamps proof + digest + header(s) → proof status |
| POST | `/anchors` | keyed or x402 | store an OTS proof for a digest |
| GET | `/anchors/<digest>` | keyless (anon cap) | fetch a stored anchor proof (ETag-aware) |
| POST | `/payment-receipts` | internal (`X-Internal-Token`) | sign + store a content-free settlement receipt (gateway-emitted on settle) |
| GET | `/receipts/<tx>` | keyless (anon cap) | fetch a settlement receipt (ETag-aware) |
| GET | `/revocations/<issuer>` | keyless (anon cap) | stored revocation document (ETag-aware) |
| POST | `/erc8004/validate` | keyed or x402 | ERC-8004 validation: bundle + task_hash → outcome (0: REJECT, 1: ACCEPT, 2: NEEDS_EVIDENCE) + signed attestation |
| POST | `/crystals` | keyed or x402 | store sealed Zero-Knowledge Memory Crystal (2 MiB max; ChaCha20-Poly1305 / AES-256-GCM) |
| GET | `/crystals/<id>` | keyless (anon cap) | fetch stored Memory Crystal (`X-Tenant-Hash` or `?tenant=`) |
| GET | `/crystals/lineage` | keyless (anon cap) | fetch DAG lineage of crystals for a tenant |
| POST | `/mcp` | discovery keyless; `tools/call` keyed or x402 | MCP surface: `verify_bundle`, `verify_anchor`, `get_revocations` |
| POST | `/conformance` | keyed or x402 | verifier submission → signed conformance report |

## POST /verify

- **Auth:** `Authorization: Bearer <key>` or `X-WM-Key: <key>`; or x402
  (`X-PAYMENT`) — a keyless call to a keyed method answers HTTP 402 with
  payment requirements. Self-serve evaluation keys from
  `https://mcp.whitemagic.dev/keys` work here too: keyd mirrors every issued
  key into both lanes' key stores (2026-09-28).
- **Body:** a receipt bundle object, `{"bundle": { … }}`, or
  `{"bundles": [ … ]}` (batch, max 50). Max **1 MiB** (413 over).
- **Response:** HTTP 200 with `{"verdict": …, "errors": […], …}`. The verdict
  is the payload — a well-formed bundle that fails verification is still HTTP
  200 with `UNTRUSTED`. 400 invalid JSON/shape · 401 missing/invalid key ·
  429 daily cap reached. Responses that pass auth carry
  `X-RateLimit-Limit` / `X-RateLimit-Remaining` / `X-RateLimit-Reset`
  (daily buckets, 00:00 UTC reset; 429s also carry `Retry-After: 86400`).
- **Verdicts:** `TRUSTED` · `PROVISIONAL` · `INSUFFICIENT_EVIDENCE` · `UNTRUSTED`.

```bash
curl -s https://api.whitemagic.dev/verify \
  -H "Authorization: Bearer $WM_API_KEY" \
  -H 'Content-Type: application/json' \
  -d @bundle.json | jq .verdict
```

### Opt-in digest cache (`?cache=1`)

Single-bundle requests may opt into an in-memory digest→verdict cache: a
repeated submission of the same bytes (same `require_anchor`) returns the
cached verdict payload with `"cache": {"hit": true, "cached_at": …,
"age_s": …}`. TTL 15 minutes, ≤256 entries, memory only — digest + verdict
payload, never content on disk. Without `?cache=1` nothing is retained.
Batch requests are not cached.

### Verification receipts (`?receipt=1`)

Adds `verification_receipt`: a signed record of the **whole verification
result** — verdict, `error_codes`, every `error` (code/detail/receipt id),
`provisional_reasons`, `insufficient_reasons`, and the `summary` — so a
holder can audit how and why the result was reached, not just what it was.
`bundle_digest` is the SHA-256 of the **JCS-canonical bytes of the bundle
object**, so any holder of the bundle can reproduce it regardless of
serialization. The result's consistency is checkable offline (`error_codes`
must equal the codes in `errors`; the verdict must be the class implied by
the errors/reasons), and the signature covers JCS canonical bytes minus
`sig`. Verify with any `continuity-receipt` implementation ≥ 0.3.3 (0.4.0 current),
or with `POST /verify-receipt`. Format, schema, and 21 vectors:
`VERIFICATION_RECEIPTS.md`; recipe: `VERIFY_IN_5_MIN.md`. The issuer DID is
published at `/info`.

## POST /verify-receipt

Body: the receipt object (or `{"receipt": {…}}`) with optional inputs —
`"bundle": {…}` (or `"bundle_bytes_b64": "…"` with the bundle's JSON bytes)
to check `bundle_digest`, and `"revocations": [ … ]` (statements in the
bundle shape) to check the issuer key's revocation status. Returns
`{"valid": bool, "errors": [...], "digest_match": bool|null, …}`. Checks:
shape, internal consistency, signature, optional digest match, optional
revocation.

## POST /verify-anchor

- **Body:** `{"proof": "<base64 OpenTimestamps detached proof>",
  "digest": "sha256:<hex>", "headers": {"<block height>": "<80-byte header hex>"}}`
  (or `"header"` + `"height"` for a single header).
- **Response:** `{"status": "verified|unverified|mismatch|invalid", "code": …,
  "detail": …, "attestations": […], "confirmed": {…}}`. Header supply stays
  caller-owned: merkle-root equality only; no proof-of-work/chain validation.

## Settlement receipts (`POST /payment-receipts`, `GET /receipts/<tx>`)

On a successful x402 settle, the gateway asks this service (loopback only,
`X-Internal-Token`) to sign a content-free record of the payment: kind
`wm.payment-receipt`, version 1, `issued_at`, `network`, `asset`, `amount`,
`pay_to`, `tx`, `path`, and the caller's bounded UA family/fingerprint. The
signature is Ed25519 over the JCS canonical bytes minus `sig` (the same rule
as verification receipts); `receipt_digest` is SHA-256 of those bytes. The
gateway records the digest on its `x402-settled` audit line, binding the log
to the artifact. Retrieval is keyless under the anon cap; storage is
idempotent per transaction hash.

## Anchors (hosting)

`POST /anchors` stores an OpenTimestamps proof for a digest (the proof must
parse and match the digest). `GET /anchors/sha256:<hex>` returns the stored
document, keyless under the anon cap, with `ETag`/`Cache-Control` and
`If-None-Match` support. Retention is operator policy; anchors are the one

Published example (2026-09-28): the lab's first-settlement attestation,
`https://www.whitemagic.dev/api/first-settle-attestation.json`
(`sha256:68a9edb9aba0216ca20fa3c070b0f698756c954c4942669cb941b29e5beb4c1c`),
is hosted here; the OpenTimestamps proof is upgraded and re-hosted daily.
opt-in storage service (verification itself remains stateless).

## POST /conformance

Body: `{"implementation": {"name", "version", "language", "url"?},
"results": {"bundles": {"<vector>": {"verdict", "codes"}},
"verification_receipts": {"<vector>": {"valid", "errors"}},
"hostile_inputs": {"<case>": {"structured", "verdict", "codes"}}}}` — the
corpus pins bundles (40), verification receipts (21), and hostile inputs
(208); `hostile_inputs` is required while the hostile corpus is deployed.
The hostile section grades structured outcomes: every pinned case must report
`structured: true` with a valid verdict and coded errors; the documented
reproductions and boundary cases additionally pin the reference verdict and
code set. The referee grades against the pinned manifests (counts and digests
in `/info`) and returns `{"report": {…}, "service": …}`: a signed
`continuity-receipt-conformance` statement binding the submission and each
corpus section by digest, with per-section matches/mismatches and a verdict
`CONFORMANT` / `PARTIAL` / `NONCONFORMANT`. Build a submission with
`tools/conformance_submit.py` from the spec repo (it runs your verifier over
the vectors and hostile corpus); the referee never executes submitted code.
400 malformed submission · 503 corpus not deployed.

## MCP surface

`POST /mcp` speaks JSON-RPC 2.0: `initialize`, `tools/list`, `tools/call`,
`ping`. Discovery methods are keyless (anonymous cap); `tools/call` requires a
key or x402 payment. Tools:

- `verify_bundle {bundle, require_anchor?, receipt?}`
- `verify_anchor {proof, digest, headers?}`
- `get_revocations {issuer}`

Server card: `/.well-known/mcp/server-card.json`.

## x402 payments

Keyed methods accept **x402 v1 on Base (USDC)** — this lane still runs v1 via
`facilitator.payai.network` (a keyless call returns HTTP 402 with `accepts`
requirements, `payTo 0x213b…7DaA8`, ~$0.01/call; send `X-PAYMENT` with the
signed authorization). **Both lanes now run x402 v2 via the
CDP facilitator** (KYT screening, gas sponsored; free tier 1,000 tx/mo then
$0.001); pay with `PAYMENT-SIGNATURE`. Settlements are recorded in the
content-free audit line. Prefer a key if you have one.

Verifier **0.4.1**; accepted bundle specs `continuity-receipt/0.1`–`/0.4`
(0.4: the offer → accept binding carried through the chain). Spec +
conformance vectors + both implementations:
`github.com/lbailey94/continuity-receipt` · PyPI `continuity-receipt`
0.4.1 · crates.io `continuity-receipt` 0.4.0.

## POST /erc8004/validate (Wedge A)

ERC-8004 is the emerging trust & validation registry standard for autonomous
agents on Base mainnet (`eip155:8453`).

- **Auth:** keyed or x402 — $0.01 USDC (5-minute task lease) or $0.50 USDC (24-hour day pass) on Base.
- **Body:** `{"request_id": "0x...", "task_hash": "0x...|sha256:...", "bundle": {...}, "registry_address": "0x..."}`
- **Task Binding:** verifies `task_hash` binds to `task_id`, `response_hash`, or the terminal receipt digest (returns 422 if mismatched).
- **Outcomes:** `1: ACCEPT` (TRUSTED), `0: REJECT` (UNTRUSTED), `2: NEEDS_EVIDENCE` (PROVISIONAL / INSUFFICIENT).
- **Response:** deterministic outcome, full verification checks, and signed EIP-712 attestation envelope ready for Base contract submission.

## Memory Crystals (/crystals, Wedge B)

Zero-Knowledge, client-side encrypted state store for sovereign agent continuity.
Host never sees plaintext or decryption keys.

- **Storage:** `/var/lib/whitemagic-hosted-api/tenant-crystals/<tenant_hash>/`
- **POST /crystals:** upload sealed crystal envelope (`wm-crystal/1.0`, max **2 MiB** ciphertext). Keyed or x402 metered ($0.01 USDC 5-minute lease / $0.50 day pass).
- **GET /crystals/<crystal_id>:** retrieve crystal. Keyless under anonymous cap; specify `X-Tenant-Hash` header or `?tenant=<tenant_hash>`.
- **GET /crystals/lineage:** retrieve DAG lineage for a tenant (`?tenant=<tenant_hash>`).
- **Ciphers:** `chacha20-poly1305` (default) and `aes-256-gcm`.
- **Isolation:** dark tenant silos; zero indexing into public recall.

## Signing-key rotation

The service signs receipts with `verify_signing.key` in its state directory.
To rotate: run `rotate-signing-key.py` on the box (as `whitemagic`) — it
publishes a self-signed revocation statement for the old DID into the
revocation store, replaces the key, and prints the new DID — then
`sudo systemctl restart receipt-api`. Receipts signed by the old key remain
verifiable; consumers that fetch `GET /revocations/<old-issuer>` see the
retirement. The new DID is published at `/info`. The weekly store backup now
also archives the api state (signing key, revocations, hosted anchors) as
`api-state-*.tar.gz`; `keys.json` is excluded deliberately — bearer tokens
are replaceable, the signing key is not.

## Keys & limits

- **Free beta keys:** request via `whitemagic.dev/contact` (mention "api beta").
- **Invoiced keys:** caps per contract; **$5.00 per 10,000 successful calls**,
  Net 15, metered from content-free audit lines (key id, method, status, bytes,
  latency). 429s and errors are never billed.
- Anonymous keyless paths are capped (shared daily budget).

## Error codes

`400` invalid JSON/shape · `401` missing/invalid key · `402` x402 payment
required · `413` body over 1 MiB · `429` daily cap reached · `500` stored
document invalid. Verifier errors are inside the 200 payload as `errors[]`
codes (`bad_signature`, `chain_break`, `offer_mismatch`, `offer_expired`, …).

## Privacy

Verification is stateless: bundles and proofs are verified in memory and never
stored or logged. `?cache=1` opts into an in-memory digest→verdict cache
(15 min, ≤256 entries, never on disk); without it nothing is retained. Audit
lines carry no content. Storage exists only for the two opt-in distribution
services (revocation documents, hosted anchors) and the service's signing
key. See `/privacy` on mcp.whitemagic.dev.
