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.

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

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

{ amount, currency, merchantAddress, paywallId }

On-Chain Payment
User / BrowserApproves on-chain token transfer
User / BrowserBase Chain
ERC-20 transfer(merchantAddress, amount)
Base ChainTransaction mined on Base (~2 sec)
Proof Submission
Your AppCoal
POST /api/paywalls/:id/pay

{ txHash, address }

CoalBase Chain
Verify transfer event + recipient + amount
Base ChainCoal
Transfer confirmed
CoalYour App
200 OK

{ accessToken, expiresAt }

  1. Client requests accessGET /api/paywalls/:id/verify?address=0x...
  2. Server replies 402 — includes the price, merchant wallet address, and paywall ID.
  3. Client pays on-chain — sends the configured settlement token to the merchant address.
  4. Client submits proofPOST /api/paywalls/:id/pay with the txHash.
  5. Server verifies and responds 200 — returns the gated content or a short-lived access token.

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}

Verifying Access (Public API)

Step 1 — Check if access is permitted

GET /api/paywalls/:id/verify?address=0x...

If the address has not yet paid (or payment expired), returns 402:

json
1{
2 "status": 402,
3 "paywallId": "pw_clx9abc123def456",
4 "amount": "1.00",
5 "currency": "USDC",
6 "merchantAddress": "0xMerchantWallet...",
7 "description": "Premium API Access",
8 "pricingModel": "per_call"
9}

If the address has valid access, returns 200 with the protected resource or an access token:

json
1{
2 "status": 200,
3 "accessToken": "act_clx9tok...",
4 "expiresAt": "2026-03-22T13:00:00.000Z"
5}

Step 2 — Submit payment proof

POST /api/paywalls/:id/pay

json
1{
2 "txHash": "0xabc123def456...",
3 "address": "0xClientWallet..."
4}

Response on success:

json
1{
2 "status": 200,
3 "sessionId": "clx7k2p3q0000abc12345",
4 "accessToken": "act_clx9tok...",
5 "expiresAt": "2026-03-22T13:00:00.000Z"
6}

Agentic / AI Integration

Paywalls are a natural fit for AI agents that need to call paid APIs autonomously. Below is a full LangChain tool that handles the x402 flow end-to-end.

typescript
1import { tool } from '@langchain/core/tools';
2import { z } from 'zod';
3import { ethers } from 'ethers';
4
5const SETTLEMENT_TOKEN_ADDRESS = '0xSettlementTokenAddress';
6const COAL_API = 'https://api.usecoal.xyz/api';
7
8// ABI fragment — ERC-20 transfer
9const ERC20_ABI = [
10 'function transfer(address to, uint256 amount) returns (bool)',
11];
12
13export const callPaidApi = tool(
14 async ({ paywallId, walletPrivateKey }) => {
15 const provider = new ethers.JsonRpcProvider(process.env.RPC_URL);
16 const wallet = new ethers.Wallet(walletPrivateKey, provider);
17 const address = wallet.address;
18
19 // 1. Check access
20 const checkRes = await fetch(
21 `${COAL_API}/paywalls/${paywallId}/verify?address=${address}`
22 );
23
24 if (checkRes.status === 200) {
25 const { accessToken } = await checkRes.json();
26 return { accessToken, paid: false };
27 }
28
29 if (checkRes.status !== 402) {
30 throw new Error(`Unexpected status ${checkRes.status}`);
31 }
32
33 // 2. Parse payment instructions
34 const { amount, currency, merchantAddress } = await checkRes.json();
35 // 3. Pay on-chain
36 const token = new ethers.Contract(SETTLEMENT_TOKEN_ADDRESS, ERC20_ABI, wallet);
37 const decimals = 6; // configured settlement token precision
38 const value = ethers.parseUnits(amount, decimals);
39 const tx = await token.transfer(merchantAddress, value);
40 const receipt = await tx.wait();
41
42 // 4. Submit proof
43 const payRes = await fetch(`${COAL_API}/paywalls/${paywallId}/pay`, {
44 method: 'POST',
45 headers: { 'Content-Type': 'application/json' },
46 body: JSON.stringify({ txHash: receipt.hash, address }),
47 });
48
49 if (!payRes.ok) {
50 throw new Error('Payment verification failed');
51 }
52
53 const { accessToken } = await payRes.json();
54 return { accessToken, paid: true, txHash: receipt.hash };
55 },
56 {
57 name: 'call_paid_api',
58 description:
59 'Pays a Coal x402 paywall with the configured settlement token and returns an access token for the gated resource.',
60 schema: z.object({
61 paywallId: z.string().describe('The Coal paywall ID (pw_...)'),
62 walletPrivateKey: z.string().describe('Private key of the funding wallet'),
63 }),
64 }
65);

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