coal
coal

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

Discovery
Your AppCoal
GET /api/paywalls/:id/verify?address=0x...
CoalYour App
402 Payment Required

{ x402Version, accepts: [{ scheme, network, maxAmountRequired, asset, payTo, ... }] }

Sign EIP-3009
User / BrowserWallet signs transferWithAuthorization (no gas)
Settlement
Your AppCoal
POST /api/paywalls/:id/verify

X-PAYMENT: base64({ scheme, network, payload: { signature, authorization } })

CoalBase Chain
transferWithAuthorization (operator pays gas)
Base ChainCoal
Tx mined on Base (~2 sec)
CoalYour App
200 OK

X-PAYMENT-RESPONSE: base64({ success, transaction, payer })

  1. Client requests the resource (or GET /api/paywalls/:id/verify?address=...)
  2. Server replies 402 — body is the standard x402 envelope { x402Version: 1, accepts: [...] } listing scheme, network (eip155:8453 for Base), exact amount in base units, settlement asset (USDC), and payTo
  3. Client signs an EIP-3009 authorization — no on-chain transaction yet, just an off-chain signature
  4. Client POSTs the signature to the verify URL with X-PAYMENT: base64(JSON)
  5. Coal settles on-chain — submits transferWithAuthorization via the operator wallet, returns 200 + X-PAYMENT-RESPONSE containing 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:

text
1api.usecoal.xyz/p/twitter-aio/user/{id} $0.003
2api.usecoal.xyz/p/twitter-aio/user/by/username/{handle} $0.003
3api.usecoal.xyz/p/twitter-aio/user/{id}/tweets $0.005
4api.usecoal.xyz/p/twitter-aio/tweet/{id}/retweets $0.005
5api.usecoal.xyz/p/twitter-aio/search/{term} $0.008
6... 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

bash
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

FieldTypeRequiredDescription
namestringYesHuman-readable label shown on the payment prompt
pricenumberYesAmount required to unlock this paywall
currencystringNoSettlement currency. Defaults to USDC
contentTypestringYesapi | content | download
pricingModelstringYesone_time | per_call

Response

json
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

json
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

typescript
1const authorization = {
2 from: payerAddress,
3 to: payTo,
4 value: maxAmountRequired, // base units, as string
5 validAfter: '0',
6 validBefore: String(Math.floor(Date.now() / 1000) + 300), // 5 min
7 nonce: '0x' + crypto.randomBytes(32).toString('hex'),
8};
9
10const 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

typescript
1const paymentPayload = {
2 x402Version: 1,
3 scheme: 'exact',
4 network: 'eip155:8453',
5 payload: { signature, authorization },
6};
7
8const xPayment = Buffer.from(JSON.stringify(paymentPayload)).toString('base64');
9
10const 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):

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 maxAmountRequired today.
  • 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.