Agent side
AP2 mandates
Require proof that a human approved this exact purchase before a payment is allowed to settle.
With AP2 enabled, a paid resource can require a signed AP2 v0.2.0 Direct Checkout Mandate — proof that the human behind the agent approved this exact purchase — verified before the payment is allowed to settle. A mandate never unlocks a resource on its own and never moves money.
What a verified mandate proves
- A configured issuer signed it, with a key that issuer declared.
- It has not expired, and was not issued in the future.
- It is addressed to this merchant.
- It binds a checkout document the merchant signed.
- That document authorises the resource, input, price and rail in front of us.
- It has not been spent before.
Set it up
- Install the peers
bash npm install @devlab.group/agent-commerce jose @sd-jwt/core canonicalize - Trust the issuers
yaml authorization: ap2: enabled: true specVersion: "0.2.0" mode: direct clockSkewSeconds: 60 replay: path: ./data/ap2-authorizations.sqlite trust: mandateIssuers: # who may issue a Checkout Mandate - issuer: https://surface.example audience: merchant.example keys: - kid: mandate-2026-01 jwk: { kty: EC, crv: P-256, x: "...", y: "..." } checkoutIssuers: # who may sign YOUR checkout JWT - issuer: https://merchant.example audience: agent-commerce keys: - kid: checkout-2026-01 jwk: { kty: EC, crv: P-256, x: "...", y: "..." }Public keys only. Nothing is fetched — no JWKS, no
jku, nox5u. A JWK carrying private material is refused at load. - Require it on a paid resource
yaml resources: market_report: pricing: { type: fixed, amount: "0.01", currency: USDC } payments: [x402] authorization: required: [ap2]Requiring AP2 on a free resource is refused: there is no settlement to gate.
- Sign the checkout JWT in your own process
typescript import { createCheckoutJwt } from '@devlab.group/agent-commerce/ap2'; const jwt = await createCheckoutJwt({ privateKey, // a private JWK, or a PKCS#8 PEM kid: 'checkout-2026-01', // must match a configured key issuer: 'https://merchant.example', audience: 'agent-commerce', resourceId: 'market_report', input: { city: 'Berlin' }, // hashed with RFC 8785 (JCS) amount: '0.01', // a string, from your own catalogue currency: 'USDC', paymentMethod: 'x402', destination, network, asset, // as the 402 published them });The mandate that wraps this JWT is signed by the buyer's agent or credential provider with a key listed under
mandateIssuers.
Carrying a mandate
One envelope, three transports:
{ "method": "ap2", "payload": "<the SD-JWT presentation>" }| Surface | Carrier |
|---|---|
| HTTP | Agent-Authorization header, base64url of that JSON (max 8192 bytes) |
| MCP | the reserved _authorization tool argument |
| A2A | the reserved _authorization input field |
Reserved fields are stripped before validation, so _authorization never reaches your backend and never enters the input hash.
Errors
| Code | HTTP | When |
|---|---|---|
AUTHORIZATION_REQUIRED | 403 | the resource requires a mandate and none was presented |
AUTHORIZATION_INVALID | 403 | signature, trust, binding, time or purchase mismatch |
AUTHORIZATION_REPLAYED | 409 | the mandate is good, and already spent |
AUTHORIZATION_PROVIDER_UNAVAILABLE | 503 | our verifier or store failed — retryable |
403, not 402: the buyer's money is not the problem, and a client that auto-pays on 402 would be charged for a request that was never going to be delivered.
Rotating a key
List the new public key beside the old one under the same issuer and deploy; move the signer to the new kid; once nothing old is in flight, remove the old key and deploy again. Removing a key and restarting is the revocation.
Not implemented: autonomous mode and open mandates, intent and cart mandates, constraint evaluation, cnf-bound keys, algorithms other than ES256, and AP2 over ACP.