Paywalls (x402 Protocol)
⚡ Try it live →
Paywalls let you gate any API endpoint or piece of content behind a micropayment. A request either carries proof of payment and receives the resource, or it is rejected with an HTTP 402 Payment Required and instructions on how to pay.
This pattern is defined by the x402 protocol — an open standard that assigns real semantics to the long-dormant HTTP 402 status code. Coal acts as the facilitator: the client signs an EIP-3009 transferWithAuthorization, Coal validates and submits it on-chain via an operator wallet. The client never needs ETH for gas.
Paywalls are ideal for pay-per-call APIs, premium content downloads, AI agent tool access, and any flow where charging a subscription is overkill.
How x402 Works
{ x402Version, accepts: [{ scheme, network, maxAmountRequired, asset, payTo, ... }] }
X-PAYMENT: base64({ scheme, network, payload: { signature, authorization } })
X-PAYMENT-RESPONSE: base64({ success, transaction, payer })
- Client requests the resource (or
GET /api/paywalls/:id/verify?address=...) - Server replies 402 — body is the standard x402 envelope
{ x402Version: 1, accepts: [...] }listing scheme, network (eip155:8453for Base), exact amount in base units, settlement asset (USDC), andpayTo - Client signs an EIP-3009 authorization — no on-chain transaction yet, just an off-chain signature
- Client POSTs the signature to the verify URL with
X-PAYMENT: base64(JSON) - Coal settles on-chain — submits
transferWithAuthorizationvia the operator wallet, returns 200 +X-PAYMENT-RESPONSEcontaining the tx hash
The three types of paywall
Every Coal paywall is a {method, path, price} tuple — what varies is the scope: a single URL, a group of URLs that share an origin, or a route inside your own server.
1. Standalone gate
One URL, one price, no upstream. Coal owns the route at api.usecoal.xyz/p/{slug} and returns either a 402 challenge or the content you stored (contentUrl, contentData, or a presigned download).
Reach for it when you have one thing to sell — a CSV, an article, a webhook endpoint, a single API call. Setup is two fields: a name and a price.
2. Bundled endpoints (API Bundle)
A bundle is one origin + many paywalls. Coal serves every child as api.usecoal.xyz/p/{bundle-slug}/{path-template}, looks up the matching paywall by method + path, charges its price, and forwards the request to your origin with your encrypted auth header attached.
Reach for it when you're selling an API surface — typically more than 3 endpoints that all share the same origin URL and (usually) the same upstream API key. The bundle saves you from configuring origin + auth + slug N times.
Concrete example — twitter-aio with 14 endpoints:
1api.usecoal.xyz/p/twitter-aio/user/{id} $0.0032api.usecoal.xyz/p/twitter-aio/user/by/username/{handle} $0.0033api.usecoal.xyz/p/twitter-aio/user/{id}/tweets $0.0054api.usecoal.xyz/p/twitter-aio/tweet/{id}/retweets $0.0055api.usecoal.xyz/p/twitter-aio/search/{term} $0.0086... 9 more
One bundle row in your console, 14 paywall rows inside it, one encrypted x-rapidapi-key configured once. Buyers pay per endpoint at the price you set; you keep the upstream key out of the response. See API Bundles →.
You can bootstrap a bundle three ways: paste an origin URL (proxy mode), import an OpenAPI spec (one paywall per operation, auto-priced), or skip the proxy and run the SDK in your own handler.
3. SDK-mode gate
Your server runs coal-payments directly — Coal never sees the upstream traffic, only the settle call. The paywall row still lives in your console (so it's discoverable and analytics still work), but proxyEnabled: false.
Reach for it when you can't or won't put your origin behind Coal — e.g. streaming responses, latency-sensitive routes, requests that need access to your app's session, or compliance reasons. Three lines of code for Next.js / Express / Hono / Fastify.
Creating a Paywall
Open the Developer Console, navigate to Paywalls, click New Paywall ▾, and pick the type above. Most fields are inferred — for a standalone gate you only need name + price; for a bundle you need origin URL + (optional) auth header; for SDK mode you get a copy-paste snippet for your stack.
Via the API
POST /api/console/paywalls
1curl -X POST https://api.usecoal.xyz/api/console/paywalls \2 -H "Content-Type: application/json" \3 -H "Authorization: Bearer <privy_access_token>" \4 -d '{5 "name": "Premium API Access",6 "price": 1.00,7 "currency": "USDC",8 "contentType": "api",9 "pricingModel": "per_call"10 }'
Body Parameters
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable label shown on the payment prompt |
price | number | Yes | Amount required to unlock this paywall |
currency | string | No | Settlement currency. Defaults to USDC |
contentType | string | Yes | api | content | download |
pricingModel | string | Yes | one_time | per_call |
Response
1{2 "id": "pw_clx9abc123def456",3 "name": "Premium API Access",4 "price": "1.00",5 "currency": "USDC",6 "contentType": "api",7 "pricingModel": "per_call",8 "createdAt": "2026-03-22T12:00:00.000Z"9}
The x402 Wire Format
Step 1 — GET /api/paywalls/:id/verify?address=0x... → 402
1{2 "x402Version": 1,3 "accepts": [4 {5 "scheme": "exact",6 "network": "eip155:8453",7 "maxAmountRequired": "1000000",8 "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",9 "payTo": "0xMerchantPayoutAddress...",10 "resource": "https://api.usecoal.xyz/api/paywalls/pw_clx9.../verify",11 "description": "Premium API Access",12 "mimeType": "application/json",13 "maxTimeoutSeconds": 60,14 "extra": { "name": "USD Coin", "version": "2" }15 }16 ]17}
maxAmountRequired is denominated in base units (USDC has 6 decimals, so 1000000 = $1.00).
Step 2 — Sign EIP-3009 with the payer's wallet
1const authorization = {2 from: payerAddress,3 to: payTo,4 value: maxAmountRequired, // base units, as string5 validAfter: '0',6 validBefore: String(Math.floor(Date.now() / 1000) + 300), // 5 min7 nonce: '0x' + crypto.randomBytes(32).toString('hex'),8};910const signature = await wallet.signTypedData(11 { name: 'USD Coin', version: '2', chainId: 8453, verifyingContract: USDC_ADDRESS },12 { TransferWithAuthorization: [13 { name: 'from', type: 'address' },14 { name: 'to', type: 'address' },15 { name: 'value', type: 'uint256' },16 { name: 'validAfter', type: 'uint256' },17 { name: 'validBefore', type: 'uint256' },18 { name: 'nonce', type: 'bytes32' },19 ]20 },21 authorization,22);
Step 3 — POST /api/paywalls/:id/verify with X-PAYMENT
1const paymentPayload = {2 x402Version: 1,3 scheme: 'exact',4 network: 'eip155:8453',5 payload: { signature, authorization },6};78const xPayment = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');910const res = await fetch(verifyUrl, {11 method: 'POST',12 headers: { 'X-PAYMENT': xPayment, 'content-type': 'application/json' },13 body: '{}',14});
Server replies 200 with X-PAYMENT-RESPONSE: base64(JSON):
1{2 "success": true,3 "transaction": "0xTxHashOnBase...",4 "network": "eip155:8453",5 "payer": "0xPayerAddress..."6}
The endpoint is idempotent: a second POST with a different valid signature for the same paywall+payer short-circuits to 200 without re-charging.
On settlement failure (insufficient amount, expired auth, nonce already used, wrong recipient) the server replies 402 again with the same accepts array plus an X-PAYMENT-RESPONSE containing errorReason.
Agent Integration
For AI agents, the Coal Agent SDK bundles all of the above into a single tool call. See the agent-payments docs for the pay_x402_paywall example.
How buyers are billed
Two pricingModel values, with two optional knobs on top:
per_call— each request needs a fresh proof of payment. The default for APIs and agent tools.one_time— first payment unlocks the resource for the payer's address forever. The default for downloads and articles.
Layer on accessDuration (TTL in seconds) and callQuota (max requests inside the TTL) to turn per_call into a windowed pass — e.g. "$1 buys 1,000 calls over the next hour" — without standing up a subscription primitive. For recurring billing proper, use Subscriptions instead; that's how Coal Pro bills itself.
Three contentType values pick what gets returned on a successful settle: api returns an access token, content returns the stored HTML body or signed URL, download returns a short-lived presigned URL.
What's in (and what's not)
Coal paywalls cover the surface a pay-per-call API or content gate needs end-to-end: per-call and one-time billing, TTL + quota windows, path-template matching, OpenAPI bulk import, encrypted upstream auth headers, on-chain settlement in USDC on Base (operator pays gas, payer needs zero ETH), non-custodial splits via the CoalFeeRouter, refunds, and HMAC-signed webhooks on every state change. Both x402 v1 and OKX APP v2 envelopes are emitted in the same accepts[] array, so the same paywall serves agents on either protocol without configuration.
Coal is deliberately not an app-store IAP layer (mobile IAP belongs to RevenueCat / native stores), not custodial (funds never sit in a Coal wallet, even briefly — the router splits atomically in the settle tx), and not a per-region card processor (settlement is always USDC; use the Moonpay on-ramp for card → USDC).
A few capabilities are on the roadmap rather than shipped today:
- Graduated / tiered usage brackets — Stripe-Billing-style "first 10k calls at $0.001, next 90k at $0.0005". Today a paywall has a single price.
- "Up to N" with actual-usage settle — the x402 spec lets a client authorize a maximum and only get charged the actual amount used; Coal settles the exact
maxAmountRequiredtoday. - Off-chain voucher batching — for sub-cent micro-payments where on-chain settle per call is wasteful. Under research.
- Hosted-paywall A/B testing & template gallery — one curated checkout theme today; remote-config variants are RevenueCat's mobile strength and are not yet on the web/agent side.
For context on where Coal sits in the wider payments stack: Stripe Billing vs RevenueCat, x402 protocol explainer, xpay paywall-as-a-service.
