coal
coal

SDK Mode (withCoalPaywall)

Wrap any handler in your own server with coal-payments. Three lines for Next.js, Express, Hono, or Fastify. Coal stays off the request path — the SDK only calls Coal's backend to settle the payment.


Install

bash
1npm install coal-payments

coal-payments is the successor to coal-react. The React checkout components moved to the coal-payments/react sub-path; everything else is new.


Next.js (App Router)

ts
1// app/api/search/route.ts
2import { withCoalPaywall } from 'coal-payments/next';
3
4export const GET = withCoalPaywall(
5 { paywallId: 'pw_xxx' },
6 async (req) => {
7 const q = new URL(req.url).searchParams.get('q');
8 const hits = await search(q);
9 return Response.json({ hits });
10 },
11);

The wrapped handler only runs after a successful USDC settle on Base. The response gets x-payment-response: 0x<txHash> stamped automatically.


Express

ts
1import express from 'express';
2import { coalPaywallMiddleware } from 'coal-payments/express';
3
4const app = express();
5
6app.get(
7 '/api/search',
8 coalPaywallMiddleware({ paywallId: 'pw_xxx' }),
9 (req, res) => {
10 // req.coalSettleTx is set after a successful settle.
11 res.json({ hits: search(req.query.q) });
12 },
13);
14
15app.listen(3000);

Hono

ts
1import { Hono } from 'hono';
2import { coalPaywall } from 'coal-payments/hono';
3
4const app = new Hono();
5
6app.get('/api/search', coalPaywall({ paywallId: 'pw_xxx' }), (c) =>
7 c.json({ hits: search(c.req.query('q')) }),
8);
9
10export default app;

Works in Bun, Deno, Cloudflare Workers, and Node.


Fastify

ts
1import Fastify from 'fastify';
2import { coalPaywallHook } from 'coal-payments/fastify';
3
4const app = Fastify();
5
6app.get(
7 '/api/search',
8 { preHandler: coalPaywallHook({ paywallId: 'pw_xxx' }) },
9 async (req, reply) => {
10 return { hits: search(req.query.q) };
11 },
12);
13
14app.listen({ port: 3000 });

Generic (Bun, Deno, Workers)

If your runtime exposes the standard Request / Response types but you do not use any of the framework adapters:

ts
1import { withCoalPaywall } from 'coal-payments';
2
3const handler = withCoalPaywall(
4 { paywallId: 'pw_xxx' },
5 async (req) => new Response(JSON.stringify({ ok: true }), {
6 headers: { 'content-type': 'application/json' },
7 }),
8);
9
10export default { fetch: handler };

Options

ts
1interface WithCoalPaywallOptions {
2 paywallId: string;
3 apiBaseUrl?: string; // default https://api.usecoal.xyz
4 attachPayer?: boolean; // if true, forwards x-coal-payer + x-coal-settle-tx into your handler
5 fetch?: typeof fetch; // override for test envs / Workers
6}

When attachPayer is true, your handler receives a clone of the original Request with two extra headers:

  • x-coal-payer — the payer's wallet address
  • x-coal-settle-tx — the on-chain settlement tx hash

Use these to log per-call attribution or apply per-payer business logic.


How the SDK talks to Coal

The SDK is a thin shell. On every call:

  1. No X-Payment header on the inbound request → SDK calls GET /api/agent/paywalls/{id}/verify to fetch the current PaymentRequirements, returns 402 to the agent.
  2. X-Payment header present → SDK calls POST /api/agent/paywalls/{id}/settle with the payload. Coal validates + settles on Base + returns { ok, txHash, payer }.
  3. On settle success → SDK invokes your handler and forwards its response with x-payment-response stamped on.

The paywall ID is your identity — and your payout

The only thing you configure is paywallId: 'pw_xxx'. That's enough because the paywall is already bound to you and your wallet — you created it signed into the console, so Coal stored it under your merchant account with your payout address. On every settle, Coal resolves pw_xxx → your account → your wallet, and pays it (atomically splitting the fee through the CoalFeeRouter if you're enrolled). The caller can't redirect it. See how Coal knows it's your API and pays your wallet.

No Coal API key needed here

The payment SDK needs no coal_live_ API key — the paywall ID is the entire identity. The coal_live_ key (from /console/keys) is a separate credential, only for calling Coal's management REST API (checkouts, programmatic paywall management). Don't confuse it with the COAL_FORWARDER_SECRET used by coalOriginGuard in proxy mode.


Migrating from coal-react

coal-react@0.4.1 still installs and works. The new package adds the middleware sub-paths and renames the React entry point. See migration guide.