x402, вправа з монетизації статичного блогу

2026-07-05

x402

Пару днів тому Cloudflare опублікував анонс запуску Monetization Gateway. Це революційно з багатьох причин. Зникає високий поріг входу в монетизацію, з'являється дуже точний контроль над тим, за що саме брати гроші і наскільки маленьким може бути платіж. Це буквально може змінити те, як ми користуємось інтернетом, зробивши рекламну економіку, на якій інтернет тримається, непотрібною.

Звучить як ідеальний інструмент для монетизації блогу чи, скажімо, API. І хоч монетизувати мені особливо нічого, технологія виглядає занадто важливою, щоб її не спробувати. Тож саме це і зробимо.

План

На жаль, сервіс Cloudflare, схоже, поки що в закритій беті(?), тобто просто так доступ не отримати - треба записуватись у вейтліст. Тож залишається зробити все власноруч. Що навіть цікавіше, чи не так? На щастя, сетап звучить досить просто. Так, до статичного блогу додається пара рухомих деталей, але нічого такого, що не можна прикрутити зверху за день.

Отже, що нам треба, щоб ця штука запрацювала?

  1. Гаманець, куди отримувати гроші
  2. Воркер, який виконуватиме логіку обробки платежів
  3. Ресурс, який ховаємо за пейвол

Пункти 1 і 3 в мене є, а от 2 - ні. На цю роль поки візьму Cloudflare Workers, бо саме там хоститься мій сайт. Це ж диктує і вибір мови, на якій все це писати - TypeScript. Може, пізніше перепишу це під свій локальний сервер, але щоб не роздувати скоуп експерименту - поки що зупинимось на клаудфлері.

Мідлвара

Тож одразу до діла. Перше, що нам потрібно - мідлвара, яка прийматиме запит і слідкуватиме, щоб доступ видавався лише після оплати, а заодно розповідатиме про ціну. В офіційній документації є приклад реалізації на Hono, ним і скористаємось. Робимо пару модифікацій з поправкою на те, що код крутитиметься на клаудфлер воркері, а не на окремому сервері, вписуємо свої гаманці і замінюємо ендпоінт з прикладу на значно корисніший - і отримуємо ось це:

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;
Воркеру також потрібен wrangler.jsonc (клаудфлерська специфіка)
{
  "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" }
      ]
    }
  }
}

Тут два неочевидні моменти: nodejs_compat потрібен, бо пакети x402 імпортують нодівські вбудовані модулі (events, crypto, url), і без нього білд падає. А роут живе лише в production оточенні не просто так: якщо роут визначено, wrangler dev емулює хост цього роута, і URL-и, які генерує мідлвара, вказували б на реальний домен замість localhost.

Запускаємо локально:

npx wrangler dev

І, надіславши тестовий запит, бачимо пейвол:

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

Заголовок PAYMENT-REQUIRED - це закодований в base64 список способів оплати і цін:

{
  "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"
      }
    }
  ]
}

І це саме те, що ми хочемо бачити: 2 тестнети з точною сумою в 1 цент. Ну що, спробуємо заплатити? Щоб протестувати це в такому вигляді, знадобився б програмний клієнт, який вміє провести платіж і повторити запит з підтвердженням. Але в цьому конкретному випадку мені цікаво, чи впорається кінцевий користувач зі звичайного браузера. І, на щастя, впорається! Якщо відкрити той самий URL у браузері - побачимо ось це:

browser-failed-paywall.png

що людською мовою пояснює, що зробити, аби все запрацювало. Тож робимо:

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);
 });

І тепер при спробі доступу нас зустрічає приємний UI, який допомагає оплатити доступ:

browser-success-paywall.png

Посилання на поповнення там тому, що ми поки на тестнеті. В продакшені його не буде. Спробуємо заплатити через MetaMask.

Спершу підключаємо MetaMask:

mm-connected.png

Тепер, якщо продовжити до самої оплати:

signature-request.png

І в кінці нарешті отримуємо контент, за який заплатили:

success.png

Пара спостережень щодо цього фінального запиту:

Клієнт надсилає підпис, який підтверджує платіж, у заголовку PAYMENT-SIGNATURE, що декодується в

{
  "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"
    }
  }
}

Як бачите, тут вся інформація, яка потрібна нашій мідлварі, щоб переконатися, що платіж таки відбувся. Тож вона відповідає успішним заголовком PAYMENT-RESPONSE:

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

І тілом відповіді з самим контентом.

Інший цікавий момент: браузер відкрив оплачений контент за іншим URL - URL-ом об'єкта в пам'яті, створеного з відповіді. Я спершу подумав, що це такий класний UI-захист від подвійного списання при оновленні сторінки, але схоже, що ні: пейвол витягує контент за лаштунками, і оскільки наша відповідь - JSON, а не HTML, йому нема чого відрендерити на місці, тож він переходить на URL об'єкта в пам'яті, створеного з відповіді. Сам протокол - це оплата за кожне використання, тож щоразу, як смикаєте API, з вас зніматимуть гроші.

Замикаємо коло

Ок, тепер, коли все працює, перемикаємось на бойові мережі і викочуємо все назовні:

 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 },

Фасилітатор x402.org сетлить лише тестнети, тож для справжніх мереж вказуємо на Dexter PayAI - безкоштовний фасилітатор, який сетлить Base і Solana мейннет без смс та реєстрації акаунтів та API ключів. Dexter мав би вміти те саме, але транзакції, які він анонсував, так і не доходили до реальної мережі, і платіж завершувався таймаутом. З PayAI же все працює як годинник.

І це все. Можете натиснути кнопку нижче і спробувати самі. Тільки не забудьте перед цим закинути на гаманець трохи USDC в мережі Base або Solana.

🎩 Отримати преміум жарт ($0.01)

Підсумок

x402 годиться не лише для пейволінгу AI агентів - він вже цілком готовий допомогти вам монетизувати блог. Не треба возитися з сервісами на кшталт 'Buy Me a Coffee' чи дратувати користувачів рекламою. І все завдяки тому, що браузери вже реалізували простий x402 клієнт для сторінок.

Він легко інтегрується навіть у статичний сайт, хоча трохи компьюту таки знадобиться. Зовсім трохи.

Обмеження, які я помітив під час реалізації:

  • Немає готового рішення з коробки для мультимережевої оплати динамічного контенту, який сам вбудується в DOM вашої сторінки.
  • Протокол обмежений разовими платежами. Тобто якщо хочеться чогось більшого - якоїсь підписки чи повторної видачі контенту на основі попередньої оплати - доведеться будувати це самотужки.