Skip to content

vercel/acp-handler

Repository files navigation

acp-handler

A TypeScript handler for implementing the Agentic Commerce Protocol (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

pnpm add acp-handler

Peer Dependencies

The handler requires a key-value store for session storage. Redis is recommended:

pnpm add redis

Optional dependencies:

pnpm add next  # For Next.js catch-all route helper

Quick Start

1. Implement Required Handlers

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)

// 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:

// 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:

pnpm add @whatwg-node/server
// 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.

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

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

type Webhooks = {
  orderUpdated(evt: {
    checkout_session_id: string;
    status: string;
    order?: Order;
  }): Promise<void>;
};

Storage

Provides a key-value store for session data and idempotency.

type KV = {
  get(key: string): Promise<string | null>;
  set(key: string, value: string, ttlSec?: number): Promise<void>;
  setnx(key: string, value: string, ttlSec?: number): Promise<boolean>;
};

Built-in Redis adapter:

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:

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:

// 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:

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:

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 directory for a complete Next.js implementation with:

  • AI chat demo (simulate ChatGPT)
  • Complete checkout flow
  • Mock products, payments, and webhooks
  • Redis storage
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.

import { createNextCatchAll } from 'acp-handler/next';

const { GET, POST } = createNextCatchAll(handlers);
export { GET, POST };

createStoreWithRedis(namespace)

Creates a Redis-backed KV store.

import { createStoreWithRedis } from 'acp-handler';

// Uses REDIS_URL environment variable
const { store } = createStoreWithRedis('acp');

createOutboundWebhook(config)

Helper for signing outbound webhooks to ChatGPT.

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

Contributing

Contributions welcome! Please open an issue or PR.

License

MIT


Questions? Open an issue or check the ACP documentation.

About

Integrate the Agentic Commerce Protocol (ACP) into your servers

Topics

Resources

License

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published