x402, a static blog monetization excercise

on 2026-07-05

x402

Cloudflare has published an announcement couple of days ago about Monetization Gateway launch. This is revolutionary for many reasons. It removes the high entry barrier for monetization, it lets you have a very precise control over what you charge for and how low the payment can be. This literally can change how we use internet making advertisement economy internet runs on obsolete.

It sounds like an ideal tool to monetize a blog, or an API, for example. And even though I don't have anything to monetize, the technology seems too important for me not to try it out. So let's do just that.

The plan

Unfortunately the Cloudflare service still seems to be in a closed beta(?), meaning you can't get access to it freely as of now, you need to join a waitlist. So what we are left with is to implement the thing ourselves. Which is even more fun, isn't it? Fortunately it sounds like a pretty simple set up. That does add a couple of moving parts to a static blog, though nothing that you can't bolt on top in a day.

So what do we need for the thing to work?

  1. A wallet to receive funds to
  2. A worker to execute the payment processing logic
  3. A resource we want to paywall

I have items 1 and 3, but not 2. In this role for now I will go with Cloudflare Workers, since I host my site there. This also dictates the choice of a language we will implement the thing on, TypeScript. Maybe later I will reimplement it to be hosted on my local server, but to keep the scope of the experiment manageable - let's stick with cloudflare for now.

The middleware

So let's jump right in. The first thing we need is a middleware that will receive the request, and make sure the access is granted only on payment as well as handling the price disclosure. The official docs have an example of how to implement it in Hono, so we'll use that. Making a couple of modifications to accommodate for the fact that the code will run on a cloudflare worker rather than a standalone server, putting our wallets, and replacing the example endpoint with a much more useful one - we get this:

import { Hono } from "hono";
import type { MiddlewareHandler } from "hono";
import { paymentMiddleware, x402ResourceServer } from "@x402/hono";
import { ExactEvmScheme } from "@x402/evm/exact/server";
import { ExactSvmScheme } from "@x402/svm/exact/server";
import { HTTPFacilitatorClient } from "@x402/core/server";

const app = new Hono();
const evmAddress = "0xD040AEACCdFf083C5D4cB221F1533e8719a84F0e";
const svmAddress = "HjexCvNzgxJT3Ni7rb98WF4pQGEX2fN7G6Zh26B19mBc";

const facilitatorClient = new HTTPFacilitatorClient({
  url: "https://x402.org/facilitator",
});

let payment: MiddlewareHandler | undefined;

app.use(async (c, next) => {
  payment ??= paymentMiddleware(
    {
      "GET /api/joke": {
        accepts: [
          {
            scheme: "exact",
            price: "$0.01",
            network: "eip155:84532",
            payTo: evmAddress,
          },
          {
            scheme: "exact",
            price: "$0.01",
            network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
            payTo: svmAddress,
          },
        ],
        description: "A premium, hand-picked joke",
        mimeType: "application/json",
      },
    },
    new x402ResourceServer(facilitatorClient)
      .register("eip155:84532", new ExactEvmScheme())
      .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()),
  );
  return payment(c, next);
});

const jokes = [
  "I told my wife she was drawing her eyebrows too high. She looked surprised.",
  "Why do programmers prefer dark mode? Because light attracts bugs.",
  "I asked the blockchain for a joke. It's still reaching consensus.",
  "There are only two hard things in computer science: cache invalidation, naming things, and off-by-one errors.",
];

app.get("/api/joke", (c) => {
  return c.json({
    joke: jokes[Math.floor(Math.random() * jokes.length)],
  });
});

export default app;
The worker also needs a wrangler.jsonc (cloudflare specifics)
{
  "name": "x402-poc",
  "main": "src/index.ts",
  "compatibility_date": "2026-07-01",
  "compatibility_flags": ["nodejs_compat"],
  "env": {
    "production": {
      "name": "x402-poc",
      "routes": [
        { "pattern": "shtein.me/api/*", "zone_name": "shtein.me" }
      ]
    }
  }
}

Two non-obvious bits in there: nodejs_compat is required because the x402 packages import Node built-ins (events, crypto, url) and the build fails without it. And the route lives only in the production environment on purpose: with a route defined, wrangler dev emulates the route's host, so the URLs the middleware generates would point at the real domain instead of localhost.

Running it locally:

npx wrangler dev

And sending a test request - we see the paywall:

curl -i http://localhost:8787/api/joke
HTTP/1.1 402 Payment Required
Content-Length: 2
Content-Type: application/json
PAYMENT-REQUIRED: eyJ4NDAyVmVyc2lvbiI6MiwiZXJyb3IiOiJQYXltZW50IHJlcXVpcmVkIiwicmVzb3VyY2UiOnsidXJsIjoiaHR0cDovL2xvY2FsaG9zdDo4Nzg3L2FwaS9qb2tlIiwiZGVzY3JpcHRpb24iOiJBIHByZW1pdW0sIGhhbmQtcGlja2VkIGpva2UiLCJtaW1lVHlwZSI6ImFwcGxpY2F0aW9uL2pzb24ifSwiYWNjZXB0cyI6W3sic2NoZW1lIjoiZXhhY3QiLCJuZXR3b3JrIjoiZWlwMTU1Ojg0NTMyIiwiYW1vdW50IjoiMTAwMDAiLCJhc3NldCI6IjB4MDM2Q2JENTM4NDJjNTQyNjYzNGU3OTI5NTQxZUMyMzE4ZjNkQ0Y3ZSIsInBheVRvIjoiMHhEMDQwQUVBQ0NkRmYwODNDNUQ0Y0IyMjFGMTUzM2U4NzE5YTg0RjBlIiwibWF4VGltZW91dFNlY29uZHMiOjMwMCwiZXh0cmEiOnsibmFtZSI6IlVTREMiLCJ2ZXJzaW9uIjoiMiJ9fSx7InNjaGVtZSI6ImV4YWN0IiwibmV0d29yayI6InNvbGFuYTpFdFdUUkFCWmFZcTZpTWZlWUtvdVJ1MTY2VlUyeHFhMSIsImFtb3VudCI6IjEwMDAwIiwiYXNzZXQiOiI0ek1NQzlzcnQ1Umk1WDE0R0FnWGhhSGlpM0duUEFFRVJZUEpnWkpEbmNEVSIsInBheVRvIjoiSGpleEN2TnpneEpUM05pN3JiOThXRjRwUUdFWDJmTjdHNlpoMjZCMTltQmMiLCJtYXhUaW1lb3V0U2Vjb25kcyI6MzAwLCJleHRyYSI6eyJmZWVQYXllciI6IkNLUEtKV05kSkVxYTgxeDdDa1oxNEJWUGlZNnkxNlN4czdvd3pucXRXWXA1In19XX0=

The PAYMENT-REQUIRED header is a base64 encoded list of accepted payment methods and prices:

{
  "x402Version": 2,
  "error": "Payment required",
  "resource": {
    "url": "http://localhost:8787/api/joke",
    "description": "A premium, hand-picked joke",
    "mimeType": "application/json"
  },
  "accepts": [
    {
      "scheme": "exact",
      "network": "eip155:84532",
      "amount": "10000",
      "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
      "payTo": "0xD040AEACCdFf083C5D4cB221F1533e8719a84F0e",
      "maxTimeoutSeconds": 300,
      "extra": {
        "name": "USDC",
        "version": "2"
      }
    },
    {
      "scheme": "exact",
      "network": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
      "amount": "10000",
      "asset": "4zMMC9srt5Ri5X14GAgXhaHii3GnPAEERYPJgZJDncDU",
      "payTo": "HjexCvNzgxJT3Ni7rb98WF4pQGEX2fN7G6Zh26B19mBc",
      "maxTimeoutSeconds": 300,
      "extra": {
        "feePayer": "CKPKJWNdJEqa81x7CkZ14BVPiY6y16Sxs7owznqtWYp5"
      }
    }
  ]
}

And that is exactly what we want to see, 2 testnets with an exact amount of 1 cent. Let's try to pay now, shall we? In order to test it in this form - we'd need a programmatic client that could make a payment and resend a request with a confirmation. In this particular usecase though I want to see if an end user from a browser can do that. And luckily they can! If you visit the same URL from a browser now - you will see this:

browser-failed-paywall.png

which tells us in plain English what to do to make this work, so let's do it:

npm install @x402/paywall
 import { HTTPFacilitatorClient } from "@x402/core/server";
+import { createPaywall, evmPaywall, svmPaywall } from "@x402/paywall";
@@
     new x402ResourceServer(facilitatorClient)
       .register("eip155:84532", new ExactEvmScheme())
       .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()),
+    { appName: "Igor's corner", testnet: true },
+    createPaywall().withNetwork(evmPaywall).withNetwork(svmPaywall).build(),
   );
   return payment(c, next);
 });

And trying to access it now we see a nice UI helping us pay for access:

browser-success-paywall.png

The top up link is there because we are currently on the testnet. In production we won't have that. Let's try and pay with MetaMask.

Connect MetaMask first:

mm-connected.png

Now if we continue to the actual payment:

signature-request.png

And at the end we finally get the content we paid for:

success.png

Couple observations about this final request:

The client sends a signature confirming the payment in PAYMENT-SIGNATURE header which decodes into

{
  "x402Version": 2,
  "payload": {
    "authorization": {
      "from": "0xAF40224e2B8fF2B4840c99d8458A0Fd803b15a0e",
      "to": "0xD040AEACCdFf083C5D4cB221F1533e8719a84F0e",
      "value": "10000",
      "validAfter": "1783263532",
      "validBefore": "1783264432",
      "nonce": "0x82806d883f9d46e376b9dc9c065560521f1c9064cf58d1d4ea53e9e3be5f01df"
    },
    "signature": "0x7284c4c0da5cd029fff1f07fd4e5ff5b5c51bf13064b9a50e787f83f6e67555a419b85db77f27cb47a2101e03c38b93d1af9e6e4a3b2712675f35c975ea284d51c"
  },
  "resource": {
    "url": "http://localhost:8787/api/joke",
    "description": "A premium, hand-picked joke",
    "mimeType": "application/json"
  },
  "accepted": {
    "scheme": "exact",
    "network": "eip155:84532",
    "amount": "10000",
    "asset": "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
    "payTo": "0xD040AEACCdFf083C5D4cB221F1533e8719a84F0e",
    "maxTimeoutSeconds": 300,
    "extra": {
      "name": "USDC",
      "version": "2"
    }
  }
}

As you can see it contains all the information our middleware needs to make sure the payment was conducted. So it responds with a success PAYMENT-RESPONSE header:

{
  "success": true,
  "payer": "0xAF40224e2B8fF2B4840c99d8458A0Fd803b15a0e",
  "transaction": "0x88aefe88050c4faaa650383460451ba1faebe3bdc398793970592e6a4ae787dc",
  "network": "eip155:84532"
}

And a response body with the actual content.

The other interesting part is that the browser opened the paid content in another URL, a URL of an in memory object created from the response. I first thought it is a nice UI to prevent double spending on refresh, but looks like no: the paywall fetches the content behind the scenes, and since our response is JSON rather than HTML it has nothing to render in place, so it navigates to an in-memory object URL created from the response. The protocol itself is pay per use, so every time you hit an API - you will be charged.

Closing the loop

Ok, now when we see everything working, let's switch to the production networks and put it all out:

 const facilitatorClient = new HTTPFacilitatorClient({
-  url: "https://x402.org/facilitator"
+  url: "https://x402.dexter.cash"
 });
@@
-            network: "eip155:84532",
+            network: "eip155:8453",
@@
-            network: "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1",
+            network: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp",
@@
     new x402ResourceServer(facilitatorClient)
-      .register("eip155:84532", new ExactEvmScheme())
-      .register("solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", new ExactSvmScheme()),
-    { appName: "Igor's corner", testnet: true },
+      .register("eip155:8453", new ExactEvmScheme())
+      .register("solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", new ExactSvmScheme()),
+    { appName: "Igor's corner", testnet: false },

The x402.org facilitator only settles testnets, so for the real networks we point at Dexter β€” a free facilitator that settles Base and Solana mainnet with no account or API keys.

And that's it. Press the button, pay a real cent, get a premium joke:

🎩 Get a premium joke ($0.01)

Summary

x402 is good not only for paywalling AI agents, but is pretty much ready to help you monetize your blog. No need to fiddle with services like 'Buy Me a Coffee' or annoy your users with ads.

It is easily integratable even into a static site, though you will need some compute. Not much though.

And there is no out of the box solution to make a multinetwork payment for dynamic content that will embed itself into DOM of your page. At least I haven't found one during this short excursion. But the browsers seem to be ready for pay-walling the pages, making it a very easy thing to implement.