# AP2 Mandates
## Overview
The Agent Payment Protocol (AP2) is a cryptographic authorization framework for agentic commerce. It ensures that AI agents can only spend what the user explicitly authorized, through a chain of three signed mandates: **Intent**, **Cart**, and **Payment**.
Each mandate is a JWS (JSON Web Signature) compact serialization token, signed with ES256 (ECDSA with P-256 curve) and carrying a `mandate+jwt` type header.
---
## Mandate Types
### 1. Intent Mandate
The Intent Mandate is issued by the **buyer's agent** (or the buyer directly). It declares the maximum amount the agent is authorized to spend, the currency, and optional constraints such as allowed merchant IDs and product categories.
```json
{
"id": "mandate_intent_abc123",
"type": "intent",
"status": "active",
"issuer": "buyer_agent_001",
"subject": "buyer@example.com",
"payload": {
"max_amount": 10000,
"currency": "USD",
"categories": ["electronics", "accessories"],
"merchant_ids": ["merchant_xyz"],
"constraints": {
"max_items": 5
}
},
"signature": "",
"issued_at": "2026-02-18T10:00:00.000Z",
"expires_at": "2026-02-18T11:00:00.000Z"
}
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `max_amount` | `number` | Maximum authorized spend in minor units (e.g., 10000 = $100.00) |
| `currency` | `string` | ISO 4217 currency code (e.g., "USD") |
| `categories` | `string[]?` | Optional list of allowed product categories |
| `merchant_ids` | `string[]?` | Optional list of allowed merchant identifiers |
| `constraints` | `Record<string, unknown>?` | Custom constraints (extensible) |
### 2. Cart Mandate
The Cart Mandate is issued by the **merchant gateway** after the cart is finalized. It locks the exact line items, totals, and checkout ID into a signed token. The merchant's signature proves that these prices and quantities are genuine.
```json
{
"id": "mandate_cart_def456",
"type": "cart",
"status": "active",
"issuer": "checkout_session_789",
"subject": "buyer@example.com",
"payload": {
"checkout_id": "checkout_session_789",
"line_items": [
{
"id": "line_001",
"product_id": "gid://shopify/Product/100",
"variant_id": "gid://shopify/ProductVariant/200",
"title": "Wireless Headphones",
"quantity": 1,
"unit_amount": 4999,
"total_amount": 4999,
"type": "product"
}
],
"totals": {
"subtotal": 4999,
"tax": 400,
"shipping": 599,
"discount": 0,
"fee": 25,
"total": 6023,
"currency": "USD"
},
"merchant_signature": "eyJhbGciOiJFUzI1NiJ9..."
},
"signature": "",
"issued_at": "2026-02-18T10:05:00.000Z",
"expires_at": "2026-02-18T10:35:00.000Z"
}
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `checkout_id` | `string` | Reference to the checkout session |
| `line_items` | `LineItem[]` | Exact cart contents at time of signing |
| `totals` | `CheckoutTotals` | Exact calculated totals (subtotal, tax, shipping, discount, fee, total) |
| `merchant_signature` | `string` | Merchant's attestation that these prices are real |
### 3. Payment Mandate
The Payment Mandate is issued by the **buyer's agent** to authorize the final payment. It references the Cart Mandate by ID, specifying the exact amount, currency, and payment handler to use.
```json
{
"id": "mandate_payment_ghi789",
"type": "payment",
"status": "active",
"issuer": "buyer_agent_001",
"subject": "buyer@example.com",
"payload": {
"checkout_id": "checkout_session_789",
"amount": 6023,
"currency": "USD",
"payment_handler_id": "shopify_payments",
"cart_mandate_id": "mandate_cart_def456"
},
"signature": "",
"issued_at": "2026-02-18T10:06:00.000Z",
"expires_at": "2026-02-18T10:36:00.000Z"
}
```
**Fields:**
| Field | Type | Description |
|-------|------|-------------|
| `checkout_id` | `string` | Reference to the checkout session |
| `amount` | `number` | Exact payment amount in minor units |
| `currency` | `string` | ISO 4217 currency code |
| `payment_handler_id` | `string` | Selected payment handler from negotiation |
| `cart_mandate_id` | `string` | Reference to the Cart Mandate being paid |
---
## Signature Chain Flow
The mandate chain forms a cryptographic proof that every step of the purchase was authorized:
```
+------------------+ +------------------+ +------------------+
| INTENT MANDATE | | CART MANDATE | | PAYMENT MANDATE |
| | | | | |
| Issuer: Buyer | | Issuer: Merchant| | Issuer: Buyer |
| max_amount: 100 | ref | total: 60.23 | ref | amount: 60.23 |
| currency: USD +--------->+ checkout_id: X +--------->+ cart_mandate_id |
| categories: [] | | line_items: [...] | | handler: shopify|
| | | merchant_sig: Y | | |
+--------+---------+ +--------+---------+ +--------+---------+
| | |
| ES256 Sign | ES256 Sign | ES256 Sign
v v v
eyJhbGciOiJF... eyJhbGciOiJF... eyJhbGciOiJF...
(JWS Compact) (JWS Compact) (JWS Compact)
```
### Lifecycle
```
Step 1: User tells agent "Buy me headphones under $100"
|
+-> Agent creates INTENT MANDATE
- max_amount: 10000 (=$100)
- currency: USD
- Signs with agent's ES256 private key
Step 2: Agent searches catalog, adds items to cart, negotiates terms
|
+-> Gateway creates CART MANDATE
- Locks exact line items and totals
- Includes merchant_signature proving prices are real
- Signs with merchant's ES256 private key
Step 3: Agent reviews cart mandate and authorizes payment
|
+-> Agent creates PAYMENT MANDATE
- amount: exact match to cart total
- cart_mandate_id: reference to cart mandate
- Signs with agent's ES256 private key
Step 4: Agent calls execute_checkout with all 3 mandates
|
+-> Gateway verifies the full chain
+-> Gateway checks guardrails
+-> Gateway collects fee
+-> Gateway creates order
```
---
## Verification Process
The `MandateVerifier` performs both individual mandate verification and full chain verification.
### Individual Mandate Verification
Each mandate is verified independently:
1. **Signature Verification** -- The JWS compact token is verified against the signer's public key (local JWK or remote JWKS endpoint).
2. **Type Header Check** -- The `typ` header must be `mandate+jwt`.
3. **Mandate Type Check** -- The `type` field must match the expected mandate type.
4. **Expiry Check** -- The `expires_at` timestamp must be in the future.
5. **Status Check** -- The `status` must be `active`.
6. **Payload Validation** -- Type-specific checks:
- **Intent**: `max_amount` must be positive, `currency` must be a 3-letter ISO code.
- **Cart**: `merchant_signature` must be present, `checkout_id` must be present, `line_items` must be non-empty.
- **Payment**: `amount` must be positive, `cart_mandate_id` must be present, `payment_handler_id` must be present.
### Chain Verification
After individual verification passes, the chain is validated:
```
Chain Verification Steps:
1. Verify each mandate individually (in parallel)
- verifyIntent(intent_token)
- verifyCart(cart_token)
- verifyPayment(payment_token)
2. Cart total <= Intent max_amount
- Ensures the agent didn't overspend the user's authorization
3. Cart currency == Intent currency
- Prevents currency mismatch attacks
4. Payment amount == Cart total
- Ensures exact payment (no overpayment, no underpayment)
5. Payment currency == Cart currency
- Consistent currency throughout the chain
6. Payment cart_mandate_id == Cart mandate ID
- Ensures the payment references the correct cart
7. If Intent restricts merchant_ids:
- Cart issuer must be in the allowed list
```
### Error Handling
Verification returns a structured result, never throws:
```typescript
interface MandateVerificationResult {
valid: boolean;
mandate: Mandate | null; // Decoded mandate if valid
error?: string; // Human-readable error if invalid
}
// Chain verification
interface ChainResult {
valid: boolean;
errors: string[]; // All failed checks listed
}
```
---
## Security Model
### Cryptographic Primitives
| Component | Algorithm | Purpose |
|-----------|-----------|---------|
| Signing | ES256 (ECDSA P-256) | Sign mandates as JWS compact tokens |
| Key Format | JWK (JSON Web Key) | Interoperable key representation |
| Key Discovery | JWKS (JSON Web Key Set) | Remote key resolution via `/.well-known/jwks.json` |
| Token Format | JWS Compact Serialization | `header.payload.signature` |
| Token Type | `mandate+jwt` | Distinguishes mandate tokens from regular JWTs |
### Key Management
```typescript
// Generate a new key pair
const { publicKey, privateKey, kid } = await MandateSigner.generateKeyPair();
// publicKey: Share with verifiers (publish at JWKS endpoint)
// privateKey: Keep secret (used for signing)
// kid: Key ID for rotation and identification
```
### Mandate Lifecycle
```
active -> used (after successful checkout execution)
active -> expired (after expires_at timestamp passes)
active -> revoked (manually revoked by issuer)
```
### Attack Mitigations
| Attack Vector | Mitigation |
|---------------|------------|
| Agent overspend | Intent mandate `max_amount` enforced by chain verification |
| Price manipulation | Merchant signs cart mandate with real prices; guardrail validates against Shopify |
| Replay attack | Each mandate has a unique ID; `MandateStore` prevents re-use |
| Token forgery | ES256 signature verification with published public keys |
| Currency mismatch | Chain verification enforces consistent currency across all 3 mandates |
| Expired authorization | Expiry check on every mandate (default 30-minute TTL for cart mandates) |
| Unauthorized merchant | Intent mandate `merchant_ids` constraint checked during chain verification |
### JWS Protected Header
Every mandate token carries this protected header:
```json
{
"alg": "ES256",
"kid": "ap2_unique-key-id",
"typ": "mandate+jwt"
}
```
The `kid` field enables key rotation: verifiers look up the correct public key from the JWKS endpoint using the key ID.