Setting Up Multi-Cryptocurrency Payment Acceptance
The task sounds simple: accept BTC, ETH, USDT, USDC, SOL and several more tokens. In practice this means supporting at least three fundamentally different protocols (UTXO, EVM account-based, Solana), incompatible address formats, different confirmation logics, and you need to consolidate all this into a single payment status for business logic. Plus exchange rates change while the user is looking at the invoice.
Architectural Options
Option 1: Ready-made provider (NOWPayments, CoinPayments, BitPay). Quick, but vendor lock-in, commission 0.5-1% on each transaction, limited control over UX and data. For MVP or small volumes — reasonable choice.
Option 2: Custom integration — your own node or RPC provider for each network, HD wallet for address derivation, own monitoring. Full control, zero provider commissions, but operational burden and longer development.
Option 3: Hybrid — EVM networks through one provider (Alchemy/Infura supporting multiple networks), BTC through separate integration or Bitcore, Solana through Helius. Reasonable balance.
For volumes > $50k/month custom integration pays off in 3-6 months just on saved provider fees.
HD Wallet Derivation
BIP-44 defines hierarchy: m / purpose' / coin_type' / account' / change / address_index. For each new payment a new address is generated via incrementing address_index. One master seed — addresses for all networks.
from hdwallet import HDWallet
def derive_address(master_seed: str, coin_type: int, index: int) -> str:
wallet = HDWallet()
wallet.from_mnemonic(master_seed)
# BTC: coin_type=0, ETH: coin_type=60, SOL: coin_type=501
wallet.from_path(f"m/44'/{coin_type}'/0'/0/{index}")
return wallet.p2pkh_address() # for BTC
# wallet.address() for ETH
Important: address_index must monotonically increase and be stored in DB. Never reuse addresses — it violates privacy and complicates reconciliation.
Network Integration
EVM Networks (Ethereum, Polygon, BSC, Arbitrum, Base)
One codebase — multiple RPC endpoints. Monitor Transfer events from ERC-20 + native ETH/MATIC transfers.
import { createPublicClient, http, parseAbi } from 'viem';
import { mainnet, polygon, arbitrum } from 'viem/chains';
const chains = [
{ chain: mainnet, rpc: process.env.ETH_RPC, tokens: ETH_TOKENS },
{ chain: polygon, rpc: process.env.POLY_RPC, tokens: POLY_TOKENS },
{ chain: arbitrum, rpc: process.env.ARB_RPC, tokens: ARB_TOKENS },
];
// Single handler for all EVM networks
async function watchEVMPayment(client, tokenAddress, recipientAddress, orderId) {
return client.watchContractEvent({
address: tokenAddress,
abi: ERC20_ABI,
eventName: 'Transfer',
args: { to: recipientAddress },
onLogs: (logs) => handlePayment(logs, orderId),
});
}
Bitcoin
Bitcoin UTXO model — no "balance", there's a set of unspent outputs. An address is considered paid when it receives UTXO with required amount.
Integration options:
-
Electrum Protocol (ElectrumX/Fulcrum) —
blockchain.scripthash.subscribeto subscribe to address changes. Much easier than full node. - BlockCypher/Mempool.space API — without own infrastructure, but depends on external service.
- Bitcoin Core + ZMQ — full node with ZeroMQ notifications about new transactions.
For production I recommend Fulcrum (fast SPV-compatible node) + Bitcoin Core in pruned mode.
Bitcoin confirmations: 1 confirmation (~10 minutes) — for small amounts, 3+ — standard, 6 — for large payments.
TRON (USDT TRC-20)
TRON is separate case because USDT TRC-20 occupies huge share of crypto payments in certain regions. API via TronGrid (HTTP) or own node. Addresses in Base58Check format (start with T).
import tronpy
client = tronpy.Tron(network='mainnet')
def check_trc20_payment(address: str, contract: str, min_amount: int) -> list:
txns = client.get_token_trc20_transfers(
contract_address=contract,
to_address=address,
min_timestamp=int((time.time() - 3600) * 1000)
)
return [tx for tx in txns if tx['value'] >= min_amount]
Solana
Solana uses SPL Token standard. Each token has its Associated Token Account (ATA) for each wallet. Address for receiving USDC — not the wallet itself, but its ATA for USDC.
import { getAssociatedTokenAddress } from '@solana/spl-token';
const usdcMint = new PublicKey('EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v');
const paymentATA = await getAssociatedTokenAddress(usdcMint, paymentKeypair.publicKey);
// paymentATA.toBase58() — this is the address for receiving USDC on Solana
Solana finality: when using commitment level finalized — ~32 slots (~13 seconds).
Unified Monitoring System
Regardless of network, payment goes through one state:
pending_payment → mempool_detected → confirmed (N conf) → settled
PostgreSQL schema:
CREATE TABLE payment_orders (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
order_id VARCHAR(100) UNIQUE NOT NULL,
currency VARCHAR(20) NOT NULL, -- 'BTC', 'ETH', 'USDT_ERC20', 'USDT_TRC20'
network VARCHAR(20) NOT NULL, -- 'bitcoin', 'ethereum', 'tron'
payment_address VARCHAR(200) NOT NULL,
expected_amount NUMERIC(30, 8) NOT NULL,
received_amount NUMERIC(30, 8) DEFAULT 0,
tx_hash VARCHAR(200),
confirmations INTEGER DEFAULT 0,
required_confirmations INTEGER NOT NULL,
status VARCHAR(30) DEFAULT 'pending',
expires_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
INDEX idx_payment_address (payment_address),
INDEX idx_status_expires (status, expires_at)
);
Rates and Tolerance
User sees "pay 0.001523 BTC" — rate is locked for 15-30 minutes. In this time rate can change by 1-2%. You need tolerance:
TOLERANCE_PERCENT = {
'BTC': 1.0, # ±1%
'ETH': 1.5,
'USDT': 0.1, # stablecoin — small tolerance
'USDC': 0.1,
}
def is_payment_sufficient(currency: str, expected: Decimal, received: Decimal) -> bool:
tolerance = TOLERANCE_PERCENT.get(currency, 1.0) / 100
return received >= expected * (1 - tolerance)
Rates with minimal delay: CoinGecko API (free, 60 sec cache) or Binance WebSocket (real-time, for high-frequency).
Security
- Private keys never on web server. HD wallet seed in HSM or minimum in encrypted storage (AWS KMS, HashiCorp Vault).
- Sweep transactions — automatic transfer of received funds to cold wallet by schedule or amount threshold.
- Double-spend protection — for BTC and ETH don't confirm payment on first unconfirmed. Set required_confirmations appropriately to amount.
- Address validation — before saving address passes checksum validation (EIP-55 for ETH, Base58Check for BTC). Address error = lost funds.
Implementation Process
Analytics (1-2 days): which currencies are needed, volumes, geography (TRON popular in CIS/Asia), confirmation requirements.
Infrastructure (2-4 days): RPC providers or own nodes, HD wallet integration, DB schema.
Monitoring (3-4 days): workers for each network, confirmation logic, webhook notifications.
Testing (1-2 days): testnet for EVM and Solana, Bitcoin testnet, edge cases (underpayment, overpayment, expired order, reorg).
Total 1-2 weeks depending on number of networks.







