Skip to main content

Examples

Complete, copy-paste-ready examples for building and consuming x402 endpoints.

Handler Examples

Simple JSON API

// x402/weather/index.ts
export default async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
const city = url.searchParams.get("city") ?? "New York";

return Response.json({
city,
temperature: 72,
conditions: "sunny",
timestamp: new Date().toISOString(),
});
}

External API Proxy

// x402/crypto-prices/index.ts
export default async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
const symbol = url.searchParams.get("symbol")?.toUpperCase() ?? "BTC";

const apiKey = process.env.COINGECKO_API_KEY;
const res = await fetch(
`https://api.coingecko.com/api/v3/simple/price?ids=${symbol}&vs_currencies=usd`,
{ headers: { "x-cg-demo-api-key": apiKey! } },
);

if (!res.ok) {
return Response.json({ error: "Failed to fetch price" }, { status: 502 });
}

const data = await res.json();
return Response.json({ symbol, price: data[symbol.toLowerCase()]?.usd });
}

POST Endpoint with Body Parsing

// x402/sentiment/index.ts
export default async function handler(req: Request): Promise<Response> {
if (req.method !== "POST") {
return Response.json({ error: "POST required" }, { status: 405 });
}

const body = await req.json();
const text = body.text;

if (!text || typeof text !== "string") {
return Response.json({ error: "text field required" }, { status: 400 });
}

// Call an AI model for analysis
const analysis = await analyzeSentiment(text);

return Response.json({
text: text.slice(0, 100),
sentiment: analysis.sentiment,
score: analysis.score,
confidence: analysis.confidence,
});
}

async function analyzeSentiment(text: string) {
const res = await fetch("https://llm.bankr.bot/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BANKR_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model: "claude-sonnet-4-6",
messages: [
{
role: "system",
content:
"Analyze sentiment. Return a single JSON string that can be passed direclty to JSON.parse(): {sentiment, score, confidence} NO MARKDOWN",
},
{ role: "user", content: text },
],
response_format: { type: "json_object" },
}),
});

const data = await res.json();
return JSON.parse(data.choices[0].message.content);
}

Using npm Dependencies

Your handler can use any npm package. Add a package.json to your service directory and install packages with bun add. Dependencies are bundled automatically during deployment.

cd x402/my-service
bun add zod @langchain/openai @langchain/langgraph
// x402/my-service/index.ts
import { ChatOpenAI } from "@langchain/openai";
import { z } from "zod";

const InputSchema = z.object({
query: z.string().min(1),
});

export default async function handler(req: Request): Promise<Response> {
if (req.method !== "POST") {
return Response.json({ error: "POST required" }, { status: 405 });
}

const body = await req.json();
const input = InputSchema.safeParse(body);
if (!input.success) {
return Response.json({ error: input.error.message }, { status: 400 });
}

const llm = new ChatOpenAI({
model: "gemini-2.5-flash",
configuration: {
baseURL: "https://llm.bankr.bot/v1",
defaultHeaders: { "X-API-Key": process.env.BANKR_LLM_KEY! },
},
apiKey: process.env.BANKR_LLM_KEY!,
});

const result = await llm.invoke(input.data.query);
return Response.json({ response: result.content });
}

Verify your bundle before deploying:

bankr x402 build my-service
Performance

Handlers have a 30-second execution limit. For AI-powered endpoints, use fast models like gemini-2.5-flash through the Bankr LLM Gateway and keep external API calls to a minimum. If your handler makes multiple LLM calls, consider restructuring to use a single call with a detailed prompt.

Handler Patterns

Your handler receives a standard Web Request and returns a standard Web Response. No special framework — just the platform APIs you already know.

Reading Query Parameters

export default async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);

const city = url.searchParams.get("city") ?? "New York";
const units = url.searchParams.get("units") ?? "fahrenheit";
const limit = parseInt(url.searchParams.get("limit") ?? "10");

return Response.json({ city, units, limit });
}

Reading Headers

export default async function handler(req: Request): Promise<Response> {
const lang = req.headers.get("Accept-Language") ?? "en";
const userAgent = req.headers.get("User-Agent") ?? "unknown";

return Response.json({ lang, userAgent });
}

Parsing a JSON Body

export default async function handler(req: Request): Promise<Response> {
if (req.method !== "POST") {
return Response.json({ error: "POST required" }, { status: 405 });
}

const body = await req.json();
// body is typed as `any` — validate what you need
const { text, language } = body;

if (!text) {
return Response.json({ error: "text is required" }, { status: 400 });
}

return Response.json({ received: text, language: language ?? "auto" });
}

Parsing Form Data

export default async function handler(req: Request): Promise<Response> {
const form = await req.formData();
const name = form.get("name") as string;
const file = form.get("file") as File;

return Response.json({
name,
fileName: file?.name,
fileSize: file?.size,
});
}

Returning JSON

The most common pattern. Response.json() sets Content-Type: application/json automatically.

return Response.json({ result: "success", data: [1, 2, 3] });

// With a custom status code
return Response.json({ error: "not found" }, { status: 404 });

// With custom headers
return Response.json(
{ data: "ok" },
{
headers: { "X-Request-Id": "abc123" },
},
);

Returning HTML

export default async function handler(req: Request): Promise<Response> {
const html = `
<html>
<body>
<h1>Hello from x402</h1>
<p>This endpoint costs $0.001 per request.</p>
</body>
</html>
`;

return new Response(html, {
headers: { "Content-Type": "text/html" },
});
}

Returning an Image

export default async function handler(req: Request): Promise<Response> {
// Fetch or generate an image
const res = await fetch("https://api.example.com/generate-chart?data=...");
const imageBuffer = await res.arrayBuffer();

return new Response(imageBuffer, {
headers: { "Content-Type": "image/png" },
});
}

Returning Plain Text

return new Response("Hello, world!", {
headers: { "Content-Type": "text/plain" },
});

Returning a Stream

export default async function handler(req: Request): Promise<Response> {
const stream = new ReadableStream({
async start(controller) {
for (const chunk of ["Hello ", "from ", "x402!"]) {
controller.enqueue(new TextEncoder().encode(chunk));
await new Promise((r) => setTimeout(r, 500));
}
controller.close();
},
});

return new Response(stream, {
headers: { "Content-Type": "text/plain" },
});
}

Using Environment Variables

Set secrets via the CLI or dashboard — they're available as process.env in your handler.

bankr x402 env set BANKR_API_KEY=sk_...
bankr x402 env set DATABASE_URL=postgres://...
export default async function handler(req: Request): Promise<Response> {
const apiKey = process.env.BANKR_API_KEY;
if (!apiKey) {
return Response.json({ error: "API key not configured" }, { status: 500 });
}

// Use the secret in your handler logic
const res = await fetch("https://llm.bankr.bot/v1/chat/completions", {
headers: { Authorization: `Bearer ${apiKey}` },
// ...
});

return Response.json(await res.json());
}

Config for Multiple Services

{
"network": "base",
"services": {
"weather": {
"description": "Real-time weather data for any city",
"price": "0.001",
"methods": ["GET"],
"schema": {
"input": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name" }
},
"required": ["city"]
},
"output": {
"type": "object",
"properties": {
"temperature": { "type": "number" },
"conditions": { "type": "string" }
}
}
}
},
"sentiment": {
"description": "AI-powered sentiment analysis",
"price": "0.01",
"methods": ["POST"],
"schema": {
"input": {
"type": "object",
"properties": {
"text": { "type": "string", "description": "Text to analyze" }
},
"required": ["text"]
},
"output": {
"type": "object",
"properties": {
"sentiment": { "type": "string" },
"score": { "type": "number" }
}
}
}
},
"crypto-prices": {
"description": "Real-time cryptocurrency prices",
"price": "0.001",
"methods": ["GET"]
}
}
}

Client Examples

TypeScript — x402-fetch

The simplest way to call an x402 endpoint. x402-fetch wraps the standard fetch API and handles the 402 payment flow automatically.

bun add x402-fetch viem
import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";
import { wrapFetchWithPayment } from "x402-fetch";

// Set up wallet (needs USDC on Base)
const account = privateKeyToAccount("0xYOUR_PRIVATE_KEY" as `0x${string}`);
const wallet = createWalletClient({ account, chain: base, transport: http() });

// Wrap fetch — max 1 USDC per request
const paidFetch = wrapFetchWithPayment(fetch, wallet, BigInt(1_000_000));

// GET request
const weather = await paidFetch(
"https://x402.bankr.bot/0xOwnerWallet/weather?city=London",
);
console.log(await weather.json());

// POST request
const sentiment = await paidFetch(
"https://x402.bankr.bot/0xOwnerWallet/sentiment",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "Bankr is amazing!" }),
},
);
console.log(await sentiment.json());

TypeScript — Manual 402 Flow

If you want to handle the payment flow yourself:

import { createWalletClient, http } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { base } from "viem/chains";

const ENDPOINT = "https://x402.bankr.bot/0xOwnerWallet/weather?city=NYC";

// 1. Make initial request — expect 402
const initial = await fetch(ENDPOINT);

if (initial.status === 402) {
const { accepts, facilitator } = await initial.json();
const requirements = accepts[0]; // First accepted payment option

console.log("Payment required:");
console.log(" Amount:", requirements.amount, "atomic units");
console.log(" Asset:", requirements.asset);
console.log(" Network:", requirements.network);
console.log(" Pay to:", requirements.payTo);

// 2. Sign the payment (use x402-fetch or x402 SDK for this)
// ... signing logic ...

// 3. Retry with payment header
const paid = await fetch(ENDPOINT, {
headers: { "PAYMENT-SIGNATURE": signedPaymentBase64 },
});

console.log("Response:", await paid.json());
}

cURL — Inspect Payment Requirements

# See what an endpoint charges
curl -s https://x402.bankr.bot/0xOwnerWallet/weather | jq .

# Output:
# {
# "x402Version": 2,
# "error": "Payment Required",
# "accepts": [{
# "scheme": "exact",
# "network": "base",
# "amount": "1000",
# "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
# "payTo": "0xOwnerWallet",
# "maxTimeoutSeconds": 60,
# "extra": { "name": "USD Coin", "version": "2" }
# }],
# "facilitator": "https://api.bankr.bot/facilitator"
# }

Bankr Agent Integration

Bankr AI agents can discover and call x402 endpoints automatically. If your endpoint has a description and schema in the config, agents can find it and call it on behalf of users.

{
"services": {
"weather": {
"description": "Get real-time weather data for any city worldwide",
"price": "0.001",
"methods": ["GET"],
"category": "data",
"tags": ["weather", "forecast", "temperature"],
"schema": {
"input": {
"type": "object",
"properties": {
"city": { "type": "string", "description": "City name or zip code" }
},
"required": ["city"]
},
"output": {
"type": "object",
"properties": {
"temperature": { "type": "number", "description": "Current temperature" },
"conditions": { "type": "string", "description": "Weather conditions" },
"humidity": { "type": "number", "description": "Humidity percentage" }
}
}
}
}
}
}

With this schema, an agent can:

  1. Search for "weather data" → finds your endpoint
  2. Show the user the price ($0.001)
  3. Call it with payment → return the results