---
title: "Agents Now Pay for API Access: An x402 Report"
description: "A live x402 paid API endpoint on Cloudflare Workers: HTTP 402, USDC settlement on Base, two independent replay layers, and what testnet does not prove."
author: "Hugues Clouâtre"
pubDatetime: 2026-10-23T12:00:00.000Z
modDatetime: 2026-10-24T05:00:00.000Z
url: https://clouatre.ca/posts/agent-payments-x402/
tags:
  - "agentic-ai"
  - "ai-engineering"
  - "implementation-guide"
---

Agents can now pay for API access on this site with zero account infrastructure: a live endpoint, `POST /v1/resume-eval`, returns a deterministic resume evaluation for a flat $0.05, settled in USDC (USD Coin, a dollar-pegged stablecoin) on Base Sepolia. Payment is the authentication; there are no accounts, no API keys, and no OAuth flows. The proof is a confirmed settlement transaction on chain, with a `PAYMENT-RESPONSE` header carrying the payer, the transaction hash, and the network; Table 1 reproduces the full receipt.

Machine-payable APIs are real, small, and shippable. This post documents the shipped stack, the settlement state machine, two barely-documented replay-protection findings, and what the testnet demonstration does not yet prove. The site's agent surface was already machine-readable ([Agent-Ready Websites](/posts/agent-ready-website)); HTTP 402 adds the incentive layer on top of that addressability.

*Table 1: Production settlement receipt for `POST /v1/resume-eval`, observed October 2026.*

| Field | Observed value |
| --- | --- |
| Challenge status | HTTP 402 with Base64-encoded `PAYMENT-REQUIRED` header |
| Price | 50000 base units ($0.05; USDC uses 6 decimals) |
| Asset | USDC at `0x036CbD53842c5426634e7929541eC2318f3dCF7e` |
| Network | `eip155:84532` (Base Sepolia testnet) |
| Settlement transaction | `0x384144ffd91fb9a3dc01ef1069b4607fae0794f8e2c71b8ddbe4598939986429` |
| `PAYMENT-RESPONSE` | `{ success: true, payer, transaction, network }` |

## What Is x402?

x402 is an open protocol that turns HTTP status code 402 into a machine-negotiable payment request, in its version 2 specification from December 2025. Historically, 402 sat unused in the HTTP standard; x402 activates it for agents: a server answers an unauthenticated request with 402 plus a structured challenge, and the client pays and retries.

### Three Headers, One Payment

The protocol runs on three headers. `PAYMENT-REQUIRED` carries the challenge: a Base64-encoded JSON document declaring the price, the asset, and the network. `PAYMENT-SIGNATURE` carries the client's signed payment on the retry. `PAYMENT-RESPONSE` returns the settlement receipt on the successful response.

Version 2 renamed the v1 `X-PAYMENT` headers to these scheme-neutral names and adopted CAIP-2 chain identifiers such as `eip155:84532`. The scoped `@x402/hono` package implements v2; the unscoped legacy package implements v1.

### The Challenge Is Self-Describing

The decoded challenge from production shows how self-describing the contract is. The agent learns the exact amount, the token contract, and the chain identifier without any out-of-band configuration.

*Code Snippet 1: The 402 challenge returned by production, Base64-decoded and abridged. Values are as observed; `amount` is in the token's base units. Once the `X402_PAY_TO` secret is set, the challenge also carries a `payTo` receiving address, elided here.*

```json file="PAYMENT-REQUIRED header (decoded)"
{
  "resource": {
    "url": "https://clouatre.ca/v1/resume-eval",
    "description": "Resume rubric evaluation",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:84532",
      "amount": "50000", // [!code highlight]
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "maxTimeoutSeconds": 60
    }
  ]
}
```

## How Does the Settlement State Machine Work?

Figure 1 shows the machine as its four actors see it; this section walks it gate by gate, because each gate exists to keep a specific failure from reaching settlement.

### The Ordering Is the Security Property

The route verifies the payment signature first via the facilitator (a public service that checks payment proofs on behalf of resource servers; the verify call is free), then checks for replays, then validates input, then executes, and settles only after a 2xx response exists. The verify-first order is deliberate: because verify is free, unauthenticated garbage never writes to Durable Object storage, and duplicate detection costs only the free call.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 502" width="640" height="502" data-chart="build-chart" role="img" aria-label="One paid x402 request across client, worker, replay guard, and facilitator">
<style>text{font-family:'JetBrains Mono',ui-monospace,Menlo,monospace;font-size:12px;}.actor-label{fill:#18181b;font-weight:bold;}.label{fill:#18181b;}.label-accent{fill:#075985;}.lifeline{stroke:#d4d4d8;stroke-width:1.5;}.actor{fill:#e6e6e6;stroke:#282728;stroke-width:1.5;}.msg{stroke:#52525b;stroke-width:1.5;}.head{fill:#52525b;}.msg-dash{stroke:#52525b;stroke-width:1.5;stroke-dasharray:5,4;}.head-dash{fill:#52525b;}.self{stroke:#52525b;stroke-width:1.5;fill:none;}.tail{stroke:#075985;stroke-width:1.5;stroke-dasharray:2,4;}html[data-theme="dark"] .actor-label,html[data-theme="dark"] .label{fill:#f4f4f5;}html[data-theme="dark"] .label-accent{fill:#38bdf8;}html[data-theme="dark"] .lifeline{stroke:#27272a;}html[data-theme="dark"] .actor{fill:#27272a;stroke:#52525b;}html[data-theme="dark"] .msg,html[data-theme="dark"] .self{stroke:#a1a1aa;}html[data-theme="dark"] .head{fill:#a1a1aa;}html[data-theme="dark"] .msg-dash{stroke:#a1a1aa;}html[data-theme="dark"] .head-dash{fill:#a1a1aa;}html[data-theme="dark"] .tail{stroke:#38bdf8;}</style>
<rect x="22" y="20" width="116" height="30" rx="4" class="actor"/>
<text x="80" y="39" text-anchor="middle" class="actor-label">Client</text>
<line x1="80" y1="50" x2="80" y2="472" class="lifeline"/>
<rect x="182" y="20" width="116" height="30" rx="4" class="actor"/>
<text x="240" y="39" text-anchor="middle" class="actor-label">Worker</text>
<line x1="240" y1="50" x2="240" y2="472" class="lifeline"/>
<rect x="342" y="20" width="116" height="30" rx="4" class="actor"/>
<text x="400" y="39" text-anchor="middle" class="actor-label">ReplayGuard</text>
<line x1="400" y1="50" x2="400" y2="472" class="lifeline"/>
<rect x="502" y="20" width="116" height="30" rx="4" class="actor"/>
<text x="560" y="39" text-anchor="middle" class="actor-label">Facilitator</text>
<line x1="560" y1="50" x2="560" y2="472" class="lifeline"/>
<line x1="80" y1="88" x2="231" y2="88" class="msg"/><polygon points="240,88 231,84 231,92" class="head"/>
<text x="160.0" y="81" text-anchor="middle" class="label">1. POST + signature</text>
<line x1="240" y1="134" x2="551" y2="134" class="msg"/><polygon points="560,134 551,130 551,138" class="head"/>
<text x="400.0" y="127" text-anchor="middle" class="label">2. verify (free)</text>
<line x1="560" y1="180" x2="249" y2="180" class="msg-dash"/><polygon points="240,180 249,176 249,184" class="head-dash"/>
<text x="400.0" y="173" text-anchor="middle" class="label">3. reject: 402</text>
<line x1="240" y1="226" x2="391" y2="226" class="msg"/><polygon points="400,226 391,222 391,230" class="head"/>
<text x="320.0" y="219" text-anchor="middle" class="label">4. check sig hash</text>
<line x1="400" y1="272" x2="249" y2="272" class="msg-dash"/><polygon points="240,272 249,268 249,276" class="head-dash"/>
<text x="320.0" y="265" text-anchor="middle" class="label">5. duplicate: 409</text>
<line x1="240" y1="352" x2="551" y2="352" class="msg"/><polygon points="560,352 551,348 551,356" class="head"/>
<text x="400.0" y="345" text-anchor="middle" class="label-accent">8. settle (1-3s tail)</text>
<line x1="560" y1="398" x2="249" y2="398" class="msg-dash"/><polygon points="240,398 249,394 249,402" class="head-dash"/>
<text x="400.0" y="391" text-anchor="middle" class="label">9. receipt</text>
<line x1="240" y1="444" x2="89" y2="444" class="msg"/><polygon points="80,444 89,440 89,448" class="head"/>
<text x="160.0" y="437" text-anchor="middle" class="label">10. 200 + PAYMENT-RESPONSE</text>
<path d="M 240 302 h 34 v 22 h -34" class="self"/>
<polygon points="240,324 248,320 248,328" class="head"/>
<text x="282" y="317" class="label">6. validate input, 7. evaluate</text>
<line x1="240" y1="330" x2="240" y2="350" class="tail"/>
<text x="232" text-anchor="end" y="345" class="label-accent">on-chain wait</text>
</svg>
*Figure 1: One paid request across the four actors. The two facilitator hops are the call's entire outbound subrequest budget; the synchronous wait during step 10 is where the one-to-three-second chain tail lands. Dotted arrows are rejections and receipts.*

The load-bearing detail is the final step. Settlement, the on-chain transfer of the $0.05, fires strictly on 2xx. A malformed request dies at validation with a 400, which is not a 2xx, so the middleware never charges for it. A duplicate signature dies at the replay check with a 409 before the handler runs. The route costs the operator nothing unless the caller receives a valid response, a property worth calling fail-closed commerce.

One consequence matters to client implementers: the replay guard records the signature before input validation, so a payment signature is single-attempt per HTTP transaction regardless of status code. A client whose first attempt died with a 400 cannot retry the same signature; the retry gets a 409 even though nothing settled.

### Latency and Subrequest Budget

The ordering also bounds the latency and subrequest budget. Each paid call makes two outbound facilitator subrequests, one for verify and one for settle, plus the evaluation compute. Settlement is an on-chain operation and adds a one to three second tail; because the response releases only after settle, client-facing timeout expectations must account for that tail. The `maxTimeoutSeconds` value of 60 in the challenge exists for exactly this reason.

### A Deliberately Small Stack

The shipped stack is deliberately small: Hono v4 (a lightweight router framework) dispatched only for the `/v1/*` prefix, `@x402/hono` 2.28.0, the public facilitator at `https://x402.org/facilitator`, and a Durable Object (a single-threaded Cloudflare stateful actor) named `ReplayGuard`. The 600-line production handler is untouched; the dispatch is a three-line prefix check.

## What Does Replay Protection Look Like in Practice?

Replay protection, the defense against one payment being reused for many requests, turned out to have two independent layers, and the interaction between them produced the most interesting findings of the project. Neither layer relies on application logic staying correct; one is enforced by chain semantics, the other by execution isolation.

### Layer One: Execution Isolation

The first layer is the `ReplayGuard` Durable Object. Its `check()` method performs an atomic check-and-record of a payment signature's hash; Durable Object execution is single-threaded, so no two requests can interleave inside it. Entries expire after 120 seconds, swept lazily on access and by a single rolling alarm that purges every expired entry and rearms to the earliest remaining expiry. A newer signature can move the alarm earlier, which is safe: the alarm only sweeps expired entries, so firing sooner merely purges them sooner. The 120-second window is not arbitrary: the `@x402` SDK (2.28.0) derives the EIP-3009 authorization's `validBefore` deadline from the challenge's `maxTimeoutSeconds` (60), so a signature older than 60 seconds is permanently unspendable on chain, and the guard's memory outlives the signature's cryptographic validity by design (Figure 2).

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 640 240" width="640" height="240" data-chart="build-chart" role="img" aria-label="Replay guard coverage over time: signature validity ends at 60 seconds, Durable Object memory at 120 seconds, chain nonce enforcement is permanent">
<style>text{font-family:'JetBrains Mono',ui-monospace,Menlo,monospace;font-size:12px;}.label{fill:#18181b;}.tick{fill:#71717a;}.axis{stroke:#52525b;stroke-width:1.5;}.bar-accent{fill:#075985;}.bar-mid{fill:#38bdf8;fill-opacity:.75;}.bar-full{fill:#e6e6e6;stroke:#52525b;stroke-width:1;}html[data-theme="dark"] .label,html[data-theme="dark"] .tick{fill:#f4f4f5;}html[data-theme="dark"] .tick{fill:#a1a1aa;}html[data-theme="dark"] .axis{stroke:#a1a1aa;}html[data-theme="dark"] .bar-accent{fill:#38bdf8;}html[data-theme="dark"] .bar-mid{fill:#0369a1;fill-opacity:.9;}html[data-theme="dark"] .bar-full{fill:#27272a;stroke:#a1a1aa;}</style>
<text x="40" y="64" class="label">EIP-3009 validity (validBefore)</text>
<rect x="40" y="70" width="280" height="22" rx="3" class="bar-accent"/>
<text x="40" y="110" class="label">ReplayGuard DO memory (TTL)</text>
<rect x="40" y="116" width="560" height="22" rx="3" class="bar-mid"/>
<text x="40" y="156" class="label">Chain nonce (permanent)</text>
<rect x="40" y="162" width="560" height="22" rx="3" class="bar-full"/>
<line x1="40" y1="205" x2="600" y2="205" class="axis"/>
<line x1="40" y1="205" x2="40" y2="210" class="axis"/>
<text x="40" y="225" text-anchor="middle" class="tick">0s</text>
<line x1="320" y1="205" x2="320" y2="210" class="axis"/>
<text x="320" y="225" text-anchor="middle" class="tick">60s</text>
<line x1="600" y1="205" x2="600" y2="210" class="axis"/>
<text x="600" y="225" text-anchor="middle" class="tick">120s</text>
<line x1="320" y1="58" x2="320" y2="205" class="axis" stroke-dasharray="3,4"/>
<text x="326" y="52" class="tick">signature expires</text>
<line x1="600" y1="58" x2="600" y2="205" class="axis" stroke-dasharray="3,4"/>
<text x="594" y="52" text-anchor="end" class="tick">DO memory ends</text>
</svg>
*Figure 2: Coverage overlap over one signature's life. The Durable Object's memory ends 60 seconds after the signature itself expires, so no gap exists between the application guard and the chain guard.*

*Code Snippet 2: The core of `check()` in the ReplayGuard Durable Object. The check and the record happen in one call, which makes the duplicate window impossible to interleave.*

```typescript file="src/lib/replay-guard.ts"
async check(sigHash: string): Promise<"ok" | "duplicate"> {
  const now = Date.now();
  const existing =
    await this.ctx.storage.get<number>(`nonce:${sigHash}`);
  if (existing !== undefined) {
    if (existing > now) return "duplicate"; // [!code highlight]
    await this.ctx.storage.delete(`nonce:${sigHash}`);
  }
  const expiresAt = now + TTL_MS;
  await this.ctx.storage.put(`nonce:${sigHash}`, expiresAt);
  const alarm = await this.ctx.storage.getAlarm();
  if (alarm === null || expiresAt < alarm) {
    await this.ctx.storage.setAlarm(expiresAt);
  }
  return "ok";
}
```

### Layer Two: Chain-Enforced Nonces, and Two Field Findings

Which layer catches a replay depends on when it arrives, and the answer changes twice over one signature's life; Table 2 walks the windows.

*Table 2: The three defense windows against payment replay, over one signature's lifetime.*

| Window | Elapsed time | Catching layer | Returned status |
| --- | --- | --- | --- |
| In flight | 0 to ~3 seconds | `ReplayGuard` DO, atomic check-and-record | 409 `replay_blocked` |
| Post-settlement | ~3 to 60 seconds | Facilitator, spent EIP-3009 nonce on chain | 402 |
| Post-expiry | Over 60 seconds | EIP-3009 `validBefore` deadline passed | 402 |

Finding one is about test environments. Under `wrangler dev`, Cloudflare's local development server, the Worker's configured custom domain rewrites the request host, so the SDK derives the resource URL as the production domain even on localhost. The public facilitator accepts verify and settle calls for that dev-origin resource without complaint, which is less mysterious than it looks: the facilitator validates the signed payment against the requirements that the resource server itself submits, resource URL included, and never probes the origin. Teams reproducing x402 handshakes locally should expect test runs to reference production URLs and to settle anyway.

The intuitive expectation for a post-settlement replay is a conflict error; the field result is HTTP 402. That mismatch is the finding: the facilitator rejects the spent EIP-3009 nonce (EIP-3009 is the Ethereum standard that lets USDC transfers carry a single-use nonce, so the chain itself enforces one-use payments) before the handler ever runs. The Durable Object's 409 therefore covers only the concurrent verify window, the seconds between the first verify and the settlement landing on chain. The two layers are independent and both fail closed; across three settled test runs, no double-settlements occurred.

## What Did the KISS Re-Scope Cut?

The initial architecture plan sequenced the full Hono re-route of the production Worker, a dedicated custom domain, Coinbase Developer Platform secrets, and telemetry before verifying a single transaction. The implementation re-scope inverted that: it cut everything not strictly security-bearing, validated the full handshake under `wrangler dev` before any deploy, and shipped in three incremental changesets within a single session.

*Table 3: Planned architecture versus shipped implementation.*

| Original plan | KISS decision |
| --- | --- |
| Full Hono re-route of the 600-line handler | Hono dispatched only for `/v1/*`; default branch untouched |
| `api.clouatre.ca` custom domain plus CORS | Existing production routes; no DNS changes |
| CDP facilitator with JWT secrets | Public facilitator; no secrets issued |
| Analytics Engine telemetry and KV audit layer | Deferred to a later graduation stage |
| Playwright suite and worker typecheck gate | One client smoke test as the merge gate |

### The Lesson for Live Workers

Minimal means not touching the default branch of the fetch handler; a prefix dispatch preserves existing production behavior exactly. Because the full handshake passed locally before any deployment, the merge became a non-event. The deferred features remain the production end-state; the initial release proved the payment primitive first.

## What Doesn't This Prove Yet?

The honest answer is that the strongest claim this demonstration supports is narrower than it looks.

Testnet USDC has zero value. No real money moved, and no charity has received anything yet; proceeds are designated for the Montreal Indigenous Community NETWORK, so the mainnet switch carries a real obligation. Mainnet itself is a deferred, telemetry-gated decision that requires a Coinbase Developer Platform facilitator, JWT secrets, and a real receiving wallet. Replay protection covers a 120-second window, after which expiry removes the guard's memory; the chain's single-use nonce is what protects late replays. Volume is roughly four payments ever, which is evidence of correctness, not evidence of demand.

For readers reproducing the handshake, one caveat belongs on the record: settlement requires the `X402_PAY_TO` receiving-address secret, and the challenge omits `payTo` until it is set, which blocks any funded run against the deployed URL. The receipt in Table 1 was captured only after that step completed.

## What Comes Next?

The gate to mainnet is a strict graduation checklist: production telemetry first, then the CDP facilitator with authenticated secrets, then a dedicated receiving wallet, and only then a value-bearing asset. Until it opens, the endpoint keeps settling at $0.05 in testnet USDC.

Two extensions are already scoped. The first is an MCP (Model Context Protocol) tool surface at `/mcp`, where the same evaluation core is exposed as a paid tool with the price declared in tool metadata, extending the read-only tools evaluated in [Does WebMCP Pay Off for Content Sites?](/posts/webmcp-beyond-checkout) from free reads to a paid write. The second is a paid decision-judgment endpoint: typed questions in, typed verdicts out, priced per judgment batch, which turns an existing stateless service into a metered one with the 402 challenge as its entire access-control layer.

For technical executives, the takeaway is concrete: machine-payable APIs require no billing vendor and no identity system, just a Worker, a payment middleware mount, and a receiving address. The open questions, demand and treasury operations on mainnet, are business decisions that the protocol deliberately leaves untouched.