Собрал платный API для ИИ‑агентов на x402. Рынка не нашёл — зато нашёл четыре бага

в 13:55, , рубрики: ai-агенты, claude code, DevSecOps, docker, Ed25519, fastapi, HTTP 402, postgresql, x402

TL;DR. Я собрал платный API для ИИ‑агентов на протоколе x402: агент получает HTTP 402, платит в USDC и получает подписанный сертификат проверки своего кода. Довёл до продакшена, прогнал сквозной путь, задеплоил. Потом проверил доступные данные о рынке и понял, что покупателей в том объёме, на который я рассчитывал, пока не видно. По дороге поймал четыре бага, каждый из которых выглядел как правильно работающий код. Ниже — что построил, что сломалось, что показали цифры и какое одно правило ловило всё.

Гипотеза

x402 — протокол, который возвращает к жизни забытый код ответа HTTP 402 Payment Required. Сервер отвечает 402 с условиями оплаты, клиент подписывает платёж в стейблкоине и повторяет запрос. Без аккаунтов, без API‑ключей, без подписок. С апреля 2026 года протокол развивается под Linux Foundation.

Смысл был такой. ИИ‑агенты пишут код, и этот код часто содержит уязвимости. По данным Veracode, ИИ‑код вносит уязвимости из OWASP Top 10 в 45% случаев. По данным CodeRabbit, security‑находок в нём в 1,57 раза больше, чем в человеческом, а по XSS отдельно — в 2,74 раза. Если агент перед релизом будет сам платить пару центов за независимую проверку и получать подписанный протокол — это M2M‑рынок, где покупатель не человек, а программа.

Что построил

  • FastAPI‑шлюз. Эндпоинт /v1/audit/{suite_id} без оплаты отвечает 402 с x402-payload: сеть, актив, сумма, адрес получателя.

  • Facilitator — отдельный компонент, который проверяет и проводит платёж. Шлюз сам не держит ключей плательщика.

  • Изолированный прогон. После оплаты артефакт проверяется в одноразовом контейнере: docker run --rm, без сети, read‑only, non‑root, с лимитами CPU и памяти.

  • Тест‑сьют xss-cwe80-v1 — детерминированные сигнатурные правила на XSS: innerHTML, dangerouslySetInnerHTML, v-html, |safe, eval и так далее.

  • Attestation — результат, подписанный ed25519: хэш артефакта, идентификатор сьюта, сколько проверок прошло, сколько провалилось, распределение по severity, время.

  • Rate limit в Redis: больше 100 отказов в минуту с одного адреса — 429. Запросы с проверенной оплатой под этот лимит не попадают.

  • PostgreSQL для журнала транзакций, деплой на Railway.

Так выглядит ответ на запрос без оплаты:

{
  "x402Version": 1,
  "accepts": [{
    "scheme": "exact",
    "network": "base-sepolia",
    "asset": "USDC",
    "payTo": "0x0000000000000000000000000000000000000000",
    "maxAmountRequired": "100000",
    "resource": "/v1/audit/xss-cwe80-v1",
    "description": "AI-Auditor attestation run: xss-cwe80-v1",
    "mimeType": "application/json",
    "maxTimeoutSeconds": 120
  }]
}

О самом продукте важно сказать одно: сертификат не утверждает «уязвимостей нет». Конечный набор проверок физически не может доказать отсутствие дефекта. Он фиксирует только то, что можно подтвердить.

Чем проверено — test_suite_id с версией сьюта. Что именно проверено —artifact_hash: изменил одну строку, и сертификат к этому коду уже не относится. Когда — timestamp. С каким итогом — N проверок, M провалов и распределение по severity, а не «безопасно». Кто за это отвечает — подпись issuer_signature.

Это не заменяет пентест и не гарантия безопасности — это протокол ограниченного набора проверок.

Четыре бага, которые выглядели как рабочий код

1. Защита от повторной оплаты, которая никогда не срабатывала

Facilitator хранит использованные nonce, чтобы один платёж нельзя было предъявить дважды. Код выглядел корректно:

async def handle_audit(suite_id: str, request: Request):
    settings = get_settings()
    facilitator = build_facilitator(settings)  # новый объект на каждый запрос
    ...

Проблема в одной строке. Facilitator создавался заново на каждый HTTP‑запрос — вместе с пустым множеством использованных nonce. Проверка «этот nonce уже был» не могла сработать ни разу: к моменту второго запроса память первого уже не существовала. Ветка nonce_already_used была мёртвым кодом.

От двойного учёта спасал только UNIQUE(tx_hash) в базе. Но tx_hash в тестовом режиме вычислялся из кошелька, суммы и nonce — достаточно поменять сумму, и повтор проходил.

Фикс — перенести создание в lifespan, один экземпляр на процесс:

@asynccontextmanager
async def lifespan(app: FastAPI):
    await database.connect()
    app.state.facilitator = build_facilitator(get_settings())
    yield
    await database.disconnect()

Проверка после фикса: два вызова с одним nonce и разными суммами — первый 200 с сертификатом, второй 402 nonce_already_used.

Инсайт: время жизни объекта — часть контракта, а не деталь реализации.

2. Сайт упал, а логи чистые

Первый деплой на Railway отдавал 502: приложение слушало не тот порт, на который платформа отправляла трафик. В Dockerfile порт был захардкожен:

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"]

Исправил на чтение переменной окружения:

CMD ["sh", "-c", "uvicorn app.main:app --host 0.0.0.0 --port ${PORT:-8000}"]

Заработало. На следующем автодеплое снова 502. В логах приложения — ни одной ошибки: Uvicorn running on http://0.0.0.0:8080.

Причина: у домена target port зафиксирован на 8000, а платформа при новом деплое выдала контейнеру другой $PORT. Приложение слушает, но не там, куда идёт трафик. В первый раз сработало по совпадению.

Фикс — явная переменная PORT=8000 на сервисе, чтобы значение не зависело от того, что платформа выдаст в этот раз.

Инсайт: «заработало» после фикса ещё не значит «починилось». Иногда это значит «совпало».

3. Таблица, которой нет

После деплоя всё выглядело нормально: /healthz отвечал 200, платформа показывала зелёный статус. Первый же запрос, который писал в базу, упал с 500:

asyncpg.exceptions.UndefinedTableError: relation "agent_transactions" does not exist

Health check базу не трогал, поэтому и не мог заметить, что в ней ничего нет.

Схему никто никогда не применял руками. Локально это делал Docker: официальный образ Postgres выполняет скрипты из docker-entrypoint-initdb.d при первом старте контейнера с пустым volume. Шаг происходил молча, и поэтому его не было ни в одной инструкции деплоя. На managed‑базе этот механизм не срабатывает — схему пришлось накатить вручную и дописать этот шаг в runbook.

Инсайт: «работает у меня» часто означает «работает благодаря шагу, который никто не делал сознательно». И health check, который не проверяет зависимости, сообщает о здоровье сервиса, а не системы.

4. Режим для разработки, который оказался в продакшене

Чтобы разрабатывать и тестировать платёжный путь без реального блокчейна и реальных денег, в проекте есть тестовый facilitator. Он принимает строку вида wallet:...;amount:...;nonce:..., проверяет формат и сумму — и считает платёж проведённым.

Сквозной тест с ним проходил идеально: 402, оплата, прогон, подписанный сертификат. Слишком идеально — заглушка принимает любую строку правильного формата. Подставил сумму и новый nonce — получил сертификат, не заплатив ничего.

А потом я посмотрел на переменные продакшен‑окружения. Там стояло FACILITATOR_MODE=mock.

Закрыл флагом PUBLIC_MODE: при нём платный роутер вообще не регистрируется в приложении. Проверил не значение флага, а поведение: при true путь /v1/audit/* отсутствует в /openapi.json, при false — присутствует.

Инсайт: тестовый режим сам по себе не дыра. Дыра — когда ничто не мешает ему доехать до продакшена.

Что показали цифры

Пока я отлаживал, стоило проверить главное допущение — что агенты действительно платят. Сразу оговорюсь: собственный рынок я не измерял. Ссылку на сервис я никому не показывал, поэтому основное ниже — внешние данные, а не результаты моего продукта.

Доля реально агентского трафика. TRM Labs разобрала около $52,7 млн x402-платежей — почти 199 млн расчётов в Base, Solana и Polygon с мая 2025 года. После фильтрации активности, не похожей на настоящую коммерцию, агентской выглядит лишь 0,6–7,5% объёма. Остальное — скрипты, плановые процессы и транзакции самому себе, которые в блокчейне неотличимы от платежей ИИ‑агентов. Сами исследователи оговариваются, что методика может занижать долю: агент, который раз за разом покупает один и тот же сервис, по их критериям выглядит как скрипт. Но главный вывод это не отменяет: рост транзакций x402 — не прокси роста автономной коммерции (пересказ на PYMNTS).

Каталог, в который сложно попасть. Один из основных путей, которым агенты находят x402-сервисы, — авто‑индексация Bazaar: после проведённого платежа facilitator сам добавляет сервис в каталог. Судя по сообщениям разработчиков, это работает нестабильно: платёж проходит, а сервис в каталоге не появляется. В одних случаях facilitator вообще не возвращает служебный заголовок EXTENSION-RESPONSES (#2112), в других возвращает статус «в обработке», но индексации так и не происходит (#2691). Похожие жалобы есть и в августе (#3045), и у другого facilitator'а — PayAI (#53). Сам я до этого шага не дошёл, так что это чужой опыт, а не мой.

Экономика — не барьер. CDP Facilitator: первые 1000 расчётов в месяц бесплатно, дальше $0,001 за транзакцию. Инфраструктура почти ничего не стоит.

Мои собственные метрики. Внешнего трафика не было вовсе. В журнале шесть записей — три открытия лендинга и три обращения к двум эндпоинтам‑заглушкам, которые проверяют интерес к будущим функциям, — и все сделаны в день тестирования моими проверочными запросами. Это не данные о спросе, это отсутствие измерения. Ещё одна находка: request_ip за прокси платформы — это адрес прокси, а не клиента, поэтому считать уникальных посетителей по нему нельзя. Отличать свои запросы от чужих пришлось по времени, поэтому после теста я начал записывать User‑Agent — чтобы в следующий раз не гадать.

Итог: инфраструктура x402 готова, работает и дешёвая. Массового потока платящих агентов, которых можно было бы поймать публикацией API, по этим данным пока не видно.

Одно правило, которое ловило всё

Код в этом проекте писал ИИ‑агент — Claude Code. Я ставил задачи, проверял результат и принимал решения. И почти все находки сводятся к одной формуле:

Декларация — не подтверждённый факт.

Все четыре бага — один класс ошибки: что‑то сообщало о себе не то, чем являлось.

  • Ветка nonce_already_used в коде — не то же самое, что работающая защита от повтора.

  • «Заработало после фикса» — не то же самое, что «починилось».

  • Зелёный health check — не то же самое, что рабочая система.

  • Успешный сквозной тест — не то же самое, что проверенная оплата.

Тот же класс встречался и за пределами багов. Автоматически сгенерированная OpenAPI‑спецификация обещала 200, реальный вызов вернул 404. Отчёт агента «исправил» — не то же самое, что дифф.

Практика, которая это ловит, скучная и простая:

  1. Требовать сырой вывод, а не пересказ. Не «покажи, что всё хорошо», а «покажи вывод команды». Это работает в обе стороны: однажды строку контекста в диффе приняли за дублирование кода, и только полный файл показал, что всё в порядке.

  2. Проверять поведение, а не конфигурацию. Не «переменная выставлена», а «эндпоинт отсутствует в спеке при этом значении».

  3. Постоянный контекст для агента — файл CLAUDE.md в корне репозитория: принципы, команды, текущие решения. Новая сессия начинается не с нуля.

  4. Журнал решений — decisions.md, только дописывается, не редактируется. Каждое решение: вопрос, варианты, выбор, обоснование. Плюс стоп‑лист отклонённых путей, чтобы агент не предлагал их заново.

Отдельно про секреты. За проект пароль от продовой базы один раз попал в вывод команды — CLI платформы показал его открытым текстом там, где я ожидал маску. Ротировал сразу. Приватный ключ подписи генерировался только в файл, минуя вывод в терминал, — агент сам отказался печатать его в чат, сославшись на решение, записанное в репозитории ранее. Журнал решений — решает.

Что забрал

Продукт не нашёл рынка — в том виде и в тот момент, на которые я рассчитывал. Но цикл занял дни, а не месяцы, и закончился данными.

Если бы начинал заново — сначала проверил бы покупателя, потом строил бы инфраструктуру. Звучит очевидно, но когда строить интереснее, чем проверять, очевидное легко пропустить.

Если у вас есть данные по реальному агентскому трафику — x402 или любому другому протоколу оплаты для агентов, — интересно сверить. Особенно если ваши цифры расходятся с моими.

Автор: Laratok

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js