# 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 | `/notarize` | keyed or x402 | sign caller-supplied context/prompt digests and bounded claims; does not observe the underlying context or execution |
| GET | `/notarize/<digest>` | keyless (anon cap) | retrieve the caller-claim attestation (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` | authenticated, owner-mapped principal plus normal gateway authorization/payment | store sealed Memory Crystal (2 MiB max; ChaCha20-Poly1305 / AES-256-GCM) |
| GET | `/crystals/owner-locator` | authenticated, owner-mapped principal | return the locator assigned by the explicit gateway registry |
| GET | `/crystals/<id>` | authenticated, owner-mapped principal | fetch a crystal only from the authenticated owner's scope |
| GET | `/crystals/lineage` | authenticated, owner-mapped principal | fetch DAG lineage only from the authenticated owner's scope |
| 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).
- **Crystal ownership:** x402 payment and session passes authorize billing or
  access to a metered route; they do not create or select a crystal owner.
  Crystal operations also require an authenticated gateway principal with an
  explicit registry `owner_id`. The production registry currently has three
  credential records and zero owner mappings, so existing credentials are
  denied Crystal access until an operator assigns mappings. There is no
  first-reader claim or automatic owner assignment.
- **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.

The hosted verifier runtime reports **0.5.1** at `/info` and accepts bundle
specs `continuity-receipt/0.1`–`/0.6`. Spec 0.5 and the Python/Rust 0.5.1
implementations are published; 0.6 is an experimental, unpublished spec.
These versions are independently versioned. Spec + conformance vectors +
implementations: `github.com/lbailey94/continuity-receipt` · PyPI
`continuity-receipt` 0.5.1 · crates.io `continuity-receipt` 0.5.1.

## Context notarization

`POST /notarize` signs bounded, caller-supplied context or prompt digests and
claims. The service does not observe the underlying context, prompt, agent
execution, or external events. Its Ed25519 signature binds the submitted
statement to the service signer; it does not establish that a claim is true.
`GET /notarize/<digest>` retrieves the stored statement keylessly.

## 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:** the hosted adapter derives an outcome from the core verifier
  verdict and task binding, then returns an Ed25519-signed canonical JSON
  statement. It does not produce an EVM-compatible EIP-712 signature or relay
  a transaction on chain. Issuer policy is not evaluated and anchors are not
  required for the adapter outcome. The checks `chain_integrity`,
  `signatures_valid`, `policy_compliant`, `resource_caps_respected`, and
  `anchors_verified` are reported as `null`, not successful. `termination_present` only
  checks for a receipts-list item whose type is exactly `task.termination`.
  See `/info` for the full adapter limits.

## Memory Crystals (/crystals, Wedge B)

Client-side encrypted state store for agent continuity. The host stores sealed
ciphertext and metadata; it does not receive client decryption keys or inspect
plaintext as part of the Crystal API.

- **Storage:** `/var/lib/whitemagic-hosted-api/tenant-crystals/<tenant_hash>/`
- **Envelope:** `wm-crystal/1.0` shape and AAD are retained. The authenticated
  client obtains its `tenant_hash` through `GET /crystals/owner-locator`; the
  gateway registry explicitly assigns that locator to the authenticated
  principal. Credentials without an explicit owner mapping are denied; the
  public descriptor does not expose registry counts or customer locators.
- **POST /crystals, GET /crystals/<crystal_id>, GET /crystals/lineage:** all
  require a valid gateway assertion for a registry-mapped owner. Caller
  `X-Tenant-Hash`/`?tenant=` values cannot select another owner's scope.
  Foreign or absent item IDs return a uniform not-found response.
- **Payment and ownership:** a key, session pass, or x402 payment does not by
  itself establish ownership. Credentials without an explicit owner assignment
  fail closed until an operator records a mapping.
- **Legacy data:** the owner-unknown legacy envelope is preserved in a
  root-only quarantine namespace and is not available to Crystal reads or
  lineage. It is not assigned to the first reader.
- **Caching:** Crystal responses use `Cache-Control: private, no-store` and
  vary by authentication headers. Conditional `ETag`/`304` handling occurs
  only after owner assertion validation; a `304` does not bypass auth or
  permit shared caching.
- **Ciphers:** `chacha20-poly1305` (default) and `aes-256-gcm`.
- **Isolation:** owner-scoped storage; no 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.
