Multi-Crypto Betting Setup for Casino
Accepting bets in BTC, ETH, USDT, BNB, SOL and twenty other tokens is technically non-trivial. Each network has different address models, finality, fees and speed. Without proper design, you'll either freeze funds (deposit not credited due to verification failure) or create accounting holes (USDT on Ethereum vs USDT on TRON — different tokens, different networks, but one database row).
Multi-currency betting architecture
HD wallets and unique addresses
Each user needs unique address per network — otherwise can't auto-match incoming payment with account. Standard approach: HD wallet (BIP-32/BIP-44) with address derivation.
Master seed → derivation by path m/44'/coin_type'/account'/0/index:
import { HDKey } from '@scure/bip32'
import { mnemonicToSeedSync } from '@scure/bip39'
const masterSeed = mnemonicToSeedSync(process.env.MASTER_MNEMONIC!)
const masterKey = HDKey.fromMasterSeed(masterSeed)
function deriveAddress(coinType: number, userId: number): string {
// BIP-44: m/44'/coinType'/0'/0/userId
const child = masterKey.derive(`m/44'/${coinType}'/0'/0/${userId}`)
// For EVM networks coinType=60, Bitcoin=0, Solana=501
return toChecksumAddress(child.publicKey)
}
For EVM-compatible networks (Ethereum, BNB Chain, Polygon, Arbitrum) — one address works in all, but DIFFERENT balances. Don't mix: user may send ETH to his BSC address — coins will be on different network and never credit.
Monitoring incoming transactions
Two approaches:
Webhook-based via payment provider (NOWPayments, CoinsPaid, Binance Pay API). Provider monitors addresses, sends webhook on receipt. Quick to integrate, but: provider has access to your funds, higher fees, less control.
Self-hosted monitoring — own service listens to nodes and indexes incoming transactions. Full control, lower costs at volume, but: infrastructure responsibility.
// Monitor EVM addresses via eth_getLogs + transfer topic
const TRANSFER_TOPIC = '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef'
async function monitorIncomingTransfers(
addresses: string[],
fromBlock: number
) {
const logs = await client.getLogs({
fromBlock: BigInt(fromBlock),
toBlock: 'latest',
topics: [
TRANSFER_TOPIC,
null, // from: any
addresses.map(padAddress), // to: our addresses
],
})
for (const log of logs) {
const token = log.address // ERC-20 contract
const to = unpadAddress(log.topics[2])
const amount = BigInt(log.data)
await processDeposit({ token, to, amount, txHash: log.transactionHash, blockNumber: log.blockNumber })
}
}
Finality by network: How many confirmations to wait
Main mistake — credit stake after 1 confirmation on all networks. Reorgs happen, stake will be credited without real funds.
| Network | Recommended confirmations | Approx time |
|---|---|---|
| Bitcoin | 3-6 | 30-60 min |
| Ethereum | 12-15 (PoS safe) | 3-4 min |
| BNB Chain | 15-20 | 60-90 sec |
| Polygon | 256 (checkpoint on ETH) | ~10 min |
| Solana | 32 (finalized) | ~15 sec |
| TRON | 19 (SR solid) | ~60 sec |
| Arbitrum | 1 (L2 finality) | sec (soft), 7 days (challenge) |
Reasonable tradeoff for betting: Ethereum — 12 confirmations, fast L1s (BSC, TRON) — 20 confirmations, Solana — finalized status.
Multi-currency balance accounting
Database must distinguish not just "token", but "token + network":
CREATE TABLE user_balances (
user_id BIGINT NOT NULL,
network VARCHAR(50) NOT NULL, -- 'ethereum', 'bsc', 'tron'
token_address VARCHAR(100), -- NULL for native token
token_symbol VARCHAR(20) NOT NULL,
raw_amount NUMERIC(78, 0) NOT NULL, -- wei/lamports, no decimals
decimals SMALLINT NOT NULL,
PRIMARY KEY (user_id, network, COALESCE(token_address, 'native'))
);
-- USDT on different networks — DIFFERENT rows
-- user_id=1, network='ethereum', token='0xdAC17F...', symbol='USDT'
-- user_id=1, network='tron', token='TR7NHqjeKQ...', symbol='USDT'
Never add raw_amount of tokens with different decimals without normalization. USDT = 6 decimals, most ERC-20 = 18 decimals.
Converting to single accounting currency
Casino needs to calculate GGR (Gross Gaming Revenue) in single currency (usually USD or EUR). For this need price feed:
// Fix USD value at bet time
async function recordBet(userId: number, currency: string, network: string, rawAmount: bigint, decimals: number) {
const humanAmount = Number(rawAmount) / Math.pow(10, decimals)
const usdPrice = await priceOracle.getPrice(currency) // Chainlink, CoinGecko, Binance
const usdValue = humanAmount * usdPrice
await db.query(`
INSERT INTO bets (user_id, currency, network, raw_amount, decimals, usd_value_at_time, placed_at)
VALUES ($1, $2, $3, $4, $5, $6, NOW())
`, [userId, currency, network, rawAmount.toString(), decimals, usdValue])
}
Fix USD value at bet time — otherwise PnL reports depend on current price, making financial reporting unpredictable.
Security: Hot and cold wallets
Don't hold all user funds on hot wallet. Standard scheme:
- Hot wallet — 5-10% of daily turnover. Instant payouts.
- Cold/warm wallet (multisig) — rest. Sweep happens on schedule.
Auto sweep: once balance exceeds threshold, automatically transfer to hot wallet aggregation address. Otherwise funds scattered across thousands of addresses, unmanageable.
Providers vs self-hosted
| Parameter | Provider (NOWPayments, CoinsPaid) | Self-hosted |
|---|---|---|
| Integration time | 1-3 days | 2-4 weeks |
| Commission | 0.5-1% of turnover | Only gas |
| Private key control | With provider | With you |
| Multi-chain | 30-200+ coins out-of-box | As much as you implement |
| Confirmation customization | Limited | Full |
For small casino (< $100k daily turnover) — provider justified. As volume grows, provider commission becomes significant, self-hosted pays for itself in months.







