> For the complete documentation index, see [llms.txt](https://docs.intelligenceonchain.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.intelligenceonchain.com/technical-module-guidance/tornado-cash-withdrawal-viewer.md).

# Tornado.Cash Withdrawal Viewer

*How these two modules work, what they can tell you, and what they can't.*

***

## Overview

These are two modules that are designed to be used in sequence.

**The Withdrawal Viewer** answers a population question: *which addresses came out of the Tornado Cash ETH pools?* Give it a set of pools and a date range (or a single address) and it returns every recipient wallet, how many withdrawals each one took, from which pools, for how much, and over what period.

**The Cluster Analyser** answers a relationship question: *which of those recipient addresses are probably controlled by the same person?* Give it the list of recipients from the first module and it maps the connections between them, filters out the connections that are artefacts rather than evidence, and groups what remains into confidence-scored clusters.

The first module is enumeration. The second is inference. The distinction matters a great deal, and the rest of this document keeps coming back to it.

<figure><img src="/files/tJ7V8VItHtvlAqvlpn51" alt=""><figcaption></figcaption></figure>

***

## Background: why withdrawals are enumerable at all

Tornado Cash breaks the link between a deposit and a withdrawal cryptographically. When you deposit, you receive a secret note; when you withdraw, you prove with a zero-knowledge proof that you hold *a* valid note without revealing *which* one. There is no on-chain record connecting your withdrawal back to your deposit, and no amount of chain analysis recovers it. That property is real and this tool does not defeat it.

What the protocol does not hide is the withdrawals themselves. Every payout is an ordinary value transfer out of a pool contract, visible to anyone. Three design details make those payouts easy to identify and characterise:

* **Fixed denominations.** The ETH pools are separate contracts, each paying out a single fixed amount (0.1, 1, 10 and 100 ETH). A wallet's mix of denominations is itself a weak fingerprint.
* **Relayers.** Withdrawing to a fresh wallet requires gas, and paying that gas from a funded wallet would defeat the point. Most users therefore pay a relayer to submit the transaction, taking their fee from the withdrawal. So the amount that lands is slightly *under* the denomination.
* **Public payout legs.** The recipient transfer and the relayer fee transfer both appear on chain as internal transfers originating from the pool contract.

So: enumerating who withdrew is straightforward. Determining which withdrawal corresponds to which deposit is impossible. Everything these modules do lives in that gap.

***

## Module 1 — Withdrawal Viewer

### The two modes

**Pools + date range.** Select any combination of the four ETH pools and a start and end date. The module scans those pool contracts over that window and aggregates every withdrawal by recipient wallet.

**Single address.** Enter one wallet and the pool and date controls disappear. The module scans that wallet's full inbound history against all pools and returns a single row in the identical format. Useful when you already have a suspect address and want to know its Tornado exposure without guessing at a date window.

Both modes run through the same backend and share the same detection logic, so the numbers agree — a wallet looked up directly will match what a date-range scan covering the same period reports.

### How a withdrawal is identified

A payout is recognised as a transfer *from* a known pool contract whose value falls inside a tolerance band around that pool's fixed denomination.

The band is what makes this work in practice. Because relayer fees are deducted from the payout, an exact match on the denomination would miss most real withdrawals. A band with a floor comfortably below the denomination catches them. The band's floor also does a second job for free: the relayer's own fee leg is far smaller than the denomination, so it falls outside the band and is never mistaken for a payout. There's no need to reconstruct transaction groupings to tell the two legs apart.

Amounts are reported **net** — the ETH that actually landed in the recipient's wallet, not the note's face value. On a large scan the difference across all the relayer fees is not trivial, so it's worth knowing which figure you're reading.

The four pool contracts are public and shown in the interface:

| Pool    | Contract                                     |
| ------- | -------------------------------------------- |
| 0.1 ETH | `0x12D66f87A04A9E220743712cE6d9bB1B5616B8Fc` |
| 1 ETH   | `0x47CE0C6eD5B0Ce3d3A51fdb1C52DC66a7c3c2936` |
| 10 ETH  | `0x910Cbd523D972eb0a6f4cAe4618aD62622b39DbF` |
| 100 ETH | `0xA160cdAB225685dA1d56aa342Ad8841c3b53f291` |

### What you get back

One row per recipient address, containing per-pool withdrawal counts and ETH totals, a grand total, and the first and last withdrawal dates. Where available, individual withdrawal timestamps and links to the transactions are attached so you can drill from a summary row down to specific events.

Results export to CSV, or copy as a Markdown table for pasting into a case file.

### Known limits of this module

* Wide date ranges across busy pools produce a lot of data. Scans are paged and capped; when a cap is hit the interface flags it, and the fix is to narrow the window rather than trust a truncated result.
* A withdrawal with an unusually large relayer fee could fall below the band's floor and go uncounted. In practice fees are nowhere near that size, but the tolerance is a deliberate trade-off between false negatives and false positives, not a perfect filter.
* **Enumeration is not attribution.** This tells you who withdrew. It does not tell you whose money it was.

***

## Module 2 — Cluster Analyser

Take the recipient list from Module 1 and ask which of those wallets share an operator.

The premise is that mixer output is rarely the end of the journey. Funds get consolidated, split into sub-wallets, forwarded to an exchange, bridged, or used to gas up the next wallet. Those onward movements happen *outside* the mixer, where nothing is hidden, and they leave a pattern. A single operator running twenty fresh wallets tends to touch the same places in the same ways.

### Step 1 — Gather onward activity

For each address in the set, the analyser pulls that wallet's transaction history and builds a list of counterparties. Several things are excluded up front:

* **Tornado infrastructure** — every wallet in the set transacted with the pools, so that's a shared counterparty carrying zero information.
* **Failed transactions** — no value moved, so no relationship is evidenced.
* **Sub-threshold transfers** — below a small floor, transfers are noise and, worse, are frequently adversarial (see poisoning below).

Each wallet's scan is capped for performance. When the cap is reached the interface says so, because an unreported cap silently converts "no link found" into a false negative.

### Step 2 — Detect links

Three relationship types are detected, and they are deliberately **not** treated as equivalent:

| Link type               | What it means                                                      | Strength                                                                              |
| ----------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------------------------- |
| **Direct transfer**     | One recipient sent funds straight to another                       | Strongest — hard to explain innocently between two supposedly unrelated fresh wallets |
| **Shared intermediary** | Two recipients each transacted with the same third address         | Moderate — meaningful only once the intermediary is shown not to be a service         |
| **Two-hop**             | Two recipients connected through a chain of intermediate addresses | Weakest — the number of innocent explanations grows fast with distance                |

Every link carries a confidence score. Scoring is driven by the link type, the number of transfers evidencing it, and how many wallets the relationship spans. Higher scores come from links that are harder to produce accidentally.

### Step 3 — Strip out the artefacts

This is the part that determines whether the output is useful or garbage, and it's where most of the engineering effort went.

**Hub filtering.** Clusters are formed partly by shared intermediaries — so a single intermediary that everyone touches will merge everything. Exchange deposit addresses, bridges, routers and popular token contracts are exactly that: they connect to enormous numbers of unrelated wallets. Left alone, one such address collapses an entire scan into a single meaningless blob ("150 wallets, 1 shared intermediary"), which is technically true and completely useless.

The analyser therefore measures each intermediary's **fan-out** — how many distinct wallets in your set it touches — and drops any intermediary whose fan-out is implausibly high for a co-control relationship, using both an absolute ceiling and a proportional one so the rule behaves sensibly on small and large scans alike. This happens *before* clusters are assembled, so genuine pairwise links surface instead of drowning.

Removing a hub un-glues wallets that shared *only* that service. Any pair still joined by a direct transfer or a genuinely low-fan-out intermediary stays clustered on that remaining evidence. The result is several real clusters instead of one blob.

**Address-poisoning filtering.** Poisoning is an active attack on exactly this kind of analysis: an adversary sends tiny transfers to many wallets, or from lookalike addresses that visually resemble a real counterparty, manufacturing connections that were never real. The analyser applies a set of heuristics to identify intermediaries behaving this way and removes them before clustering.

**Nothing is dropped silently.** Every filtered hub is returned with its fan-out and transfer count, every poisoning candidate is listed, and both appear in the diagnostics panel. If you think an exclusion is wrong — say a filtered "hub" is actually a genuine consolidation wallet — you can see it, inspect it by hand, and adjust. A filter you can't audit is a filter you can't defend, and this output is meant to survive scrutiny.

### Step 4 — Assemble clusters

Surviving links are treated as edges in a graph and the connected components become the clusters. Each cluster is presented with its member wallets, the evidence that joined them (how many direct transfers, how many shared intermediaries, how many two-hop chains), an aggregate confidence rating, and the total ETH the group withdrew.

### Step 5 — Co-timing views

Alongside the link-based view, results can be regrouped by **day** or by **hour** of withdrawal.

This exists because timing is a signal that survives the hub problem entirely. Independent, unrelated users very rarely withdraw inside the same narrow window. A group of wallets all firing within the same hour is a strong common-control tell that owes nothing to intermediary analysis. An optional filter shows only windows containing two or more distinct wallets, so you can go straight to the interesting ones.

The two views are best used against each other. A tight cluster in the link view that *also* fires co-timed in the same hour is a considerably stronger signal than either finding alone.

### Output and handoff

<figure><img src="/files/gG3oSxUG0GCUICoA2i4f" alt=""><figcaption></figcaption></figure>

* Full addresses everywhere — no truncation, since a partial address is useless downstream.
* One-click bulk copy per cluster or per time window, as a clean newline-separated list ready to paste into another tool.
* Export as CSV or as a Markdown report for a case file.
* Direct handoff of a cluster to Arkham for visual exploration.
* A diagnostics panel showing per-address scan results, what was filtered and why, and any caps that were hit.
* A built-in "How to read these results" panel restating the caveats below at the point of use.

***

## How to read the results

**A cluster is a lead, not a finding.** The module produces a shortlist of addresses that plausibly share an operator. That's the beginning of an investigation, not the end of one. Verification is separate work.

Some specific things to hold in mind:

* **Confidence is relative, not probabilistic.** A 0.9 link is stronger evidence than a 0.5 link. It is not a 90% chance of anything.
* **Shared intermediaries have innocent explanations.** Two people who used the same OTC desk, the same bridge, the same NFT marketplace or the same small exchange share an intermediary and share nothing else. Hub filtering removes the obvious cases; it cannot remove the subtle ones. Check what the intermediary actually is before you lean on the link.
* **Absence of links is not absence of relationship.** Scan caps, a date window that stops short, an operator disciplined enough never to let their wallets touch, or funds moved by a route the scan doesn't cover — all produce a clean-looking result that means nothing.
* **Co-timing can be coincidence.** It's much less likely to be coincidence for three wallets in the same ten minutes than for two wallets in the same day. Read the window size, not just the fact of a grouping.
* **None of this links a withdrawal to a deposit.** If a report seems to imply that it does, the report is wrong.

***

## Constraints worth knowing

* Analysis covers the Ethereum mainnet ETH pools. Other chains and the ERC-20 pools are out of scope here.
* Everything is derived from public block explorer data. There is no privileged data source, and no capability that isn't in principle available to anyone reading the chain.
* Scan depth per wallet is capped. Very old or very high-activity wallets may have earlier links that fall outside the window scanned, and this is reported rather than hidden.
* Filtering thresholds are conservative by design — tuned to avoid hiding genuine links, which means some noise gets through. That's the deliberate direction to err in, since a visible false positive can be dismissed by an analyst while an invisible false negative can't.

***

## Scope of this document

This describes the methodology, not the implementation. Deliberately excluded: infrastructure and endpoint details, authentication and credentialling, exact filter thresholds and scoring coefficients, and source code. Those are omitted partly because they're operational and partly because publishing precise thresholds tells anyone who wants to evade the analysis exactly what to stay under.

Everything above should be enough to understand what the modules do, judge whether their output is fit for a given purpose, and challenge a result you disagree with.
