back to Guard

recipe updated 2026-05-05

Safe x402 agent with Cloudflare Agents

Cloudflare Agents (the platform behind agents-sdk + Workers AI Gateway + vectorize) supports outbound paid HTTP via the standard fetch API. When your agent calls an x402 endpoint, the request goes through wrapFetchWithPaymentFromConfig (or your equivalent) and signs PAYMENT-SIGNATURE against the wallet you've bound.

Cloudflare's own agent-readiness scanner has flagged endpoint trust as one of the top open problems — agents on Workers can't easily tell whether a paid x402 URL is real, slow, or a $1,000–$500,000 honeypot. This recipe wraps the agent's fetch in x402station-middleware (Guard) so a preflight check runs before every PAYMENT-SIGNATURE. Fail-closed by default.

What you need

Install

npm install x402station-middleware @x402/fetch @x402/evm viem

x402station-middleware is pure ESM and works in the Workers runtime. No Node-only dependencies.

Wrangler config — secret binding

Put the agent's private key in a Workers secret, never in wrangler.toml:

wrangler secret put AGENT_PRIVATE_KEY
# paste the 0x-prefixed private key

If you want to use bulk credits ($0.50 = 1000 prepaid preflights):

wrangler secret put X402STATION_CREDIT_ID
# paste the creditId returned by POST /api/v1/credits

Or store it in a KV namespace if you'll rotate it programmatically.

The agent file

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

export interface Env {
  AGENT_PRIVATE_KEY: string;
  X402STATION_CREDIT_ID?: string;
}

export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const account = privateKeyToAccount(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) },
      ],
    });

    const safeFetch = wrapWithPreflight(x402Fetch, {
      creditId: env.X402STATION_CREDIT_ID,
      // Defaults: blocks dead / zombie / decoy_price_extreme / never_paid_zombie /
      // dead_7d / mostly_dead. fail-closed on preflight unreachable.
    });

    // Now use `safeFetch` for any x402 endpoint your agent reaches.
    // This is the entire safety integration — one wrap, no other config.
    const targetUrl = new URL(req.url).searchParams.get("target");
    if (!targetUrl) return new Response("missing ?target", { status: 400 });

    try {
      const upstream = await safeFetch(targetUrl, { method: "POST", body: req.body });
      return new Response(await upstream.text(), { status: upstream.status });
    } catch (e) {
      if ((e as Error).name === "PreflightBlockedError") {
        // Guard refused to sign. Body of the error includes the warnings array
        // and the suggested recommended_action (typically "use_alternatives").
        return new Response(JSON.stringify({ error: "blocked_by_guard", detail: (e as Error).message }), {
          status: 403, headers: { "content-type": "application/json" },
        });
      }
      throw e;
    }
  },
};

Edge cases the Workers runtime adds

With Durable Objects

If your agent state lives in a Durable Object, the same pattern applies — bind AGENT_PRIVATE_KEY to the DO and instantiate safeFetch once in constructor():

export class AgentDO {
  private safeFetch: typeof fetch;

  constructor(private state: DurableObjectState, env: Env) {
    const account = privateKeyToAccount(env.AGENT_PRIVATE_KEY as `0x${string}`);
    const x402Fetch = wrapFetchWithPaymentFromConfig(fetch, {
      schemes: [{ network: "eip155:8453", client: new ExactEvmScheme(account) }],
    });
    this.safeFetch = wrapWithPreflight(x402Fetch, {
      creditId: env.X402STATION_CREDIT_ID,
    });
  }

  async paidCall(url: string, body: unknown) {
    return this.safeFetch(url, {
      method: "POST",
      body: JSON.stringify(body),
      headers: { "content-type": "application/json" },
    });
  }
}

Each DO instance gets its own preflight cache, scoped to that user's agent.

Test against a known decoy

# Pull one current decoy URL (this curl itself is a paid call: $0.005 USDC).
curl -sS -X POST https://x402station.io/api/v1/catalog/decoys \
  -H 'content-type: application/json' \
  --header "PAYMENT-SIGNATURE: <your-signed-header>" \
  -d '{}' | jq '.entries[0].url'

Hit your Worker with that URL as ?target=. Expect 403 blocked_by_guard instead of an attempted payment to the decoy.

Rollout checklist

Source links

back to GuardAPI docs