Розробка платформи оплаті в криптовалюті
Різниця між "додати кнопку оплаті криптовалютою" та "побудувати платіжну платформу" — принципіальна. У першому випадку інтегрується третій сервіс з його гарантіями та обмеженнями. У другому — будується власна інфраструктура: генерація адрес, мониторинг транзакцій, управління ключами, settlement, reconciliation. Ця сторінка — про другий варіант: коли обсяг, нормативні вимоги або бізнес-модель не дозволяють йти через посередника.
Чому будуть власну платформу
Типові причини:
- Обсяг — при >$10M/місяць комісія 0.5–1% процесингу стає дорожче власної інфраструктури
- Контроль над даними — фінансові дані не повинні йти до третіх осіб
- Кастомна логіка — умовні платежі, escrow, рекурентні платежі, split платежів
- 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:
"""
Детермінована генерація адреси: один інвойс = одна адреса
merchant_id: account index (BIP44 рівень 3)
invoice_id: address index (BIP44 рівень 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}") # відносне від 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 (дешевше газу), 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 на газ, потім 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: проблема газу
Слабке місце ERC-20 платежів: адреса отримання пуста (немає ETH для газу). Перед 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 місяців.







