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
{ amount, currency, merchantAddress, paywallId }
{ txHash, address }
{ accessToken, expiresAt }
- Client requests access —
GET /api/paywalls/:id/verify?address=0x... - Server replies 402 — includes the price, merchant wallet address, and paywall ID.
- Client pays on-chain — sends the configured settlement token to the merchant address.
- Client submits proof —
POST /api/paywalls/:id/paywith thetxHash. - Server verifies and responds 200 — returns the gated content or a short-lived access token.
Creating a Paywall
Via the Console (Phase 3 UI)
- Open the Developer Console.
- Navigate to Paywalls → Create Paywall.
- Set the name, price, content type, and pricing model.
- Copy the generated paywall ID.
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}
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:
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:
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
1{2 "txHash": "0xabc123def456...",3 "address": "0xClientWallet..."4}
Response on success:
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.
1import { tool } from '@langchain/core/tools';2import { z } from 'zod';3import { ethers } from 'ethers';45const SETTLEMENT_TOKEN_ADDRESS = '0xSettlementTokenAddress';6const COAL_API = 'https://api.usecoal.xyz/api';78// ABI fragment — ERC-20 transfer9const ERC20_ABI = [10 'function transfer(address to, uint256 amount) returns (bool)',11];1213export 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;1819 // 1. Check access20 const checkRes = await fetch(21 `${COAL_API}/paywalls/${paywallId}/verify?address=${address}`22 );2324 if (checkRes.status === 200) {25 const { accessToken } = await checkRes.json();26 return { accessToken, paid: false };27 }2829 if (checkRes.status !== 402) {30 throw new Error(`Unexpected status ${checkRes.status}`);31 }3233 // 2. Parse payment instructions34 const { amount, currency, merchantAddress } = await checkRes.json();35 // 3. Pay on-chain36 const token = new ethers.Contract(SETTLEMENT_TOKEN_ADDRESS, ERC20_ABI, wallet);37 const decimals = 6; // configured settlement token precision38 const value = ethers.parseUnits(amount, decimals);39 const tx = await token.transfer(merchantAddress, value);40 const receipt = await tx.wait();4142 // 4. Submit proof43 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 });4849 if (!payRes.ok) {50 throw new Error('Payment verification failed');51 }5253 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
| Model | Behaviour | Best for |
|---|---|---|
one_time | A single payment unlocks access permanently for that address | Content downloads, lifetime API keys |
per_call | Each API call requires a fresh payment (or valid cached access token) | Metered APIs, AI tool calls, pay-per-use data |
Content Types
| Value | Intended use |
|---|---|
api | REST or GraphQL endpoints — returns an access token |
content | Articles, videos, PDFs — returns a signed download URL or HTML body |
download | Binary file downloads — returns a short-lived presigned URL |
