Paywalls API (x402)
Paywalls let you gate a URL or API resource behind a payment using the x402 payment protocol. A user proves they have paid by presenting a verified payment record, granting them access to the protected resource.
This page documents the console-managed paywall configuration surface. Public merchant API actions still use
x-api-keyon checkout/session routes;/api/console/*is Privy-authenticated.
Base URL: https://api.usecoal.xyz
Create a Paywall
POST /api/console/paywalls
Creates a new paywall that gates access to a specific URL or resource.
Authentication
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <privy_access_token> from an authenticated console session |
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name shown in the Console and payment prompt. |
price | number | Yes | Price to unlock access. |
currency | string | No | Settlement currency. Defaults to the configured settlement token (USDC unless overridden). |
description | string | No | Description shown to the user on the payment prompt. |
contentType | string | No | Content type such as api, content, or download. |
contentData | object | No | Optional JSON payload used by the protected resource. |
contentUrl | string | No | Optional URL for the protected resource. |
pricingModel | string | No | Pricing model such as one_time or per_call. |
Response
1201 Created
| Field | Type | Description |
|---|---|---|
id | string | The paywall's unique ID (starts with pw_). |
name | string | The paywall name. |
price | string | The required payment amount. |
currency | string | Settlement currency. |
contentType | string | Protected resource type. |
pricingModel | string | Access model for the paywall. |
createdAt | string | ISO 8601 creation timestamp. |
cURL Example
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": "Research Report — Q1 2026",6 "price": 5.00,7 "currency": "USDC",8 "contentType": "download",9 "contentUrl": "https://yoursite.com/reports/q1-2026.pdf",10 "description": "Pay once to download the Q1 2026 market research report.",11 "pricingModel": "one_time"12 }'
Response Example
1{2 "id": "pw_cm9x4k2j00003lb08n5qz7v1r",3 "merchantId": "usr_cm9x1merch",4 "name": "Research Report — Q1 2026",5 "price": "5.00",6 "currency": "USDC",7 "contentType": "download",8 "contentUrl": "https://yoursite.com/reports/q1-2026.pdf",9 "description": "Pay once to download the Q1 2026 market research report.",10 "pricingModel": "one_time",11 "active": true,12 "createdAt": "2026-03-22T10:00:00.000Z"13}
List Paywalls
GET /api/console/paywalls
Returns all paywalls for the authenticated merchant.
Authentication
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <privy_access_token> from an authenticated console session |
cURL Example
1curl https://api.usecoal.xyz/api/console/paywalls \2 -H "Authorization: Bearer <privy_access_token>"
Response Example
1{2 "paywalls": [3 {4 "id": "pw_cm9x4k2j00003lb08n5qz7v1r",5 "name": "Research Report — Q1 2026",6 "price": "5.00",7 "currency": "USDC",8 "contentType": "download",9 "pricingModel": "one_time",10 "active": true,11 "createdAt": "2026-03-22T10:00:00.000Z"12 }13 ]14}
Update a Paywall
PUT /api/console/paywalls/:id
Updates an existing paywall. Only the fields you provide are changed.
Authentication
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <privy_access_token> from an authenticated console session |
Path Parameters
| Parameter | Type | Description |
|---|---|---|
id | string | The paywall ID (pw_…). |
Request Body
Any subset of the create fields: name, price, description, contentData, contentUrl, active, or pricingModel.
cURL Example
1curl -X PUT https://api.usecoal.xyz/api/console/paywalls/pw_cm9x4k2j00003lb08n5qz7v1r \2 -H "Content-Type: application/json" \3 -H "Authorization: Bearer <privy_access_token>" \4 -d '{5 "price": 7.50,6 "pricingModel": "per_call"7 }'
Response Example
1{2 "id": "pw_cm9x4k2j00003lb08n5qz7v1r",3 "name": "Research Report — Q1 2026",4 "price": "7.50",5 "pricingModel": "per_call",6 "updatedAt": "2026-03-22T11:00:00.000Z"7}
Delete a Paywall
DELETE /api/console/paywalls/:id
Soft-deletes a paywall by marking it inactive. Existing valid access grants are not revoked.
Authentication
| Header | Required | Description |
|---|---|---|
Authorization | Yes | Bearer <privy_access_token> from an authenticated console session |
cURL Example
1curl -X DELETE https://api.usecoal.xyz/api/console/paywalls/pw_cm9x4k2j00003lb08n5qz7v1r \2 -H "Authorization: Bearer <privy_access_token>"
Response Example
1{2 "deleted": true,3 "id": "pw_cm9x4k2j00003lb08n5qz7v1r"4}
Verify Access (Public)
GET /api/paywalls/:id/verify?address=0x…
Checks whether a given wallet address has a valid (unexpired) access grant for this paywall. This endpoint is public - no API key required.
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
address | string | Yes | The EVM wallet address to check access for. |
Response — Access Granted
1200 OK
1{2 "hasAccess": true,3 "expiresAt": "2026-03-22T23:00:00.000Z",4 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b"5}
Response — No Access
1200 OK
1{2 "hasAccess": false3}
cURL Example
1curl "https://api.usecoal.xyz/api/paywalls/pw_cm9x4k2j00003lb08n5qz7v1r/verify?address=0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"
Record a Payment (Public)
POST /api/paywalls/:id/pay
Submits a transaction hash to prove payment and create an access grant for the wallet address. This endpoint is public - no API key required. It is called by your frontend (or the Coal widget) after the user sends the on-chain transfer.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
txHash | string | Yes | The settlement-token transfer transaction hash on Base. |
address | string | Yes | The buyer's EVM wallet address. |
Response
1200 OK
1{2 "status": "verifying",3 "paywallId": "pw_cm9x4k2j00003lb08n5qz7v1r",4 "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",5 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",6 "message": "Payment received. Access will be granted once the transaction is confirmed on Base."7}
After submission, Coal's cron job verifies the transaction within ~1–2 minutes. Poll GET /api/paywalls/:id/verify?address=0x… to check when access is granted.
cURL Example
1curl -X POST https://api.usecoal.xyz/api/paywalls/pw_cm9x4k2j00003lb08n5qz7v1r/pay \2 -H "Content-Type: application/json" \3 -d '{4 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b",5 "address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"6 }'
Error Responses
| Status | Code | Description |
|---|---|---|
400 | INVALID_TX_HASH | The txHash is not a valid Ethereum transaction hash. |
403 | SANCTIONS_MATCH | The address is on the Chainalysis sanctions list. |
404 | PAYWALL_NOT_FOUND | No paywall found with the given ID. |
409 | ALREADY_VERIFIED | This txHash has already been used to grant access. |
429 | RATE_LIMITED | Too many requests. See Rate Limits. |
Node.js: Middleware Example
Use Coal's verify endpoint as middleware to gate access to API routes:
1// middleware/requireCoalAccess.ts2import { NextRequest, NextResponse } from 'next/server';34const PAYWALL_ID = process.env.PAYWALL_ID!;5const COAL_API = 'https://api.usecoal.xyz';67export async function requireCoalAccess(8 req: NextRequest,9 handler: (req: NextRequest) => Promise<NextResponse>10) {11 const address = req.headers.get('x-wallet-address');1213 if (!address) {14 return NextResponse.json({ error: 'Wallet address required' }, { status: 401 });15 }1617 const verifyRes = await fetch(18 `${COAL_API}/api/paywalls/${PAYWALL_ID}/verify?address=${address}`,19 { next: { revalidate: 0 } }20 );21 const { hasAccess } = await verifyRes.json();2223 if (!hasAccess) {24 return NextResponse.json(25 { error: 'Payment required', paywallId: PAYWALL_ID },26 { status: 402 }27 );28 }2930 return handler(req);31}
