Safe x402 agent with Lucid
Lucid agents can pay x402 endpoints through the same fetch path any Node or Bun agent uses: construct a payment-enabled fetch, hand it to the agent action, and let the model choose the target URL.
That last step is the dangerous part. A model-selected URL may be a decoy priced at thousands of USDC, a zombie service that always errors after settlement, or an endpoint with no successful payment history. This recipe inserts Preflight by x402station.io before the wallet signs PAYMENT-SIGNATURE.
The pattern is deliberately small: build x402Fetch, wrap it with wrapWithPreflight, then expose only the guarded function to the Lucid action.
What you need
- A Lucid / Daydreams TypeScript agent project.
- A Base mainnet wallet controlled by the agent, with a small USDC balance.
x402station-middleware,@x402/fetch,@x402/evm, andviem.
Install
bun add x402station-middleware @x402/fetch @x402/evm viem zod
Build a guarded paid-call helper
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,
});
By default the guard is fail-closed. It refuses to sign on critical signals such as dead, zombie, decoy_price_extreme, never_paid_zombie, dead_7d, and mostly_dead.
Expose one Lucid action
Use the exact action helper your Lucid project already uses. The important boundary is that the action calls safeX402Fetch, not raw fetch or raw x402Fetch.
import { z } from "zod";
import { safeX402Fetch } from "./safe-x402-fetch";
export const payX402Endpoint = {
name: "pay_x402_endpoint",
description:
"Pay an x402 HTTP endpoint after x402station.io Preflight clears the target. Refuses decoys, zombies, dead endpoints, and never-paid zombies before PAYMENT-SIGNATURE.",
schema: z.object({
url: z.string().url(),
body: z.record(z.unknown()).optional(),
}),
handler: async ({ url, body }: { url: string; body?: Record<string, unknown> }) => {
try {
const response = await safeX402Fetch(url, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body ?? {}),
});
const text = await response.text();
try {
return JSON.parse(text);
} catch {
return { ok: response.ok, status: response.status, body: text };
}
} catch (error) {
if ((error as Error).name === "PreflightBlockedError") {
return {
blocked: true,
blocked_by: "x402station.io Preflight",
message: (error as Error).message,
};
}
throw error;
}
},
};
Then register payX402Endpoint with your Lucid agent exactly as you register other actions. The model should see one payment action, and that action should be the only path to x402 URLs.
Agent instruction
Add a short system instruction near the tool definition:
When pay_x402_endpoint returns blocked=true, do not retry the same URL.
Read the block reason and choose another endpoint or ask for an alternative.
The block happens before payment, so there is no refund to request.
This prevents the model from treating a blocked preflight as a transient HTTP failure.
Optional: direct x402station client
If you want the agent to inspect risk before choosing whether to pay, expose separate tools backed by the public x402station APIs:
preflightforensicscatalog_decoysalternativeswhats_new
The preserved Lucid client snapshot in contrib/daydreams-lucid-x402station/ shows the same method shape: x402Station({ account }).preflight({ url }) returns { result, paymentReceipt }, so the agent can audit both the safety verdict and the settled payment receipt.
Bulk credits
For high-volume agents, buy one credit bundle and pass the returned creditId into wrapWithPreflight:
export const safeX402Fetch = wrapWithPreflight(x402Fetch, {
creditId: process.env.X402STATION_CREDIT_ID,
});
$0.50 buys 1000 prepaid preflights, lowering effective preflight cost from $0.001 to $0.0005 per call.
Rollout checklist
-
safeX402Fetchwraps the payment-enabled fetch. - The Lucid action uses
safeX402Fetch, not rawfetch. - The model sees a
blocked=trueresult as a final refusal for that URL. -
AGENT_PRIVATE_KEYis stored in a secret manager or deployment env, not in source. - Optional
X402STATION_CREDIT_IDis configured for bulk credits. - A known decoy URL returns a pre-payment block before any wallet signature.
Source links
- npm:
x402station-middleware - API docs: https://x402station.io/api
- OpenAPI: https://x402station.io/api/openapi.json
- Lucid snapshot:
contrib/daydreams-lucid-x402station - Reach the team: hello@x402station.io