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.







