coal
coal

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-key on 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

HeaderRequiredDescription
AuthorizationYesBearer <privy_access_token> from an authenticated console session

Request Body

FieldTypeRequiredDescription
namestringYesHuman-readable name shown in the Console and payment prompt.
pricenumberYesPrice to unlock access.
currencystringNoSettlement currency. Defaults to the configured settlement token (USDC unless overridden).
descriptionstringNoDescription shown to the user on the payment prompt.
contentTypestringNoContent type such as api, content, or download.
contentDataobjectNoOptional JSON payload used by the protected resource.
contentUrlstringNoOptional URL for the protected resource.
pricingModelstringNoPricing model such as one_time or per_call.

Response

text
1201 Created
FieldTypeDescription
idstringThe paywall's unique ID (starts with pw_).
namestringThe paywall name.
pricestringThe required payment amount.
currencystringSettlement currency.
contentTypestringProtected resource type.
pricingModelstringAccess model for the paywall.
createdAtstringISO 8601 creation timestamp.

cURL Example

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": "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

json
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

HeaderRequiredDescription
AuthorizationYesBearer <privy_access_token> from an authenticated console session

cURL Example

bash
1curl https://api.usecoal.xyz/api/console/paywalls \
2 -H "Authorization: Bearer <privy_access_token>"

Response Example

json
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

HeaderRequiredDescription
AuthorizationYesBearer <privy_access_token> from an authenticated console session

Path Parameters

ParameterTypeDescription
idstringThe paywall ID (pw_…).

Request Body

Any subset of the create fields: name, price, description, contentData, contentUrl, active, or pricingModel.

cURL Example

bash
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

json
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

HeaderRequiredDescription
AuthorizationYesBearer <privy_access_token> from an authenticated console session

cURL Example

bash
1curl -X DELETE https://api.usecoal.xyz/api/console/paywalls/pw_cm9x4k2j00003lb08n5qz7v1r \
2 -H "Authorization: Bearer <privy_access_token>"

Response Example

json
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

ParameterTypeRequiredDescription
addressstringYesThe EVM wallet address to check access for.

Response — Access Granted

text
1200 OK
json
1{
2 "hasAccess": true,
3 "expiresAt": "2026-03-22T23:00:00.000Z",
4 "txHash": "0x3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9c0d1e2f3a4b"
5}

Response — No Access

text
1200 OK
json
1{
2 "hasAccess": false
3}

cURL Example

bash
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

FieldTypeRequiredDescription
txHashstringYesThe settlement-token transfer transaction hash on Base.
addressstringYesThe buyer's EVM wallet address.

Response

text
1200 OK
json
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

bash
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

StatusCodeDescription
400INVALID_TX_HASHThe txHash is not a valid Ethereum transaction hash.
403SANCTIONS_MATCHThe address is on the Chainalysis sanctions list.
404PAYWALL_NOT_FOUNDNo paywall found with the given ID.
409ALREADY_VERIFIEDThis txHash has already been used to grant access.
429RATE_LIMITEDToo many requests. See Rate Limits.

Node.js: Middleware Example

Use Coal's verify endpoint as middleware to gate access to API routes:

typescript
1// middleware/requireCoalAccess.ts
2import { NextRequest, NextResponse } from 'next/server';
3
4const PAYWALL_ID = process.env.PAYWALL_ID!;
5const COAL_API = 'https://api.usecoal.xyz';
6
7export async function requireCoalAccess(
8 req: NextRequest,
9 handler: (req: NextRequest) => Promise<NextResponse>
10) {
11 const address = req.headers.get('x-wallet-address');
12
13 if (!address) {
14 return NextResponse.json({ error: 'Wallet address required' }, { status: 401 });
15 }
16
17 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();
22
23 if (!hasAccess) {
24 return NextResponse.json(
25 { error: 'Payment required', paywallId: PAYWALL_ID },
26 { status: 402 }
27 );
28 }
29
30 return handler(req);
31}