# Contracts (draft for `interop-contracts`) What the four sides of the POC agree on, so that a buyer, an authority and a verifier can be built without reading the seller's code: signed artifacts, their schemas, one JWS policy, one canonicalization rule, the reason codes, the threat model with stable IDs, the freeze manifest, and vectors. Drafted on 2026-09-07 for the review of Tuesday 8 September; to be lifted into the `interop-contracts` repository once its home and licence are decided (question 10). **Published as they stand at https://provedby.dev/contracts/ since 2026-09-16**: the copy the verifier validates against, refreshed with each deploy of the verifier; the repository home and the licence stay open. There is no freeze date: the one set for 25 September was dropped on 18 September. Changes are versioned in git, the verifier reports drift against `freeze-manifest.json`, and that file keeps the release and the worker version together, which is what a rollback needs. No client library, no shared verifier, no crypto helper: the sides share these documents and nothing else. ## Start here, by role Nine of the files below are enough to build a buyer; the rest exist because the POC also proves attribution and is verified by a third party. - **A buyer against this seller** reads `jws-policy.md`, `canonicalization.md`, `schemas/request-envelope.schema.json`, `schemas/key-attestation.schema.json` and `schemas/agent-keys.schema.json` (what it signs and publishes), `checkout.schema.json`, `schemas/quote.schema.json` and `schemas/receipt.schema.json` (what it sends and gets back), `reason-codes.json` (why it was refused), and, to obtain the mandate, `authority-api.md` with `schemas/mandate-request.schema.json` and `schemas/mandate.schema.json`, or `mandate-ap2-profile.md` with `schemas/mandate-ap2.schema.json` for the AP2 v0.2 form. `vectors/positive/` is the acceptance test: what the seller issues and what it accepts. - **A guest buyer** (no introduction, no authority, no web server: a DNSSEC-signed domain and one key) reads `guest-buyer.md` and runs `guest-buyer.mjs`; the rest of the buyer's list above explains what that file does. - **An authority** reads `authority-api.md`, `schemas/mandate-request.schema.json`, `schemas/mandate.schema.json` or `schemas/mandate-ap2.schema.json`, `schemas/authority-status.schema.json`, `jws-policy.md`, `canonicalization.md`, and, to seal its chain, `schemas/checkpoint.schema.json` with `schemas/checkpoints-document.schema.json`. - **A verifier** reads everything; `schemas/scorecard.schema.json` is what it publishes. - **The rest** is what the four sides built beyond the purchase: `schemas/ledger-entry.schema.json` (attribution), `evidence-api.md`, `external-witness.md`, `threat-model.json`, `freeze-manifest.json`, `schemas/ap2/` (the reference's own schemas, vendored), `schemas/ucp-2026-08-25/` (UCP's catalog schemas, vendored) and the three scripts (`generate-vectors.mjs`, `generate-ucp-vector.mjs`, `ap2-v0.2-intermediate-hop-repro.py`). ## Status of each document | Document | What | Status | |---|---|---| | `jws-policy.md` | The header policy every signed artifact is held to | **Enforced** by the seller (R-21..R-23 are tests) | | `canonicalization.md` | JCS for ledger entries; raw payload octets for `quote_hash` | **Enforced**; `jcs-vector.json` reproduced by an independent Python implementation | | `jcs-vector.json` | Cross-implementation vector | Reproduced 2026-09-07 | | `reason-codes.json` | The vocabulary, by step, with status, entry and who raises it; since 2026-09-26 also `details`, the causes a refusal names where one code has several (D-136). `reason-codes.html` is its reading, generated by `generate-reason-codes-html.mjs`: the page every refusal's `contract` field points to | Seller's codes **live**; buyer codes raised by agent-buyer (since 2026-09-09) and authority codes by agent-authority (since 2026-09-11) | | `threat-model.json` | R-01..R-29 with expected code and entry; the not-tested list | Seller status per scenario; source IDs of the not-tested five TBD. R-28 since 2026-09-13: the verifier's test holds two heads for one height, each with a receipt from the external log | | `freeze-manifest.json` | Versions, hashes, domains, key ids, expiries, skew, offsets | **Draft, no freeze date since 2026-09-18** (the rollback anchor: release and worker version together): seller, buyer, authority and verifier values read off the live infrastructure on 2026-09-11 and 2026-09-12; rogue domain TBD | | `schemas/quote.schema.json` | Payload of `quote+jwt` | **Implemented**; the seller is tested against it | | `schemas/receipt.schema.json` | Payload of `receipt+jwt` | **Implemented**; tested | | `schemas/ledger-entry.schema.json` | Entry shape, the seller's facts per type, the buyer's facts for the two types both sides write, and what a clean purchase writes per side | **Implemented** for the seller (five types since 2026-09-09: `checkout.completed` ends a checkout and names the rail; `claimed_exercise` marks a caller's test-vector claim); also written by agent-buyer (ten types, since 2026-09-09) and agent-authority (three types, facts in `authority-api.md`, since 2026-09-11). Since 2026-09-12 `request.refused` and `payment.ambiguous` are keyed by the signer's path as well as the type, with the buyer's own `$defs`: the buyer's entries of those types validate (they did not before, the seller's facts were demanded of them), and the three live ledgers validate end to end | | `checkout.schema.json` | The checkout body | **Implemented** (`mandate_jws` read by checks 4 to 10; since 2026-09-13 `mandate_ap2: {checkout, payment}` as the other format, exactly one of the two; `exercise` optional) | | `schemas/mandate.schema.json` | Payload of `mandate+jwt` (AP2 stages) | **Implemented** on both sides: the seller verifies it (checks 4 to 10, `test/mandate.test.mjs`, `test/authority-vector.test.mjs`); agent-authority issues it at https://sealedby.dev since 2026-09-11 | | `schemas/request-envelope.schema.json` | Payload of `request+jwt` | **Implemented**: the seller verifies it (check 2); agent-buyer signs it from https://buyer.onbehalf.dev since 2026-09-09 | | `schemas/key-attestation.schema.json` | Payload of `key-attestation+jwt`, X509-SVID or JWT-SVID signed | **Implemented**: the seller verifies both forms (check 3); agent-buyer publishes the JWT-SVID form since 2026-09-09, and the X509-SVID form waits on SPIRE on its host | | `schemas/agent-keys.schema.json` | `/.well-known/agent-keys.json` on the buyer | **Implemented**: consumed by the seller, published by agent-buyer since 2026-09-09; the optional `revocations` member since 2026-09-13 | | `schemas/key-revocation.schema.json` | Payload of `key-revocation+jwt`: the thumbprints a buyer's domain no longer vouches for, signed like an attestation | **Implemented** 2026-09-13 (evening): the seller refuses a listed kid with `identity_revoked` at step 3 (R-29); agent-buyer publishes it after `identity revoke`; the verifier times the identity half of revocation (F-07b) | | `schemas/authority-status.schema.json` | `GET /mandates/` on the authority | **Implemented**: the seller calls it (check 10, fail closed); agent-authority answers it at https://sealedby.dev since 2026-09-11 | | `authority-api.md` | The authority's HTTP contract: key set, mandate request and its document, the approval page, status, revocation, ledger, checkpoints, its reason codes | **Implemented** by agent-authority (live at https://sealedby.dev since 2026-09-11; `GET /checkpoints` since 2026-09-12) and spoken by the buyer's `HttpAuthorityTransport`; v0.1, written on 2026-09-11 from what the seller and the buyer already read, with what the implementation settled folded back the same day; v0.14 (2026-09-26): the day's allowance, one new entry type (`allowance.set`) and one new reason (`allowance_exceeded`) | | `schemas/mandate-request.schema.json` | `POST /mandate-requests` body and the request document it and `GET /mandate-requests/` answer | **Implemented** by agent-buyer and agent-authority, both since 2026-09-11. Since 2026-09-13 the request may carry `format` and `cnf_jwk` and every document says its `format`; an issued document in `ap2-v0.2` also carries `ap2` (implemented by agent-authority; the buyer still sends neither) | | `schemas/scorecard.schema.json` | What the verifier publishes | **Implemented** by agent-verifier (first run 2026-09-07; published signed at https://provedby.dev since 2026-09-11; 9 of 9 PROVED since the run of 2026-09-12 13:17Z, the accountable person named on it). Since 2026-09-12 `run.instances` names the SPIFFE ID and key behind each chain and `run.checkpoints` the latest verified checkpoint and receipt per side, so the page shows them (§2, §5.10) instead of burying them in check details | | `schemas/checkpoint.schema.json` | Payload of `checkpoint+jwt`, `GET /checkpoints` | **Implemented** by the seller and the buyer (sealed per checkout and on a timer) and, since 2026-09-12, by the authority (sealed after every issuance, denial and revocation, and on a timer) | | `schemas/checkpoints-document.schema.json` | The document `GET /checkpoints` answers: the checkpoints beside their receipts or errors, the witness, the timer cadence, the signer and where its key is | **Implemented** by the seller and the buyer since 2026-09-11 and by the authority since 2026-09-12 (one shape, read by the verifier's checkpoints check for all three and by the witness emulator, which resolves the authority's key from its `jwks.json`); written 2026-09-12 from the two live documents; since 2026-09-13 the optional `external` receipt per checkpoint and `external_witness` on the document | | `schemas/checkpoint-receipt.schema.json` | What the witness returns | **Implemented**: issued by the verifier's emulator at https://provedby.dev/witness since 2026-09-11 (`emulator: true`), verified by the seller and the buyer; a transparency service may replace it after the freeze | | `external-witness.md` | The second receipt: each head registered, behind a flag, at a public transparency log none of the four parties operates; the API used, the COSE receipt as observed, what the keeper stores unverified and what the verifier checks (inclusion, signature, consistency with the log's tree head) | **Implemented on the three keepers 2026-09-13** (`external` beside each checkpoint, `external_witness` on the document) and verified by agent-verifier the same day; `vectors/external-receipt.json` is one registration as the log answered it | | `mandate-ap2-profile.md` | The map for the second mandate format: AP2 v0.2 roles, artifacts, the ten checks read from the AP2 form, and what the profile leaves out | **Built on all four sides on 2026-09-13** (written that morning against tag `v0.2.0` = commit `b4587ac`): the authority issues, the buyer closes, the seller reads, the verifier walks with the reference SDK (§6). Section 6.1 records what the reference SDK does not check | | `evidence-api.md` | `GET /evidence/{jti}` on the seller: the chains of a purchase in AP2 form, kept for the dispute window and served to an allow-listed verifier on a signed request (`evidence-request+jwt`), the same shape as the buyer's export; since v0.2 (2026-09-25) kept under the issuer and the `jti`, a guest's asked for with `?issuer=` | **Implemented** by the seller (2026-09-13, `src/evidence.mjs`, `VERIFIERS`, `EVIDENCE_RETENTION_DAYS`) and by the buyer (`agent-buyer evidence`, `AGENT_BUYER_VERIFIERS`); read by agent-verifier the same day, which asks both and requires the two copies to agree; the buyer's export file stays as the offline path | | `ap2-v0.2-intermediate-hop-repro.py` | Standalone reproduction: at that tag the reference SDK accepts `expected_aud` and `expected_nonce` on an intermediate delegation hop and checks neither, nor `iat` | **Reproduced 2026-09-13**, and the SDK's own two tests for it fail (186 passed, 2 failed); already fixed upstream in an open PR (#313), the repository runs no test CI (#325). Bounded: the SDK's chain verifier never asks for those on intermediate hops. Why the seller binds the terminal hop itself and refuses chains with intermediate hops (profile §6.1) | | `schemas/mandate-ap2.schema.json` | The AP2 v0.2 form of the mandate: the open content of the two roots the authority issues, the closed content of the two hops the buyer adds, how each is bound (cnf.jwk, sd_hash, checkout_hash, payment.reference), the wire form and the receipts | **Implemented by agent-authority** (issuance with the reference SDK at commit `b4587ac`, since 2026-09-13) **and by the seller** (`src/ap2.mjs`: RFC 9901 processing, checks 4 to 10, the two receipts and the Error receipt, since 2026-09-13; held to the reference-built chain vector and to 34 wrong chains in `test/ap2.test.mjs`). The buyer's own hop encoding is C5 | | `schemas/ap2/` | The reference's own JSON schemas, vendored unchanged from `google-agentic-commerce/AP2` at tag `v0.2.0` with their `$id` kept (six mandate and receipt types, their shared types, the UCP Checkout types) | Copied 2026-09-13; see the README there | | `schemas/ucp-2026-08-25/` | UCP's own JSON schemas for the Catalog capability, vendored unchanged from `https://ucp.dev/2026-08-25/schemas/` with their `$id` kept (catalog search and lookup, every schema they reference, the Error Response) | Copied 2026-09-25; every catalog answer of the seller is validated against them (`test/ucp-catalog.test.mjs`, `ucp-profile.md` §8); see the README there | | `guest-buyer.md` | The guest tier: a buyer's keys in a DNSSEC-validated TXT record set at `_agent-keys.`, a mandate its own domain signs, and what a guest gives up (ceiling, a life of minutes, revocation by TTL, a daily count, the anonymous search budget) | **Implemented by the seller 2026-09-18** behind `GUEST_BUYERS` (`src/guest.mjs`, `test/guest.test.mjs`); v0.1. The record name and tags are this contract's convention, not a standard | | `guest-buyer.mjs` | The guest buyer in one file, no dependencies: key, record, check, search, buy, trace | Runs as its own process against the seller in `test/guest-reference.test.mjs`; shares no code with it | | `ans-verification.schema.json` | ANS verification response (pre-v0.2) | Kept | | `vectors/positive/` | A quote, a receipt and a ledger as the seller issues them; a request envelope, a key attestation and a cart mandate as the seller accepts them; a cart and an intent mandate as agent-authority issued them (`mandate-authority*.json`, 2026-09-11); the open Checkout and open Payment Mandate of one AP2 v0.2 approval as agent-authority issued them with the reference SDK (`mandate-ap2-checkout.json`, `mandate-ap2-payment.json`, 2026-09-13: the root with its disclosures, the resolved content, its `sd_hash`, the request document with both forms, the chain); and the two chains a seller receives, those roots closed by hops the reference SDK built with a throwaway buyer key (`mandate-ap2-chain.json`, 2026-09-13, the buyer's private key inside on purpose, for negative tests), and the same roots closed by agent-buyer's own code (`mandate-ap2-chain-buyer.json`, 2026-09-13, generated by `agent-buyer/scripts/ap2-chain-vector.py`, same `closed` content): both accepted by the seller's checks 4 to 9 in `test/ap2-vector.test.mjs`, and every wrong presentation of either refused there; each with the verifying key. Since 2026-09-18 `guest-purchase.json`: one guest purchase as `guest-buyer.mjs` encoded it and the seller accepted it (the record, the mandate, the checkout body and its envelope), replayed by `test/guest-reference.test.mjs`. Since 2026-09-24 `ucp-closed-chains.json` (written again on 2026-09-25, its quote found through the catalog and its platform profile declaring the catalog search, as the buyer's does): one purchase closed over UCP (`ucp-profile.md` §4, §7), written by `generate-ucp-vector.mjs` with keys made for it alone: the authority's key and the two roots as agent-authority issues them, the buyer's key and platform profile, the seller's instance key (kid its RFC 7638 thumbprint), the checkout the seller answered for the session with its `ap2.merchant_authorization`, the two chains as the buyer sends them on completion (nonce the session id, `checkout_jwt` the merchant authorization reattached) with the completion body, and `expected`, what a verifier derives (the nonce, the checkout hash, the transaction id, aud, the quote, the amount and currency, and the digests the ledgers name); the buyer's and the seller's private keys inside on purpose, for the variants each side derives. The seller completes it in both shapes of check 7 and refuses another session's nonce and another key's merchant authorization (`test/ucp-vector.test.mjs`); agent-buyer holds its `bind()` reading of the session and its `ap2.close` over the same roots to it (`tests/test_ucp_vector.py`); agent-verifier's reference opinion walks it and derives its negative variants from it (`tests/test_ap2_reference.py`) | **Seller-repo-generated**, except the authority's. The independent encoding exists on the other side: `agent-buyer/vectors/checkout-envelope.json` (Python) is accepted by the seller's checks 2 to 9 (`test/cross-implementation.test.mjs`). The seller verifies every vector here; the buyer verifies the six seller vectors and the verifier four of them plus the ledger; neither reads the authority's yet. All three read `ucp-closed-chains.json`, whose roots and hops the seller's script writes the way the authority and the buyer write theirs, each side's test holding its own code to them | | `vectors/negative/` | R-21, R-22, R-23: token, key set, expected code, and what the seller observed | Generated and replayed by the seller's tests | ## How the seller is held to this folder `test/contracts.test.mjs` validates every quote, receipt and ledger entry the seller produces against the schemas here, checks that every reason code the request path raises is documented, replays the negative vectors and verifies the positive ones under the keys they carry. `node contracts/generate-vectors.mjs` regenerates the vectors with throwaway keys, and `node contracts/generate-ucp-vector.mjs` the UCP one (`ucp-closed-chains.json`), which `test/ucp-vector.test.mjs` replays through the UCP face at the moment its chains were closed. ## Numbers a stranger needs Clock skew tolerance 60 s everywhere; request timestamp window 60 s, nonces kept 120 s; quote validity 600 s in CI (demo profile set from the rehearsed approval time); mandate validity 300 s (the authority's default); `jti` rows kept 30 days past `exp` plus skew; refusal budget 10 per minute per verified subject, 2 per unverified source and 10 for all unverified sources together; anonymous searches (no credential at all, allowed since 2026-09-14) 40 per UTC day and 120 per UTC month since 2026-09-15 (10 and 60 before; sized at half the flight provider's free quota of 250 a month), over which `search_budget_exhausted` (429), never applied to an authorised search; bodies up to 65536 bytes; flight date = today plus 30 days computed at run time. All in `freeze-manifest.json`. ## Reading a ledger without reading the code Added 2026-09-09, after a reviewer pasted a live `/ledger` into a language model and was told the checkout "broke every time". Nine had completed; every refusal was a vector fired on purpose. Three rules now, all in `ledger-entry.schema.json` and served by the seller as `reading` beside the events: 1. A checkout **ends** in `checkout.completed`, whose `facts.settlement` names the rail and what "settled" means on it (`simulated`; `intent_created` and never confirmed; or, on `stripe-test` since 2026-09-12, `captured` with a test card in test mode, `charged: true`, never real money). `payment_intent.created` is step 11, not the end. 2. A refusal is the merchant working. `facts.reason` and `facts.step` say which check. 3. A caller may send `exercise: "R-06"`; the seller records it as `facts.claimed_exercise` in every entry the call writes. It is the caller's **claim**, read by no check: a request labelled as a test is refused exactly like one that is not. The rehearsal and the verifier's vectors send it; a customer never would. The artifact for a third party is the verifier's signed scorecard, with the ledger behind it as evidence. The raw chain is proof of sequence and authorship, not a verdict. ## What is deliberately not here A verifier of anyone else's ledger, a client for the seller, a canonicalization helper. Each side implements the documents; agreement is proved by the vectors, not by sharing code.