https://github.com/vercel/acp-handler
Integrate the Agentic Commerce Protocol (ACP) into your servers
https://github.com/vercel/acp-handler
acp agentic commerce express hono nextjs protocol
Last synced: 9 months ago
JSON representation
Integrate the Agentic Commerce Protocol (ACP) into your servers
- Host: GitHub
- URL: https://github.com/vercel/acp-handler
- Owner: vercel
- License: mit
- Created: 2025-09-29T19:07:02.000Z (9 months ago)
- Default Branch: main
- Last Pushed: 2025-10-03T13:54:50.000Z (9 months ago)
- Last Synced: 2025-10-03T14:39:12.388Z (9 months ago)
- Topics: acp, agentic, commerce, express, hono, nextjs, protocol
- Language: TypeScript
- Homepage:
- Size: 255 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- awesome-agentic-commerce - github.com/vercel/acp-handler
README
# acp-handler
A TypeScript handler for implementing the [Agentic Commerce Protocol](https://developers.openai.com/commerce) (ACP) in your web application. Handle ACP checkout requests with built-in idempotency, signature verification, and OpenTelemetry tracing.
## What is ACP?
An open standard for programmatic commerce flows between buyers, AI agents, and businesses. This package handles the protocol implementation so you can focus on your business logic.
**Key Features:**
- ✅ Full ACP spec compliance
- ✅ Type-safe TypeScript API
- ✅ Built-in idempotency (prevents double-charging)
- ✅ OpenTelemetry tracing support
- ✅ Web Standard APIs (works with Next.js, Hono, Express, Cloudflare Workers, Deno, Bun, Remix)
- ✅ Production-ready patterns
- ✅ Comprehensive test suite
## Installation
```bash
pnpm add acp-handler
```
### Peer Dependencies
The handler requires a key-value store for session storage. Redis is recommended:
```bash
pnpm add redis
```
Optional dependencies:
```bash
pnpm add next # For Next.js catch-all route helper
```
## Quick Start
### 1. Implement Required Handlers
```typescript
import { createHandlers } from 'acp-handler';
const handlers = createHandlers(
{
// Product pricing logic
products: {
price: async ({ items, customer, fulfillment }) => {
// Fetch products from your database
const products = await db.products.findMany({
where: { id: { in: items.map(i => i.id) } }
});
// Calculate pricing
const itemsWithPrices = items.map(item => {
const product = products.find(p => p.id === item.id);
return {
id: item.id,
title: product.name,
quantity: item.quantity,
unit_price: { amount: product.price, currency: 'USD' }
};
});
const subtotal = itemsWithPrices.reduce(
(sum, item) => sum + item.unit_price.amount * item.quantity,
0
);
return {
items: itemsWithPrices,
totals: {
subtotal: { amount: subtotal, currency: 'USD' },
grand_total: { amount: subtotal, currency: 'USD' }
},
ready: true, // Ready for payment
};
}
},
// Payment processing
payments: {
authorize: async ({ session, delegated_token }) => {
// Integrate with your payment provider (Stripe, etc.)
const intent = await stripe.paymentIntents.create({
amount: session.totals.grand_total.amount,
currency: session.totals.grand_total.currency,
payment_method: delegated_token,
});
if (intent.status === 'requires_capture') {
return { ok: true, intent_id: intent.id };
}
return { ok: false, reason: 'Authorization failed' };
},
capture: async (intent_id) => {
const intent = await stripe.paymentIntents.capture(intent_id);
if (intent.status === 'succeeded') {
return { ok: true };
}
return { ok: false, reason: 'Capture failed' };
}
},
// Webhook notifications
webhooks: {
orderUpdated: async ({ checkout_session_id, status, order }) => {
// Notify ChatGPT about order updates
await fetch(process.env.OPENAI_WEBHOOK_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Signature': hmacSign(payload, secret)
},
body: JSON.stringify({ checkout_session_id, status, order })
});
}
}
},
{
// Storage backend (Redis recommended)
store: createStoreWithRedis('acp')
}
);
```
### 2. Mount Handlers to Your Framework
#### Next.js (App Router)
```typescript
// app/checkout_sessions/[[...segments]]/route.ts
import { createNextCatchAll } from 'acp-handler/next';
const { GET, POST } = createNextCatchAll(handlers);
export { GET, POST };
```
#### Hono
Hono natively supports Web Standard APIs, so no adapter needed:
```typescript
// server.ts
import { Hono } from 'hono';
import { handlers } from './checkout-handlers';
import {
parseJSON,
validateBody,
CreateCheckoutSessionSchema,
UpdateCheckoutSessionSchema,
CompleteCheckoutSessionSchema,
} from 'acp-handler';
const app = new Hono();
app.post('/checkout_sessions', async (c) => {
const parsed = await parseJSON(c.req.raw);
if (!parsed.ok) return parsed.res;
const validated = validateBody(CreateCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.create(c.req.raw, validated.data);
});
app.get('/checkout_sessions/:id', async (c) => {
const id = c.req.param('id');
return handlers.get(c.req.raw, id);
});
app.post('/checkout_sessions/:id', async (c) => {
const id = c.req.param('id');
const parsed = await parseJSON(c.req.raw);
if (!parsed.ok) return parsed.res;
const validated = validateBody(UpdateCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.update(c.req.raw, id, validated.data);
});
app.post('/checkout_sessions/:id/complete', async (c) => {
const id = c.req.param('id');
const parsed = await parseJSON(c.req.raw);
if (!parsed.ok) return parsed.res;
const validated = validateBody(CompleteCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.complete(c.req.raw, id, validated.data);
});
app.post('/checkout_sessions/:id/cancel', async (c) => {
const id = c.req.param('id');
return handlers.cancel(c.req.raw, id);
});
```
#### Express / Node.js
The core handlers use Web Standard `Request`/`Response` objects. For Node.js frameworks like Express, use [`@whatwg-node/server`](https://github.com/ardatan/whatwg-node):
```bash
pnpm add @whatwg-node/server
```
```typescript
// server.ts
import express from 'express';
import { createServerAdapter } from '@whatwg-node/server';
import { handlers } from './checkout-handlers';
import {
parseJSON,
validateBody,
CreateCheckoutSessionSchema,
UpdateCheckoutSessionSchema,
CompleteCheckoutSessionSchema,
} from 'acp-handler';
const app = express();
// Helper to extract route params
const getId = (req: Request) => req.url.split('/').filter(Boolean)[1];
// POST /checkout_sessions
app.post('/checkout_sessions', createServerAdapter(async (req: Request) => {
const parsed = await parseJSON(req);
if (!parsed.ok) return parsed.res;
const validated = validateBody(CreateCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.create(req, validated.data);
}));
// GET /checkout_sessions/:id
app.get('/checkout_sessions/:id', createServerAdapter(async (req: Request) => {
const id = getId(req);
return handlers.get(req, id);
}));
// POST /checkout_sessions/:id
app.post('/checkout_sessions/:id', createServerAdapter(async (req: Request) => {
const id = getId(req);
const parsed = await parseJSON(req);
if (!parsed.ok) return parsed.res;
const validated = validateBody(UpdateCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.update(req, id, validated.data);
}));
// POST /checkout_sessions/:id/complete
app.post('/checkout_sessions/:id/complete', createServerAdapter(async (req: Request) => {
const id = getId(req);
const parsed = await parseJSON(req);
if (!parsed.ok) return parsed.res;
const validated = validateBody(CompleteCheckoutSessionSchema, parsed.body);
if (!validated.ok) return validated.res;
return handlers.complete(req, id, validated.data);
}));
// POST /checkout_sessions/:id/cancel
app.post('/checkout_sessions/:id/cancel', createServerAdapter(async (req: Request) => {
const id = getId(req);
return handlers.cancel(req, id);
}));
app.listen(3000);
```
**Note:** This approach works with Express, Fastify, Koa, and any Node.js HTTP framework.
#### Other Frameworks
The handlers use Web Standard APIs and work natively with:
- Cloudflare Workers
- Deno Deploy
- Bun
- Vercel Edge Functions
- Remix
Just call the handlers directly with `Request` objects!
### 3. Done!
Your ACP-compliant checkout API is now ready. ChatGPT can create checkout sessions, update cart items, and complete purchases.
## Core Concepts
### Products Handler
Calculates pricing, taxes, and shipping. Called on every create/update.
```typescript
type Products = {
price(input: {
items: Array<{ id: string; quantity: number }>;
customer?: CustomerInfo;
fulfillment?: FulfillmentInfo;
}): Promise<{
items: CheckoutItem[];
totals: Totals;
fulfillment?: Fulfillment;
messages?: Message[];
ready: boolean; // Can checkout proceed to payment?
}>;
};
```
### Payments Handler
Handles payment authorization and capture (two-phase commit).
```typescript
type Payments = {
authorize(input: {
session: CheckoutSession;
delegated_token?: string;
}): Promise<
| { ok: true; intent_id: string }
| { ok: false; reason: string }
>;
capture(intent_id: string): Promise<
| { ok: true }
| { ok: false; reason: string }
>;
};
```
### Webhooks Handler
Notifies ChatGPT about order updates (completion, cancellation, etc.).
```typescript
type Webhooks = {
orderUpdated(evt: {
checkout_session_id: string;
status: string;
order?: Order;
}): Promise;
};
```
### Storage
Provides a key-value store for session data and idempotency.
```typescript
type KV = {
get(key: string): Promise;
set(key: string, value: string, ttlSec?: number): Promise;
setnx(key: string, value: string, ttlSec?: number): Promise;
};
```
**Built-in Redis adapter:**
```typescript
import { createStoreWithRedis } from 'acp-handler';
const { store } = createStoreWithRedis('namespace');
```
## Advanced Features
### Signature Verification
Verify that requests are actually from OpenAI/ChatGPT and haven't been tampered with:
```typescript
import { createHandlers } from 'acp-handler';
const handlers = createHandlers(
{ products, payments, webhooks },
{
store,
signature: {
secret: process.env.OPENAI_WEBHOOK_SECRET, // Provided by OpenAI
toleranceSec: 300 // Optional: 5 minutes default
}
}
);
```
**How it works:**
- HMAC-SHA256 signature verification
- Protects against unauthorized requests
- Prevents replay attacks (timestamp must be recent)
- Constant-time comparison (timing attack protection)
**Returns 401 if:**
- Signature header is missing
- Timestamp header is missing
- Signature doesn't match
- Request is too old (replay attack)
- Body has been tampered with
**Optional:** Signature verification is disabled by default for easier development. Enable it in production by providing the `signature` config.
### Idempotency
Automatically handles idempotency for all POST operations to prevent double-charging:
```typescript
// Automatically handled by acp-handler
POST /checkout_sessions/:id/complete
Headers:
Idempotency-Key: idem_abc123
// Retries with same key return cached result
// Payment only charged once!
```
### OpenTelemetry Tracing
Add distributed tracing to monitor performance:
```typescript
import { trace } from '@opentelemetry/api';
const tracer = trace.getTracer('my-shop');
const handlers = createHandlers(
{ products, payments, webhooks },
{ store, tracer } // Add tracer
);
```
**Spans created:**
- `checkout.create`, `checkout.update`, `checkout.complete`
- `products.price` - See pricing performance
- `payments.authorize`, `payments.capture` - Track payment operations
- `session.get`, `session.put` - Monitor storage
- `webhooks.orderUpdated` - Track webhook delivery
**Attributes:**
- `session_id`, `idempotency_key`, `payment_intent_id`
- `items_count`, `session_status`, `idempotency_reused`
### Testing
The package provides test helpers for integration testing:
```typescript
import { createMemoryStore, createMockProducts } from 'acp-handler/test';
const handlers = createHandlers(
{
products: createMockProducts(),
payments: createMockPayments(),
webhooks: createMockWebhooks()
},
{ store: createMemoryStore() }
);
// Test complete checkout flow
const res = await handlers.create(req, { items: [...] });
const session = await res.json();
// ...
```
## Examples
See the [`examples/basic`](./examples/basic) directory for a complete Next.js implementation with:
- AI chat demo (simulate ChatGPT)
- Complete checkout flow
- Mock products, payments, and webhooks
- Redis storage
```bash
cd examples/basic
pnpm install
pnpm dev
```
## API Reference
### `createHandlers(handlers, options)`
Creates checkout handlers implementing the ACP spec.
**Parameters:**
- `handlers.products: Products` - Product pricing implementation
- `handlers.payments: Payments` - Payment processing implementation
- `handlers.webhooks: Webhooks` - Webhook notifications
- `options.store: KV` - Key-value storage backend
- `options.tracer?: Tracer` - OpenTelemetry tracer (optional)
**Returns:** Handlers object with methods:
- `create(req, body)` - POST /checkout_sessions
- `update(req, id, body)` - POST /checkout_sessions/:id
- `complete(req, id, body)` - POST /checkout_sessions/:id/complete
- `cancel(req, id)` - POST /checkout_sessions/:id/cancel
- `get(req, id)` - GET /checkout_sessions/:id
### `createNextCatchAll(handlers, schemas?)`
Creates Next.js catch-all route handlers.
```typescript
import { createNextCatchAll } from 'acp-handler/next';
const { GET, POST } = createNextCatchAll(handlers);
export { GET, POST };
```
### `createStoreWithRedis(namespace)`
Creates a Redis-backed KV store.
```typescript
import { createStoreWithRedis } from 'acp-handler';
// Uses REDIS_URL environment variable
const { store } = createStoreWithRedis('acp');
```
### `createOutboundWebhook(config)`
Helper for signing outbound webhooks to ChatGPT.
```typescript
import { createOutboundWebhook } from 'acp-handler';
const webhook = createOutboundWebhook({
webhookUrl: process.env.OPENAI_WEBHOOK_URL,
secret: process.env.OPENAI_WEBHOOK_SECRET,
merchantName: 'YourStore'
});
await webhook.orderUpdated({
checkout_session_id: session.id,
status: 'completed',
order: { id: 'order_123', status: 'placed' }
});
```
## Project Structure
```
agentic-commerce-protocol-template/
├── packages/
│ └── sdk/ # acp-handler package
│ ├── src/
│ │ ├── checkout/ # Checkout implementation
│ │ │ ├── handlers.ts # Core business logic
│ │ │ ├── next/ # Next.js adapter
│ │ │ ├── hono/ # Hono adapter
│ │ │ ├── storage/ # Storage adapters
│ │ │ ├── webhooks/ # Webhook helpers
│ │ │ └── tracing.ts # OpenTelemetry helpers
│ │ ├── feeds/ # Product feeds (coming soon)
│ │ └── index.ts
│ └── test/ # Test helpers
└── examples/
└── basic/ # Example Next.js app
```
## Resources
- [ACP Checkout Spec](https://developers.openai.com/commerce/specs/checkout)
- [ACP Product Feeds Spec](https://developers.openai.com/commerce/specs/feed)
- [Apply for ChatGPT Checkout](https://chatgpt.com/merchants)
- [Example Implementation](./examples/basic)
## Contributing
Contributions welcome! Please open an issue or PR.
## License
MIT
---
**Questions?** Open an issue or check the [ACP documentation](https://developers.openai.com/commerce).