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. Its time claims, when present, show that it has not expired and is not issued or valid only in the future.
  3. Any audience it names is this merchant.
  4. It binds a checkout document the merchant signed.
  5. That document authorizes the resource, input, price and rail in front of us.
  6. It has not been spent before.

Mandate shape

AP2's Trusted Agent Provider signs the SD-JWT. After resolving disclosures, the verifier reads the mandate from the single object in delegate_payload. The token header must name a configured kid. The mandate itself carries vct: mandate.checkout.1, the compact checkout_jwt and its checkout_hash.

AP2 makes the token's aud, iat and exp optional, so the verifier checks them only when present unless you set requireMandateAudience or requireMandateExpiry. The checkout JWT always requires an audience and expiry. A token without iss is matched by kid alone. These are refused as unsupported_mandate_type:

  • mandate claims at the top level, without delegate_payload;
  • presentations with a KB-JWT, ~~-joined delegation chains and a delegation hop presented alone;
  • mandates carrying cnf, since the gateway accepts no KB-JWT to prove the holder key;
  • a closed mandate carrying constraints, which only open mandates use and the verifier does not evaluate.

Set it up

  1. Install the peers
  2. Trust the issuers
    yaml
    authorization:
      ap2:
        enabled: true
        specVersion: "0.2.0"
        mode: direct
        clockSkewSeconds: 60
        requireMandateAudience: false   # true refuses a token without aud
        requireMandateExpiry: false     # true refuses a mandate without exp
        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. Give mandate issuers distinct kids: a token without iss whose kid two issuers share is refused.

  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: { symbol: 'ETH' },          // hashed with RFC 8785 (JCS)
      amount: '0.01',                    // a string, from your own catalog
      currency: 'USDC',
      paymentMethod: 'x402',             // or 'mpp': the rail that will settle
      destination, network, asset,       // the payTo, network and asset of the offer to pay
    });

    With several x402 offers, choose one before creating the JWT. The gateway checks the mandate against the offer the proof selects, so approval for USDC on Base does not authorize USDC on Solana or NEAR. Copy Solana and NEAR addresses exactly; only EVM addresses compare case-insensitively. Keep amount and currency as the resource's price, even when the payment records the settled token.

    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. AP2 v0.2.0 defines no transport carrier, so all three are this gateway's own; a mandate under another key, such as UCP's ap2.checkout_mandate, is not read. With the A2A x402 extension, the payment message can carry the mandate when the first message did not.

Errors

CodeHTTPWhen
AUTHORIZATION_REQUIRED403after payment verification, the required mandate was absent
AUTHORIZATION_INVALID403signature, trust, binding, time or purchase mismatch
AUTHORIZATION_REPLAYED409the mandate is good, and already spent
AUTHORIZATION_PROVIDER_UNAVAILABLE503our verifier or store failed — retryable

With no payment proof at all, the gateway answers the 402 challenge first and does not check the mandate. After that, 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. Rejection reasons stay coarse, such as purchase_mismatch, so a caller cannot read a mandate by elimination.

Replay and receipts

Since 2.6.0, replay state uses the receipt database. To import a 2.5 file, keep replay.path set for the first start; see Deprecated store files. Hashing the signed token makes selective-disclosure variants collide. Different signatures over the same claims have different hashes, so reserving the checkout JWT's jti blocks those variants.

StateMeaningReusable?
reservedsettlement outcome not known yetno
consumedsettlement succeededno
releaseda failure proved no funds movedonly if no other non-released row has the same checkout jti
uncertainsettlement may have been broadcastno

Under x402's default authorization flow, backend failure releases the mandate because settlement has not started. Replay rows are not swept. Deleting rows other than released, or restoring an older backup, can make mandates reusable. Receipts retain a sha256: mandate digest, issuers and checkout ID, excluding the presentation. Keep dispute evidence elsewhere.

Once a payment is resolved, agent-commerce reconcile and manual resolution (agent-commerce resolve or POST /api/payment-attempts/:id/resolve) try to move a pending reserved or uncertain hold to consumed for a settled payment or released for a rejected one, when AP2 is enabled. Failed updates stay pending in the journal for a later retry. These transitions leave consumed and released rows unchanged and pass the reserving request ID, so a late release cannot change a later request's reservation. For orphan reserved holds without a payment attempt, agent-commerce reconcile releases those older than --min-age (default 15 minutes).resolve does not release orphans.

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, Payment Mandates (mandate.payment.1), constraint evaluation, cnf-bound keys, delegation chains and key-binding JWTs, User Credential delegation through OpenID4VP, remote key discovery, algorithms other than ES256, mandate issuance, and AP2 over ACP.