Development of Multichain Crypto Payments System
Accepting crypto payments "in one network" is long solved problem. But once client wants to accept payments in ETH, USDC, BNB, SOL, USDT on Tron and also Bitcoin — this becomes architectural task. Each network has own address model, transaction finality, double-spend risks, SDKs and node requirements. Gluing all this into single reliable system is nontrivial.
Key Architectural Decisions
Before writing code, need to make three decisions defining entire architecture.
Custodial vs Non-custodial
Custodial — system stores funds until merchant withdrawal. Simpler technically, but requires licensing (in most jurisdictions storing others' crypto assets = financial activity). Requires HSM or MPC for keys, regular audits.
Non-custodial — funds go directly to merchant addresses, system only detects payments. Technically harder (no single hot wallet), but cleaner regulatorily. Most B2B solutions use this approach.
HD Wallet vs Smart Contract Addresses
HD Wallet (BIP32/BIP44) — generate unique deposit address for each payment from one seed. m/44'/60'/0'/0/invoice_id — each invoice gets own address. Works for all EVM networks and Bitcoin. Monitoring: subscribe to events of all generated addresses.
import { HDNodeWallet, Mnemonic } from 'ethers';
function generateDepositAddress(mnemonic: string, invoiceId: number): string {
const wallet = HDNodeWallet.fromPhrase(
mnemonic,
`m/44'/60'/0'/0/${invoiceId}`
);
return wallet.address; // same address for all EVM networks
}
Important: Bitcoin and Solana need different derivation paths (BIP44 coin types: 0 for BTC, 501 for SOL, 60 for ETH).
Smart Contract approach (Forward Contract / Payment Splitter) — each merchant gets or is assigned smart contract that automatically forwards funds to main address. Convenient for EVM networks: one address, any tokens, automatic processing. CREATE2 allows computing contract address before deploy — can give client address immediately, deploy contract only on first payment.
// CREATE2 factory for deterministic deposit addresses
contract DepositFactory {
function getDepositAddress(bytes32 salt) external view returns (address) {
return Create2.computeAddress(salt, keccak256(type(ForwardDeposit).creationCode));
}
function deployDeposit(bytes32 salt, address recipient) external returns (address) {
return address(new ForwardDeposit{salt: salt}(recipient));
}
}
Confirmation Requirements
Different networks require different confirmation counts for safe finality:
| Network | Recommended Confirmations | Time |
|---|---|---|
| Bitcoin | 3-6 | 30-60 min |
| Ethereum | 12-20 | 3-4 min |
| BNB Chain | 15-20 | 45-60 sec |
| Polygon | 256 | ~8 min |
| Solana | 32 (finalized) | ~15 sec |
| Tron | 20 | ~1 min |
| Arbitrum | 1 (L2) | <1 sec |
Polygon PoS has deep reorgs — 256 confirmations for safe finality not exaggeration. Arbitrum inherits finality from Ethereum after settlement.
System Architecture
[Payment Gateway API]
↓
[Invoice Service] ← stores invoice state, triggers, webhooks
↓
[Address Generator] ← HD wallet or CREATE2 factory
↓
[Chain Monitors] ← one process per chain
├─ EthereumMonitor (WebSocket eth_subscribe)
├─ BscMonitor (WebSocket)
├─ SolanaMonitor (WebSocket account subscribe)
├─ TronMonitor (Event API polling)
└─ BitcoinMonitor (ZMQ or Electrum)
↓
[Confirmation Tracker] ← waits N confirmations
↓
[Webhook Dispatcher] ← notifies merchant
Chain Monitors — most critical component. Each monitor must:
- Survive node connection breaks (auto-reconnect + catch-up)
- Handle reorgs (invalidate pending confirmations)
- Detect both native coins and ERC-20/BEP-20/SPL tokens
- Work independently — one monitor failure shouldn't crash others
EVM Monitoring
import { createPublicClient, webSocket, parseAbiItem } from 'viem';
const client = createPublicClient({
chain: mainnet,
transport: webSocket('wss://eth-mainnet.g.alchemy.com/v2/...'),
});
// Monitoring ERC-20 Transfer to our addresses
const unwatch = client.watchEvent({
event: parseAbiItem('event Transfer(address indexed from, address indexed to, uint256 value)'),
args: { to: monitoredAddresses },
onLogs: async (logs) => {
for (const log of logs) {
await processIncomingTransfer({
chain: 'ethereum',
token: log.address,
from: log.args.from,
to: log.args.to,
amount: log.args.value,
txHash: log.transactionHash,
blockNumber: log.blockNumber,
});
}
},
});
Solana Monitoring
Solana has different model: tokens stored not directly on user address, but in Associated Token Accounts (ATA). To accept USDC need to know user's ATA address for that token:
import { Connection, PublicKey } from '@solana/web3.js';
import { getAssociatedTokenAddress, TOKEN_PROGRAM_ID } from '@solana/spl-token';
const USDC_MINT = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
async function getUsdcDepositAddress(userPublicKey: PublicKey): Promise<string> {
const ata = await getAssociatedTokenAddress(USDC_MINT, userPublicKey);
return ata.toBase58();
}
// Monitoring via WebSocket
const connection = new Connection('wss://api.mainnet-beta.solana.com');
connection.onAccountChange(ataAddress, (accountInfo) => {
// process balance change
});
Bitcoin Monitoring
For Bitcoin without custom node can use Electrum Protocol or third-party services (BlockCypher API, Tatum). For serious production system recommend own bitcoind + Electrs (Electrum Rust server):
from electrum_client import ElectrumClient
client = ElectrumClient('127.0.0.1', 50001)
# Subscribe to address history
async def monitor_bitcoin_address(address: str):
script_hash = address_to_scripthash(address) # sha256(scriptPubKey), reversed
await client.subscribe_scripthash(script_hash, callback=on_history_change)
Token Processing and Conversion
Token whitelist. Don't accept arbitrary tokens — only pre-approved list. Otherwise attacker can send worthless ERC-20 token which technically is "payment".
Slippage on conversion to stablecoins. If merchant wants USD equivalent, system must convert volatile assets. Use aggregators (1inch) with tight slippage tolerance and minimum conversion amount for profitability.
Underpayment handling. Client paid 99.5 USDC instead of 100. Need policy: acceptable variance (usually 0.5-1%), partial payment (invoice marked as partially paid, requires top-up), or automatic refund. All this should be in business logic, not smart contract.
Webhook Reliability
Merchant notification about payment is critical. Webhook can fail, hang, return 5xx. Pattern for reliable delivery:
class WebhookDispatcher:
MAX_ATTEMPTS = 5
RETRY_DELAYS = [30, 120, 600, 3600, 86400] # sec: 30s, 2m, 10m, 1h, 24h
async def dispatch_with_retry(self, webhook_url: str, payload: dict, attempt: int = 0):
try:
async with aiohttp.ClientSession() as session:
resp = await session.post(
webhook_url,
json=payload,
headers={'X-Signature': self.sign_payload(payload)},
timeout=aiohttp.ClientTimeout(total=30)
)
if resp.status == 200:
await self.mark_delivered(payload['event_id'])
return
except Exception as e:
log.error(f"Webhook failed attempt {attempt}: {e}")
if attempt < self.MAX_ATTEMPTS:
await asyncio.sleep(self.RETRY_DELAYS[attempt])
await self.dispatch_with_retry(webhook_url, payload, attempt + 1)
HMAC signature of payload (X-Signature header) allows merchant to verify notification authenticity.
Infrastructure and Security
HD wallet keys — master seed stored in KMS (AWS KMS or HashiCorp Vault). Address generation service doesn't store seed locally — requests KMS on each operation. Derivation indices stored in DB — loss means inability to find payments.
Rate limiting and abuse. Invoice generation must be rate-limited at API key level. Unused invoices with expired deadline — archive, don't delete (needed for audit).
Reconciliation. Daily check: sum all confirmed payments per our data vs hot wallet balance (if custodial). Discrepancies — alert immediately.
Stack and Timeline
Technology stack:
| Layer | Choice |
|---|---|
| API | FastAPI (Python) or Fastify (Node.js) |
| Chain monitoring | Python asyncio / Node.js workers, one process per chain |
| DB | PostgreSQL (invoices, transactions) + Redis (pending confirmations, cache) |
| Queues | Redis Streams or RabbitMQ for webhook dispatch |
| Deploy | Kubernetes (High Availability critical) |
Development phases:
- Basic EVM monitoring + invoice flow: 3-4 weeks
- Add Bitcoin and Solana: +2-3 weeks
- Conversion to stablecoins: +1-2 weeks
- Webhook system + merchant dashboard: +2-3 weeks
- Load testing, security review, production deployment: +2 weeks
Total for full-featured system with 5-6 supported networks: 10-14 weeks.







