Wallets
Missed deposits: why crypto casinos lose track of incoming funds
The transfer is confirmed on-chain, but the player's balance never moves. The usual causes — scanner gaps, reorgs, token edge cases — and how to design them out.
- Author
- Gamicy Engineering
- Published
- Reading time
- 4 min
A player sends funds to their deposit address. A block explorer shows the transfer as confirmed. Your casino shows nothing. The player opens a support ticket, support escalates to engineering, and someone credits the balance by hand.
That manual credit fixes one player and hides the actual problem: somewhere in your deposit pipeline there is a path where confirmed money doesn't become a ledger entry. Until it is found, it will keep happening — usually at the worst moment, such as after a deployment or during a network spike.
Where deposits go missing
Missed deposits rarely come from one dramatic bug. They usually come from a handful of quiet assumptions:
- The scanner restarts from the latest block. After downtime or a deploy, the watcher picks up at the chain head. Every block produced while it was down is silently skipped.
- Reorgs are ignored. A deposit is seen in a block that is later replaced. Depending on the code, it is either credited without ever being confirmed or dropped permanently when its block disappears.
- Only one transfer type is watched. On EVM chains, native transfers, ERC-20
Transferevents and value moved by internal contract calls appear in different places. A watcher that only reads one of them misses the others. - Address registration races. A deposit address is generated and shown to the player before it is registered with the watcher. Funds sent in that window have no owner.
- Memo and tag chains. On chains that use a shared address plus a destination tag or memo, a missing or mistyped tag leaves the deposit unattributed unless there is a process for it.
- A single RPC provider is trusted completely. Nodes lag, return partial data or briefly disagree. A watcher that treats one response as the truth inherits every inconsistency.
Design the watcher around a cursor, not a stream
The most important property of a deposit watcher is that it can be stopped at any moment and restarted without losing or duplicating anything. That means persisting progress, re-reading a safety window, and making credits idempotent.
const SAFETY_WINDOW = 12; // re-scan recent blocks to catch reorgs
async function scan(chain: Chain) {
const cursor = await db.cursors.get(chain.id); // last fully processed height
const head = await chain.getBlockNumber();
const from = Math.max(0, cursor - SAFETY_WINDOW);
for (let height = from; height <= head; height++) {
const block = await chain.getBlock(height);
for (const transfer of extractTransfers(block)) { // native + token + internal
await recordDeposit(chain.id, block, transfer); // idempotent upsert
}
await db.cursors.set(chain.id, height, block.hash);
}
}recordDeposit must be safe to call many times for the same transfer. The simplest way to guarantee that is a unique constraint on the transfer's natural identity — not an if exists check in application code, which races under concurrency.
CREATE TABLE deposits (
id BIGSERIAL PRIMARY KEY,
chain_id TEXT NOT NULL,
tx_hash TEXT NOT NULL,
log_index INTEGER NOT NULL, -- position of the transfer inside the tx
block_hash TEXT NOT NULL,
address TEXT NOT NULL,
amount_minor NUMERIC(78) NOT NULL, -- integer minor units, never floats
status TEXT NOT NULL, -- seen | confirming | credited | orphaned
UNIQUE (chain_id, tx_hash, log_index)
);Treat confirmations and reorgs as states
A deposit should move through explicit states: seen when it first appears, confirming while it waits for the chain-specific confirmation threshold, and credited once the ledger entry is written. Showing a pending deposit to the player is fine; crediting playable balance before the threshold is a business decision that should be made deliberately, per chain.
Reorg handling falls out of the same model. Because the watcher stores the block hash for every deposit and re-scans a safety window, it can detect when a stored block hash no longer matches the canonical chain at that height. Deposits in the replaced block move to orphaned if they don't reappear in the new canonical block. Since nothing is credited before the threshold, an orphaned deposit never touches the ledger.
Reconciliation is the safety net
Even a well-designed watcher can be broken by a future change. A scheduled reconciliation job is what tells you about it before players do. Per chain and asset, compare what the ledger says should have arrived against what the chain says did arrive, and alert on any difference.
- Sum of credited deposits per address versus on-chain inflows to that address.
- Deposits stuck in seen or confirming for longer than the chain's normal confirmation time.
- Inflows to known deposit addresses with no matching deposit row at all.
If you want a second pair of eyes on your deposit pipeline, our platform audit covers exactly these failure modes. For new builds, see how we approach wallets and crypto payments.
Related services
- Wallets and crypto payments
Multi-chain deposits, withdrawals, hot/cold wallet architecture and a double-entry ledger that reconciles.
- Platform audit
A fixed-scope technical review of your wallet, payment and integration code — the lowest-risk way to start working with us.