PAY-007
Wait for confirmations before you call a deposit final
Zero-confirmation deposits can be reversed. A confirmations table with a per-coin threshold makes the wait explicit and the crediting safe.
A transaction in the mempool is a request, not a payment. It can be replaced, dropped, or orphaned by a chain reorganisation. Crediting on sight is fine for a five cent faucet deposit and reckless for anything larger.
The pattern is two states. Seen means recorded with zero confirmations and shown to the user as pending. Credited happens when the confirmation count crosses the threshold for that coin, and only once.
Thresholds are per coin because block times differ. Six blocks on Bitcoin is an hour; six on a fast chain is a minute.
function poll_pending_deposits(PDO $pdo, array $rpc): int
{
$thresholds = [
'BTC' => 2,
'LTC' => 4,
'DOGE' => 6,
'BCH' => 4,
];
$rows = $pdo->query(
'SELECT * FROM deposits WHERE status = "seen" AND created_at > DATE_SUB(NOW(), INTERVAL 30 DAY)'
)->fetchAll(PDO::FETCH_ASSOC);
$credited = 0;
foreach ($rows as $d) {
$tx = rpc($rpc[$d['currency']], 'gettransaction', [$d['txid']]);
$confirmations = (int) ($tx['confirmations'] ?? 0);
if ($confirmations < 0) {
// Negative means the transaction was orphaned by a reorg.
$pdo->prepare('UPDATE deposits SET status = "orphaned" WHERE id = ?')->execute([$d['id']]);
continue;
}
$pdo->prepare('UPDATE deposits SET confirmations = ? WHERE id = ?')
->execute([$confirmations, $d['id']]);
if ($confirmations >= ($thresholds[$d['currency']] ?? 6)) {
// Idempotent: the unique reference means a second pass credits nothing.
credit_deposit($pdo, (string) $d['ref'], (int) $d['units'], (string) $d['txid']);
$credited++;
}
}
return $credited;
}
Using it
Show the confirmation count to the user. "2 of 4 confirmations" prevents almost every support message a pending deposit would otherwise generate.
Run the poll from cron with a lock, and stop checking after a month — anything still unconfirmed by then is not coming.
What bites people
A negative confirmation count means the transaction was orphaned. It is not an error value and must not be treated as zero.
Raise the threshold with the amount. A large deposit deserves more waiting than a small one, and one fixed number cannot serve both.
Never credit twice after a reorg puts a transaction back. Route everything through the same idempotent crediting path.
This one touches real money. Point it at a throwaway wallet and watch a full cycle before you trust it with a live balance.