Veridis.

How the check works

Your bank checks card payments for fraud before they go through. This does the same job for stablecoin transfers.

A fraud check, not a blocklist

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.

  1. Day 0 The address is created. It has no history, and there is nothing yet for anyone to see.
  2. Week 3 or so Strangers start paying it, each one once, and the money leaves within minutes. There is already enough history here to tell what it is. This is where the check runs. We can answer here
  3. Soon after The money is swept onward and gone. No one can reverse a stablecoin transfer: not us, not the issuer, not the chain.
  4. Months later Tether freezes the address, usually at law-enforcement request, and it lands on every list at once. In our held-out cases the typical gap was 195 days. A blocklist answers here

Why it is hard

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.

Collection point Busy legitimate address 
Hundreds of unrelated payers, almost none returningThe same, and completely innocent same
Forwards nearly everything it receivesThe same: nothing is meant to sit here same
Money moves on within minutes or hoursThe same: deposits are swept on a schedule same
Outflow goes to a small number of addressesThe same: one hot wallet, every time same

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:

Flagged high Not flagged 
Held 2.4 hours before forwarding Held 30 minutes. The legitimate ones are faster inverts
20% of outflow to its biggest payee 32%. The legitimate ones sweep more tightly inverts
$75,800 per paying wallet $7,759. Ten times less separates
$13.4m received in total $837k separates
569 days old 216 days. A difference, but a weak one separates

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.

One step further than the address

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.

Collection point Busy legitimate address 
Forwards to a wallet fed by a handful of addressesForwards to an exchange, fed by thousandsseparates

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.

What happens when you press Send

  1. You give an address and an amount. nothing is sent, and nothing is signed
  2. Your browser reads that address's whole transfer history from a public block explorer. often more than 1,000 transfers, a few seconds
  3. It turns that history into 17 numbers: how many strangers pay it, how fast money leaves, how much it keeps, how many places it pays out to, how old it is. 37 numbers when a wallet is connected, adding your relationship to it and the amount
  4. It follows the money one more step: it finds the wallet this address pays most, reads that wallet's history too, and counts how many other wallets pay it. the only part of the check that needs a second address
  5. 400 decision trees vote on those numbers, and the score lands in one of three bands. the trees run on your machine, not ours

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.

How well it does

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 stoppedany warning
1 day36.2%83.8%
1 week32.5%83.8%
1 month28.7%77.5%
3 months22.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.

Can you extrapolate that to yourself?

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.

Is it just memorising the frauds it was shown?

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.

Where the right answers come from

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.

It runs in your browser

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.

Published before the fact

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.

What it does not do

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.