> 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.

Official SDK demos