Mule Detection on Realtime TRON Streams
Live demo: https://muledetection.cryptogrammar.xyz/

A money mule wallet on TRON does not look like a wallet, it looks like a funnel. Funds drip in from many sources. They sit for a few minutes or hours. They then split outward into a fresh ring of addresses. By the time a human investigator opens a case file, the funds are already three hops downstream and the hub wallet is empty.
The only way to catch that pattern in time is to be already watching the chain when it happens. That means consuming a realtime stream of transfers. It means running structural detection on the live window, not on a CSV pulled the next morning.
The detection logic in this article is data-source agnostic. The pattern we look for is aggregate, pause, fan out. Any pipeline that emits the tuple (time, sender, receiver, amount, amount_usd) fast enough is enough to run it. For this POC, that pipeline is Bitquery's GraphQL streaming API.
For background, the same gather-scatter logic shows up in our TRON USDT/USDD mule offramp post. It also appears in the Tornado Cash demixer. Once you can see the transfers in time order, the rest is graph arithmetic.
What "realtime" actually means for mule detection
Before we list sources, it is worth pinning the latency budget. A mule cycle on TRON typically completes inside a few hours. The detector only needs to see the inbound legs and the outbound legs in the same observation window for the structural pattern to fire. So the practical bands are as follows.
Sub-block latency through mempool feeds is overkill for this problem. It is useful for MEV and frontrunning, not for mule detection.
One to ten seconds behind tip is ideal. The block lands, transfers are parsed, and the detector runs on a sliding window. Almost every realtime onchain provider operates here.
Minutes behind tip still works. Mule cycles are slow enough that a five minute refresh catches the gather-scatter pattern before the funds have moved out of reach. That is exactly what the deployed UI uses for Recent Transfers mode.
Hours or next-day batch is too slow. By then the hub wallet is drained and the pattern is a forensic exercise, not a detection.
The detector in mathematical form
Let T be the set of transfers observed in the current window. Each transfer is a tuple (time, sender, receiver, amount, amount_usd). For a candidate hub address h, define the inbound and outbound transfer subsets, the inbound senders, and the outbound receivers:
T_in(h) = transfers in T whose receiver is h
T_out(h) = transfers in T whose sender is h
S_in(h) = unique senders appearing in T_in(h)
R_out(h) = unique receivers appearing in T_out(h)
Fan-in fires for receiver h when both a count threshold and a USD threshold are met:
|S_in(h)| >= N_in (default N_in = 4)
sum of amount_usd over T_in(h) >= U_in (default U_in = $10,000)
Fan-out fires for sender h symmetrically:
|R_out(h)| >= N_out (default N_out = 4)
sum of amount_usd over T_out(h) >= U_out (default U_out = $10,000)
An address h is a structural intermediary, i.e. a gather-scatter hub, when both fan-in and fan-out fire on the same address. The mule candidate set is then the structural intermediaries minus a configured exchange-wallet set E:
M = (intermediaries) \ E
The reason for E is that Binance, OKX, and MEXC TRON hot wallets all match the structural pattern by definition, but they are not mules.

The causal gate is the part that distinguishes a mule from a market maker. For each hub h, sort T_in(h) and T_out(h) by time. Let F_in(h, t) and F_out(h, t) be the cumulative inbound and outbound USD up to time t, and let U_in(h) and U_out(h) be the respective totals. Pick mass fractions alpha, beta in (0, 1], defaulting to 0.5. Define the time at which each side crosses its mass fraction:
t_in(h, alpha) = earliest time at which F_in(h, t) >= alpha * U_in(h)
t_out(h, beta) = earliest time at which F_out(h, t) >= beta * U_out(h)
The hub passes the causal gate when accumulation strictly precedes dispersal:
t_in(h, alpha) < t_out(h, beta)
A market maker fails this check, because their inbound and outbound flows interleave continuously and the two crossing times collapse onto each other. A mule passes it cleanly, because the inbound mass lands before the outbound legs begin.

The mule score score(h) is then a small linear sum, capped at 99:
score(h) = min( 99,
2 if h is in M and passes the causal gate, else 0
+ 1 if there is a fan-in row centred on h
+ 1 if there is a fan-out row centred on h
+ 1 if |unique inbound sources| > 10
+ 1 if |unique outbound sinks| > 10
)
The two unique-count terms are not causal-gated. The candidate weight and the row weights are.
A useful auxiliary number is the passthrough ratio:
rho(h) = U_out(h) / U_in(h)
Values close to one indicate pass-through behaviour, which is exactly what a mule looks like in steady state. Values far above one indicate the wallet is a redistribution endpoint. Values far below one indicate accumulation that has not yet been dispersed.
None of this math cares which provider gave you the rows. That is the point.
Why structural detection is the right primitive on a stream
A lot of AML tooling is address-list driven. You have a sanctions list. You check every transfer against it. You alert when a known-bad address shows up. That works for OFAC compliance. It does not work for mule detection.
The mule wallet is fresh. It has no history. It is not on any list. The funding wallets are fresh too. They are often funded ten minutes earlier from an exchange withdrawal. The only thing that is stable about a mule operation is the shape of the transfer graph. The shape is many-to-one, then one-to-many. The inbound mass arrives before the outbound mass leaves.
That shape is what the realtime detector watches for. It does not need to know who any of the addresses are. It only needs the stream.
For the same reason, the causal gate matters more than any single threshold. A market maker also receives from many counterparties and pays out to many counterparties. Their inbound and outbound flows are interleaved continuously. A mule wallet has a clear accumulate, then disperse shape on the timeline. The causal check, t_in(h, alpha) < t_out(h, beta), is what filters one from the other on a live feed.
Try it
The deployed tool runs in two modes.
Seed-wallet mode takes a TRON address as input. The detector does a breadth-first search up to a configured depth around that wallet. It returns the structural risk for that wallet's neighbourhood.
Recent transfers mode takes no input. The latest N transfers from the network are pulled. The detector runs on the whole window. Any gather-scatter hub above threshold gets surfaced. This is the "what is happening on TRON right now" view.

Both modes produce the same JSON intelligence package. That includes risk_summary, flow nodes and edges, transfers_with_time, intermediary_addresses, and mule_candidate_addresses.
I posted the original walkthrough and screenshots on LinkedIn. Feedback from compliance and AML folks there is what shaped the causal-gate addition. If you are working on AMLA or MiCA-aligned monitoring, or running an investigation team, that thread is the right place to push back on the heuristics.
For a worked example of a wallet flagged by this exact tool, see the TRON USDT/USDD mule offramp case study.