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.

Experimental, and the verifying half onlyThe gateway verifies mandates. It holds no signing key and issues no Checkout Receipt.

What a verified mandate proves

  1. A configured issuer signed it, with a key that issuer declared.
  2. It has not expired, and was not issued in the future.
  3. It is addressed to this merchant.
  4. It binds a checkout document the merchant signed.
  5. That document authorises the resource, input, price and rail in front of us.
  6. It has not been spent before.

Set it up

  1. Install the peers
    bash
    npm install @devlab.group/agent-commerce jose @sd-jwt/core canonicalize
  2. 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, no x5u. A JWK carrying private material is refused at load.

  3. 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.

  4. 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:

json
{ "method": "ap2", "payload": "<the SD-JWT presentation>" }
SurfaceCarrier
HTTPAgent-Authorization header, base64url of that JSON (max 8192 bytes)
MCPthe reserved _authorization tool argument
A2Athe reserved _authorization input field

Reserved fields are stripped before validation, so _authorization never reaches your backend and never enters the input hash.

Errors

CodeHTTPWhen
AUTHORIZATION_REQUIRED403the resource requires a mandate and none was presented
AUTHORIZATION_INVALID403signature, trust, binding, time or purchase mismatch
AUTHORIZATION_REPLAYED409the mandate is good, and already spent
AUTHORIZATION_PROVIDER_UNAVAILABLE503our 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.