An agent that buys something on your behalf touches at least five organisations: the software that talks to you, the agent that shops, the wallet that holds your card, the merchant that sells, and the processor that moves the money. None of them fully trusts the others, and each one carries a different loss if the purchase turns out to be unauthorised. The Agent Payments Protocol (AP2) is, at heart, a design for splitting one purchase across those parties so that every party can check exactly the part it is accountable for, without having to trust an LLM's account of what the user wanted.
This article explains that multiparty architecture from first principles, using the v0.2 specification published at ap2-protocol.org, checked on 2026-10-01. Other pages on this site, such as the AP2 overview, use the earlier framing of Intent and Cart Mandates; v0.2 restructures the same ideas into a Checkout Mandate and a Payment Mandate, each of which can be open or closed. The ideas carry over, but the names you will implement against are the v0.2 ones, and the protocol is young enough that you should re-read the specification for the version you target.
Why a purchase needs many parties
In a card purchase today, the person at the keyboard is the cardholder, and a checkout page plus the card network's own checks are enough evidence of intent. With an agent in the middle, the entity pressing buy is a program driven by a language model that can be confused, prompt-injected or simply wrong. Every downstream party now asks a new question: how do I know a human approved this specific purchase, or at least approved rules that this purchase obeys?
AP2's answer is to give each party a signed artifact covering only its concern. The merchant needs proof that the user approved these items at this price. The wallet and network need proof that the user approved paying with this instrument. The processor needs proof that the credential it charges was issued for this checkout alone. Each check stays small and deterministic, which matters when the component producing the request is not.
The five roles
The specification defines five roles. A single company can play several of them, but the protocol treats them as separate trust domains, and so should your architecture.
| Role | Job | Agentic? |
|---|---|---|
| Shopping Agent | Discovers products, builds the checkout, assembles mandates and executes the purchase | Expected to be |
| Trusted Surface | UI trusted to obtain informed user consent before a user-signed mandate exists | MUST NOT be |
| Credential Provider | Source of payment credentials, such as a wallet; checks this agent may use them | May be |
| Merchant | Provides and completes the checkout; checks the agent was approved to buy these items | May be |
| Merchant Payment Processor | Processes the payment; checks the credential was authorised for this checkout | May be |
The specification defines agentic precisely: communication to or from the role is handled by a non-deterministic LLM. Non-agentic means deterministic code verifies authenticity and correctness, with no delegation to a model. The rule that the Trusted Surface must be non-agentic is the root of the whole design. If a model rendered the consent screen or chose what to sign, a prompt injection could make the user approve something other than what they saw. A card network, where one takes part, verifies the Payment Mandate alongside the Credential Provider and receives the Payment Receipt.
Two mandates, open and closed
A mandate is a signed statement of authorisation, carried as an SD-JWT (a selective-disclosure JSON Web Token). The Checkout Mandate secures what is being bought. The Payment Mandate secures how that checkout may be paid. Both are bound to the same purchase by one value: checkout_hash, the cryptographic hash of the Checkout JWT that the merchant issued. Because the merchant signs the checkout and the user or agent signs its hash, nobody can swap in a different cart or price after approval without breaking the binding. The specification requires the Checkout JWT to use a non-deterministic signature scheme such as ECDSA rather than a deterministic one such as Ed25519.
Each mandate comes in two forms. A closed mandate is bound to one specific checkout. An open mandate carries constraints instead of a checkout, such as a price ceiling or an allowed merchant, plus the agent's public key in a cnf claim, which says which key may later sign closed mandates under it. Mandate types are versioned by their vct claim, and implementations must match the exact string, which matters when several parties upgrade at different times.
Direct mode, step by step
- The Shopping Agent asks the Merchant for a checkout and receives a closed Checkout JWT, signed by the merchant, listing items, prices and total.
- The agent builds the content of the Checkout Mandate and Payment Mandate, both containing the checkout hash.
- The agent hands them to the Trusted Surface. The user sees the actual checkout, approves it, and the surface signs with the user's key.
- The agent sends the Payment Mandate to the Credential Provider and, if applicable, the network. They verify it and return a payment credential.
- The agent gives the Merchant the payment credential and the Checkout Mandate. The Merchant recomputes the checkout hash and compares it with the
checkout_hashclaim. - The Merchant initiates payment with its processor, which checks that the credential is scoped to this checkout. The specification suggests carrying the closed Payment Mandate inside the credential to make that check possible.
- The Merchant returns a Checkout Receipt to the agent; the processor returns a Payment Receipt to the agent, the Credential Provider and, where applicable, the network.
Autonomous mode: pre-approval with a leash
When the user is not present, for example buy concert tickets the moment they go on sale under 150 dollars, there is no closed checkout to approve in advance. The user instead approves open mandates on the Trusted Surface: the constraints, and the agent key allowed to act under them. Later the agent assembles a checkout on its own, signs closed Checkout and Payment Mandates with its own key, and submits both the user-signed open mandates and its agent-signed closed ones. Each verifier checks that the closed mandate satisfies every constraint of the open one. The specification also limits disclosure: the agent must present only the disclosures from the open mandates needed to evaluate the closed ones, so the merchant learns the rule that applies to it, not your whole shopping brief.
The design consequence is that constraints must be machine-checkable by the verifier from data it already has. A ceiling on the total is checkable because the total is in the merchant-signed checkout. The user wants good seats is not. Here is a sketch of the merchant-side check; the hash function and constraint shapes are this article's choices, since v0.2 does not name them:
import hashlib, json
# Sketch only. AP2 v0.2 does not name the hash algorithm for checkout_hash, and the
# constraint shape below is this article's invention; use what your spec version defines.
def h(checkout_jwt: str) -> str:
return hashlib.sha256(checkout_jwt.encode("ascii")).hexdigest()
def verify_checkout_mandate(closed_cm, checkout_jwt, open_cm=None):
verify_agent_authorization(closed_cm) # signature, key binding, expiry, exact vct
if closed_cm["checkout_hash"] != h(checkout_jwt):
raise Reject("checkout changed after it was approved")
if open_cm is not None: # autonomous mode
checkout = decode_verified(checkout_jwt) # the merchant's own signed checkout
for rule in open_cm["constraints"]: # only the disclosed constraints
if not satisfies(checkout, rule):
raise Reject(f"constraint failed: {rule['type']}")
return "accept"
def satisfies(checkout, rule):
if rule["type"] == "max_total":
return checkout["total"]["currency"] == rule["currency"] and \
checkout["total"]["minor_units"] <= rule["minor_units"]
if rule["type"] == "merchant_in":
return checkout["merchant_id"] in rule["allowed"]
return False # unknown constraint: fail closedNote the last line: an unknown constraint fails closed. A verifier that ignores a rule it does not understand has quietly widened the user's authorisation.
Worked example: one pair of shoes
A user asks their assistant for running shoes in size 44, under 130 euros. The agent finds a pair at 119 euros and requests a checkout; the merchant returns a signed Checkout JWT with that item, 119.00 EUR and a 4.90 EUR shipping line, total 123.90 EUR. The agent builds both mandates with the hash of that JWT and the user approves on their phone's wallet screen, the Trusted Surface. The wallet, acting as Credential Provider, verifies the Payment Mandate and returns a credential scoped to this checkout.
Now suppose the merchant's pricing service reprices shipping to 6.90 EUR between approval and submission and reissues the checkout. The new JWT hashes differently, so the merchant's own verification rejects the mandate. That is the protocol working: the user approved 123.90, not 125.90, so the answer is a new approval, never a quiet retry. In autonomous mode under a 130 EUR ceiling, the agent could re-sign for 125.90 on its own, which shows that the ceiling is the real authorisation.
Deployment topology and keys
Map roles to deployable services and each gets a small set of keys and a small verification job. The Trusted Surface holds, or can invoke, the user's signing key, ideally hardware-backed on the device, and runs no model. The Shopping Agent holds an agent key whose public half the user binds into open mandates; treat it like a service credential, rotate it, and never let the LLM read it, since the model should call a signing tool that enforces policy. The Merchant holds the key that signs Checkout JWTs and runs a deterministic verifier in front of order creation. The Credential Provider and processor run their own verifiers.
Log hashes and decisions, not contents. Each verifier should record the mandate's hash, the checkout hash, the vct, the key identifier and the decision, which is enough to reconstruct a dispute without spreading cart contents or personal data into every log store. Receipts close the loop: each carries a reference to the hash of the closed mandate it settles, so a dispute can recompute the checkout hash independently, check the Checkout Receipt against the closed Checkout Mandate and the Payment Receipt against the closed Payment Mandate. The AP2 receipts article covers what to retain.
Where multiparty flows break
- Checkout drift. Prices, tax or stock change between approval and submission. The hash check rejects it; surface the rejection to the user as needs re-approval, not as a payment failure.
- Split-brain completion. The processor authorised, but the merchant timed out before returning the Checkout Receipt. The agent must retry with the same idempotency key, never a new mandate, or the user pays twice. See idempotency in AP2.
- Version skew. One party upgrades a mandate type and emits a new
vctstring; exact matching means every verifier that has not upgraded rejects it. Accept both versions during a migration window, and emit the new one only after all counterparties accept it. - Over-disclosure. An agent that forwards every disclosure of an open mandate leaks the user's intent to parties who do not need it, contrary to the specification's rule.
- Agent key compromise. In autonomous mode a stolen agent key can sign closed mandates up to the open mandate's constraints. Short expiries, tight ceilings and revocation keep the loss bounded.
- Agentic consent. A consent screen generated or paraphrased by the model breaks the Trusted Surface rule. Render the merchant-signed checkout fields directly.
Many merchants, one basket
A mandate binds to one merchant's checkout, and agent-to-agent delegation is out of scope in v0.2. A basket spanning three merchants is therefore three checkouts, three pairs of mandates and three receipts, and nothing makes them atomic. If the user needs all or nothing, the agent owns a saga: buy each part with a stable idempotency key, and refund the successes if one part fails.
# One basket, three merchants = three independent checkouts. Nothing in the protocol
# makes them atomic, so the agent owns the saga.
results = []
for merchant, items in basket.by_merchant():
key = idempotency_key(user_id, basket.id, merchant) # stable across retries
try:
results.append(purchase(merchant, items, key)) # one CM + one PM each
except Reject as e:
results.append(Failed(merchant, e))
if any(isinstance(r, Failed) for r in results) and basket.all_or_nothing:
for r in results:
if isinstance(r, Succeeded):
request_refund(r.checkout_receipt, reason="basket incomplete")
report_to_user(results) # never silently partialTwo things are often confused with this. One checkout paid out to several parties, such as a marketplace seller, the platform fee and tax, is a single AP2 purchase; the split happens after payment, in the merchant's settlement, and is covered in AP2 split payments. Holding funds until delivery is a separate instrument, covered in AP2 escrow. Keep authorisation, distribution and conditional release apart in your design.
What to do next
- Decide which of the five roles you play, and draw each as a separate trust domain even if one team runs several.
- Pin the specification version, and build a verifier that matches
vctstrings exactly and fails closed on unknown constraints. - Put a deterministic verifier in front of every state change: order creation for merchants, credential issuance for wallets, capture for processors.
- Make the Trusted Surface render merchant-signed fields directly, with no model in the path.
- Keep agent keys out of model context; expose signing as a policy-checked tool, and rotate keys.
- Log hashes, key identifiers and decisions; retain mandates and receipts for your dispute window.
- Use one idempotency key per checkout and test the processor-succeeded, merchant-timed-out case explicitly.
- For multi-merchant baskets, write the compensation path before the happy path.