> x402
Pay per request with USDC on Base
Overview
JSONBill supports x402 v2 for pay‑per‑request USDC onchain payments. This is an alternative to Quaily credits.
Note: when paying with x402, you do not need the Authorization header.
Supported network/asset: Base (eip155:8453) USDC.
x402 is an open payment protocol designed for programmatic payments by AI agents and software over HTTP. It was driven by Coinbase, and Coinbase + Cloudflare launched x402 to advance it as an open Internet standard.
Prerequisites
- A Base wallet with enough USDC for the request.
- Your wallet private key in
X402_PRIVATE_KEY(used by the client SDK).
Step 1: Prepare the request JSON
Create a minimal request body (same shape as the API docs):
{
"type": "invoice",
"lang": "en",
"theme": "classic",
"params": {
"id": "demo-001",
"date": "2026-01-27T00:00:00Z",
"currency": "USD",
"company_name": "Demo Co",
"bill_to_company": "Client",
"doc": {
"title": "Invoice",
"description": "Demo"
},
"summary": {
"title": "Service",
"total_include_tax": 1
},
"detail_items": [
{
"title": "Item",
"desc": "Work",
"total_include_tax": 1
}
]
}
}
Settlement statement: set "type": "settlementstatement" and use recipient_company / recipient_address. payment.instruction is ignored; only payment.result is shown.
Step 2: Send the unpaid request
curl -i -X POST https://api.jsonbill.com/tasks/docs \
-H 'Content-Type: application/json' \
-d @doc.json
You will receive 402 Payment Required with a PAYMENT-REQUIRED header (base64‑encoded PaymentRequired JSON).
Step 3: Sign & retry with x402 SDK
Go (x402 SDK)
package main
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
x402sdk "github.com/coinbase/x402/go"
x402http "github.com/coinbase/x402/go/http"
evmscheme "github.com/coinbase/x402/go/mechanisms/evm/exact/client"
evmsigner "github.com/coinbase/x402/go/signers/evm"
)
func main() {
pk := os.Getenv("X402_PRIVATE_KEY")
signer, _ := evmsigner.NewClientSignerFromPrivateKey(pk)
x402Client := x402sdk.Newx402Client()
x402Client.Register(x402sdk.Network("eip155:*"), evmscheme.NewExactEvmScheme(signer))
x402HTTP := x402http.NewClient(x402Client)
body, _ := os.ReadFile("invoice.json")
httpClient := &http.Client{Timeout: 30 * time.Second}
req, _ := http.NewRequest("POST", "https://api.jsonbill.com/tasks/docs", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
resp, _ := httpClient.Do(req)
respBody, _ := io.ReadAll(resp.Body)
resp.Body.Close()
if resp.StatusCode != http.StatusPaymentRequired {
fmt.Printf("status: %s\nbody:\n%s\n", resp.Status, string(respBody))
return
}
headers := map[string]string{}
for k, v := range resp.Header {
if len(v) > 0 {
headers[k] = v[0]
}
}
required, _ := x402HTTP.GetPaymentRequiredResponse(headers, respBody)
selected, _ := x402Client.SelectPaymentRequirements(required.Accepts)
payload, _ := x402Client.CreatePaymentPayload(context.Background(), selected, required.Resource, required.Extensions)
payloadBytes, _ := json.Marshal(payload)
paymentHeaders := x402HTTP.EncodePaymentSignatureHeader(payloadBytes)
payReq, _ := http.NewRequest("POST", "https://api.jsonbill.com/tasks/docs", bytes.NewReader(body))
payReq.Header.Set("Content-Type", "application/json")
for k, v := range paymentHeaders {
payReq.Header.Set(k, v)
}
payResp, _ := httpClient.Do(payReq)
defer payResp.Body.Close()
payBody, _ := io.ReadAll(payResp.Body)
fmt.Printf("status: %s\nbody:\n%s\n", payResp.Status, string(payBody))
}
TypeScript (x402 SDK)
import fs from "node:fs";
import { wrapFetchWithPayment, x402Client } from "@x402/fetch";
import { ExactEvmScheme, toClientEvmSigner } from "@x402/evm";
import { createWalletClient, http } from "viem";
import { base } from "viem/chains";
import { privateKeyToAccount } from "viem/accounts";
const pk = process.env.X402_PRIVATE_KEY!;
const account = privateKeyToAccount((pk.startsWith("0x") ? pk : `0x${pk}`) as `0x${string}`);
const walletClient = createWalletClient({ chain: base, transport: http(), account });
const signer = toClientEvmSigner({
address: account.address,
signTypedData: (args) =>
walletClient.signTypedData({
account,
domain: args.domain,
types: args.types,
primaryType: args.primaryType,
message: args.message,
}),
});
const client = new x402Client().register(
`eip155:${base.id}`,
new ExactEvmScheme(signer)
);
const x402Fetch = wrapFetchWithPayment(fetch, client);
const payload = JSON.parse(fs.readFileSync("invoice.json", "utf-8"));
const resp = await x402Fetch("https://api.jsonbill.com/tasks/docs", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
const text = await resp.text();
console.log(`status: ${resp.status}`);
if (text) {
try {
console.log(JSON.stringify(JSON.parse(text), null, 2));
} catch {
console.log(text);
}
}
Step 4: Check task status / download
The response includes a trace_id. It also includes a short‑lived download_url and download_token for direct PDF download (valid for 15 minutes).
Example response:
{
"task": {
"id": 1,
"trace_id": "f4730554-791c-463b-9a27-aeacdfb02cf2",
"status": 0,
// ...
},
"download_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"download_token_expires_at": "2026-01-28T09:15:00Z",
"download_url": "https://api.jsonbill.com/tasks/f4730554-791c-463b-9a27-aeacdfb02cf2.pdf?download_token=..."
}
Notes
- The required amount, asset, and timeout are returned in
PAYMENT-REQUIRED. - If you use x402, you do not need JWT/API key headers.