Detecting fake USDT deposits in code: a developer's guide
Do not inspect tokens and compare symbol() to 'USDT' — that asks the attacker to self-certify. Subscribe to Transfer events emitted by one hardcoded contract address, credit only after a confirmation depth with a receipt re-check for reorgs, and enforce idempotency with a database unique constraint on (txHash, logIndex).
Human verification fails eventually — it is repetitive, and scammers are patient. Code does not get tired. Here is how to build a deposit watcher that counterfeit tokens are structurally invisible to, and the seven mistakes that make one exploitable.
The wrong way
A check like *“read symbol() from the token, and if it equals USDT then credit the user”* accepts every counterfeit ever deployed. symbol(), name() and decimals() all return values the deployer chose, and deploying a contract that returns "USDT" costs about two dollars — the mechanism is here.
The right way: filter at the log level
Do not scan transactions and inspect tokens. Subscribe to Transfer events emitted by one specific contract address. A counterfeit contract emits its own logs, from its own address, which your filter never matches. You are not detecting fakes — you are structurally incapable of seeing them.
- Hardcode the contract address and decimals as constants. Not in editable config, not from an environment variable someone can typo — wrong here means crediting counterfeits.
- Build the filter from the contract instance (
usdt.filters.Transfer()) and query a block range, rather than scanning all transactions. - Assert
log.addressmatches your constant anyway, as defence in depth, even though the filter already guarantees it. - Skip any log whose
tois not one of your deposit addresses, and any whosevalueis zero — zero-value transfers are address-poisoning noise. - Record
(transactionHash, logIndex)as the unique key for the deposit. Never the transaction hash alone: one transaction can carry several transfers. - Format the amount with
formatUnits(value, DECIMALS)using your per-chain constant, nevertx.value— which is always zero for token transfers.
Confirmations and reorgs
Seeing a log is not settlement. Blocks get reorganised, and a transfer that existed at height *N* can be absent at *N+2*. Record on sight, credit on depth. When the depth threshold is reached, re-fetch the receipt: if it is missing, or its status is not 1, or the block hash no longer matches what you stored, the deposit never happened — mark it orphaned rather than crediting it.
| Chain | Suggested depth | Approx. wait |
|---|---|---|
| BNB Chain | 15 | ~45 s |
| Ethereum | 12 | ~2.5 min |
| Tron | 19 | ~1 min |
| Polygon | 128 | ~4 min (deeper reorgs observed) |
Idempotency — the bug that costs more than fake tokens
Counterfeit tokens cost you one bad trade. A double-credit bug costs you your float. Restarts, retries, overlapping scan ranges and concurrent workers all replay the same log.
Enforce uniqueness in the database, not in application logic. A UNIQUE (tx_hash, log_index) constraint plus INSERT … ON CONFLICT DO NOTHING RETURNING id makes a replay a no-op. An application-level “check, then insert” races under concurrency; a unique constraint does not. Write the deposit row and the balance update inside one transaction, and only apply the balance change when the insert actually returned a row.
Seven mistakes that make a watcher exploitable
| # | Mistake | What it lets through | Fix |
|---|---|---|---|
| 1 | Matching on symbol() / name() | Every counterfeit token | Match the contract address only |
| 2 | Contract address in editable config | One typo or insider = total loss | Hardcode; assert at boot |
| 3 | Crediting on pending | Dropped and replaced transactions | Confirmation depth + receipt re-check |
| 4 | Assuming 18 decimals everywhere | 10¹² over-credit on a 6-decimal chain | Per-chain constant, asserted at boot |
| 5 | No unique constraint on (hash, logIndex) | Double credits on any replay | DB-level unique index |
| 6 | Trusting tx.value for token transfers | Always 0 — silent mis-crediting | Read the Transfer log args |
| 7 | Ignoring zero-value transfers | Address-poisoning noise in user history | Skip value == 0 explicitly |
Operational hardening
- Two independent RPC providers. Cross-check confirmations before crediting large amounts. A single compromised or lagging node should never authorise a credit alone.
- Persist the last scanned block, and overlap. Re-scan the last ~50 blocks every pass. Idempotency makes overlap free; gaps make it necessary.
- Alert on unknown-token transfers to deposit addresses. You will not credit them, but a spike is an early warning that your users are being targeted.
- Cap auto-credit. Above a threshold, route to manual review — cheap insurance against a bug class you have not thought of yet.
- Never expose a “manual credit by hash” endpoint without a second approval. Social engineering targets the admin panel once the code is solid.
- Log the emitting contract address on every processed event. If you are ever wrong, you will want to know exactly what you accepted.
The principle
Everything above reduces to one sentence: identity is the contract address, and nothing else is identity. Names, symbols, logos, screenshots and user claims are all attacker-controlled. Build your system so a counterfeit token is not *rejected* but *invisible*, and you close the entire category permanently — including variants nobody has written yet.
Everything in this guide is production practice on FastXP2P, not a thought experiment — contract-address allowlisting, confirmation depth, reorg re-checks, decimal assertions and idempotent crediting on every deposit.
Trade USDT safelyFrequently asked questions
Why not just check the token symbol?
Because symbol() returns a string the contract deployer chose. A counterfeit token returns 'USDT' exactly as convincingly as the real one. Only the contract address is an identity the attacker cannot forge.
How many confirmations should I wait for?
As a starting point: 15 on BNB Chain, 12 on Ethereum, 19 on Tron and 128 on Polygon. Re-fetch the receipt at that depth and confirm the block hash still matches what you recorded, to catch reorgs.
References
Primary sources for the rules and mechanics described above. Rules change — check the original before you act on anything here.
- 1National Cyber Crime Reporting PortalMinistry of Home Affairs, Government of India
- 2How Tether worksTether
- 3Tether reserves and transparency reportsTether
- 4Sanchar Saathi — report fraud communicationsDepartment of Telecommunications, Government of India
- engineering
- web3
- deposits
- ethers.js