Crypto Payment Confirmation System Development
Imagine a client pays an order in USDT on Polygon, but a minute later the network reorganizes — the transaction disappears. You already shipped the order, but the money never arrived. A reliable payment confirmation system is not just a hash check; it's a finite state machine with explicit state transitions and protection against all edge cases. Our implementation uses separate monitors for each network, a tolerance window to handle amount fluctuations, and idempotency at the txHash level. For example, on Ethereum PoS we require 12 confirmations (average 12-second block time), providing reliability comparable to bank clearing but 10 times faster.
We build such systems from scratch or integrate them into existing infrastructure. We rely on the EIP-1559 and Ethereum JSON-RPC API specifications for correct transaction processing. Operational cost savings on payment processing can reach $2,000 per month. The system typically pays for itself in 3–4 months. Turnkey delivery in 2–4 weeks. Contact us to discuss your scenario.
What Problem Are We Solving?
A naive implementation: receive hash → check amount → credit. It breaks at the first reorg, double spend, or when the user sends payment an hour after session expiry. Main pain points:
- Reorg: A block is abandoned, transaction disappears. Without status rollback, you credit non-existent funds.
- Floating point: Conversion via wei introduces rounding errors; user pays 47.50 USDT, but the system sees 47.499999.
- Exchange fees: Transferred amount is 1–2% less than expected.
- Session timeouts: Payment arrives after the time limit, and the address is no longer valid.
Each of these problems is resolved within a unified finite state model.
How Does the System Protect Against Reorg?
Reorg is a chain reorganization where a previously accepted block is replaced by another. On Ethereum PoS this is unlikely (1–2 block depth), on Polygon it's more common. Our approach: on every confirmation check, we fetch a fresh transaction receipt. If the receipt disappears, the status rolls back to DETECTED, the counter resets, and the monitor begins re-searching.
async function processConfirmations(paymentId: string) { const payment = await db.findPayment(paymentId); const currentBlock = await provider.getBlockNumber(); const receipt = await provider.getTransactionReceipt(payment.txHash); if (!receipt) { await db.updatePayment(paymentId, { status: 'DETECTED', confirmations: 0, reorgDetected: true, }); return; } const confirmations = currentBlock - receipt.blockNumber + 1; const isConfirmed = confirmations >= payment.requiredConfirmations; await db.updatePayment(paymentId, { confirmations, status: isConfirmed ? 'CONFIRMED' : 'CONFIRMING', confirmedAt: isConfirmed ? new Date() : null, }); } Payments in CONFIRMING status are rechecked every N blocks — we never trust stale data.
What If the User Sends Less or More?
Due to fees and floating point, the transaction amount rarely matches the expected amount exactly. A sensible tolerance window solves this. Verification code:
function isAmountSufficient( received: bigint, expected: bigint, toleranceBps: number = 50 ): 'exact' | 'underpaid' | 'overpaid' { const tolerance = expected * BigInt(toleranceBps) / 10000n; const min = expected - tolerance; const max = expected + expected / 10n; if (received >= min && received <= max) return 'exact'; if (received < min) return 'underpaid'; return 'overpaid'; } On underpaid, the system notifies the operator; on overpaid (up to 10%), it accepts the payment and credits the surplus to the user's balance or generates a refund.
Payment State Machine
Each payment passes through strictly defined states: PENDING → DETECTED → CONFIRMING → CONFIRMED → SETTLED ↓ ↓ EXPIRED UNDERPAID / OVERPAID ↓ REFUNDED
| State | Description |
|---|---|
| PENDING | Address issued, waiting for transaction |
| DETECTED | Transaction in mempool (0 confirmations) |
| CONFIRMING | 1+ confirmations, not yet final |
| CONFIRMED | Confirmation threshold reached, amount correct |
| SETTLED | Business logic executed (order created, subscription activated) |
| EXPIRED | Timer elapsed, no transaction received |
| UNDERPAID | Transaction received but amount less than expected |
Blockchain Monitor Architecture
Monolithic monitoring of all networks in a single process is a bad idea. We use a separate worker per network with an independent retry mechanism. Implementation for EVM networks:
Basic monitor code (EVM)
interface ChainMonitor { network: string; start(): Promise<void>; stop(): void; onTransaction(handler: (tx: IncomingTransaction) => Promise<void>): void; } class EvmChainMonitor implements ChainMonitor { private provider: ethers.JsonRpcProvider; private watchedAddresses = new Set<string>(); async start() { const activePayments = await db.query( "SELECT address FROM payments WHERE status IN ('PENDING', 'DETECTING', 'CONFIRMING')" ); activePayments.rows.forEach(p => this.watchedAddresses.add(p.address)); this.provider.on('block', async (blockNumber) => { await this.processBlock(blockNumber); }); } private async processBlock(blockNumber: number) { const block = await this.provider.getBlock(blockNumber, true); for (const tx of block.transactions) { if (tx.to && this.watchedAddresses.has(tx.to.toLowerCase())) { await this.handleNativeTransfer(tx, blockNumber); } } await this.scanErc20Transfers(blockNumber); } } Confirmation Requirements for Different Networks
| Network | Recommended confirmations | Average block time |
|---|---|---|
| Ethereum (L1) | 12 | ~12 s |
| Polygon (PoS) | 64 | ~60 s |
| BNB Chain | 15 | ~3 s |
| Arbitrum | 12 | ~0.5 s |
| Base | 12 | ~2 s |
Idempotency and Duplicate Protection
One txHash must be credited exactly once. We use INSERT with ON CONFLICT DO NOTHING: if the same hash already processed, it returns an empty result.
INSERT INTO payment_transactions (payment_id, tx_hash, amount, block_number) VALUES ($1, $2, $3, $4) ON CONFLICT (tx_hash) DO NOTHING RETURNING id; Notifications and Webhooks
After transition to CONFIRMED — immediate notification to external systems via a queue (Bull/BullMQ) with exponential backoff. Direct HTTP call in the block handler would lose events on failures.
async function dispatchPaymentConfirmed(payment: Payment) { await eventBus.emit('payment.confirmed', { paymentId: payment.id, orderId: payment.orderId, amount: payment.receivedAmount, txHash: payment.txHash, }); if (payment.webhookUrl) { await webhookQueue.add('payment-webhook', { url: payment.webhookUrl, payload: { event: 'payment.confirmed', data: payment }, }, { attempts: 5, backoff: { type: 'exponential', delay: 2000 }, }); } } Process and Deliverables
- Analysis — we dissect your business requirements, number of networks, tokens, refund scenarios.
- State machine design — refine transitions, tolerance, confirmation thresholds.
- Implementation — code monitors, handlers, webhooks, integration tests.
- Testing — cover edge cases: reorg, underpaid, timeout, double-spend.
- Deployment and monitoring — deploy in your infrastructure, set up alerts.
What is included in the result:
- Source code repository with launch instructions.
- API and architecture documentation.
- Database migrations.
- Load tests and simulation scripts.
- Support for 2 weeks after launch (extended support on request).
Order the development of a system for your project — we will prepare a detailed estimate within 1 day.
Timeline and Guarantees
Typical delivery time is 2 to 4 weeks, depending on the number of networks and business logic complexity. Pricing is calculated individually, but we guarantee transparent cost breakdown. We have been working with blockchain projects for over 5 years and have implemented dozens of such systems. We guarantee stable operation under a load of up to 10,000 transactions per hour.
Get a consultation: write to us, and we will evaluate your project for free.







