X402, a static blog monetization excercise

Hacker News Top Tools

Summary

A developer implements a paywall for a static blog using the x402 payment middleware on Cloudflare Workers, exploring the potential of microtransaction-based monetization to replace ad revenue.

No content available
Original Article
View Cached Full Text

Cached at: 07/06/26, 02:04 PM

# x402, a static blog monetization excercise Source: [https://shtein.me/posts/x402-poc/](https://shtein.me/posts/x402-poc/) ## x402 Cloudflare has[published an announcement](https://blog.cloudflare.com/monetization-gateway/)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](https://docs.x402.org/getting-started/quickstart-for-sellers#2-add-payment-middleware)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 = []; 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](https://shtein.me/posts/x402-poc/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](https://shtein.me/posts/x402-poc/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](https://shtein.me/posts/x402-poc/mm-connected.png) Now if we continue to the actual payment: ![signature-request.png](https://shtein.me/posts/x402-poc/signature-request.png) And at the end we finally get the content we paid for: ![success.png](https://shtein.me/posts/x402-poc/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://facilitator.payai.network" }); @@ - 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](https://dexter.cash/facilitator)[PayAI](https://facilitator.payai.network/)— a free facilitator that settles Base and Solana mainnet with nosms or registrationaccount or API keys\. Dexter supposed to do the same, but the transactions announced by it never reached the actual network resulting in a payment timeout, with PayAI everything works like a charm\. And that's it\. You can press the button below to try it yourself\. Don't forget to top up your wallet on base or solana network with USDC before doing that\. [🎩 Get a premium joke \($0\.01\)](https://shtein.me/api/joke) ## Summary [x402](https://x402.org/)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\.

Similar Articles

Monetization Gateway

Hacker News Top

Cloudflare announces the Monetization Gateway, enabling customers to charge for any protected resource (web pages, APIs, datasets, MCP tools) via the x402 protocol, with stablecoin settlements at the edge, targeting the shift to usage-based pricing for AI agents.

@dulipeng: https://x.com/dulipeng/status/2067450611529093311

X AI KOLs Timeline

This article is a practical tutorial that details how to use the Cloudflare Workers/Pages free tier to deploy a low-cost VPN, based on the open-source project edgetunnel, and used with clients like Clash and Shadowrocket.

Rebuilding my blog with machines, for machines ·

Lobsters Hottest

The author rebuilt their blog to include full structured data markup (JSON-LD, microformats) and an AI co-writer guided by a prompt that avoids common LLM patterns, with CI validation to prevent breakage.