coal-payments
v0.5.3
Published
Payment rails for humans and AI agents on Base. Wrap any REST endpoint with x402 USDC settlement, direct to merchant wallet. Includes Next.js, Express, Hono, Fastify middleware plus React checkout components.
Maintainers
Readme
coal-payments
Payment rails for humans and AI agents on Base. Wrap any REST endpoint with x402 + USDC settlement direct to the merchant wallet. Ships per-runtime adapters for Next.js, Express, Hono, and Fastify, plus the original React checkout components.
npm install coal-paymentscoal-payments is the successor to coal-react. Everything that worked in coal-react still works (the React, server, and next sub-paths). The new surface is withCoalPaywall and its per-framework adapters.
Three-line integration
Next.js (App Router)
import { withCoalPaywall } from 'coal-payments/next';
export const GET = withCoalPaywall(
{ paywallId: 'pw_xxx' },
async (req) => Response.json({ hits: await search(req) }),
);Express
import { coalPaywallMiddleware } from 'coal-payments/express';
app.get('/api/search', coalPaywallMiddleware({ paywallId: 'pw_xxx' }),
(req, res) => res.json({ hits: search(req.query.q) }));Hono
import { coalPaywall } from 'coal-payments/hono';
app.get('/api/search', coalPaywall({ paywallId: 'pw_xxx' }),
(c) => c.json({ hits: search(c.req.query('q')) }));Fastify
import { coalPaywallHook } from 'coal-payments/fastify';
app.get('/api/search',
{ preHandler: coalPaywallHook({ paywallId: 'pw_xxx' }) },
async (req) => ({ hits: search(req.query.q) }));The wrapped handler only runs after a successful USDC settle on Base. Coal returns 402 with x402 PaymentRequirements on calls without X-Payment, settles on calls with X-Payment, and stamps x-payment-response: 0x<txHash> on the response.
Sub-paths
| Import | What lives here | Runtime |
|---|---|---|
| coal-payments | withCoalPaywall (framework-agnostic) | Node, Bun, Deno, Workers |
| coal-payments/react | CoalProvider, CoalProducts, CoalCheckoutButton, CoalAgentPublisher, safeJsonForScriptTag | Browser |
| coal-payments/next | withCoalPaywall + manifest route helpers (createAgentCardRoute, createLlmsTxtRoute, createX402ManifestRoute) | Next.js server |
| coal-payments/server | publishCoalCatalog (server-only catalog indexer) | Node, Edge |
| coal-payments/express | coalPaywallMiddleware | Node |
| coal-payments/hono | coalPaywall middleware | Bun, Deno, Workers, Node |
| coal-payments/fastify | coalPaywallHook preHandler | Node |
Options
interface WithCoalPaywallOptions {
paywallId: string;
apiBaseUrl?: string; // default https://api.usecoal.xyz
attachPayer?: boolean; // forward x-coal-payer + x-coal-settle-tx into your handler
fetch?: typeof fetch; // override for test envs / Workers
}When attachPayer: true, your handler receives a clone of the request with two extra headers:
x-coal-payer— payer wallet addressx-coal-settle-tx— on-chain settlement tx hash on Base
How it works
The SDK is a thin shell over two Coal backend endpoints:
- No
X-Payment→ SDK callsGET /api/agent/paywalls/{id}/verifyfor the requirements, returns 402 to the agent. X-Paymentpresent → SDK callsPOST /api/agent/paywalls/{id}/settle. Coal validates the signed EIP-3009 authorization, submitstransferWithAuthorizationon Base, returns{ ok, txHash, payer }.- Settle success → SDK invokes your handler, returns its response with
x-payment-responsestamped on.
The SDK does NOT need a Coal API key. The paywall ID is the entire identity.
Settlement
USDC on Base. Your payer wallet does not need ETH — that's the entire point of x402 + EIP-3009.
Who pays gas:
- x402 / EIP-3009 settles (the SDK paywall path, AI agents) — Coal's operator wallet pays gas. The buyer's wallet never holds or spends ETH.
- Coinbase Smart Wallet checkouts (human shoppers via the hosted checkout) — Coinbase paymaster covers gas. The Smart Wallet user signs and pays $0 in fees.
CoalFeeRouter (singleton)
Every settle routes through one immutable contract — CoalFeeRouter at 0x2392eBC8bc520315581EF3203A0492d54f1CA47e on Base mainnet. In a single atomic transaction it:
- Pulls USDC from the payer via
transferWithAuthorization - Computes Coal's fee from the merchant's on-chain tier
- Sends
(gross − fee)to the merchant's payout address - Sends
feeto Coal's fee recipient
The router's balance is zero at the end of every settle — Coal never custodies funds, not even briefly. The pattern is the same one Uniswap's PAY_PORTION command, Daimo Pay, and AAVE flash-loan repays use.
Pricing
| Tier | Fee | Monthly | Volume cap | |---|---|---|---| | Free | 1.8% | $0 | $500 / mo | | Pro | 1.0% | $15 | unlimited | | Design Partner | 0% | $0 | unlimited |
Coal can never charge more than 2% — MAX_FEE_BPS = 200 is hardcoded in the router's bytecode. Tier downgrades (better fee for you) apply instantly; tier upgrades (higher fee) are timelocked 24 hours so you have time to migrate elsewhere before any rate change applies. Your payout address is also timelock-protected — Coal can't redirect your USDC by changing where it's sent.
Refunds
USDC settles are final on Base; Coal doesn't custody funds, so chargebacks aren't a thing. For refunds, you initiate a USDC transfer from your payout wallet back to the buyer — Coal records it against the original receipt if you call POST /api/console/refunds.
Why no merchant ID in the SDK
The paywall ID (pw_...) already maps to a unique merchant in Coal's backend. The SDK can be open-source — no secrets, no API key, no merchant ID to leak. The price, payout address, fee tier, and proxy mode all come from the paywall row at settle time.
Migrating from coal-react
// before
import { CoalProvider } from 'coal-react';
// after
import { CoalProvider } from 'coal-payments/react';Every coal-react export has a 1:1 successor in coal-payments. Full migration guide: https://usecoal.xyz/docs/migration/coal-react-to-coal-payments.
[email protected] keeps working — migrate when you have a quiet afternoon.
License
MIT. See LICENSE.
Links
- Docs: https://usecoal.xyz/docs
- Console: https://usecoal.xyz/console
- Discord: ask in GitHub issues
