x402station specification

v1 · stable

Formal specification of the payment-safety layer for x402-paying agents. Every section is structured for an LLM consumer reading top-to-bottom. Humans can skim by the section headers; agents should ingest the entire document.

Machine-readable companions: /llms.txt · /api/openapi.json · /.well-known/x402 · /.well-known/agent-card.json · /skill.md

§0 TL;DR for AI agents

Three lines, in order. If your agent only reads §0, it should still be safe.

// before signing PAYMENT-SIGNATURE for any unfamiliar x402 URL:
const verdict = await fetch("https://x402station.io/api/v1/preflight", {
  method: "POST",
  headers: { "content-type": "application/json", /* x402 PAYMENT-SIGNATURE */ },
  body: JSON.stringify({ url: targetUrl }),
}).then(r => r.json()); // { ok, warnings, metadata }

if (!verdict.ok) {
  // do not pay. consult /api/v1/alternatives for a healthy peer.
  return abort();
}

Cost: $0.001 USDC per pre-flight. Settled via x402 itself. Bulk credits available at $0.0005/call (/guard).

§1 Overview

x402station is the payment-safety layer for x402-paying agents. We probe every endpoint in the agentic.market catalog every ~10 minutes, classify them against an open signal vocabulary (§3), and expose four paid agent surfaces:

surfacepurposecadenceprice
Guardsynchronous pre-flight before paymentper call$0.001 USDC
Forensics7-day diagnostic for one URLad-hoc$0.001 USDC
Watchasync webhook on state-flipsubscription$0.01 / 30 days
Verifiedsupply-side: provider mints a cert30 days$1 USDC

All four surfaces share the same signal vocabulary. The provider that pays for Verified cannot pay to remove a Guard signal — different paths, different gates.

§2 Authentication / payment

Every paid endpoint follows the canonical x402 v2 challenge-response:

  1. First request returns HTTP 402 Payment Required with payment-required header (base64-encoded JSON challenge) and www-authenticate: x402.
  2. Client signs the challenge per x402 spec, retries with PAYMENT-SIGNATURE header.
  3. Facilitator (x402.org/facilitator) settles the USDC transfer on Base mainnet (or Base Sepolia for testnet). Server returns HTTP 200 with the response body.

No accounts, no API keys, no rate limits. The signed payment header is the authentication. X-Credit-Id (returned from /api/v1/credits) is an alternative for bulk-prepaid calls.

§3 Signal vocabulary (open spec)

Six signal codes. Protocol-agnostic — x402 is the first applied profile, but the vocabulary can describe any agent-payment endpoint surface. Used identically across Guard / Forensics / Watch.

codemeaningevidence requirementtone
decoy_price_extremeendpoint listed at ≥ $1,000 USDC per callcurrent price_amount in catalogbad
zombieevery endpoint of this service is failing simultaneously≥2 endpoints, all latest probes 5xx or network errorbad
deadthis single endpoint failing on every probe in last hour≥6 probes in window, 0 healthybad
price_driftendpoint price changed by more than 10× since last snapshotdelta vs prior 24h price_amountwarn
high_concentrationendpoint's provider exceeds catalog concentration thresholdprovider share ≥ 50% of catalogwarn
recoveredendpoint previously firing a bad signal is now healthytransition: bad → healthy on latest probegood

The vocabulary is protocol-agnostic by design: future profiles (Stripe agent payments, Solana, AP2, generic HTTP 402) reuse the same codes. Each profile maps codes to its own evidence shape.

Iron rule: a provider can pay for an audit, but cannot pay to remove a risk signal. Signal classification is 100% probe-data-driven.

§4 Endpoint reference

per call (Guard)$0.001 USDC

Synchronous pre-flight check before paying any x402 URL. Agent calls this first; on ok: false, agent must not pay the target.

Body:

{ "url": "https://api.example.com/x402-endpoint" }

Response (200, after PAYMENT-SIGNATURE):

{
  "ok": true | false,                 // false if any critical signal fires
  "warnings": [                       // active signal codes (see §3)
    "decoy_price_extreme" | "zombie" | "dead" | "price_drift" |
    "high_concentration" | "no_history" | "unknown_endpoint"
  ],
  "metadata": {
    "service_id": "...",
    "service_name": "...",
    "domain": "...",
    "price": { "amount": "0.005", "currency": "USDC", "network": "Base" },
    "uptime_pct_1h": 98.4,
    "p99_latency_ms": 904,
    "first_seen_at": "2026-04-12T08:14:22Z",
    "category": "Inference",
    "verified": true | false
  }
}
ad-hoc$0.001 USDC

7-day diagnostic for a single URL. Use at integration time, not per-call. See /forensics for full response shape.

POST /api/v1/forensics
{ "url": "https://api.example.com/x402-endpoint" }

→ { url, window_days: 7, uptime_pct, latency_ms: {p50, p90, p99},
    status_breakdown, error_share_pct, signals: [...], price,
    first_seen_at, peer_comparison: { category, rank_in_category, ... } }
subscription$0.01 USDC / 30 days

Subscribe to webhook alerts when an endpoint flips state. 100 prepaid alerts. See /watch for full subscription shape.

POST /api/v1/watch
{
  "url":     "https://api.example.com/x402-endpoint",
  "webhook": "https://your-agent.example/x402-alerts",
  "signals": ["dead", "zombie", "decoy_price_extreme", "price_drift",
              "high_concentration", "recovered"]
}
→ { watchId, expiresAt }

// Webhook delivery (when a signal fires):
POST <your webhook>
Idempotency-Key: <unique>
{
  "watchId": "wch_...",
  "url": "...",
  "signal": "zombie",
  "evidence": { ... },
  "firedAt": "2026-05-05T12:34:56Z"
}
ad-hoc$0.005 USDC

Routing fallback. When Guard returns ok: false, call this with the flagged URL or a task class hint to get a healthy peer.

POST /api/v1/alternatives
{ "flaggedUrl": "https://api.bad.example/x402", "taskClass": "search" }
→ { alternatives: [ { url, score, reason, ... }, ... ] }
ad-hoc$0.005 USDC

Full blacklist export of currently-flagged endpoints. Use to seed an agent's local denylist; refresh hourly.

GET /api/v1/catalog/decoys
→ { generated_at, count, endpoints: [ { url, signals, evidence }, ... ] }
poll hourly$0.001 USDC

Catalog diff polling. Returns endpoints added or removed since since.

POST /api/v1/whats-new
{ "since": "2026-05-04T00:00:00Z", "limit": 200 }
→ { added_endpoints: [...], removed_endpoints: [...], summary: {...} }
bulk$0.50 USDC = 1000 prepaid

Buy bulk pre-flight credits. Effective rate $0.0005/call (50% off the per-call price). Pass returned creditId via X-Credit-Id on each call; on exhaustion, fall through to per-call x402 automatically.

POST /api/v1/credits
{}  // payment via x402

→ { creditId, balance: 1000, expiresAt, ... }

// later, on every preflight:
POST /api/v1/preflight
X-Credit-Id: <creditId>
{ "url": "..." }
→ HTTP 200 (no x402 settlement; balance auto-decremented)
30 days$1 USDC

Provider-side. Mint a 30-day signed certificate proving an endpoint passes the verified-tier criteria (uptime ≥95% over 7d, p99 < 5,000 ms, prices in $0.0001–$5 USDC band, ≥20 probes per endpoint, no critical signals).

POST /api/v1/verified
{ "url": "https://api.your-service.example/x402-endpoint" }
→ { certificateId, url, tier, issuedAt, expiresAt,
    publicAuditUrl: "https://x402station.io/verified/<id>" }

§5 Error codes for agents

statusmeaningagent action
200Payment processed, response body returnedUse the response. Continue.
402Payment required (x402 challenge)Sign the challenge, retry with PAYMENT-SIGNATURE header.
400Malformed body (e.g. missing url)Fix the request. Do not retry without changes.
422URL is not a valid x402 endpoint or otherwise unprocessableTreat as ok=false. Surface in agent decision log.
429Rate-limited (very rare; mostly soft limits)Back off ~10s, then retry. Use bulk credits to avoid.
503Statement timeout / probe pipeline degradedTreat as failed pre-flight. Do not pay the target.

§6 Integration patterns

6.1 npm middleware (recommended)

npm install x402station-middleware @x402/fetch @x402/evm viem
import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { wrapWithPreflight } from "x402station-middleware";

const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, { schemes: [...] });
const safeFetch = wrapWithPreflight(x402Fetch);

// fail-closed: throws PreflightBlockedError on any critical signal
await safeFetch("https://api.example.com/x402-endpoint", {
  method: "POST",
  body: JSON.stringify({ /* ... */ }),
});

6.2 MCP server (Claude Code / Cursor / Windsurf / Continue)

npx -y x402station-mcp

Exposes ten tools to the host: preflight, forensics, alternatives, catalog_decoys, whats_new, buy_credits, watch_subscribe, plus free credits_status / watch_status / watch_unsubscribe. Auto-signs PAYMENT-SIGNATURE via AGENT_PRIVATE_KEY (Base mainnet wallet).

MCP Registry: io.github.sF1nX/x402station

6.3 Raw API (any HTTP client)

# 1. cold call returns 402 with payment requirements
curl -i -X POST https://x402station.io/api/v1/preflight \
  -H "content-type: application/json" \
  -d '{"url":"https://api.example.com/x402-endpoint"}'
→ HTTP/1.1 402 Payment Required
   payment-required: <base64 challenge>
   www-authenticate: x402

# 2. retry with signed PAYMENT-SIGNATURE header
curl -i -X POST https://x402station.io/api/v1/preflight \
  -H "content-type: application/json" \
  -H "PAYMENT-SIGNATURE: <signed challenge>" \
  -d '{"url":"https://api.example.com/x402-endpoint"}'
→ HTTP/1.1 200 OK
   { "ok": true, "warnings": [], "metadata": {...} }

6.4 Framework templates

§7 Agent integration patterns

7.1 The fail-closed pre-flight gate

The canonical pattern. Wrap every paid x402 fetch with a Guard call. On any critical warning, abort.

async function paidFetch(url, init) {
  const verdict = await stationFetch.preflight(url);
  if (!verdict.ok) {
    throw new PreflightBlockedError(verdict.warnings, verdict.metadata);
  }
  return await x402Fetch(url, init);
}

7.2 Routing fallback

When pre-flight blocks a payment, ask /alternatives for a healthy peer in the same task class. Only one extra paid call.

async function paidFetchWithFallback(url, taskClass) {
  const verdict = await stationFetch.preflight(url);
  if (!verdict.ok) {
    const { alternatives } = await stationFetch.alternatives({
      flaggedUrl: url,
      taskClass,
    });
    if (alternatives.length === 0) throw new NoAlternativesError(verdict);
    return paidFetchWithFallback(alternatives[0].url, taskClass); // recurse
  }
  return await x402Fetch(url, /* ... */);
}

7.3 Long-running policy mode

For agents running >1 hour, subscribe a Watch on each integrated URL. The agent doesn't poll pre-flight; it only consults Guard if the local cache says the URL is dirty (received Watch alert).

// at integration time:
await stationFetch.watchSubscribe({
  url: "https://api.example.com/x402-endpoint",
  webhook: "https://your-agent.example/x402-alerts",
  signals: ["dead", "zombie", "decoy_price_extreme", "high_concentration"],
});

// in your webhook handler:
app.post("/x402-alerts", (req) => {
  const { url, signal } = req.body;
  agentDirtyCache.add(url, signal);
});

// in the paid path:
async function paidFetch(url, init) {
  if (agentDirtyCache.isDirty(url)) {
    const verdict = await stationFetch.preflight(url);
    if (!verdict.ok) throw new PreflightBlockedError(verdict.warnings);
  }
  return await x402Fetch(url, init);
}

7.4 Bulk pre-flight via credits

For agents making many paid calls per minute, buy 1,000 prepaid credits at $0.50 (50% off per-call rate).

const { creditId } = await stationFetch.buyCredits();
// pass credit id on every call; auto-fallthrough to per-call when balance hits 0
const verdict = await stationFetch.preflight(url, { creditId });

§8 Pricing

surfaceunitprice (USDC)notes
pre-flightper call0.001Guard sync gate
pre-flight (bulk)per call0.0005via $0.50 → 1000 credits
forensicsper report0.0017-day diagnostic
alternativesper call0.005routing fallback
catalog/decoysper call0.005full blacklist export
whats-newper call0.001catalog diff polling
watchper 30 days0.01100 prepaid alerts
verifiedper 30 days1.00supply-side mint

All prices in USDC on Base mainnet (eip155:8453). Settled via x402.org/facilitator.

§9 Rate limits & quotas

  • No per-IP / per-account rate limit for paid endpoints — payment is the throttle.
  • Pre-flight statement timeout: 5 s. On timeout, server returns 503; agent should treat as ok: false.
  • Free endpoints (credits/status, watch/status) are softly rate-limited at 60 rpm per IP.
  • Webhook delivery for Watch retries 5× with exponential backoff (1s / 2s / 4s / 8s / 16s) before burning a quota credit and giving up.

§10 Invariants

Properties guaranteed by the implementation. Agents may rely on these.

  1. Signal independence. A provider that pays for Verified status cannot pay to remove a Guard warning. The two code paths are orthogonal.
  2. Probe transparency. Every signal fires from raw probe data; the evidence shape is public in §3 and the source SQL is open in the public mirror.
  3. Settlement idempotency. Each endpoint accepts the same payment_tx at most once. Retries with the same signed payment return the prior response, never double-charge.
  4. Catalog identity. An endpoint is identified by canonical URL (lowercased host, no trailing slash, query params normalised). The same URL always maps to the same endpoint_id.
  5. Time monotonicity. All timestamps are UTC ISO 8601 from a single Postgres clock. No client timestamps cross the API boundary.

§11 Machine-readable surfaces

For AI scrapers / agent runtimes that prefer structured ingestion over this HTML page:

§12 End-to-end examples

12.1 Minimal Bun agent

import { wrapFetchWithPaymentFromConfig } from "@x402/fetch";
import { wrapWithPreflight } from "x402station-middleware";
import { privateKeyToAccount } from "viem/accounts";

const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY!);
const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, {
  account,
  facilitator: "https://x402.org/facilitator",
});
const safeFetch = wrapWithPreflight(x402Fetch);

const res = await safeFetch("https://api.exa.ai/search", {
  method: "POST",
  headers: { "content-type": "application/json" },
  body: JSON.stringify({ query: "x402 agent payments" }),
});
console.log(await res.json());

12.2 Cloudflare Workers

export default {
  async fetch(request, env) {
    const account = privateKeyToAccount(env.AGENT_PRIVATE_KEY);
    const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, { account });
    const safeFetch = wrapWithPreflight(x402Fetch);
    try {
      const r = await safeFetch(targetUrl, { method: "POST", body });
      return r;
    } catch (e) {
      if (e instanceof PreflightBlockedError) {
        return new Response(JSON.stringify({ blocked: e.warnings }), { status: 422 });
      }
      throw e;
    }
  }
}

12.3 LangChain DynamicStructuredTool

const safePayTool = new DynamicStructuredTool({
  name: "safe_x402_pay",
  description: "Pay an x402 endpoint after pre-flight check. Use for any paid HTTP call.",
  schema: z.object({ url: z.string().url(), body: z.unknown() }),
  func: async ({ url, body }) => {
    const r = await safeFetch(url, { method: "POST", body: JSON.stringify(body) });
    return await r.text();
  },
});

§13 Contact / changelog

Bugs, feature requests, edge cases, ideas: hello@x402station.io. Operator input shapes the roadmap directly.

Changelog and 60-day data: /blog. Source mirrors: x402station-mcp · x402station-middleware.

This specification covers v1 (stable). v2 will add a protocol-agnosticx402-signals open-spec repository; the signal vocabulary in §3 is the v2 candidate.