Your bank checks card payments for fraud before they go through. This does the same job for stablecoin transfers.
When your card is declined on a strange purchase, your bank is not consulting a register of bad shops. It asks whether this purchase looks like you: wrong city, wrong hour, far bigger than usual, right after a run of small test charges. The shop is often perfectly legitimate and the payment is stopped anyway.
Most crypto risk tools work the other way. They ask whether the receiving address is on a list of known-bad addresses. That answer comes too late, and here is why.
Start with the obvious signals. A scam collection point and a busy legitimate address, such as a shared exchange deposit address or a payment processor, are not distinguishable on any of them.
Worse than unhelpful, two of them point the wrong way. Taking the 3,186 indexed addresses that share this shape and splitting them by what the model says:
So it is not age, and it is certainly not "money left quickly". The strongest single signal here is how much each wallet sends before it stops paying, which is the difference between somebody moving their savings and somebody topping up an account. Those groups are 91% and 10% later frozen respectively, so the split is real and not an artefact of the model's own opinion.
No row decides it on its own. The model gets payer counts at one day, seven days, thirty days and ever, and dollar totals at seven and thirty days, so it sees the rate and whether the rate is changing rather than a lifetime count. It gets two quantiles of holding time rather than one, so an address that sweeps some deposits instantly and lets others sit looks different from one that treats everything the same. Nineteen numbers, 400 trees, and the interactions between them are the model.
This is also why a hand-written rule fails. "Warn when an address forwards everything it receives" fires on three quarters of ordinary addresses, because a wallet that gets paid and then spends has forwarded everything it received.
Everything above is read from one address's own history. The last signal is not. It asks where the money goes next, and then who else sends money there.
The useful part is which way round it goes. A wallet that almost nobody else pays is the warning; one that everybody pays is an exchange, and your money is going somewhere ordinary. We had this backwards in our own code for a long time, on the reasonable-sounding theory that scam addresses sweep into shared wallets. Measured, scam destinations forward to wallets with a median of 4 other payers, and ordinary ones to wallets with 71.
It is worth +2.5 points of recall at the same false-alarm rate, and it is the one signal here that a blocklist cannot have in principle: nothing about it depends on anyone having reported anything. It costs one more read of the chain, which is why the check takes a moment longer than a lookup would.
The bands are not round numbers. "High" is set where only the top 1% of ordinary wallets land. A stop means the address behaves unlike 99 out of 100 of the wallets people pay every day.
Measured on 4,337 held-out transfers, each to an address the model had never seen, each scored from only the history that existed at that minute.
Every one of those cases is listed individually, misses and false alarms included, under previous checks.
Timing matters more than the rate, and it costs something. Tested on 169 addresses frozen after the training data was cut, which the model had never encountered in any form, and holding the population fixed so the comparison is the same addresses at every distance:
| Checked this long before the freeze | stopped | any warning |
|---|---|---|
| 1 day | 36.2% | 83.8% |
| 1 week | 32.5% | 83.8% |
| 1 month | 28.7% | 77.5% |
| 3 months | 22.5% | 73.8% |
Three months out the check still warns on nearly three quarters of them, which is three months before any list has anything to say. The stop rate does fade, from 36% to 23%, and we said otherwise here until 2026-10-08: the earlier numbers looked flat because only the older addresses can be scored that far back, so each row was a different and easier population. Held fixed, it decays. Eighty addresses survive to every row, so read all of this as approximate.
Not directly, and the reason is worth more than the number. A catch rate does not depend on how common fraud is. How often a warning is right depends on nothing else.
Feed that through, and the same model looks like this:
So on a random payment most warnings would be false. That is not a defect being confessed, it is what any screening test does against a rare event, and it is why this is a page you choose to visit rather than a filter running over everyone's payments. Nobody checks an address at random. If you are here because something already felt wrong, your own rate is nearer the bottom of that table than the top, and the check is worth far more to you than to a random passer-by.
Three checks, because the question deserves more than an assurance.
What none of that fixes: our labels are addresses Tether froze, and Tether freezes on law-enforcement request. That selects for large, reported fraud. A scam too small for anyone to report never enters the training set or the measurement, so the catch rate is conditional on the fraud being the kind that eventually gets noticed. There is no clever split that repairs this. It is a property of the only labels that exist.
Tether's USDT contract emits an AddedBlackList event on-chain when it freezes an address. Those events are public, timestamped by the block they sit in, and produced by somebody other than us. They are the answer key.
We never consult that list when you check an address. By the time a freeze happens the money has gone. The list can tell us afterwards whether we were right. It cannot tell you now whether to send.
Every number is computed only from transfers that existed before the moment being scored. A model that has seen an address's later behaviour can look like it predicted a freeze it was actually told about. The backtest then scores well and the live system does not. Most of the work goes into enforcing that rule.
The trees ship to the page as JSON and are walked in JavaScript, so the scoring happens on your machine. The address you type goes to a public block explorer, because that is where transaction history lives. It does not come to us. There is no account and no login.
We do count that a check happened: which of the three verdicts it returned, which chain, whether a wallet was connected, and a random id kept in your browser so that four hundred checks by one person are not mistaken for four hundred people. Not the address, not the amount, not you. That is the whole payload, and the code that receives it is in the repository, so it is checkable rather than promised.
The same trees are run in Python for everything measured here. Every build checks the two against each other: same verdicts, and scores matching to 2.2e-16, the smallest gap a computer can represent. If the browser scored differently from the version we test, the numbers on this page would not describe what you actually get.
Each night the system samples addresses receiving USDT right now, drops any already on a list, scores the rest, and writes down the ones it calls high-risk, along with the score, the features behind it, and the model file's checksum.
The entries are hash-chained, so changing an earlier one breaks every entry after it, and a scheduled job on GitHub commits them with timestamps we do not control.
Only time can show whether those calls were right.
Every figure here is regenerated from the code that produces it, and every address can be checked on Tronscan or Etherscan. You can also look up any address on its own, with the full reasoning and the prediction record behind it.