/**
* AP2 Mandate Flow — Complete Example
*
* Demonstrates the full AP2 mandate chain lifecycle:
*
* 1. Generate an ES256 key pair for signing/verification
* 2. Create an Intent Mandate (buyer's spending authorization)
* 3. Create a Cart Mandate (merchant's price attestation)
* 4. Create a Payment Mandate (buyer's payment authorization)
* 5. Verify each mandate individually
* 6. Verify the full mandate chain
* 7. Store mandates and demonstrate retrieval
*
* This example uses the AP2 layer directly. In production, the
* execute_checkout MCP tool handles steps 5-7 automatically.
*/
import { SignJWT, importJWK, exportJWK, generateKeyPair } from 'jose';
import type { JWK } from 'jose';
import { v4 as uuidv4 } from 'uuid';
import { MandateSigner } from '../src/ap2/signer.js';
import { MandateVerifier } from '../src/ap2/verifier.js';
import { MandateStore } from '../src/ap2/mandate-store.js';
import type {
Mandate,
IntentPayload,
CartPayload,
PaymentPayload,
LineItem,
CheckoutTotals,
} from '../src/types.js';
// ─────────────────────────────────────────────
// Helper: Sign a mandate as a JWS compact token
// ─────────────────────────────────────────────
async function signMandate(mandate: Mandate, privateKey: JWK): Promise<string> {
const key = await importJWK(privateKey, 'ES256');
const expiresAt = new Date(mandate.expires_at);
const token = await new SignJWT(mandate as unknown as Record<string, unknown>)
.setProtectedHeader({
alg: 'ES256',
kid: privateKey.kid ?? 'default',
typ: 'mandate+jwt',
})
.setIssuedAt()
.setExpirationTime(expiresAt)
.setJti(mandate.id)
.sign(key);
return token;
}
// ─────────────────────────────────────────────
// Main Flow
// ─────────────────────────────────────────────
async function mandateFlow(): Promise<void> {
console.log('=== AP2 Mandate Flow ===\n');
// ── Step 1: Generate Key Pairs ──
// In production, these are generated once and stored securely.
// The buyer's agent has its own key pair, and the merchant has its own.
console.log('Step 1: Generating ES256 key pairs...');
const buyerKeyPair = await MandateSigner.generateKeyPair();
console.log(` Buyer key ID: ${buyerKeyPair.kid}`);
const merchantKeyPair = await MandateSigner.generateKeyPair();
console.log(` Merchant key ID: ${merchantKeyPair.kid}`);
console.log();
// ── Step 2: Create Intent Mandate ──
// The buyer (or buyer's agent) creates an Intent Mandate that declares:
// - Maximum authorized spend ($100.00 = 10000 cents)
// - Currency (USD)
// - Optional restrictions (categories, merchant IDs)
console.log('Step 2: Creating Intent Mandate...');
const now = new Date();
const oneHourLater = new Date(now.getTime() + 60 * 60 * 1000);
const intentPayload: IntentPayload = {
max_amount: 10000, // $100.00 in minor units
currency: 'USD',
categories: ['electronics'], // Only allow electronics purchases
merchant_ids: [], // No merchant restriction
};
const intentMandate: Mandate = {
id: `mandate_intent_${uuidv4()}`,
type: 'intent',
status: 'active',
issuer: 'buyer_agent_001',
subject: 'buyer@example.com',
payload: intentPayload,
signature: '',
issued_at: now.toISOString(),
expires_at: oneHourLater.toISOString(),
};
// Sign the Intent Mandate with the buyer's private key
const intentToken = await signMandate(intentMandate, buyerKeyPair.privateKey);
console.log(` Intent Mandate ID: ${intentMandate.id}`);
console.log(` Max amount: $${intentPayload.max_amount / 100}`);
console.log(` Token length: ${intentToken.length} chars`);
console.log(` Token preview: ${intentToken.substring(0, 50)}...`);
console.log();
// ── Step 3: Create Cart Mandate ──
// The merchant gateway creates a Cart Mandate after the cart is finalized.
// It locks the exact line items and totals, proving the prices are real.
console.log('Step 3: Creating Cart Mandate...');
const lineItems: LineItem[] = [
{
id: 'line_001',
product_id: 'gid://shopify/Product/100',
variant_id: 'gid://shopify/ProductVariant/200',
title: 'Wireless Headphones - Black',
quantity: 1,
unit_amount: 4999, // $49.99
total_amount: 4999,
type: 'product',
sku: 'WH-BLK-001',
},
];
const totals: CheckoutTotals = {
subtotal: 4999,
tax: 400, // ~8% tax
shipping: 599, // Standard shipping
discount: 0,
fee: 25, // 0.5% platform fee
total: 6023, // $60.23
currency: 'USD',
};
const checkoutId = `checkout_${uuidv4()}`;
const cartPayload: CartPayload = {
checkout_id: checkoutId,
line_items: lineItems,
totals: totals,
merchant_signature: 'merchant_attestation_placeholder',
};
const cartMandate: Mandate = {
id: `mandate_cart_${uuidv4()}`,
type: 'cart',
status: 'active',
issuer: checkoutId, // Merchant identified by checkout context
subject: 'buyer@example.com',
payload: cartPayload,
signature: '',
issued_at: now.toISOString(),
expires_at: new Date(now.getTime() + 30 * 60 * 1000).toISOString(), // 30 min expiry
};
// Sign the Cart Mandate with the merchant's private key
const cartToken = await signMandate(cartMandate, merchantKeyPair.privateKey);
console.log(` Cart Mandate ID: ${cartMandate.id}`);
console.log(` Checkout ID: ${checkoutId}`);
console.log(` Cart total: $${totals.total / 100} (${totals.total} cents)`);
console.log(` Line items: ${lineItems.length}`);
console.log(` Token length: ${cartToken.length} chars`);
console.log();
// ── Step 4: Create Payment Mandate ──
// The buyer's agent creates a Payment Mandate authorizing the exact amount.
// It references the Cart Mandate by ID, creating a verifiable chain.
console.log('Step 4: Creating Payment Mandate...');
const paymentPayload: PaymentPayload = {
checkout_id: checkoutId,
amount: totals.total, // Must match cart total exactly
currency: totals.currency, // Must match cart currency
payment_handler_id: 'shopify_payments',
cart_mandate_id: cartMandate.id, // References the Cart Mandate
};
const paymentMandate: Mandate = {
id: `mandate_payment_${uuidv4()}`,
type: 'payment',
status: 'active',
issuer: 'buyer_agent_001',
subject: 'buyer@example.com',
payload: paymentPayload,
signature: '',
issued_at: now.toISOString(),
expires_at: new Date(now.getTime() + 30 * 60 * 1000).toISOString(),
};
// Sign the Payment Mandate with the buyer's private key
const paymentToken = await signMandate(paymentMandate, buyerKeyPair.privateKey);
console.log(` Payment Mandate ID: ${paymentMandate.id}`);
console.log(` Payment amount: $${paymentPayload.amount / 100}`);
console.log(` References cart: ${paymentPayload.cart_mandate_id}`);
console.log(` Handler: ${paymentPayload.payment_handler_id}`);
console.log(` Token length: ${paymentToken.length} chars`);
console.log();
// ── Step 5: Verify Each Mandate Individually ──
// The gateway verifies each mandate's signature, expiry, type, and payload.
// Note: Intent and Payment are signed by the buyer's key,
// Cart is signed by the merchant's key.
console.log('Step 5: Verifying mandates individually...');
// Verify Intent (signed by buyer)
const buyerVerifier = new MandateVerifier(buyerKeyPair.publicKey);
const intentResult = await buyerVerifier.verifyIntent(intentToken);
console.log(` Intent valid: ${intentResult.valid}`);
if (!intentResult.valid) console.log(` Error: ${intentResult.error}`);
// Verify Cart (signed by merchant)
const merchantVerifier = new MandateVerifier(merchantKeyPair.publicKey);
const cartResult = await merchantVerifier.verifyCart(cartToken);
console.log(` Cart valid: ${cartResult.valid}`);
if (!cartResult.valid) console.log(` Error: ${cartResult.error}`);
// Verify Payment (signed by buyer)
const paymentResult = await buyerVerifier.verifyPayment(paymentToken);
console.log(` Payment valid: ${paymentResult.valid}`);
if (!paymentResult.valid) console.log(` Error: ${paymentResult.error}`);
console.log();
// ── Step 6: Verify the Full Mandate Chain ──
// Chain verification checks cross-mandate consistency:
// - Cart total <= Intent max_amount
// - Currencies match across all three
// - Payment amount == Cart total
// - Payment references the correct Cart Mandate ID
//
// IMPORTANT: In production, the chain verifier uses a single key or JWKS
// endpoint. Here we demonstrate with the buyer's key, which means the
// cart mandate (signed by merchant) will fail chain verification.
// In production, use a JWKS endpoint that contains both keys.
console.log('Step 6: Verifying full mandate chain...');
// For demonstration purposes, we show what chain verification looks like.
// In production, all mandates would be verified against a shared JWKS endpoint.
const chainResult = await buyerVerifier.verifyMandateChain(
intentToken,
cartToken, // This will fail because it's signed by the merchant key
paymentToken,
);
console.log(` Chain valid: ${chainResult.valid}`);
if (!chainResult.valid) {
console.log(' Chain errors (expected -- cart signed by different key):');
chainResult.errors.forEach((err) => console.log(` - ${err}`));
}
console.log();
// ── Step 7: Store Mandates ──
// After verification, mandates are persisted for audit trail and replay prevention.
console.log('Step 7: Storing mandates...');
const store = new MandateStore();
// Store all three mandates
await store.store(intentMandate);
await store.store(cartMandate);
await store.store(paymentMandate);
console.log(' Stored 3 mandates');
// Retrieve by ID
const retrieved = await store.get(intentMandate.id);
console.log(` Retrieved intent mandate: ${retrieved?.id}`);
// Retrieve by checkout ID (Cart and Payment mandates are indexed)
const checkoutMandates = await store.getByCheckout(checkoutId);
console.log(` Mandates for checkout ${checkoutId}: ${checkoutMandates.length}`);
checkoutMandates.forEach((m) => {
console.log(` - ${m.type}: ${m.id} (status: ${m.status})`);
});
// Revoke a mandate (e.g., user cancels the purchase)
await store.revoke(intentMandate.id);
const revoked = await store.get(intentMandate.id);
console.log(` Intent mandate status after revoke: ${revoked?.status}`);
console.log();
// ── Summary ──
console.log('=== Mandate Chain Summary ===');
console.log(` Intent: $${intentPayload.max_amount / 100} max authorized`);
console.log(` Cart: $${totals.total / 100} locked total`);
console.log(` Payment: $${paymentPayload.amount / 100} authorized payment`);
console.log(` Chain: Intent ($100) >= Cart ($60.23) == Payment ($60.23)`);
console.log(` Result: The agent spent $60.23 of the $100 authorization`);
console.log('\n=== Flow Complete ===');
}
// Run the example
mandateFlow().catch((err) => {
console.error('Mandate flow failed:', err);
process.exit(1);
});