Разработка шлюза приёма криптоплатежей
Типичная постановка задачи: "хотим принимать крипту как обычный Stripe, только без посредников". На практике это означает решить несколько нетривиальных проблем одновременно — детекцию входящих платежей без polling каждые несколько секунд, волатильность при конвертации, partial payments, confirmation thresholds для разных сетей, и идемпотентность при сетевых сбоях.
Архитектура: из чего состоит шлюз
Минимально жизнеспособный payment gateway состоит из четырёх компонентов:
[Клиент] → [API Gateway] → [Payment Service]
↓
[Blockchain Listener]
↓
[Event Queue (Redis/Kafka)]
↓
[Settlement Service] → [ERP/CRM]
Payment Service — создаёт заказ, генерирует уникальный адрес (или payment ID), возвращает клиенту данные для оплаты. Stateful — хранит mapping address → order.
Blockchain Listener — мониторит входящие транзакции. Это самый критичный компонент с точки зрения надёжности. Два подхода:
-
WebSocket подписка (
eth_subscribe("logs", filter)илиeth_subscribe("newHeads")) — низкая латентность, но соединение рвётся, нужен reconnect с backoff и replay пропущенных блоков. -
Polling + cursor — менее элегантно, но предсказуемо. Храним последний обработанный блок, опрашиваем
eth_getLogsс фильтром по адресам. Устойчивее к сетевым сбоям.
Для production: hybrid подход — WebSocket для низкой латентности, polling как fallback с cursor-based recovery.
Event Queue — буфер между listener и settlement. Kafka для высоких нагрузок, Redis Streams для средних. Ключевой момент: listener публикует TransactionDetected event, settlement подписывается. Это развязывает компоненты и гарантирует обработку при временном падении settlement service.
Settlement Service — проверяет confirmations, конвертирует сумму, обновляет статус заказа, нотифицирует upstream систему (webhook).
Детекция платежей: нюансы по сетям
EVM-сети (ETH, BNB, Polygon, Arbitrum...)
Нативные переводы ETH: мониторим через eth_subscribe("newHeads") + eth_getBlockByNumber и фильтруем транзакции по to адресу.
ERC-20 токены (USDT, USDC, DAI): мониторим событие Transfer(address indexed from, address indexed to, uint256 value) через eth_getLogs с фильтром:
const filter = {
fromBlock: 'latest',
topics: [
ethers.id('Transfer(address,address,uint256)'),
null, // from: любой
ethers.zeroPadValue(paymentAddress, 32), // to: наш адрес
],
};
Важно для USDT (Tether): у него нестандартный ERC-20 — функция transfer не возвращает bool. Вызов через стандартный интерфейс упадёт. Используем safeTransfer или низкоуровневый call с проверкой returndata.
Bitcoin и UTXO-модель
Для BTC нет понятия "адрес → транзакция" на уровне ноды. Используем либо:
- Electrum Server (Electrs) — индексирует UTXO по адресам, позволяет подписаться на адрес
- BlockCypher / Blockcypher WebHook API — hosted решение, но зависимость от третьей стороны
-
Bitcoin Core с
importaddress— добавляем адрес в wallet node, получаем нотификации через ZMQ
Минимальные confirmations для BTC: 1 для small amounts (<$100), 3 для medium, 6 для large. Для Ethereum достаточно 12–20 блоков (EIP-3607 finality не гарантирован без Casper finalization).
TON
TON транзакции асинхронны: входящий transfer это bounce-able сообщение, и вы должны проверять, что это именно transfer, а не bounce. Используем TonAPI или TON Center API с webhook на адрес.
Confirmation threshold и защита от double-spend
Нельзя считать платёж завершённым после первого обнаружения транзакции в mempool — это pending состояние, не подтверждённое. Минимальные пороги:
| Сеть | Порог | Обоснование |
|---|---|---|
| Ethereum | 12 блоков (~2.5 мин) | После merge finality через checkpoint, но 12 блоков — практический стандарт |
| BNB Chain | 15 блоков (~45 сек) | Централизован, но реорги бывают |
| Polygon PoS | 128 блоков (~4 мин) | Checkpoint на Ethereum каждые ~30 мин, до этого реорги возможны |
| Bitcoin | 3–6 блоков (30–60 мин) | Классика, для крупных сумм |
| Arbitrum/Optimism | 1 блок (L2 finality) | Реорги на L2 практически невозможны |
Partial payments и overpayments
Реальные пользователи иногда платят не точную сумму — биржи снимают комиссии, люди ошибаются. Нужна политика:
-
Underpayment: если получено 99–100% суммы — считаем оплаченным (tolerance 1%). Если меньше —
partially_paid, ждём доплаты X минут, потомexpired. - Overpayment: автоматически принимаем, разницу возвращаем (нужен refund flow) или зачисляем как кредит.
Волатильность: курс на момент создания заказа
Стандартная схема: фиксируем курс криптовалюты к USD на момент создания invoice, даём 15–30 минут на оплату. Источник курса — Chainlink Price Feed (on-chain) или CoinGecko/CoinMarketCap API (off-chain). Chainlink предпочтительнее — меньше vendor lock-in, но требует web3 RPC.
При истечении времени: expired статус, генерация нового invoice с актуальным курсом по запросу клиента.
Webhook и идемпотентность
Нотификация upstream системы через webhook должна быть идемпотентной — возможны повторные доставки при retry. В payload включаем payment_id (уникальный) + tx_hash + status. Upstream система должна проверять, не обрабатывала ли она уже этот payment_id.
Retry policy: экспоненциальный backoff, 5–10 попыток, после — dead letter queue для ручного разбора.
Стек
- Node.js + TypeScript или Go для listener и API — хорошая поддержка web3 библиотек
- ethers.js v6 или viem для EVM взаимодействия
- PostgreSQL для хранения платежей (ACID, транзакционность при обновлении статусов)
- Redis для rate limiting и кеша курсов
- Kafka или Redis Streams для event queue
- Grafana + Prometheus — мониторинг: lag listener vs chain head, скорость обработки, ошибки
Кастомный шлюз имеет смысл при объёме >500 платежей/день или при специфических требованиях к приватности и контролю. Для меньших объёмов — NOWPayments, CoinGate или аналоги закрывают задачу дешевле.







