I added x402 micropayments to a Next.js site. Agents pay $0.005 a post or $0.05 for the bundle in USDC on Base. Humans and search crawlers stay free. Here is the receipt.

← Back to blog

Adding an x402 Paywall to My Blog (AI Agents Pay, Humans Stay Free)

By Agnel Nieves12 min read
View as Markdown

TL;DR

I added two new endpoints to this site that charge AI agents in USDC for clean, machine-readable copies of the blog. GET /api/x402/blog/[slug] is $0.005 a post. GET /api/x402/bundle is $0.05 for every published post in one JSON payload. Settlement happens on Base via the Coinbase CDP x402 facilitator, so the full nominal amount lands in my wallet and gas is sponsored.

Browsers, RSS readers, my Rust CLI, Googlebot, GPTBot, ClaudeBot, PerplexityBot, and the rest of the existing AI bot allowlist see no change. The new endpoints live behind a Disallow: /api/x402/ rule in robots.txt so they never enter any crawler's index. The whole thing is a route wrapper around the existing post loader, not a redesign of the site. End-to-end build was a few hours, including the one bug that ate the first one.

First real Base mainnet settlement: a throwaway wallet I funded with $0.10 paid $0.05 USDC into my receiving address via Coinbase's facilitator, end to end in 1.6 seconds. Tx hash: 0x5cb501c8...20af.

Why I built it

A few days ago Erik Voorhees posted this:

The follow-up had the context that made it land:

Anecdote that prompted this tweet: I asked one of my agents to check a fedex tracking number. It spent 5m and couldn't do it, blocked on both fedex and various alternative sites. It would've happily paid $0.01 to complete its task, and everyone would be better off.

That framing stuck. Around the same time, Cloudflare's x402 announcement hit my feed, with the x402 Foundation launching alongside Coinbase. Stripe shipped x402 on Base in February. AWS Bedrock AgentCore shipped a payments product on it last week. The pieces had quietly stopped being a thought experiment.

I have been thinking about how the open web shifts when agents become most of the traffic. The shape I keep landing on is that the site itself does not have to be hostile to either humans or agents. The default surface stays free and machine-readable. A separate, opt-in surface lets anyone who wants to pay for clean, license-cleared content do so. x402 is the first piece of plumbing that makes the second part possible without running a SaaS, a payment processor, or a wallet hot key on a server.

So I spent a Wednesday adding it. This is a side project, not a business plan. I wanted to see what x402 felt like to actually run, and I wanted to have built the thing before writing about it.

How it works

Two endpoints, one wrapper, one config.

                ┌────────────────────────────────────┐
                │     agnelnieves.com (Next.js)      │
                └─────────────────┬──────────────────┘
                                  │
              ┌──────────────────┴──────────────────┐
              │                                     │
       free, indexable                       paywalled, opt-in
       /blog/[slug]                          /api/x402/blog/[slug]
       /api/blog/[slug]/raw                  /api/x402/bundle
       /feed.json, /llms.txt, etc.                  │
              │                                     │
              │                              withX402(handler)
              │                                     │
              │                          ┌──────────┴──────────┐
              │                          │  Coinbase CDP       │
              │                          │  facilitator        │
              │                          │  (Base mainnet)     │
              │                          └──────────┬──────────┘
              │                                     │
              ▼                                     ▼
   robots.txt: Allow /                  robots.txt: Disallow /api/x402/

The protocol flow on the paywalled side:

  1. Client sends GET /api/x402/bundle with no auth.
  2. Server returns HTTP 402 with a JSON body listing what it accepts: $0.05 USDC on Base, the wallet to pay, an EIP-712 description of the asset, and a 120-second deadline.
  3. An x402-aware client signs an EIP-3009 gasless transfer authorization locally. No private key leaves the client. No on-chain transaction yet.
  4. Client retries the same request with the signed authorization in an X-PAYMENT header.
  5. Server forwards the signature to Coinbase's facilitator at api.cdp.coinbase.com/platform/v2/x402 for /verify and /settle. The facilitator broadcasts the actual USDC transfer on Base, sponsors the gas, and returns a settlement receipt.
  6. Server returns HTTP 200 with the content body and an X-PAYMENT-RESPONSE header carrying the tx hash.

The wiring is small. Each route file looks like this:

// src/app/api/x402/bundle/route.ts
import { NextResponse } from "next/server";
import { withX402 } from "@x402/next";
import { getAllPosts, getPostBySlug } from "@/lib/blog/utils";
import { bundleRouteConfig, getX402Settings } from "@/lib/x402";
 
export const dynamic = "force-dynamic";
export const runtime = "nodejs";
 
const settings = getX402Settings();
 
const handler = async (): Promise<NextResponse> => {
  const meta = (await getAllPosts()).filter((p) => p.status === "published");
  const posts = (await Promise.all(meta.map((p) => getPostBySlug(p.slug))))
    .filter((p): p is NonNullable<typeof p> => p !== null);
 
  return NextResponse.json(
    { site: "https://agnelnieves.com", count: posts.length, posts, license },
    { headers: { "Cache-Control": "private, no-store" } }
  );
};
 
export const GET = settings.configured && settings.payTo
  ? withX402(handler, bundleRouteConfig(settings), settings.server)
  : notConfigured;

withX402 from @x402/next is the helper that handles the 402 dance. The route also force-dynamic and Node-runtime, with Cache-Control: private, no-store so Vercel's CDN never caches a paid response and serves it to a non-payer.

The receiver wallet is just a public address in env config. The seller never spends, never signs, never holds a hot key. That is the part of x402 that surprised me most going in: from the server's point of view, you are an HTTP endpoint that occasionally returns 402, and a payTo: "0x..." string. There is no SDK secret, no webhook, no private key in .env.

The detail that made the design make sense: AEO

This is the part I want to be loud about, because the framing I went in with was wrong and almost broke this site.

The question I asked first was: how do I make x402 charge only when the agent supports it? The honest answer is that you cannot. The protocol has no capability negotiation. There is no Accept: application/x-x402 preflight. The idiomatic shape is "always return 402, x402-aware clients recognize the payment terms and retry, everybody else sees a 4xx and walks away."

That last part is the load-bearing detail. If a 402 hits Googlebot, GPTBot, or ClaudeBot, they treat it like any other 4xx and stop indexing the page. A naive paywall on /blog/* would silently delete this site from Google, Perplexity, ChatGPT, and Claude over the following weeks. The thing I have spent the most effort on (the llms.txt, the JSON Feed, the structured data, the AI bot allowlist in robots.txt) would be the first thing to go.

The fix is layered, in this order:

  1. Gate by path, not by client. All existing routes (/blog/*, /feed.json, /api/blog/[slug]/raw, /api/projects, /api/site.json) stay exactly as they are. Free, indexable, agent-friendly.
  2. Put the paywall on a new prefix (/api/x402/*) that nothing currently links to.
  3. Disallow that prefix in robots.txt for every user-agent, including the existing AI bot allowlist. Search and AI training crawlers never see a 402 because they never crawl those paths. Their world is unchanged.
  4. Trust honest crawlers. Spoofed UAs are a separate problem, and not one a personal blog needs to solve at this scale.

The shape of the change in src/app/robots.txt/route.ts was almost trivial:

const aiRules = AI_BOTS.map(
  (bot) => `User-agent: ${bot}\nAllow: /\nDisallow: /api/x402/`
).join("\n\n");

If I had run this in the wrong order (built the route first, gated robots later) I would have shipped one hour of crawler exposure to the new endpoint. That is the kind of mistake the SEO community sees Google take weeks to recover from. Order of operations is the whole game.

The bug that ate the first hour: buyer equals seller

I built the routes, generated a throwaway test wallet, funded it with 10 USDC from Circle's Base Sepolia faucet, and pointed X402_PAY_TO at the test wallet's own address while debugging. First end-to-end run:

status:  402 (3282ms total)
body summary:
{
  "x402Version": 1,
  "error": "Failed to verify payment: invalid_payload",
  ...
}

invalid_payload from Coinbase's facilitator turned out to mean "the signed transfer cannot settle as written." The wallet had USDC. The signature was valid. The amount and asset matched. What was wrong was that the buyer and the seller were the same address. Some EIP-3009 settlement paths reject transfer-to-self as a sanity check, and the facilitator surfaces it through this generic error.

The fix took 30 seconds. I split the addresses: test wallet keeps the private key, prod wallet receives. Re-ran the demo:

status:  200 (3178ms total)
receipt:
{
  "success": true,
  "transaction": "0x5736f79f04460db0a892dba158f744a88037b314059f2349b318ceb677b07360",
  "network": "base-sepolia",
  "payer": "0x83A0A90304B8E909478FA5271f63e40c0Aa99911"
}
body summary:
{
  "site": "https://agnelnieves.com",
  "count": 9,
  "firstSlug": "rust-owns-the-javascript-toolchain-in-2026"
}

The transfer is on Base Sepolia. The bundle is the actual content of this blog. The whole loop took about three seconds, most of it the facilitator round trip plus the cold dev compile of the route file.

The lesson is small but real: testnet does not mean toy. The validators, settlement contracts, and facilitators on Sepolia run the same logic as the ones on Base mainnet. If buyer-equals-seller fails on Sepolia, it fails on Base too. Run the loop end-to-end before flipping the network env var.

What this earns

Honest section.

The Coinbase CDP facilitator covers the first 1,000 settlements per month for free, then $0.001 per settlement. Gas on Base is sponsored by the facilitator under the standard agreement, so a $0.005 invoice lands as $0.005 in my wallet. After the free tier, $0.001 of each settlement goes to Coinbase, billed separately to the CDP account.

For a personal blog that gets some agent traffic and some human traffic, the realistic monthly take is going to look like this:

Month oneBest caseHonest case
Per-post fetches20030
Bundle fetches203
Net to wallet$2$0.30

If those numbers shock you, they should. This is not real revenue. It is plumbing. The point is not the dollars. The point is that the plumbing exists. There is now a sanctioned, programmatic way for an AI agent to pay this site for clean machine-readable content, and it works without me running a Stripe account, an OAuth flow, or anything else that turns a personal blog into a product.

The realistic upside is two layers up. Premium content that does not exist on the free side. License clarity, where the paid response includes explicit terms rather than the implicit fair-use of the open web. Per-tier pricing for commercial vs casual use. None of which exists in v1, all of which is now an evening of work away.

Try it

The 402 response is public. You do not need a wallet to inspect what the endpoint asks for. x402 v2 puts the payment requirements in a Payment-Required response header (base64-encoded JSON) instead of the body, so peek at the headers:

curl -sI https://agnelnieves.com/api/x402/bundle | grep -i payment-required

Decode that header value (base64 -d | jq) and you get something like:

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": { "url": "...", "mimeType": "application/json" },
  "accepts": [{
    "scheme": "exact",
    "network": "eip155:8453",
    "amount": "50000",
    "asset": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913",
    "payTo": "0x...",
    "maxTimeoutSeconds": 300,
    "extra": { "name": "USD Coin", "version": "2" }
  }]
}

The accepts[0] entry lists the network (eip155:8453 is Base mainnet in CAIP-2 format), the asset (USDC contract on Base), the price (50000 atomic units = $0.05), and the receiving address. That part is intentionally discoverable. Reading the terms without paying is part of the spec.

To actually pay it, you need an x402 client. The shortest path in TypeScript:

import { privateKeyToAccount } from "viem/accounts";
import { x402Client } from "@x402/core/client";
import { registerExactEvmScheme } from "@x402/evm/exact/client";
import { wrapFetchWithPayment } from "@x402/fetch";
 
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const client = new x402Client();
registerExactEvmScheme(client, { signer: account });
 
const fetchPaid = wrapFetchWithPayment(fetch, client);
const res = await fetchPaid("https://agnelnieves.com/api/x402/bundle");
const data = await res.json();
console.log("paid", res.headers.get("X-PAYMENT-RESPONSE"));
console.log("got", data.count, "posts");

Same shape works from @x402/axios, @x402/hono, and the AWS Bedrock AgentCore Payments SDK. Cloudflare's withX402Client wraps Workers fetch the same way. Anthropic does not have first-party x402 client support in Claude or Claude Code as of this writing (May 2026), though MCP servers wrapped with @x402/mcp are reachable from any MCP client. That gap is the part I expect to close fast.

What's next

Maybe nothing. Maybe these, if I get bored on a Saturday:

  • Pay from the SSH CLI. The Rust binary that powers ssh agnelnieves.sh already fetches blog content from the free endpoints. Wiring up an x402 client in Rust would let the CLI pay its own paywall, which is exactly the kind of recursion side projects exist for.
  • Wrap the bundle as an MCP server. Same content, different shape, so any MCP client can spend a cent to fetch posts as a tool call. Mostly to see what that feels like.
  • A footer counter showing how many times the paywall has been paid. Probably stuck on zero for a while, which is part of the bit.

That's about it. A few hours of curiosity, not a roadmap.

What you should take away

If you run a content site and you have been blocking bots: the bot you are blocking probably would have paid you a cent to read the page, and would have happily paid the cent to do its job. The protocol to accept that cent now exists, has a working facilitator, and integrates as one route file in Next.js.

If you run an agent: the protocol to spend that cent now exists too, in TypeScript, Rust through alloy, and through any framework that wraps wrapFetchWithPayment. Most agent stacks will support it natively within the next year.

If you do not run either: the interesting trend is that the open web is starting to grow a paid lane that does not require login, ads, or a SaaS layer. It is paid in the same shape that money already moves between machines (stablecoins, in this case USDC on a low-fee L2). The cost of asking is becoming small enough that asking is the right default, even for content that almost nobody will pay for.

The free web is not going anywhere. The middle layer between "free" and "paywalled subscription" is what x402 is filling in. Worth knowing what it looks like before everyone else does.


Built by Agnel Nieves, a design engineer with 15+ years across product, design systems, and crypto. More writing on the blog.

View as Markdown