Development of Cryptocurrency Payment Widget
Typical requirement: "we want to accept crypto on the website, like PayPal but for USDT". In practice, this means solving several non-trivial tasks simultaneously: generating unique addresses for each payment, detecting incoming transactions, handling different networks and tokens, correctly working with confirmations and reorgs. Ready-made solutions like Coinbase Commerce or NOWPayments charge 0.5–1% fees and have limited customization. A custom widget is justified when volume is several thousand dollars per month or when you need deep integration with business logic.
Architecture: What's Inside the Widget
The widget is just the UI part. The real work happens on the backend:
Frontend Widget
│ create order / show address and QR
▼
Backend API
│ address generation → database record → polling/webhook
▼
Blockchain Monitoring Service
│ tracks transactions on addresses
▼
Payment Processor
│ confirmation → callback to application
Address Generation: HD Wallet
Each payment needs a unique address — otherwise it's impossible to match an incoming payment to a specific order. The standard approach is BIP-44 HD Wallet:
import { ethers } from 'ethers';
const masterWallet = ethers.HDNodeWallet.fromMnemonic(
ethers.Mnemonic.fromPhrase(process.env.PAYMENT_MNEMONIC!)
);
function derivePaymentAddress(orderId: number): string {
// path: m/44'/60'/0'/0/{orderId}
const child = masterWallet.derivePath(`m/44'/60'/0'/0/${orderId}`);
return child.address;
}
The mnemonic is stored in HSM or Vault, private keys are never materialized on the server in plain text. For multi-currency: different coin types per BIP-44 (60 for Ethereum/EVM, 0 for Bitcoin, 195 for Tron).
For EVM networks with identical addresses (Ethereum, BNB Chain, Polygon, Arbitrum), one address works on all networks — but you need to monitor each network separately.
Transaction Monitoring
Two approaches: polling RPC and webhook subscriptions.
Polling — simpler, but creates load:
async function pollAddress(address: string, network: string) {
const provider = getProvider(network);
// For ERC-20 tokens we listen to Transfer events
const usdtContract = new ethers.Contract(USDT_ADDRESS, ERC20_ABI, provider);
const filter = usdtContract.filters.Transfer(null, address);
const latestBlock = await provider.getBlockNumber();
// Check last N blocks
const events = await usdtContract.queryFilter(filter, latestBlock - 10, latestBlock);
for (const event of events) {
await processIncomingPayment({
txHash: event.transactionHash,
amount: event.args.value,
token: 'USDT',
network,
});
}
}
Webhooks — via Alchemy, QuickNode, or Moralis. Subscribe to address events, receive push notifications on each transaction. More reliable than polling, but depends on the provider:
// Alchemy Notify webhook
const webhook = await alchemy.notify.createWebhook(
'https://your-api.com/webhook/payment',
WebhookType.ADDRESS_ACTIVITY,
{ addresses: [paymentAddress] }
);
Confirmations and Double-Spend Protection
Different assets require different numbers of confirmations:
| Asset/Network | Recommended Confirmations | Time |
|---|---|---|
| ETH / ERC-20 (Ethereum) | 12–20 blocks | ~3–4 min |
| BNB Chain | 15–20 blocks | ~1 min |
| Polygon | 100–150 blocks | ~4–6 min |
| TRON TRC-20 | 20 blocks | ~1 min |
| Bitcoin | 3–6 blocks | ~30–60 min |
Polygon requires more confirmations due to higher probability of reorg. Don't count payment as final until reaching the required threshold.
For stablecoins: additionally verify that the token contract is official. A user may send a fake token named "USDT". Whitelisting contract addresses is mandatory.
Widget UI/UX Components
Minimal set for conversion:
┌─────────────────────────────────────┐
│ Pay: 47.50 USDT │
│ │
│ Network: [Ethereum ▼] [BNB Chain ▼]│
│ │
│ [QR-code] 0x7f3a...b2c4 │
│ [Copy] │
│ │
│ ⏱ Waiting for payment: 14:32 │
│ ● Expecting transaction... │
└─────────────────────────────────────┘
Critical: session timer (usually 15–30 minutes), after which the address is released and the rate is recalculated. Status updates via WebSocket or SSE — polling every 5 seconds is annoying and creates load.
Fiat-to-crypto conversion: use Chainlink Price Feeds or CoinGecko API with caching. Add 1–2% buffer to the rate accounting for volatility during waiting time.
Handling Underpayment and Overpayment
Real users often pay inexact amounts:
- Underpayment (sent less): either block the order until additional payment, or accept with "partial payment" note — depends on business logic
- Overpayment (sent more): credit the user's account, or return the difference automatically
- Network fee: for native coins (ETH, BNB) the user must have it in the wallet separately — a UX moment that's important to explain
Security
- Mnemonic in Vault (HashiCorp Vault or AWS Secrets Manager), never in
.env - Rate limiting on address generation endpoint
- Idempotency: same
orderIdalways returns the same address - Audit log of all transactions with confirmations
- Periodic reconciliation: sum of confirmed payments in DB should match on-chain balances







