Skip to main content
Version: Testnet

Pay-Per-Request Access with x402

Our API supports the x402 payment protocol, allowing you to submit proofs and use other paid endpoints using USDC on Base Sepolia — no API key or account registration required. You pay per request directly from your wallet.

How It Works

The x402 protocol uses the HTTP 402 Payment Required status code to negotiate micropayments automatically. When you use the wrapFetchWithPayment helper, the entire negotiation happens transparently:

  1. You send a request to a paid endpoint
  2. The server responds with 402 and payment details (amount, asset, recipient)
  3. Your client signs a payment authorization with your wallet and retries the request
  4. The server verifies the payment and processes your request normally

For the full protocol specification, see docs.x402.org. For the official buyer quickstart guide, see the x402 Buyer Quickstart.

Prerequisites

  • Node.js 18+
  • A wallet private key with USDC on Base Sepolia
  • A small amount of ETH on Base Sepolia for gas fees

Get Testnet Tokens

Before testing, you will need testnet tokens on Base Sepolia:

Install Dependencies

Install the required packages:

npm install @x402/core @x402/evm @x402/fetch viem

Set Up the Client

Load your wallet private key using viem:

import { privateKeyToAccount } from "viem/accounts";

const signer = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);

Initialize the x402 client and register the EVM payment scheme:

import { x402Client, x402HTTPClient } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";

const client = new x402Client();
registerExactEvmScheme(client, { signer });

Wrap the global fetch with payment handling. The returned fetchWithPayment function automatically manages 402 responses — no extra code needed when making paid requests:

import { wrapFetchWithPayment } from "@x402/fetch";

const fetchWithPayment = wrapFetchWithPayment(fetch, client);

Check Pricing

Query available endpoints and their current prices — this endpoint is free and does not require payment:

const response = await fetch("https://api-testnet.kurier.xyz/api/v1/pricing");
const pricing = await response.json();
console.log(pricing);

Response:

{
"x402Enabled": true,
"network": "eip155:84532",
"asset": {
"symbol": "USDC",
"address": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
"decimals": 6
},
"endpoints": {
"POST /api/v1/submit-proof": {
"price_usd": "0.005",
"price_raw": "5000",
"description": "Submit a zero-knowledge proof for on-chain verification via zkVerify. Supports groth16, plonky2, risc0, fflonk, ultraplonk, sp1, ultrahonk, and ezkl proof types."
},
"POST /api/v1/register-vk": {
"price_usd": "0.001",
"price_raw": "1000",
"description": "Register a verification key on-chain and receive a vkHash for future proof submissions."
},
"POST /api/v1/random-hash": {
"price_usd": "0.001",
"price_raw": "1000",
"description": "Generate a verifiable random hash backed by a zero-knowledge proof. Testnet only."
}
}
}

Make a Paid Request

Use fetchWithPayment anywhere you would normally use fetch. The client handles the 402 negotiation automatically.

Submit a ZK Proof — $0.005

const response = await fetchWithPayment(
"https://api-testnet.kurier.xyz/api/v1/submit-proof",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proofType: "groth16",
vkRegistered: false,
proofOptions: {
library: "snarkjs",
curve: "bn254",
},
proofData: {
proof: {
/* your proof */
},
publicSignals: [
/* public inputs */
],
vk: {
/* verification key */
},
},
}),
},
);

const result = await response.json();
// { jobId: "abc-123", optimisticVerify: "success" }

After submission, use jobId to poll /api/v1/job-status for verification progress. See Job Statuses for the full status reference.

Register a Verification Key — $0.001

Register a VK once and use its hash in future proof submissions with vkRegistered: true:

const response = await fetchWithPayment(
"https://api-testnet.kurier.xyz/api/v1/register-vk",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
proofType: "groth16",
proofOptions: { library: "snarkjs", curve: "bn254" },
vk: {
/* verification key */
},
}),
},
);

const { vkHash } = await response.json();
// Use vkHash in future submit-proof calls with vkRegistered: true

Generate a Random Hash — $0.001

const response = await fetchWithPayment(
"https://api-testnet.kurier.xyz/api/v1/random-hash",
{ method: "POST" },
);

const result = await response.json();
// { jobId: "...", hash: "0x...", optimisticVerify: "success", proof: {...} }

For full details on the random hash response format and proof verification, see Verifiable Random Hash.

Understanding the Payment Flow

How the payment flow works without wrapFetchWithPayment

If you need manual control — for example, to inspect the 402 response or log payment details — you can drive the flow yourself using x402HTTPClient.

Step 1: Send a request without payment

const response = await fetch(
"https://api-testnet.kurier.xyz/api/v1/submit-proof",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
/* your request body */
}),
},
);
// response.status === 402

Step 2: Parse the payment requirements

The 402 response body contains payment details:

const httpClient = new x402HTTPClient(client);

const paymentRequired = httpClient.getPaymentRequiredResponse(
(name) => response.headers.get(name),
await response.json(),
);

// paymentRequired.accepts[0]:
// {
// scheme: "exact",
// network: "eip155:84532",
// amount: "5000", // raw USDC (6 decimals) = $0.005
// asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e", // USDC on Base Sepolia
// payTo: "0x...",
// maxTimeoutSeconds: 60
// }

wrapFetchWithPayment handles steps 1–3 automatically by signing the payment authorization and retrying the original request with a PAYMENT-SIGNATURE header.

Step 3: Check settlement after success

After a successful paid request, you can verify the settlement from the response header:

const settlement = httpClient.getPaymentSettleResponse((name) =>
response.headers.get(name),
);

if (settlement) {
console.log("Payment settled:", settlement);
}

Pricing

EndpointPrice
POST /api/v1/submit-proof$0.005 USDC
POST /api/v1/register-vk$0.001 USDC
POST /api/v1/random-hash$0.001 USDC

Prices are denominated in USDC on Base Sepolia (eip155:84532). Query GET /api/v1/pricing for real-time pricing.

Rate Limits

Each wallet address is limited to 60 requests per minute. Exceeding this returns HTTP 429. The rate limit uses a sliding window — wait briefly and retry.

Error Reference

HTTP StatusMeaningWhat to Do
200SuccessRequest processed and payment settled
402Payment RequiredHandled automatically by fetchWithPayment. If using raw fetch, parse the requirements and retry with a payment header
400Bad RequestCheck your request body format. See the API docs for the expected schema
422Validation ErrorProof data or VK format does not match the expected schema for the proof type
429Rate LimitedWait ~1 minute. Limit is 60 requests per minute per wallet address
500Server ErrorRetry the request or contact support

Troubleshooting

ProblemSolution
Failed to initialize clientVerify PRIVATE_KEY is a valid 0x-prefixed 64-character hex string
402 not handled automaticallyEnsure you are using fetchWithPayment from @x402/fetch, not the global fetch
Insufficient USDC balanceGet testnet USDC from the Circle Faucet (select Base Sepolia)
429 Too Many RequestsWait ~1 minute. The rate limit is 60 requests per minute per wallet address
Payment verification failedEnsure your wallet has enough USDC and ETH for gas on Base Sepolia

Resources