An open API service indexing awesome lists of open source software.

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

Awesome Lists containing this project

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