Safe x402 agent with Vercel AI SDK
The Vercel AI SDK makes tool-calling ergonomic: define a tool with tool({ parameters, execute }), pass it to generateText or streamText, and the model can call it when needed.
For x402 payments, that tool boundary is exactly where the safety check belongs. The model proposes a URL. The tool runs Preflight by x402station.io. Only if the verdict clears does the wallet sign PAYMENT-SIGNATURE.
What you need
- A TypeScript project using the Vercel AI SDK.
- A Base mainnet wallet held by the agent with a small USDC balance.
x402station-middleware,@x402/fetch,@x402/evm,viem, andzod.
Install
npm install ai zod x402station-middleware @x402/fetch @x402/evm viem
Build the guarded fetch
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { ExactEvmScheme } from "@x402/evm";
import { privateKeyToAccount } from "viem/accounts";
import { wrapWithPreflight } from "x402station-middleware";
const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY as `0x${string}`);
const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, {
schemes: [
{ network: "eip155:8453", client: new ExactEvmScheme(account) },
{ network: "eip155:84532", client: new ExactEvmScheme(account) },
],
});
export const safeX402Fetch = wrapWithPreflight(x402Fetch, {
creditId: process.env.X402STATION_CREDIT_ID,
});
Define a Vercel AI SDK tool
import { tool } from "ai";
import { z } from "zod";
import { safeX402Fetch } from "./safe-x402-fetch";
export const payX402Endpoint = tool({
description:
"Pay an x402 HTTP endpoint with USDC on Base after x402station.io Preflight clears it. Returns blocked=true if the endpoint is a decoy, zombie, dead, or otherwise unsafe before payment.",
parameters: z.object({
url: z.string().url(),
body: z.record(z.unknown()).optional(),
}),
execute: async ({ url, body }) => {
try {
const response = await safeX402Fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body ?? {}),
});
const text = await response.text();
let parsed: unknown = text;
try {
parsed = JSON.parse(text);
} catch {
// Non-JSON paid endpoints are still valid x402 resources.
}
return {
paid: true,
status: response.status,
result: parsed,
};
} catch (error) {
if ((error as Error).name === "PreflightBlockedError") {
return {
blocked: true,
blocked_by: "x402station.io Preflight",
message: (error as Error).message,
recommended_action: "choose_alternative_endpoint",
};
}
throw error;
}
},
});
The tool result is plain JSON. That matters: AI SDK tool execution forwards the value returned by execute, not the ambient HTTP response object. x402station.io therefore puts provenance and risk detail in response bodies, with headers as supplemental metadata only.
Use the tool in generateText
import { generateText } from "ai";
import { openai } from "@ai-sdk/openai";
import { payX402Endpoint } from "./tools/pay-x402-endpoint";
const result = await generateText({
model: openai("gpt-5.2"),
tools: {
pay_x402_endpoint: payX402Endpoint,
},
system: [
"You can pay x402 endpoints only through pay_x402_endpoint.",
"If the tool returns blocked=true, do not retry the same URL.",
"Choose a different endpoint or ask for an alternative.",
].join(" "),
prompt: "Fetch a paid quote from https://api.example.com/x402/quote.",
});
console.log(result.text);
For streaming agents, pass the same tool to streamText. The safety boundary is unchanged because the tool execute function is the only code path that can sign and retry the x402 request.
Optional: add an explicit preflight tool
Some agents should inspect risk before deciding whether to pay. Add a separate tool backed by the free trial for demos or the paid preflight endpoint for production:
export const inspectX402Endpoint = tool({
description: "Inspect x402 endpoint risk before payment. Use when choosing between candidate providers.",
parameters: z.object({ url: z.string().url() }),
execute: async ({ url }) => {
const response = await fetch("https://x402station.io/api/v1/preflight-trial", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ url }),
});
return response.json();
},
});
The trial route is rate-limited and cached. Production agents should still use paid /api/v1/preflight or the middleware guard for fresh, predictable results.
Bulk credits
For agents that make repeated paid calls, buy credits once and store the returned creditId:
export const safeX402Fetch = wrapWithPreflight(x402Fetch, {
creditId: process.env.X402STATION_CREDIT_ID,
});
$0.50 buys 1000 prepaid preflights, lowering effective preflight cost to $0.0005 per call.
Rollout checklist
-
pay_x402_endpointis the only tool that can pay arbitrary x402 URLs. - The tool uses
safeX402Fetch, not rawfetch. - The system prompt treats
blocked=trueas a final refusal for that URL. - Tool results include structured block data for the model to reason over.
- Optional
inspect_x402_endpointis available when the model compares providers. -
AGENT_PRIVATE_KEYand optionalX402STATION_CREDIT_IDlive in deployment secrets.
Source links
- npm:
x402station-middleware - API docs: https://x402station.io/api
- OpenAPI: https://x402station.io/api/openapi.json
- Vercel AI SDK tools: ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling
- Reach the team: hello@x402station.io