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

Creating a Paywall

Via the Console (Phase 3 UI)

  1. Open the Developer Console.
  2. Navigate to Paywalls → Create Paywall.
  3. Set the name, price, content type, and pricing model.
  4. Copy the generated paywall ID.

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.


Pricing Models

ModelBehaviourBest for
one_timeA single payment unlocks access permanently for that addressContent downloads, lifetime API keys
per_callEach API call requires a fresh payment (or valid cached access token)Metered APIs, AI tool calls, pay-per-use data

Content Types

ValueIntended use
apiREST or GraphQL endpoints — returns an access token
contentArticles, videos, PDFs — returns a signed download URL or HTML body
downloadBinary file downloads — returns a short-lived presigned URL