Разработка платформы оплаты в криптовалюте
Разница между "добавить кнопку оплаты криптовалютой" и "построить платёжную платформу" — принципиальная. В первом случае интегрируется третий сервис с его гарантиями и ограничениями. Во втором — строится собственная инфраструктура: генерация адресов, мониторинг транзакций, управление ключами, settlement, reconciliation. Эта страница — про второй вариант: когда объём, регуляторные требования или бизнес-модель не позволяют идти через посредника.
Почему строят собственную платформу
Типичные причины:
- Объём — при >$10M/месяц комиссия 0.5–1% процессинга становится дороже собственной инфраструктуры
- Контроль над данными — финансовые данные не должны уходить к третьим сторонам
- Кастомная логика — условные платежи, эскроу, рекуррентные платежи, сплит платежей
- White-label — платформа для других бизнесов, собственный процессинг обязателен
- Регуляторные требования — некоторые юрисдикции требуют лицензии, несовместимые с использованием иностранного процессора
Архитектура: ключевые компоненты
Клиент → Payment API → Invoice Service → Address Generator
→ Blockchain Monitor
→ Settlement Engine
→ Merchant Webhook
HD кошельки и генерация адресов
Основа платформы — иерархические детерминированные кошельки (BIP32/BIP44). Один master seed генерирует неограниченное число дочерних адресов — по одному уникальному адресу на каждый инвойс:
m / purpose' / coin_type' / account' / change / index
m / 44' / 60' / 0' / 0 / invoice_id
from hdwallet import HDWallet
from hdwallet.symbols import ETH
def generate_payment_address(merchant_id: int, invoice_id: int) -> str:
"""
Детерминированная генерация адреса: один invoice = один адрес
merchant_id: account index (BIP44 level 3)
invoice_id: address index (BIP44 level 5)
"""
wallet = HDWallet(symbol=ETH)
wallet.from_xprivate_key(MASTER_XPRIV)
wallet.from_path(f"m/44'/60'/{merchant_id}'/0/{invoice_id}")
return wallet.p2pkh_address()
Критически важно: приватный ключ от master seed должен храниться в HSM (Hardware Security Module) или минимум в AWS KMS / HashiCorp Vault. Никаких plaintext ключей в конфигурационных файлах или БД.
Для публичной генерации адресов (без доступа к приватному ключу) — xpub достаточно:
# Только xpub для watch-only адресов
wallet.from_xpublic_key(MERCHANT_XPUB)
wallet.from_path(f"m/0/{invoice_id}") # relative от xpub
address = wallet.p2pkh_address()
Мониторинг транзакций
Два подхода к отслеживанию входящих платежей:
Polling через RPC — простое, работает везде:
import asyncio
from web3 import Web3
async def monitor_invoice(
address: str,
expected_amount: int, # в wei
invoice_id: str,
timeout_seconds: int = 3600
):
w3 = Web3(Web3.HTTPProvider(ETH_RPC_URL))
start_block = w3.eth.block_number
deadline = time.time() + timeout_seconds
while time.time() < deadline:
# Проверяем баланс адреса
balance = w3.eth.get_balance(address, "latest")
if balance >= expected_amount:
# Ждём confirmations
await wait_for_confirmations(address, expected_amount, confirmations=6)
await mark_invoice_paid(invoice_id, balance)
return
await asyncio.sleep(15) # раз в ~блок
await mark_invoice_expired(invoice_id)
WebSocket subscriptions — для минимальной латентности:
from web3 import AsyncWeb3
from web3.providers import WebsocketProviderV2
async def subscribe_to_address(address: str, callback):
async with AsyncWeb3(WebsocketProviderV2("wss://eth-mainnet.g.alchemy.com/v2/KEY")) as w3:
subscription_id = await w3.eth.subscribe(
"logs",
{"address": address} # получаем все logs для адреса
)
async for response in w3.socket.process_subscriptions():
if response["subscription"] == subscription_id:
await callback(response["result"])
Для ERC-20 платежей мониторинг сложнее: нужно слушать Transfer события токен-контракта с фильтром по to адресу, а не ETH баланс.
Работа с несколькими сетями и токенами
Production платформа поддерживает минимум: ETH/USDT/USDC на Ethereum, USDT/USDC на Polygon и Arbitrum (дешевле gas), BTC нативно. Архитектура мультисети:
@dataclass
class NetworkConfig:
chain_id: int
rpc_url: str
confirmations_required: int # 1 для L2, 6 для ETH mainnet
supported_tokens: dict[str, str] # symbol -> contract address
NETWORKS = {
"ethereum": NetworkConfig(
chain_id=1,
rpc_url=ETH_RPC,
confirmations_required=6,
supported_tokens={"USDT": "0xdAC17...", "USDC": "0xA0b86..."}
),
"arbitrum": NetworkConfig(
chain_id=42161,
rpc_url=ARB_RPC,
confirmations_required=1,
supported_tokens={"USDT": "0xFd086...", "USDC": "0xFF970..."}
),
# ...
}
Settlement: вывод средств
Собранные платежи нужно агрегировать и выводить мерчанту. Два паттерна:
Sweeping — периодический перевод со всех адресов на hot wallet:
async def sweep_address(from_address: str, to_address: str, token: str):
private_key = await kms.get_key(derive_key_path(from_address))
if token == "ETH":
balance = w3.eth.get_balance(from_address)
gas_estimate = 21000
gas_price = w3.eth.gas_price
amount = balance - (gas_estimate * gas_price)
if amount <= 0:
return
tx = {
"to": to_address,
"value": amount,
"gas": gas_estimate,
"gasPrice": gas_price,
"nonce": w3.eth.get_transaction_count(from_address),
"chainId": 1
}
else:
# ERC-20 sweep: сначала ETH на gas, затем transfer токена
await fund_gas(from_address)
tx = build_erc20_transfer(token, from_address, to_address)
signed = w3.eth.account.sign_transaction(tx, private_key)
tx_hash = w3.eth.send_raw_transaction(signed.rawTransaction)
return tx_hash.hex()
Batch payments — для выплат мерчантам, использовать Multicall или специализированные batch transfer контракты (например, Disperse.app паттерн) — один газ на N переводов.
Работа с ERC-20: proблема gas
Слабое место ERC-20 платежей: адрес получения пуст (нет ETH для gas). Перед sweep нужно отправить небольшое количество ETH для оплаты газа трансфера токена. Это создаёт dust проблему: ETH остаётся после sweep.
Решение — EIP-2612 Permit: токены с permit (USDC, DAI) позволяют подписывать разрешение на transfer без on-chain approve транзакции. Можно sweep токены одной транзакцией с подписью, без предварительного funding:
// USDC permit + transferFrom в одном вызове
function sweepWithPermit(
address token,
address from,
address to,
uint256 amount,
uint256 deadline,
uint8 v, bytes32 r, bytes32 s
) external {
IERC20Permit(token).permit(from, address(this), amount, deadline, v, r, s);
IERC20(token).transferFrom(from, to, amount);
}
Обменные курсы и ценообразование
Инвойс создаётся в фиатной валюте (USD/EUR), сумма в крипте вычисляется динамически. Для стейблкоинов — курс 1:1 с небольшим буфером. Для волатильных активов (ETH, BTC):
def calculate_crypto_amount(fiat_amount: Decimal, currency: str, crypto: str) -> dict:
# Агрегируем цену из нескольких источников
prices = await asyncio.gather(
fetch_binance_price(f"{crypto}USDT"),
fetch_coinbase_price(f"{crypto}-USD"),
fetch_chainlink_price(CHAINLINK_FEEDS[crypto])
)
median_price = statistics.median(prices)
crypto_amount = fiat_amount / median_price
# Добавляем буфер на случай движения цены (1-2%)
crypto_amount_with_buffer = crypto_amount * Decimal("1.015")
return {
"amount": crypto_amount_with_buffer,
"rate": median_price,
"expires_at": datetime.now() + timedelta(minutes=15),
"rate_locked": True
}
Безопасность и compliance
KYT (Know Your Transaction) — проверка адресов на санкционные списки и связи с mixers/dark markets. Провайдеры: Chainalysis, Elliptic, TRM Labs. API интегрируется в воронку платежа:
async def check_address_risk(address: str) -> RiskScore:
response = await chainalysis_client.post("/v2/address/identifications", json={"address": address})
risk = response.json()
if risk["risk"] in ["HIGH", "SEVERE"]:
await flag_for_review(address)
return RiskScore(level=risk["risk"], categories=risk["categories"])
Transaction limits — автоматические лимиты по объёму для AML: дневной лимит на кошелёк, порог для ручной проверки, обязательная верификация для крупных транзакций.
Reconciliation — ежедневная сверка: сумма выплаченных мерчантам средств + комиссии платформы + остатки на sweep адресах должны совпадать с суммой всех подтверждённых входящих транзакций. Любое расхождение — алерт.
Инфраструктура и надёжность
Нода vs провайдер: для платежной платформы с требованиями к надёжности — собственная нода или Alchemy/Infura с fallback на другой провайдер. Single point of failure неприемлем.
from web3 import Web3
from web3.middleware import ExceptionRetryMiddleware
providers = [
Web3.HTTPProvider(ALCHEMY_URL),
Web3.HTTPProvider(INFURA_URL),
Web3.HTTPProvider(QUICKNODE_URL),
]
def get_w3_with_fallback():
for provider in providers:
w3 = Web3(provider)
w3.middleware_onion.add(ExceptionRetryMiddleware, retries=3)
if w3.is_connected():
return w3
raise RuntimeError("All providers unavailable")
Idempotency: все операции должны быть идемпотентны — повторная обработка webhook или retry payment check не должны создавать дублирующие выплаты. Уникальный idempotency_key на каждую операцию.
Сроки
| Фаза | Содержание | Срок |
|---|---|---|
| Архитектура и HD wallet | Дизайн системы, генерация адресов, KMS интеграция | 1–2 нед |
| Мониторинг транзакций | Polling/WebSocket мониторинг, confirmations | 1–2 нед |
| Settlement engine | Sweep логика, ERC-20 permit, batch payments | 2–3 нед |
| Merchant API | REST API, webhooks, dashboard | 2–3 нед |
| Compliance | KYT интеграция, лимиты, reconciliation | 1–2 нед |
| Тестирование | Testnet, stress testing, security review | 2–3 нед |
Минимально жизнеспособная платформа с ETH/USDC на одной сети: 6–8 недель. Полная мультисетевая платформа с compliance: 3–5 месяцев.







