x402station-middleware
v0.1.0
Published
Drop-in fetch wrapper that calls x402station.io /preflight before every paid x402 request and refuses ok=false. One-line auto-shielding for any agent that already wraps fetch with @x402/fetch.
Maintainers
Readme
x402station-middleware
Drop-in fetch wrapper that calls x402station.io /preflight before every paid x402 request your agent makes, and refuses (throws) when the endpoint is flagged dangerous.
If your agent already wraps fetch with @x402/fetch, this is one line of code:
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) }],
});
// One-line shielding: preflight runs automatically before every call.
// Second arg is options (all optional); the wallet binding lives inside
// `x402Fetch`, the middleware itself doesn't need an account.
const safeFetch = wrapWithPreflight(x402Fetch);
// Use exactly like fetch.
const r = await safeFetch("https://api.example.com/x402-endpoint", {
method: "POST",
body: JSON.stringify({ ... }),
});
// ↑ throws PreflightBlockedError if the endpoint is decoy / zombie / dead.Why
x402 lets agents pay HTTP endpoints, but the network has shape problems:
- ~161 endpoints listed at ≥ $1000 USDC (decoy / honeypot — most are anti-scraper "swarm" routes)
- ~10 services 100 % erroring in the last hour but still listed (zombies)
- Catalog concentration (one provider can be > 40 % of the catalog)
x402station independently probes every endpoint on agentic.market every 10 minutes and flags these. This middleware makes those signals automatic — your agent code doesn't have to remember to preflight, the wrapper does it.
Install
npm install x402station-middleware
# or
pnpm add x402station-middleware
# or
bun add x402station-middlewarePeer deps (must already be installed in your agent project): @x402/fetch, @x402/evm, viem.
API
wrapWithPreflight(paidFetch, options?)
Returns a fetch-shaped function that calls /api/v1/preflight on every URL before forwarding.
| Option | Type | Default | Description |
|---|---|---|---|
| refuseOn | string[] | ["dead", "zombie", "decoy_price_extreme"] | Warnings that cause refusal. Pass [] to disable refusal (preflight still runs, but pass-through). |
| cacheTtlMs | number | 300_000 (5 min) | Per-URL preflight cache. Matches x402station's internal probe cadence (10 min) — same window, identical data. Set 0 to disable. |
| creditId | string (uuid) | — | Bulk-credit id from POST /api/v1/credits ($0.50 = 1000 prepaid preflights). When set, preflight calls go via X-Credit-Id header instead of paying $0.001 each → effective $0.0005/preflight. |
| failOpen | boolean | false | Forward the request when preflight itself fails (network / 503 / timeout). Default fail-closed. |
| preflightTimeoutMs | number | 10_000 | Per-call preflight timeout. Clamped to [1_000, 30_000]. |
| baseUrl | string | https://x402station.io | Override (testing). Only canonical x402station.io or localhost / 127.0.0.1 / [::1] accepted; any other host throws at wrap time. |
Error classes
import { PreflightBlockedError, PreflightUnavailableError } from "x402station-middleware";
try {
await safeFetch(url);
} catch (e) {
if (e instanceof PreflightBlockedError) {
console.log("blocked:", e.warnings, e.metadata);
// → e.warnings: ["zombie", "decoy_price_extreme", ...]
// → e.metadata: { url, service, provider, price_usdc, ... }
}
if (e instanceof PreflightUnavailableError) {
console.log("preflight down:", e.cause);
}
}Cost
- Per call: $0.001 USDC (the standard x402station preflight tier).
- With
creditId: $0.0005 USDC effective ($0.50 / 1000 calls). Buy viaPOST /api/v1/credits. - Cached calls: free.
For an agent making 100 paid x402 calls per day, that's ~$0.10/day in preflights, often offset by the very first decoy you avoid (a single $1000 honeypot would have wiped your wallet).
Networks
Base mainnet (eip155:8453) — the network x402 lives on. Base Sepolia (eip155:84532) for testing. The wallet you pass into wrapFetchWithPaymentFromConfig must hold USDC; preflight settles the same way every other paid x402 call does.
Caveats / design choices
- Default fail-closed. If preflight is unreachable (network blip, 503, timeout), the call throws
PreflightUnavailableError. Override withfailOpen: trueonly if availability matters more than safety. - Refusal is on warnings, not just
ok. We throw if any warning inrefuseOnis in the response, OR if the server flippedoktofalse. This is intentional belt-and-suspenders — keeps you safe even if the server'sokcalculation changes. - Self-requests skip. Calls TO
x402station.ioitself (preflight, forensics, credits, etc.) are forwarded directly without recursing. - Cache is per-instance. Re-instantiating
wrapWithPreflightgives you a fresh cache. There's no global cache; if you want one, share the wrapped fetch. - Bearer-token allow-list on
baseUrl. Onlyhttps://x402station.ioor local-dev hosts pass. Mirrors the canonical-host check inx402station-mcp— a misconfigured agent can't be tricked into routing preflight payments off-target. CodeRabbit findings (mastra-ai/mastra#15804).
Companion
Same family as the official MCP adapter x402station-mcp, the AgentKit x402station action provider, the @lucid-agents/x402station client, and the upcoming standalone Mastra package. All share signals + canonical host allow-list + Greptile/CodeRabbit security fixes.
License
MIT.
