back to Guard

recipe updated 2026-05-13

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

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

Source links

back to GuardAPI docs