# The mandate in AP2 v0.2 form: a second format, beside the first (map, 2026-09-13) Status (2026-09-13, evening): implemented in the authority (the roots, §6, by the reference SDK), in the seller (checks 4 to 10 and the receipts, §4, own parser) and in the buyer (root verification, the closing hops, the receipts, §7 steps 4 and 6, own code); the verifier's reading (§6, last row) is pending, and so is the first purchase in this form on the live hosts. The map below was written before any code, from the AP2 v0.2 specification read in the official repository (`google-agentic-commerce/AP2`, tag `v0.2.0` = commit `b4587ac`, 2026-04-28: `docs/ap2/*.md`, `code/sdk/schemas/ap2/*.json`, the Python SDK under `code/sdk/python/ap2/`) and from the individual draft it builds on (`draft-gco-oauth-delegate-sd-jwt-00`, "Delegate SD-JWT"). Nothing here replaces `mandate.schema.json` or `authority-api.md`: the POC mandate (`mandate+jwt`) stays as it is, and this document says what a second, AP2-shaped mandate looks like, how each of the seller's checks 4 to 10 reads it, and what the profile adds or leaves out, so that the decision to build it is taken on the text and not on the name. ## 1. What AP2 v0.2 is, in the parts that touch us - Five roles: Shopping Agent (SA), Credential Provider (CP), Merchant (M), Merchant Payment Processor (MPP), Trusted Surface (TS). One entity may play several. Verification MUST happen in deterministic code whatever the role is. - Two mandate types, each open or closed: the **Checkout Mandate** (`vct: mandate.checkout.1` closed, `mandate.checkout.open.1` open), verified by the Merchant; the **Payment Mandate** (`mandate.payment.1` / `mandate.payment.open.1`), verified by the CP, the network and the MPP. The closed Checkout Mandate carries `checkout_jwt` (a **merchant-signed JWT of the checkout**) and `checkout_hash` (base64url SHA-256 of that JWT string). The closed Payment Mandate carries `transaction_id` (= the checkout hash), `payee`, `payment_amount` and `payment_instrument`. - Format: SD-JWT (RFC 9901) in a **Delegate SD-JWT chain**: a root SD-JWT signed by the issuer (the user's credential, or the Agent Provider's key in the "Trusted Agent Provider" model), whose `delegate_payload` array holds the open mandate content as one array disclosure; then a KB-SD-JWT hop (`typ: kb+sd-jwt`) signed by the key the open mandate names in `cnf`, carrying `iat`, `aud`, `nonce`, `sd_hash` over the preceding token, and its own `delegate_payload` with the closed content. Compact form: `~~~~~`. A terminal hop carries no `cnf`; an intermediate one (`kb+sd-jwt+kb`) MUST, and is delegation. - Two modes. Human Present: the user approves the closed mandates on a Trusted Surface. Human Not Present: the user approves **open** mandates with constraints and `cnf` = the agent's key; the agent closes them with the KB hop. Verifiers always receive closed mandates. - Merchant verification: process the chain; `checkout_hash` equals the hash of the Checkout JWT it sent; every constraint of the open mandate evaluated against the closed content, unknown constraint = failure; then a signed **Checkout Receipt** (`status`, `iss`, `iat`, `reference` = hash of the final SD-JWT, `order_id` or `error`). The MPP verifies the Payment Mandate and returns a **Payment Receipt**. Errors: `invalid_credential`, `unresolved_constraint`, `invalid_mandate`, `mandates_not_supported`. - Constraints defined: checkout `allowed_merchants`, `line_items`; payment `amount_range`, `budget`, `agent_recurrence`, `allowed_payees`, `allowed_payment_instruments`, `allowed_pisps`, `reference` (binds a payment to an open checkout mandate), `execution_date`. - The reference SDK (Apache-2.0, Python 3.11+, deps `sd-jwt` 0.10.4, `jwcrypto`, `pydantic`, `cryptography`) does create / present / verify of chains, constraint evaluation and receipts. Its `Checkout` type is the **UCP Checkout object** (`dev.ucp.shopping.checkout 2026-04-08`, `extra: allow`): `id`, `merchant {id, name, website}`, `line_items [{id, item {id, title}, quantity, totals}]`, `status`, `currency`, `totals [{type, amount}]`, `links`. ## 2. Roles, mapped | AP2 role | In this POC | Note | |---|---|---| | Merchant | the seller (`travel.agenthaven.dev`) | signs the Checkout JWT, verifies the Checkout Mandate, returns the Checkout Receipt | | Merchant Payment Processor | the seller, same process | verifies the Payment Mandate, creates the PaymentIntent on the test rail, returns the Payment Receipt. Declared: one entity, two roles (allowed by §Roles) | | Trusted Surface | the authority's approval page | non-agentic, as required | | Trusted Agent Provider (signer of the mandates) | the authority (`sealedby.dev`) | the "Trusted Agent Provider" delegation model, with the provider being a party separate from the agent's operator; the merchant trusts it by allow-list, as today | | Shopping Agent | the buyer (`buyer.onbehalf.dev`) | closes the open mandates with its stable key (the `cnf`) | | Credential Provider | not played | the closed Payment Mandate is presented to the merchant-as-MPP directly, as the spec allows ("one way is providing the Closed Payment Mandate inside the Payment Credential"); no network token | | User Credential / OpenID4VP | not played | the approver is not authenticated (unchanged limitation) | ## 3. Artifacts, mapped | Ours today | AP2 v0.2 | What changes | |---|---|---| | `quote+jwt` (seller-signed, ES256, payload `iss, jti, iat, exp, merchant_id, currency, total, offer_id, basis, route, itinerary, provider, correlation_id`) | `checkout_jwt`: the merchant-signed Checkout JWT, payload a UCP Checkout object | The quote payload **gains the UCP Checkout fields** beside ours: `id` (= `jti`), `merchant {id: merchant_id, name, website: iss}`, `line_items [{id: offer_id, item {id: offer_id, title, price: total}, quantity: 1, totals [{type: total, amount: total}]}]`, `status: ready_for_complete`, `totals [{type: subtotal, amount}, {type: total, amount}]`, `links: []`. Additive; `quote_hash` unchanged as a rule (SHA-256 hex over the payload octets); the card and the record do not change. ES256 is non-deterministic, as the spec requires for `checkout_jwt`. **Corrected on 13/09 by the reference type, not by reading:** `item.price` is REQUIRED (unit price in minor units, equal to the total while quantity is 1) and this map's first draft omitted it, so the object was refused; and `currency` is required by the Checkout type but is already ours, at the top level, with the same ISO 4217 meaning, so it is shared rather than added. Implemented in the seller, and the generated vector validates against the reference `Checkout` model | | intent mandate (ceiling) + cart mandate (bound to `quote_hash`), both `mandate+jwt` issued by the authority | **open Checkout Mandate** (root SD-JWT, authority key): `cnf {jwk: buyer stable key}`, `constraints: [checkout.allowed_merchants [our merchant], checkout.line_items [{id: line_1, acceptable_items: [{id: offer_id, title}], quantity: 1}]]`, `iat`, `exp` (300 s) | The person approves the exact quote on the page, as today; what the authority signs is an open mandate already narrowed to one merchant, one item, one window. Profile claims added to the open content: `iss`, `sub`, `act {sub}`, `jti`, `policy_version` (see §5) | | | **closed Checkout Mandate** (KB-SD-JWT hop, buyer key): `aud` = seller base URL, `nonce` = the quote's `jti`, `sd_hash` over the root, `delegate_payload: [{vct: mandate.checkout.1, checkout_hash, checkout_jwt (disclosed)}]` | New: the buyer builds it at checkout time | | payment stage (never issued) | **open Payment Mandate** (root, authority): `cnf`, preset `payee` (= our merchant) and `payment_amount {amount: total, currency}`, `constraints: [payment.reference {conditional_transaction_id: hash of the open checkout root}]`, `iat`, `exp`; **closed Payment Mandate** (hop, buyer): `transaction_id` = `checkout_hash`, `payee`, `payment_amount`, `payment_instrument {id, type: card, description}` | New. This is the "payment credential for settlement" the program's proposal lists and the POC does not have today. The instrument is opaque: the merchant's sandbox rail charges its own test card | | `receipt+jwt` (`quote_hash`, `payment_intent_id`, `ledger_hash`, ...) | **Checkout Receipt** JWT (`status: Success|Error`, `iss`, `iat`, `reference` = base64url SHA-256 of the final SD-JWT of the checkout chain, `order_id` or `error`, `error_description`) and **Payment Receipt** JWT (`reference` of the payment chain, `payment_id` = the PaymentIntent id) | Our receipt stays; two AP2 receipts are returned beside it when the AP2 form was presented. `typ`: `checkout-receipt+jwt`, `payment-receipt+jwt` (AP2 names none; our header policy needs one) | | `mandate_jws` in the checkout body | `mandate_ap2: {checkout: "", payment: ""}` in the checkout body | Exactly one of `mandate_jws` / `mandate_ap2` per checkout; `checkout_invalid` otherwise. Ledgers record `mandate_format: poc-v1 | ap2-v0.2` | Hashes, to be exact about them: `quote_hash` = SHA-256 **hex** over the JWS payload octets (ours, unchanged, what the ledgers join on); `checkout_hash` = base64url SHA-256 over the **ASCII of the whole compact JWS** (AP2); `sd_hash` = base64url SHA-256 over the ASCII of the preceding token including its disclosures and trailing `~` (RFC 9901 §4.3, delegate draft); disclosure digests = base64url SHA-256 over the ASCII of the disclosure string; receipt `reference` = base64url SHA-256 over the ASCII of the **closed-mandate JWT alone** (the hop's issuer JWT, no disclosures), as the reference's `get_closed_mandate_jwt` defines it and its merchant sample computes it. Corrected on 13/09: the first draft said "plus its disclosures". ## 4. The ten checks, read from the AP2 form | Check | Today (`mandate+jwt`) | In the AP2 form | |---|---|---| | 4 issuer, key by domain, signature | `iss` on `MANDATE_ISSUERS`; JWKS at `/.well-known/jwks.json`; ES256; `kid` | The root has no top-level `iss` (the SDK puts only `delegate_payload` and `_sd_alg` there): the root `kid` is looked up in the JWKS of each allow-listed issuer, the signature verified, then the open content's `iss` must name the issuer whose key verified. Chain processing: RFC 9901 §7.1 on the root (digests, no duplicate, no unreferenced disclosure, `_sd_alg` sha-256 only), exactly one disclosed delegate item, hop `typ` = `kb+sd-jwt`, hop signature under the root's `cnf.jwk`, `sd_hash` matches, `iat` present, no `cnf` in the terminal hop. An intermediate hop (`kb+sd-jwt+kb`) is delegation and is refused (R-12, as `act.act` is today) | | 5 audience | `aud` = seller base URL | hop `aud` = seller base URL, and `checkout.allowed_merchants` contains our merchant (`id` match) | | 6 merchant, action, currency, amount | fields equal to the quote | `checkout.line_items` satisfied by the Checkout JWT (offer id, quantity 1); Payment: preset `payee` = us, closed `payment_amount` = `{total, currency}` of the quote. Codes `merchant_mismatch`, `currency_mismatch`, `amount_mismatch` unchanged | | 7 quote hash | `quote_hash` = recomputed | closed `checkout_hash` = base64url SHA-256 of the stored quote JWS; the disclosed `checkout_jwt` equals the stored JWS byte for byte (so `quote_hash` matches too); Payment `transaction_id` = `checkout_hash`; `payment.reference.conditional_transaction_id` = hash of the open checkout root. Code `quote_hash_mismatch` | | 8 window | `nbf`/`exp` with skew | open `iat`/`exp` (+60 s skew); hop `iat` within the window and within 60 s of now (as the envelope) | | 9 single use, key binding | `jti` claimed in the DO; `cnf.jkt` = envelope `kid`; `act.sub` = envelope `iss` | `jti` (profile claim in the open content) claimed in the same table; the hop's signing key = root `cnf.jwk`, and its RFC 7638 thumbprint = envelope `kid` (`key_binding_mismatch`, R-05); `act.sub` = envelope `iss`; hop `nonce` = the quote's `jti` | | 10 revocation | `GET /mandates/` | unchanged; AP2 has no revocation, this is the profile's | | receipts | `receipt+jwt` | plus Checkout Receipt and Payment Receipt, `reference` per chain; on refusal a Checkout Receipt with `status: Error`, `error` mapped from our code (`invalid_credential` for checks 4 and 9 signature/binding, `invalid_mandate` for 5 to 8 and 10, `unresolved_constraint` for an unknown constraint type) beside our typed error | ## 5. What the profile adds, and what it leaves out (to be said, not hidden) Added to AP2, all as visible claims in the open mandate content (allowed: "any claim in SD-JWT-VC MAY also be used"): - `iss` (the authority), `sub` (the pseudonymous person), `act {sub}` (the buyer instance SPIFFE ID), `jti` (single use, and the handle of the status endpoint), `policy_version`. - `nonce` of the hop = the quote's `jti`: verifier-issued, unique per quote, known to both sides without a round trip. AP2 says "nonce from the verifier"; this is that. - Header policy of `jws-policy.md` on every JWT in the chain: ES256 only, `jku`/`x5u`/`jwk`/`x5c`/`crit` refused; `kid` = thumbprint on the root and on the receipts. **Corrected on 13/09 by the reference's own output:** the closing hop carries **no `kid`** (`{alg: ES256, typ: kb+sd-jwt}`), because its key is the root's `cnf.jwk` and nothing else may name it; the seller verifies the hop under that key and refuses a `kid` that names another. AP2's `x5c` root-of-trust path is not used. - The root `typ` is whatever the reference SDK emits (`example+sd-jwt` in the spec examples, from `py-sd-jwt`; to confirm on the first run); the seller accepts a fixed set `{example+sd-jwt, dc+sd-jwt, vc+sd-jwt}` for the root and exactly `kb+sd-jwt` for the hop. Left out: - Human Present as AP2 draws it (the user's own credential, OpenID4VP, Digital Credentials API). Our person approves on the authority's page and the authority signs: the "Trusted Agent Provider" model, encoded as a Human Not Present chain so that the mandate stays bound to the buyer's key (check 9). Same limitation as today: the approver is not authenticated. - A Credential Provider and a network token: the Payment Mandate goes straight to the merchant-as-MPP. The instrument is a label; the rail is the merchant's sandbox. - `pisp`, `agent_recurrence`, `budget`, `execution_date`, `allowed_pisps`, `allowed_payment_instruments`: not issued, and an open mandate carrying them is refused as `unresolved_constraint` until implemented. - Dispute retrieval by `transaction_id`, receipts to a network, mandate management UI. - The word "conformant". The sentence is "AP2 v0.2 profile: Checkout and Payment Mandates as Delegate SD-JWT chains, verified by the reference SDK and by our own code, with the additions above". ## 6. Where the reference SDK runs, and where it does not | Component | AP2 SDK? | Why | |---|---|---| | Authority (Python) | **yes**, to issue the roots (`MandateClient.create` over subclassed models carrying the profile claims); the request document gains `format` and, for `ap2-v0.2`, `mandates: {checkout: , payment: }` beside the two `mandate+jwt` | issuance is the fiddly half (salts, digests, `_sd`, `delegate_payload`); the reference library doing it is the credibility argument | | Buyer (Python) | **no** at runtime: `sdjwt.py` (the format: parse, resolve, `sd_hash`, a disclosure) and `ap2.py` (the roots verified against the transaction, the hops closed, the receipt references), on the buyer's own `jws.py`. The SDK is a test extra (`reference`), never on the host: `tests/test_ap2_reference.py` has `MandateClient.verify` walk the chains this code closes and `check_checkout_constraints` evaluate the open root against the quote's Checkout | keeps the buyer free of the SDK and its pydantic/jwcrypto weight; the SDK verifying what the buyer built is the cross-check, and the seller's vector test runs the buyer-closed chains too (`mandate-ap2-chain-buyer.json`) | | Seller (Node, zero deps) | **no**: own implementation of RFC 9901 processing, the delegate chain, the constraint evaluation for the two checkout and three payment constraint types we accept, the receipts | the independent implementation; it is the one a stranger's SDK-built chain must pass | | Verifier (Python) | **yes**, as reference opinion: `MandateClient.verify` + `CheckoutMandateChain.verify` + `check_payment_constraints` on the chains a run exports; plus its own join checks as today | "verified by the standard's own code" beside "verified by ours" | The chains are never stored in a ledger (digests and join fields only, as today). Since the afternoon of 13/09 the verifier asks the seller for them (`evidence-api.md`: `GET /evidence/{jti}` on a request signed with the verifier's report key, the seller keeping each purchase's chains for the dispute window), the real-world shape: the party that charged serves the evidence to an auditor, hash-bound to what the ledgers committed. The buyer's `--export-mandates `, read with `--mandates `, stays as the offline path; with neither, the opinion is recorded as `mandate_reference_not_run` for that purchase (counted, not judged: the scorecard has no check-level NOT_TESTED, and the join stands on the verifier's own code). Built on 13/09: `mandate_reference_verified` walks both chains as of the hop's `iat` (a mandate is good for 300 s; a run comes later), ties the root `sd_hash` to the authority's entry, the hop reference to the seller's entry and the buyer's `receipt.sealed`, `checkout_hash` to the seller's own quote JWT, verifies the two AP2 receipts under the seller's instance key, and declares the payment stage (payee and amount preset and met, instrument an opaque label). Note: the SDK writes a log file (`.logs/mandate_operations.log`) under its own install root on every operation; on the authority's VM that path has to exist and be writable, or be patched out. ### 6.1 What the reference SDK does and does not check (measured on 13/09, then checked against upstream) Installed at the tag, in a throwaway venv: `v0.2.0` = commit `b4587ac`, 2026-04-28, CPython 3.12.13, the pinned dependency set (`cryptography==46.0.5`, `jwcrypto==1.5.6`, `pydantic==2.12.5`, `sd-jwt==0.10.4`). Two caveats for the freeze manifest: the distribution version string at that tag is still `0.1`, so the manifest pins **the commit**, never the package version; and the SDK is byte-identical on `main` as of 13/09 (four commits after the tag, all documentation). Its own suite, run unchanged: **186 passed, 2 failed**, both in `code/sdk/python/ap2/tests/kb_sd_jwt_intermediate_tests.py` (`test_verify_rejects_aud_mismatch`, `test_verify_rejects_nonce_mismatch`). Cause, in `ap2/sdk/sdjwt/kb_sd_jwt.py`: `verify()` accepts `expected_aud` and `expected_nonce` for any hop but calls `common.verify_expected_claims(...)` only when `typ` is terminal, so on an intermediate hop (`kb+sd-jwt+kb`) a supplied expectation is ignored, the `iat` presence check is skipped with it, and the payload is returned. The guard and the two tests were written in the same release commit. Reproduction independent of their suite: `ap2-v0.2-intermediate-hop-repro.py`. **Upstream already knows, and the exposure is smaller than the numbers suggest.** Checked on 13/09 in the owner's repository, after the first draft of this section had called it a new finding: - PR #313 (2026-08-05, open) fixes exactly that line, with the same diagnosis; PR #325 (2026-08-10, draft) adds a CI job that runs the Python suite and names the two failures; issue #319 and PR #342 (August, open) cover the neighbouring gap on the **terminal** hop: `aud` and `nonce` are not required to be *present* when the caller supplies no expectation. All four sit behind a repository-wide lint failure unrelated to Python. The repository has **no workflow that runs pytest**, which is how a release shipped with two of its own tests red. - The SDK's chain verifier (`sdjwt/chain.py`) passes `None` for both expectations on every non-terminal hop **by design**, and `MandateClient.verify` refuses a key-bound presentation unless both expectations are supplied. A merchant verifying through the SDK's public entry points never asks for an intermediate-hop binding, so the unit bug never fires on that path; #313 changes the primitive's contract, not what a merchant observes. A final verifier cannot know an intermediate hop's audience or nonce in any case: those were agreed between the holder and its delegate. **What it changes here, in order:** 1. **Nothing on the main path.** The closed Checkout Mandate is the buyer's *terminal* hop (`typ: kb+sd-jwt`); the SDK does enforce `aud` and `nonce` on it when asked, and the verifier asks. Decision 4 (`nonce` = the quote's `jti`) stands as written. 2. **The seller's own parser binds the terminal hop itself, both ways**: presence of `iat`, `aud` and `nonce` (structural, the #342 rule, which v0.2.0 does not have) and exact match against the seller's base URL and the quote's `jti`. Not because the SDK is wrong there, but because the seller does not run the SDK, and presence-without-expectation is the gap the tag still has. 3. **Chains with an intermediate hop are outside this profile and are refused** (AP2 error `mandates_not_supported`; the reason code is fixed in C3). The profile does not define what an intermediate hop's `aud` or `nonce` would be, so "check them on every hop", which this section's first draft demanded, is not a rule anyone could implement. Refusing what is undefined is. One negative vector: a two-hop chain. 4. **The verifier's reference opinion is scoped** to what the SDK binds, the terminal hop; `mandate_reference_verified` says so. 5. **Nothing is filed upstream from here.** The fix exists (#313). The most a note from this side adds is "reproduced at tag v0.2.0", as a comment on that PR, if wanted. When #313 merges the two tests go green and the reproduction prints `REJECTED`; the manifest then names whichever commit the verifier runs. ## 7. Wire, end to end 1. Buyer `search_flights` (unchanged). The quote JWS now carries the UCP Checkout fields. 2. Buyer `POST /mandate-requests` with `format: ap2-v0.2` (new optional field; default `poc-v1`). 3. Authority: same seven steps; on approve, issues the two `mandate+jwt` **and**, for `ap2-v0.2`, the open Checkout root and the open Payment root; `mandate.issued` gains `format` and the two roots' `sd_hash`-style digests. `GET /mandates/{jti}` answers for the AP2 `jti` too (one `jti`, shared by the two roots of one approval). 4. Buyer verifies the roots (issuer key set, `cnf` = its own key, `act.sub` = itself, merchant, line item = the quote's offer, amount, window, the payment's reference to the checkout root), reads the status endpoint for the pair's `jti`, writes `mandate.received` with `mandate_format: ap2-v0.2`, `ap2_jti`, `ap2_kid` and the two `sd_hash` digests, and at checkout time closes each root with a hop (`aud` = seller base URL, `nonce` = quote `jti`, `iat` = now, `sd_hash`) and sends `create_checkout` with `mandate_ap2`. A retry after `payment_unknown` closes the same roots again: fresh salts and `iat`, same `jti`, same idempotency key (F-15). Implemented 13/09 (`agent-buyer`, `ap2.py`). 5. Seller: checks 4 to 10 as in §4; PaymentIntent; ledger entries with `mandate_format`; response with `receipt_jws` and `ap2: {checkout_receipt_jwt, payment_receipt_jwt}`. 6. Buyer verifies the three receipts (`reference` recomputed over its own chains, `order_id` = the seller receipt's `jti`, `payment_id` = the PaymentIntent), writes `receipt.sealed` with `mandate_format`, `ap2_jti`, `ap2_checkout_hash` and `ap2_references`, and, on a refusal, `request.refused` with `ap2_error` (the seller's word from its Error receipt, when that receipt verifies). With `--export-mandates DIR` the chains and the receipts go to `DIR/.ap2.json` for step 7. Implemented 13/09. 7. Verifier: joins on `jti` and `quote_hash` as today, reads `format`, and, with exported chains, runs the SDK. ## 8. Contracts and vectors - `schemas/quote.schema.json`: the UCP fields, required from the freeze on. - `schemas/mandate-ap2.schema.json`: the open and closed content of both mandates with the profile claims; the SDK's `open_checkout_mandate.json`, `checkout_mandate.json`, `open_payment_mandate.json`, `payment_mandate.json`, `checkout_receipt.json`, `payment_receipt.json` vendored under `schemas/ap2/` with their `$id` kept. - `schemas/mandate-request.schema.json`: `format`; the `issued` document with both forms. - Ledger facts, as written on 13/09 (the schema constrains the seller's entries and the buyer's refusals, and names none of these; they are listed here so the verifier joins on them): authority `mandate.issued`: `format`, `ap2_jti`, `ap2_checkout_sd_hash`, `ap2_payment_sd_hash`; seller `checkout.completed` and its refusals: `mandate_format`, `ap2_checkout_reference`, `ap2_payment_reference`; buyer `mandate.received`: `mandate_format`, `ap2_jti`, `ap2_kid`, `ap2_checkout_sd_hash`, `ap2_payment_sd_hash`; buyer `receipt.sealed`: `mandate_format`, `ap2_jti`, `ap2_checkout_hash`, `ap2_references` (its `jti` is the pair's, the idempotency key); buyer `request.refused`: `mandate_format`, `ap2_jti`, `ap2_error`. The join of an AP2 purchase is on `ap2_jti` and `quote_hash`. - `reason-codes.json`: the AP2 receipt `error` beside our codes (mapping in §4). - Vectors: positive `mandate-ap2-checkout.json`, `mandate-ap2-payment.json` (the roots as the authority issued them), `mandate-ap2-chain.json` (those roots closed by the reference SDK) and `mandate-ap2-chain-buyer.json` (the same roots closed by the buyer's own code, same shape, same `closed` content), the two chain vectors accepted by the seller's `verifyAp2` in `test/ap2-vector.test.mjs` and the buyer's hops by the SDK in the buyer's tests; negative: R-04 (`allowed_merchants` without us), R-05 (hop signed by a key other than `cnf`), R-06 (`jti` replay), R-08 (`checkout_hash` of another quote; preset amount ≠ total), R-09 (currency), R-12 (intermediate hop), R-21..R-23 on root and hop, plus `sd_hash` mismatch, unreferenced disclosure, `nonce` mismatch, `cnf` in the terminal hop, unknown constraint type. ## 9. Estimate and order Authority 1 day; seller 2 days; buyer 1 day; quote fields and schema 0.5; contracts and vectors 0.5; verifier 0.5 to 1. Order: (a) quote fields + schemas, (b) authority roots + vectors, (c) seller parser + checks against the vectors, (d) buyer hop + live purchase, (e) verifier. The Payment Mandate is in the same cut as the Checkout Mandate: it is the one line of the program's authorization layer ("a payment credential for settlement") the POC has nothing for today, and the authority issues both roots in one approval. ## 10. Decisions asked before code 1. The SDK in the authority and the verifier, not in the buyer or the seller (§6). 2. The Payment Mandate in the same cut (§9), with the instrument declared opaque. 3. The wording: "AP2 v0.2 profile", never "conformant" (§5). 4. `nonce` = the quote's `jti` (§5). 5. Installing the SDK in a virtual environment on this machine (the session's classifier refused `pip install -e .` from the cloned repository) so that the vectors are produced and checked with the reference code before the seller's parser is written.