← All work

Case study

ScamFilter

A detection engine that strips scam transactions out of wallet data and flags Ethereum wallets that have been drained by bots, including attackers that aren't on any blacklist yet.

Type
Personal project
Role
Design, build & testing
When
2025 – 2026
Stack
Python · Polars · PostgreSQL · MongoDB

01 · The problem

Bad data looks like real activity

ScamFilter is a personal project built on a wallet analytics pipeline. The pipeline pulls a wallet's full transaction history from Etherscan and turns it into metrics such as active days, total gas spent and wallet age.

It's my own rebuild of ideas from the fraud-detection work I did as an associate data engineer at Crimson Umbrella Technologies, written outside the job.

Raw on-chain history is noisy. Scammers send fake airdrops and "address poisoning" transactions to millions of wallets, and those transactions show up in the victim's history as if the victim took part. Worse, when a wallet's private key leaks, a sweeper bot watches it and drains every deposit within seconds. Left alone, that wallet looks busy and healthy in the metrics.

So the pipeline needed two things before calculating anything: remove transactions that come from known scammers, and label wallets that are being drained, even when the attacker has never been reported.

02 · Where it fits

Filter first, then measure

Transactions are loaded into PostgreSQL. For each wallet, ScamFilter reads the history in time order and splits it into clean and flagged transactions. Flagged transactions are recorded in their own table with a source and severity. Only the clean set goes on to the metrics engine, which is written in Polars and merges its results into each wallet's MongoDB document without double-counting earlier runs.

POSTGRES transactions LAYER 1 · SPAMFILTER ① blacklist match ② sweeper-bot scoring ③ wallet health status POSTGRES spam_detections LAYER 2 · POLARS wallet metrics automation score MONGODB merge FLAGGED CLEAN
Fig. 1 — Scam transactions are removed before any metric is calculated.

03 · Known threats

Start with the easy wins

The first check is a blacklist. ScamFilter downloads and caches ScamSniffer's public scam database, then checks the sender and the destination of every transaction against it. Any match is removed from the clean set.

I deliberately left out a third field, contractAddress. In Etherscan's data it is only filled in when a transaction creates a contract, not when it calls one, and calls to a scam contract are already caught by the destination check. Knowing exactly what each field in the source data means kept the rules simple.

A blacklist only catches attackers someone has already reported, though. A sweeper bot can use a brand-new wallet every time.

04 · Unknown threats

Scoring the behaviour of a sweeper bot

Sweeper bots behave in a recognisable way: they empty the wallet, they do it seconds after money arrives, and they overpay for gas to get in first. Instead of one hard rule, I gave each of these behaviours a weight. Every outgoing transaction gets a score, and a transaction counts as a sweep only if both of these hold:

  • its total score is 65 points or more, and
  • at least one economic signal fired, meaning the transaction makes no financial sense for a normal user.

The second rule matters most. Plenty of honest users forward money quickly, such as payment relays and multi-sig operators, and they rack up points for speed alone. What they don't do is empty the whole wallet, massively overbid on gas or pay more in fees than they send.

IDSignalWhat it looks forPoints
S1Balance drain econSends 90% or more of the wallet's running ETH balance25
S2High gas price econGas price above the threshold for that period (see below)20
S3Upside-down economics econPays more than 10× the transferred value in fees20
S4Small extractionSends more than 0 but less than 0.1 ETH20
S5Plain ETH transferNo contract call, exactly 21,000 gas15
S6TimingSent within 30 seconds of the last deposit30
S7Permit2 drain econA signed token approval is used to take 95%+ of a token balance, with nothing given back30
S8Sequencer latency econL2 only: the drain lands within 2 positions of the deposit in the same block25
S9Paymaster drain econA sponsored ERC-4337 operation fails after burning 90%+ of its gas budget25
S10Mixer-funded recipientThe receiving wallet was first funded by Tornado Cash within 72 hours15
S11Whitehat rescueSent privately via Flashbots to a verified recovery contract−40

Two transactions, side by side

These are illustrative examples of how the score and the economic gate combine.

A sweep

0.04 ETH arrives. 12 seconds later, 98% of the balance leaves as a plain transfer at 300 gwei, in 2024.

  1. S1 · balance drain econ+25
  2. S2 · high gas econ+20
  3. S4 · small extraction+20
  4. S5 · plain transfer+15
  5. S6 · within 30 s+30
  6. Score110

Flagged — over 100, so the wallet is marked compromised right away

A quick payment

A user receives funds and 20 seconds later sends 0.05 ETH to a friend at normal gas, keeping most of the balance.

  1. S4 · small extraction+20
  2. S5 · plain transfer+15
  3. S6 · within 30 s+30
  4. Score65

Not flagged — meets the score, but no economic signal

05 · Different chains, different rules

The same signal can mean different things

The filter covers Ethereum and three layer-2 networks: Arbitrum, Optimism and Base. A rule that is a strong signal on one chain can be meaningless on another, so the filter is told which chain it is scoring and adjusts.

  • Arbitrum processes transactions first-come, first-served. Paying more gas doesn't get you in earlier, so the high-gas signal (S2) is switched off there. Serious bots compete on network speed instead, which is what S8 detects.
  • Layer-2 fees include the cost of posting data to Ethereum. Using the full fee made ordinary transfers look like they were overpaying, so on L2s the upside-down-economics signal (S3) uses only the computation part of the fee.
  • "High gas" depends on when. During the DeFi and NFT boom (June 2020 to October 2022), gas regularly went above 100 gwei for everyone. The threshold is 500 gwei for that period and 100 gwei otherwise.

06 · From transactions to a verdict

Being careful about labelling a wallet

Calling a wallet compromised is a strong claim, so a single suspicious transaction isn't enough. A wallet is marked compromised after it sends three transactions that pass both checks. For very busy wallets, those with more than 20,000 transactions, the bar rises to one strike per 5,000 transactions. A single transaction scoring 100 or more (90 for token transfers) skips the wait, because a score that high leaves little doubt.

The label also says how the wallet was attacked: an ETH sweeper, a token sweeper, a Permit2 signature drain or paymaster griefing. A wallet is never downgraded to a milder label, and it isn't blacklisted forever. After 90 days with no further sweeps, it moves to Healthy (Recovered).

Rescues needed special care. When a key leaks, the owner or a whitehat often races the bot by sending funds privately through Flashbots to a recovery wallet, and that looks a lot like a sweep. S11 takes 40 points off, but only when both conditions hold: the transaction came through a private builder, and it went to a verified recovery contract such as a Gnosis Safe. An attacker who only uses a private mempool gets no discount.

07 · Designing for missing data

Signals that switch on when the data does

Etherscan's transaction lists cover S1–S6. The newer attacks behind S7–S11, such as Permit2 signature drains, ERC-4337 paymasters, mixer funding and Flashbots bundles, need extra fields that another part of the pipeline has to work out first.

I defined those 15 fields as a contract between the ingestion pipeline and the filter. Every field is optional and defaults to a harmless value, so when a field is missing its signal simply doesn't fire. The filter runs the same on plain Etherscan data today, and picks up each advanced signal as soon as the upstream pipeline starts providing its field. No change to the filter is needed.

08 · Automation is not malice

Keeping two questions apart

The second layer looks at a wallet's whole history in Polars and asks a different question: is this wallet run by a script?

  • No sleep: activity spread almost evenly across all 24 hours of the day.
  • Block-rate bursts: more than half of transactions within 12 seconds (one Ethereum block) of the previous one.
  • Disposable wallets: over 500 transactions in its first 7 days.
  • Mempool sniping: more than half of 20+ transactions failed.
  • Airdrop spam: over 100 outgoing transactions, nearly every one to a different address.

Exchanges, bridges and market makers are automated by design and trip these flags all the time. So this layer produces a separate automation score, and it is never used to decide whether a wallet is compromised. Mixing the two would have labelled every exchange hot wallet as hacked.

09 · Testing

Testing what shouldn't be flagged

The filter has 34 unit tests. Many of them check that honest behaviour isn't flagged: a legitimate gasless swap through Permit2, a whitehat rescue, a successful sponsored transaction, a partial Permit2 transfer, two paymaster failures where three are required, and Ethereum behaving exactly as before when the L2 rules were added. In a system like this, a false accusation costs more than a missed catch, so those cases got the most tests.

Recap

Key takeaways

  1. Hello there! Welcome to the world of ON-CHAIN DATA! My name is OAK. Let me tell you what SAGUN learned building SCAMFILTER.
  2. Clean the data before you measure it. Scam transactions look just like real activity, and they quietly skew every metric.
  3. A blacklist only catches attackers someone has already reported. To catch new ones, score behaviour: draining the wallet, moving seconds after a deposit, overpaying for gas.
  4. Speed alone proves nothing. A transaction only counts as a sweep if it also makes no economic sense, and that one rule protects honest users.
  5. The same signal can mean different things on different chains. On Arbitrum, paying more gas doesn't get you in first, so high gas stops being a clue.
  6. Keep "is this a bot?" apart from "was this wallet drained?". Exchanges are automated, not hacked.
  7. Design for data you don't have yet. Optional fields let new signals switch on later without touching the filter.
  8. Test what should NOT be flagged. In fraud detection, a false accusation costs more than a missed catch. That's everything from me. Thanks for reading!

Want to talk about data pipelines or this project?

Email me