> x402

使用 Base 上 USDC 按次支付

概述

JSONBill 支持 x402 v2 的链上按次支付 USDC。这是使用 Quaily 点数之外的另一种方式。

注意:使用 x402 支付时不需要 Authorization 请求头。

x402 是为 AI Agent 与程序化支付设计的开放协议,基于 HTTP 402 进行按次付费。该协议由 Coinbase 推动,并与 Cloudflare 一起发起 x402,目标是推进为开放互联网标准。

支持网络/资产: Base(eip155:8453)USDC。

前置条件

  • Base 链钱包中有足够的 USDC。
  • 私钥放在环境变量 X402_PRIVATE_KEY(客户端 SDK 会读取)。

第一步:准备请求 JSON

创建最小请求体(与 API 文档一致的结构):

{
  "type": "invoice",
  "lang": "zh_cn",
  "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
      }
    ]
  }
}

结算单: 将 "type" 设为 "settlementstatement",并使用 recipient_company / recipient_address。payment.instruction 会被忽略,仅展示 payment.result。

第二步:发送未付款请求

curl -i -X POST https://api.jsonbill.com/tasks/docs \
  -H 'Content-Type: application/json' \
  -d @doc.json

你会收到 402 Payment Required,响应头里包含 PAYMENT-REQUIRED(base64 编码的 PaymentRequired JSON)。

第三步:使用 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);
  }
}

第四步:查询任务状态 / 下载

响应中会返回 trace_id。响应还会包含短时有效的 download_url 以及 download_token,可直接下载 PDF(有效期 15 分钟)。

响应示例:

{
  "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=..."
}

备注

  • 具体的金额、资产、超时时间由 PAYMENT-REQUIRED 返回。
  • 使用 x402 时不需要 JWT/API key 认证头。

Coinbase 官方示例