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 — 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:
- You send a request to a paid endpoint
- The server responds with 402 and payment details (amount, asset, recipient)
- Your client signs a payment authorization with your wallet and retries the request
- 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
- A small amount of ETH on Base for gas fees
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.kurier.xyz/api/v1/pricing");
const pricing = await response.json();
console.log(pricing);
Response:
{
"x402Enabled": true,
"network": "eip155:8453",
"asset": {
"symbol": "USDC",
"address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
"decimals": 6
},
"endpoints": {
"POST /api/v1/submit-proof": {
"price_usd": "0.02",
"price_raw": "20000",
"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.02",
"price_raw": "20000",
"description": "Register a verification key on-chain and receive a vkHash for future proof submissions."
},
"POST /api/v1/random-hash": {
"price_usd": "0.02",
"price_raw": "20000",
"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.02
const response = await fetchWithPayment(
"https://api.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.02
Register a VK once and use its hash in future proof submissions with vkRegistered: true:
const response = await fetchWithPayment(
"https://api.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.02
const response = await fetchWithPayment(
"https://api.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.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:8453",
// amount: "200", // raw USDC (6 decimals) = $0.02
// asset: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
// 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
| Endpoint | Price |
|---|---|
POST /api/v1/submit-proof | $0.02 USDC |
POST /api/v1/register-vk | $0.02 USDC |
POST /api/v1/random-hash | $0.02 USDC |
Prices are denominated in USDC on Base (eip155:8453). 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 Status | Meaning | What to Do |
|---|---|---|
| 200 | Success | Request processed and payment settled |
| 402 | Payment Required | Handled automatically by fetchWithPayment. If using raw fetch, parse the requirements and retry with a payment header |
| 400 | Bad Request | Check your request body format. See the API docs for the expected schema |
| 422 | Validation Error | Proof data or VK format does not match the expected schema for the proof type |
| 429 | Rate Limited | Wait ~1 minute. Limit is 60 requests per minute per wallet address |
| 500 | Server Error | Retry the request or contact support |
Troubleshooting
| Problem | Solution |
|---|---|
Failed to initialize client | Verify PRIVATE_KEY is a valid 0x-prefixed 64-character hex string |
| 402 not handled automatically | Ensure you are using fetchWithPayment from @x402/fetch, not the global fetch |
| Insufficient USDC balance | Purchase USDC on Base via a CEX or DEX and transfer to your wallet |
| 429 Too Many Requests | Wait ~1 minute. The rate limit is 60 requests per minute per wallet address |
| Payment verification failed | Ensure your wallet has enough USDC and ETH for gas on Base |
Resources
- Submit feedback or report an issue: Kurier API: Feedback
- Submit a feature request: Kurier API: New Feature Requests
- Reach out on Discord or email kurier-support@horizenlabs.io
- x402 protocol documentation: docs.x402.org
- x402 buyer quickstart: Quickstart for Buyers