---
name: rugscan
description: rugscan, the deterministic Python bundle and insider detector for Robinhood Chain tokens on the kookwatch droplet. Read before any bundle, sniper, farm, fee-exempt or launch-integrity question, before adding a token to the daily check, and before writing any new on-chain detector.
sources: [cowork]
aliases: [rug-scan, bundle-detector, launch-integrity, rugscan-timer]
---

# rugscan

Built 5 Sep 2026 after Ben pasted @AgentChud's thread ("create skills to identify bundles / early funny business... build a deterministic backend in python and improve it every run"). Lives at **/opt/rugscan** on the kookwatch droplet (159.223.88.168). Companion to [[alert-bot-fleet]] and [[alert-bot-fleet-2]].

[stated] Ben asked: can this be built, what is an RPC, what is the best way to work with it, and he has early projects that need checking every day.

## What it is
- Pure Python, no model in the loop, no paid keys. `chain.py` (RPC primitives), `detect.py` (nine measuring functions), `rugscan.py` (`judge()` turns measurements into points, plus CLI and Telegram).
- Answers "was this launch bundled, farmed or fee-exempt for insiders, and who holds it now". It does NOT predict price, and the README says so.
- Verdict: risk 0-100 -> CLEAN under 30, CAUTION 30-54, AVOID 55+. Every point carries the sentence that earned it.
- `--watchlist` alerts only on a CHANGE in risk or action; `--always` forces a send. Daily at 01:00 UTC = 08:00 Bangkok via `rugscan.timer` (oneshot service, NOT in watchdog, which only understands long-running units).
- Telegram: reuses the retired **@OxUberM_bot** token from /root/retired/uber-bots-20260903-033047.tar.gz, chat 357464289. Renamed to "rugscan" through the Bot API `setMyName`, so Ben needed no BotFather step. Verified with getMe.
- Every scan appends the full result to `/opt/rugscan/findings.jsonl`. That file is the memory the detectors improve against.

## The nine detectors
same-block cohort; first-minute NET supply share; fee exemption; uniform buy sizes; handoff (opening cluster selling straight back out); fresh wallets; relayer concentration; top-10 holder share excluding contracts; liveness (24h buys vs sells, volume-to-pool, liquidity floor).

## Calibration cases, 5 Sep 2026
- DELTA CLEAN 0 in 73s. Real infrastructure token, correct negative control.
- BUN CAUTION 40. First pass called it AVOID 95 on two bugs, both fixed.
- ZORDI CAUTION 45: 12 wallets in one block, 77% dumped in minutes, 78x volume/pool. Correct.
- SLUICE CLEAN 15 and MOGMAN CLEAN 15, both of which lost a trader money. Structure was clean; the loss was price. Do not let anyone read a CLEAN as a buy.
- MERRYMEN CLEAN 0, 52 days old, launch window thin. Launch detectors decay after roughly two weeks; old tokens lean on holders and liveness.
- **VOXEL AVOID 100, the headline result.** 29 wallets bought in ONE block taking 40.2% of supply, 37 wallets paid no launch tax while 928 paid, 14 near-identical top-25 buys, 61% dumped in minutes. Exactly the pattern @AgentChud described. A trader still made +116% on it, which is the cleanest available proof that AVOID means the structure is rigged and NOT that the chart will not move.
- Live end-to-end proof 5 Sep 2026: `--watchlist --always` scanned DELTA, OPTIMUS, CHIPS, PAIR and reported "sent 4 of 4" to Telegram. All four CLEAN. Per-token time 15-105s.

## Timeout bisect guard (added after it hung a run)
Making "log query timed out" bisect like the 10k cap turned one bad node minute into ~11,000 single-block calls and stalled OPTIMUS for 15 minutes. `_logs` now stops splitting at 64 blocks or depth 14, retries twice, then logs `[warn] gave up on blocks X-Y` and moves on.

## Bugs found and fixed while building (do not reintroduce)
- **Gross vs net.** Summing every receive per address gave "136% of supply bought in 60 seconds". Supply share must be NET (received minus sent) measured at the cutoff block, never gross churn.
- **Fee exemption over-fires.** Small buys round the tax to zero. Exempt now requires the buy to be at or above the median opening size, at least 50 taxed txs to exist, exempt wallets under 25% of buyers, AND the exempt set holding at least 8% of the opening.
- **Uniform sizes over-fires.** Launchpad max-buy caps make the top 25 look identical on honest launches. Now also requires the top 25 to be at least 3-5% of supply.
- `pkill -f rugscan.py` **kills the calling shell**, because the shell's own command line contains the pattern. Use `pgrep -f "^python3 rugscan"` and kill by pid.
- `eth_getLogs` on the RH RPC returns "log query timed out" as well as the 10,000-result cap. BOTH must bisect the block range, never retry the same range.

## RPC access, measured 5 Sep 2026
- Free public: `rpc.mainnet.chain.robinhood.com`, browser User-Agent required. 10,000-result cap plus timeouts, ~80-180s per scan.
- **dRPC does serve Robinhood Chain: `https://robinhood.drpc.org` returns chainId 0x1237 (4663) unauthenticated, but `eth_getLogs` replies "method does not exist" without a key.** Archive and MEV badges present, Growth plan advertised from $10. A paid key would unlock wide log ranges and cut scan time; `chain.py` already reads the `RH_RPC` env var so switching is one line.
- `robinhood-rpc.publicnode.com` still 403s eth_getLogs entirely.

## rugwatch, added 5 Sep 2026 (the 60-second new-buy watcher)
[stated] Ben: "check everything that i hold across fomo and basedbot and run rug checks one time on it asap. check every minute for new buys."
- `/opt/rugscan/rugwatch.py`, unit **rugwatch.service**, Restart=always, log `/opt/rugscan/rugwatch.log`, config `watch.json`, state `watch_state.json`. Same bot and chat as rugscan.
- Polls each watched wallet every 60s. A token address never seen before triggers: an immediate "bought X" Telegram message, then a full rugscan, then the verdict as a second message.
- **Airdrops are filtered by mechanism, not by threshold:** `is_purchase()` requires a USDG or WETH leg of at least $5 in the SAME transaction. It fails OPEN (alerts on error) so a real buy is never silently dropped. Same doctrine as the crashbot inbound-noise fix.
- **Baseline seeding is two-source and that matters.** Seeding from the recent transfer page alone gave 12 tokens, so any older holding would have alerted as new on its next move. It now also pulls `/api/v2/addresses/{a}/token-balances` and seeds every held contract: 46 tokens.
- **Blockscout 429s hard when a rugscan is running on the same box.** `tokentx()` therefore tries the v1 `action=tokentx` endpoint, falls back to `/api/v2/addresses/{a}/token-transfers`, and backs off 5, 15, 25... seconds over 6 attempts before skipping the cycle. Never let the two hit Blockscout unthrottled at once.
- NOT in watchdog UNITS yet. It is a long-running unit so it CAN be added, unlike the rugscan timer.

## REBUILT FOR SPEED 5 Sep 2026 — detection 13 minutes late became ~10 seconds
[stated] Ben: "the rug check needs to be quicker. You noticed at 1:20 that the wallet bought Shopee, but my first buy was at 1:07, so you're late. Check faster, maybe earlier than every minute, and the check itself needs to be very quick too."
- **ROOT CAUSE: Blockscout's tokentx index runs 10-15 minutes behind the chain.** Polling it faster changes nothing. Detection is now a DIRECT RPC LOG POLL: `eth_getLogs` with `topics=[TRANSFER, None, pad(wallet)]` over the blocks since the last cycle. One call covers every token at once, 0.63s for a 9,000-block window, and it sees the transfer in the block it lands in. POLL is now 8 seconds; `last_block` is carried in watch_state.json and capped at a 5,000-block lookback after downtime.
- Never use a block explorer as the detection surface for anything time-sensitive on this chain. The RPC is both faster and more truthful.
- **Scan time 5+ minutes -> 9.7 seconds on a 30-minute-old token, same verdict, same risk score.** Four fixes, in order of what they were worth:
  1. `fresh_wallets` cost 16s and returned `checked=0` — Blockscout again. Now parallel, sampled at 10, and hard-capped at a 4s deadline; unknown freshness is better than a late verdict.
  2. `block_at` cost 7.8s doing a timestamp binary search. **For a token under 36h old the launch block is simply the block of its FIRST transfer log, and those logs are being pulled anyway.** Estimate the window from block time (0.1014 s/block), pad 5,000 blocks each side, take `min(bn)`. Older tokens keep the search because the estimate drifts about 1%.
  3. `relayer_clusters` cost 27.5s doing 70 serial `eth_getTransactionByHash` with 0.06s sleeps. Now a 14-thread `pmap()` in chain.py: 3.0s.
  4. `fresh_wallets` and `holders_now` are both Blockscout and were running one after the other. They now run side by side, each with its own timeout.
- `pmap(fn, items, workers)` lives in chain.py and is the tool for any "one RPC call per address" loop. The RPC throttles above ~8 concurrent, so 7-14 workers is the band; 16 was slower than 8.
- First Telegram message now carries mcap, liquidity and pair age from DexScreener (0.24s) so the useful part arrives before the scan finishes.

## judge() v2, 5 Sep 2026 — HISTORY IS CONTEXT, ONLY LIVE OVERHANG SCORES
[stated] Ben, and he was right: "sometimes these things can be bundled in the beginning, but what matters is if the bundles are OUT now. They launch it, they sell it, the coin sells off, it stabilises, and it goes for another run. I don't care if it was bundled in the beginning as long as it's not bundled anymore now." And separately: high turnover after a hard pump and dump is NORMAL, not a red flag.
- Backtest run to settle it: 31 Robinhood Chain tokens, 19 with a complete peak-dump-bounce cycle. Spearman, and then partial Spearman controlling for how hard the token dumped:
  - **how hard it dumped -> size of the bounce: rho +0.85.** This dominates everything and is mostly arithmetic: a deeper hole gives a bigger multiple off the low. Any bounce study that does not control for it is measuring the hole.
  - **bundled at launch -> bounce, controlled: rho +0.41.** Bundling did NOT predict a worse outcome. Median bounce was 2.09x for launches bundled 15%+ versus 1.22x for the rest. The old judge() was charging up to 70 points for this.
  - **24h volume / mcap (turnover) -> bounce, controlled: rho +0.07.** Turnover predicts nothing once the dump is controlled for. The `vol_to_liq > 40` churn penalty is removed.
  - launch cluster still holding -> bounce, controlled: rho +0.05. Weak, and see the caveat below.
- **New split.** SCORED: what the opening cluster STILL holds (`share_300s_now`, 35/22/10 points at 25/15/8% of supply), fee-exempt insiders only if that cluster is still in, top-10 concentration today, thin pool, dead or seller-led tape. CONTEXT, printed but worth zero: same-block bundle, first-minute share, uniform sizes, relayer concentration, fresh wallets, how much the cluster dumped early, turnover. The Telegram line now ends with `history (not scored): ...`.
- Effect on real tokens: Shoppy CAUTION 40 -> **CLEAN 8** (bundled, cluster out). MOGMAN, CHIPS, OPTIMUS -> **CLEAN 0**. **VOXEL AVOID 100 -> CAUTION 50**, and now for the right reason: its launch cluster still holds 35% of supply and the fee-exempt set is still in. That is the only one of the set where the old verdict survives, and it survives on a forward-looking measure.
- **CAVEAT, do not overclaim this study.** The predictor `share_now` was computed in the backtest from a quick balanceOf over the top 40 net launch-window receivers, and that cluster is contaminated on some tokens: it read MOGMAN at 92% of supply where rugscan's own `share_300s_now` reads 2%. So the rho +0.05 for "cluster still in" is not trustworthy; rugscan's own hub-and-fee-filtered number is the better one. The bundling and turnover results do not depend on that field and stand. Also `share_now` is measured today while the outcome is also today, so it is contemporaneous, not a clean forward test. To make it one: snapshot the cluster share at T+24h from replayed logs and measure the run AFTER T+24h.
- Reusable trap from the study: a pool, router, treasury or staking contract that lands inside a "cluster" makes its full-history `eth_getLogs` trip the 10k cap forever and bisect into tens of thousands of calls. Query each cluster wallet on its own over the full range; an address that trips the cap IS the infrastructure, and dropping it costs one call.
- Second reminder of an old trap, hit again: `pkill -f bt.py` killed the calling shell, because the shell's own command line contained the pattern. Kill by pid from `pgrep`.
- Backtest artefacts: `/tmp/bt/bt2.py`, `/tmp/bt/tokens.json` (31 RH tokens with contract addresses), `/tmp/bt/results2.json`. Worth rebuilding on the droplet so it survives the container.
- Blockscout unknowns must never read as good news: a failed holder lookup returns `top10_pct = 0`, which printed as the positive "top 10 hold only 0%". Guarded.

## TWO CORRECTIONS TO judge() v2, found 6 Sep 2026 while charting the cohorts
Both are corrections to a number Ben had already been sent in a Telegram alert. The 06 Sep 01:01 UTC alert said "OPTIMUS - the launch cluster still holds 12% of supply". Both halves of that were wrong.
1. **`share_300s_now` was not now.** `net_at(c300, end)` uses `end` = the last block in the SCAN WINDOW, about five minutes after launch. judge() v2 had just been wired to score that field as the LIVE overhang, so it was scoring a five-minute-old number as if it were today's. Fixed with `detect.cluster_live(token, addrs, supply)`: a live `balanceOf` over the opening cohort, `pmap` at 10 workers, exposed as `opening.share_300s_live`, which judge() now scores (falling back to the old field only if it is missing).
2. **A launch locker is not overhang.** `0x267444D099b10fB5Ed7c3Cc7B7c767AdcA574952` holds **8.1633% of OPTIMUS**. Probed live: `withdraw`, `release`, `unlock`, `sweep`, `claim`, `token`, `unlockTime` all revert, and the runtime bytecode contains **no `a9059cbb` (transfer) selector at all** (it does contain `23b872dd`, which is how tokens get pulled in). That supply can never leave. It is the same PonsV2 `V2LaunchLocker` address the onchain-forensics skill already records on BOW, so it is shared launchpad infrastructure across launches on this chain, not per-token. `cluster_live` now splits its return into `live_pct` (EOA and 7702 wallets, scored) and `contract_pct` (lockers, vaults, staking: reported, never scored).
- Effect: OPTIMUS's real live cohort overhang is **1.16% of supply ($126k)**, not 12%. Its verdict went CLEAN 10 -> **CLEAN 0**.
- Standing rule now in code: **score only what a wallet can sell.** Contract-held supply gets named and credited, never charged.

## Launch-cohort exit ledger, 6 Sep 2026 (OPTIMUS, DELTA, TWO)
[stated] Ben: "is the optimus launch cluster selling off? give me a chart of their selling over time. do the same for delta" and then "and also for TWO/twofold".
Artifact: https://claude.ai/code/artifact/c294ac45-a424-4cdc-ae49-84cefba2269b · scripts `/tmp/bt/cluster.py`, `/tmp/bt/detail.py`, `/tmp/bt/two.py`, `/tmp/bt/build_page.py`.
- Method, and it is the reusable one: take the opening cohort, pull EACH wallet's whole transfer history with a topic-filtered `eth_getLogs` (one query per wallet over the full range), classify every outbound leg as SALE (into the derived venue set) or MOVE (anywhere else), then **anchor the curve to live `balanceOf` and walk it backwards**, so the endpoint is a fact rather than the sum of a replay.
- **Anchoring matters.** The replayed legs summed 1.9 points of supply short on OPTIMUS and 1.4 short on DELTA. Without the anchor the endpoint would have been wrong by that much; with it, only the shape of the early curve carries the uncertainty.
- **OPTIMUS**, 44 wallets, first 5 minutes: acquired 27.40%, sold into pools 12.62%, moved wallet-to-wallet 5.47%, left 9.32% of which 8.16% is the locker, so **1.16% ($126k) is live**. The selling was 29-31 Aug and one burst on 2 Sep; the last sales were 94-167 hours ago. **It is finished selling, not selling now.** Four wallets moved their whole position wallet-to-wallet and never sold; the top receivers were traced and hold $0 today.
- **DELTA**, 6 wallets, first hour (the 300s window had only one wallet because the launch was genuinely unbundled): acquired 34.97%, sold 18.57%, moved 16.39%, **left 0.006% = $741**. Gone. One wallet `0x1017e0c80fcd54b82b7e6cf2fe45a00a3acf9a5a` did 620 separate sell legs across five weeks.
- **TWO**, 45 wallets, first 5 minutes: **genesis liquidity seeding must be excluded or the answer is nonsense.** The operator `0xc8720447712e6c4c851b3884b4ec93f9ce8ad5fd` sent 841,500,000 (84.15% of supply) into the PoolManager 220 blocks after the mint. Counting that as a sale gave "sold 124% of supply". Rule now in the script: a venue transfer inside the first 500 blocks larger than 20% of supply is SEEDING, reported separately, kept out of sales and out of the curve. Corrected: acquired 177.37% (mint plus JIT returns), **sold 40.14%**, moved 40.42%, **left 2.95% ($45k), of which staked 0.00%**. The operator himself sold only 4,140,445 tokens and now holds 2,529,312 ($3,890).
- TWO staking: `StakingVaultV2 0x06E463fDa4BEb4aA096142E673240aB9719fB3A9`, accessor **`staked(address)` = selector 0x98807d84**, verified live (`balanceOf` reverts on it). The vault is explicitly excluded from the venue set: staking is neither a sale nor an exit. Every cohort wallet now reads 0 staked, consistent with emissions having ended 5 Sep 10:58 UTC.

## FOLLOW THE MOVES. Ben was right and it changes a verdict. 6 Sep 2026
[stated] Ben: "if coins are just moved to other wallets, then that typically just means that they are still held by the same people, they're just trying to obfuscate their tracks. So that should still be counted as the initial bundle."
- He is right, and the DELTA case proves it. Extended cohort = launch cohort plus every wallet it sent at least 0.02% of supply to, up to 3 hops, contracts excluded, capped at 500 wallets. Script `/tmp/bt/extend.py`.
- **DELTA: hop 0 alone said "cluster is out, $741 left". Following the moves says the network sold 39.51% of supply (not 18.57%) and still holds 3.33% = $426,635.** The early cohort did not exit, it relayed through fresh wallets and kept selling. That earlier "cluster is out" call was wrong.
- OPTIMUS: sold 12.62% -> **15.83%**, held 9.32% -> **10.70% ($1,169,417)**. Free of the locker: 1.16% -> **2.54%**.
- TWO: sold 40.14% -> **68.12%**, held 2.95% -> **5.31% ($81,649)**.
- **Report per hop, never merged.** Hop 1 is very likely the same hand. By hop 3 it is a claim about a network, not a person, and an outbound to a wallet can also be an OTC sale, a CEX or bridge deposit, or a payment. Each hop's contribution is printed separately so the reader can stop where they stop believing it.
- Disclose the leakage: DELTA lost **46.62%** of supply out of the traced network (below the dust threshold, into contracts, or past the cap), TWO 26.97%, OPTIMUS only 0.30%. A high leakage number means the extended figure is a floor.

## "COULD WE HAVE KNOWN AT THE TOP?" — the study Ben asked for, 6 Sep 2026
[stated] Ben: "Delta sold off hard for almost 2 weeks after a crazy run-up, same with TWO, sold off 90%. I want to know why and if we can identify these cases prior. No hindsight. Could we have known near the top that both would not perform well going forward."
Everything measured AT THE ATH HOUR from data that existed then; the drawdown is scored afterwards and never used as an input. n=27 tokens. Script `/tmp/bt/structure.py`, results `/tmp/bt/structure.json`, `/tmp/bt/top.json`.
- **WHAT WORKED: extension, the run above the token's own 7-day base. rho +0.62 against the subsequent drawdown.** At the top, **10x or more above base (7 tokens) went on to a median 86% drawdown; under 10x (16 tokens) a median 52%.** TWO was **51.5x** extended at its ATH, OPTIMUS **58x**, DELTA **11.6x**. The control that did NOT dump, PONS, was **4.4x** and has given back 11%. CASHCAT 1.4x, gave back 29%. HOOKR 5.9x, gave back 44%.
- Volume trend into the top: rho +0.41, and the sign is counterintuitive. RISING volume into the ATH went with a BIGGER drawdown: a blow-off is what a top looks like, not what support looks like. DELTA's last 6h before the ATH was 7.4x the prior 6h. TWO's was 0.59x, i.e. it topped on fading volume, the other exhaustion shape.
- Age at the ATH: rho -0.48. The younger a token is when it prints its high, the harder it falls.
- **WHAT DID NOT WORK, and it cuts against the obvious story: the insider overhang.** Built "days of buying to absorb the extended cohort" = overhang $ at the ATH / half of the trailing 24h volume. **DELTA: 0.33 days, and it still fell 70%. TWO: 0.71 days, fell 87%. OPTIMUS: 3.19 days, fell 64%.** No relationship. Who was holding did not decide it. Do not sell this metric to Ben as a predictor.
- **THE BASE RATE IS MOST OF THE ANSWER, and it is the single most useful number here. Across all 27 tokens the median give-back from the ATH is 64%, the interquartile range is 37% to 89%, and 67% of them fell more than half.** A hard drawdown after a top is the DEFAULT on this chain. The forecast worth making is never "will it fall" but "how far", and extension is what sizes it.
- Honest limit to repeat whenever this is used: extension and drawdown come from the same price series, so part of +0.62 is arithmetic (a bigger run leaves more room to fall). The ranking survives that; the coefficient does not.
- Chains onto the earlier finding: extension sizes the dump, and the dump size drives the bounce (rho +0.85). Extension -> drawdown -> bounce.

## IS TWO BOTTOMED? Asked and answered 6 Sep 2026 04:40 UTC: no evidence yet
- **TWO's lowest close is $0.000951471 at 06 Sep 03:00 UTC, ONE HOUR before the question was asked.** It is $0.00145941 now, 1.53x off that low inside a single hour, and still 81% below its 01 Sep ATH of $0.00749774. 24h volume is $455,921 against $2,606,836 at the ATH = **17%**.
- DELTA is in the same position: lowest close $0.0115813 at 06 Sep 02:00 UTC, two hours old, 69% below its ATH, volume at 43% of the ATH level.
- A bounce inside the same hour as the low is not a base. A bottom is a level that gets tested and holds, which needs days and higher lows.
- Falsifiers written so a later session can score this: a higher low that holds 24h; 24h volume recovering off 17% of the ATH level while price stops falling; and TWO's extended cohort's remaining 5.31% ($81,649) staying put rather than selling the first bounce.

## THE BIG ONE: INSIDERS SELL INTO STRENGTH, THEY DO NOT CAUSE THE FALL. 6 Sep 2026
[stated] Ben asked for the bundling analysis redone on the extended network, daily sold visualised, whether it is still being sold, and whether that explains the pullbacks (TWO -90%, DELTA underperforming, OPTIMUS).
Artifact rebuilt in place: https://claude.ai/code/artifact/c294ac45-a424-4cdc-ae49-84cefba2269b · scripts `/tmp/bt/insider.py` (ledger), `/tmp/bt/blame.py` (the causation test), `/tmp/bt/build2.py` (page).
- **The test, and it is the reusable one: line each day's insider selling up against that day's price return.** If the red days are the days they sold, it is them. If price fell on days they did nothing, it is not.
- **All three sold MORE on days the price ROSE than on days it fell.** OPTIMUS 2.33% of supply on green days vs 0.52% on the day it fell >10%. DELTA 0.80% vs 0.335%. TWO 4.64% vs 2.84%. rho of daily selling against same-day return is POSITIVE on all three (+0.48, +0.41, +0.43). That is distribution into demand: the only days a seller with size can move supply are the days someone is bidding.
- **So most of the decline is the bid leaving, not insiders hitting it.** Share of the total decline that happened on days the network sold almost nothing (<0.05% of supply): **OPTIMUS 47%, DELTA 24%, TWO 11%**.
- **TWO is the exception and that is why it is down the most.** Its network sold **25.5% of supply in the last 7 days and 1.0% in the last 24 hours**, on down days as well as up days. OPTIMUS sold **0.000%** in the last 24h, DELTA **0.004%**. So OPTIMUS and DELTA are finished sellers, TWO is a live one. Same drawdown story, completely different thing to wait for.
- Second reading to keep, it cuts against the finding: selling needs volume and volume comes on green days, so part of that positive correlation is arithmetic rather than intent.
- Extended-network state 6 Sep 05:27 UTC: **TWO 248 wallets, holds 5.31% ($76,552)** · **DELTA 54 wallets, holds 3.34% ($398,754)** · **OPTIMUS 88 wallets, holds 10.70% ($1,323,485)**, of which 8.16% is the locker.
- Cumulative "sold" can exceed 100% of supply (TWO reads 90%) because a wallet that buys back and resells is counted each time. **Label it selling pressure, never unique supply distributed.**

## A BUG CLASS WORTH REMEMBERING: an unknown must never default to the permissive answer
- `code()` returned `"0x"` on a failed `eth_getCode`, so any address whose lookup got rate-limited was admitted to the insider network as a wallet. One pool slipped into TWO's network and moved its held figure from **5.31% of supply to 16.24%**, a 3x error, silently.
- Fixed: `code()` returns **None for unknown**, and `is_wallet()` treats None as "do not admit". Plus a hardcoded `KNOWN_INFRA` set (PoolManager `0x8366a39cc670b4001a1121b8f6a443a643e40951`, the router `0x8876789976decbfcbbbe364623c63652db8c0904`, TWO's StakingVault `0x06e463fda4beb4aa096142e673240ab9719fb3a9`, the PonsV2 locker `0x267444d099b10fb5ed7c3cc7b7c767adca574952`).
- Same shape as the Blockscout `top10_pct = 0` bug from the day before. **On this chain, when a lookup fails, fail CLOSED.**

## Ben's wallets
- **fomo wallet: `0x01c2E8cA848486166e3D08fa987d69b65df0D00F`.** Found in Chrome localStorage on fomo.family under keys `privy:connections` and `relay-claim-storage`. That is the reliable way to get a fomo wallet address: the site has no /portfolio route and its API needs a bearer token. The address holds nothing on Base.
- Holdings on Robinhood Chain, 5 Sep 2026: only two real positions, **OPTIMUS 919,260 (~$16.2k)** and **CHANG 845,332 (~$991)**. The other 36 token balances are airdrop spam of 1 to 30 units. QI and SNAC have no DexScreener pair at all.
- Scan results: OPTIMUS **CLEAN 15**, CHANG **CAUTION 50** (17 wallets in one block, 9 near-identical top-25 buys, opening cluster sold 86% straight back out, but top 10 hold only 13% and that cluster has already left).
- **basedbot wallet is still unknown.** Ben was asked for the address; the app is not readable from here and "basedbot" resolves to several different products.

## Watchlist as seeded
DELTA 0xe8ffd7e24187f72afb08d75b1bb13088a989a791, OPTIMUS 0x0ff9072a1ead154d92c2d2fef16afba6028ce2b2, CHIPS 0xbcd09284dffe06f868a4650546233e3e9abe4d97, PAIR 0x6b1d42927b1a84ec28fa88d4fc6fa7af404966be. [stated] Ben still owes his own list of early projects to watch daily.

## How it improves
A new trick becomes a function in detect.py and a rule in judge(), never a paragraph. A rule that misfires gets a tighter threshold and its calibration case written into README.md and into this file, so a later session cannot loosen it back.

## THE MISS THAT MATTERS: SNOWBALL copycat scored CLEAN 0/100, 7 Sep 2026
- [stated] Ben pasted the alert: "This was a scam. How did you not detect that? Fix that in the future."
- The alert: `0x3fbf37267a7a0f54b9062a465a997e4698925910`, 12:16, **CLEAN risk 0/100**, $967,601 mcap, 2.9h old, "+ the launch cluster is OUT, that overhang is gone; opening was not bundled; top 10 hold only 4%".
- **It is the COPYCAT SNOWBALL already documented in [[snowball-launchpad]].** The real one is `0xb19f5a6e22f9057980b08ff05b7f5a6878ab0c8a`. Memory had the collision recorded the same day and rugscan had no way to use it.

### THREE INDEPENDENT FAILURES, all now fixed in code on the droplet
**1. NOT ONE OF THE NINE DETECTORS EVER READ THE TOKEN CONTRACT.** Every one measures wallet flow. The copycat's 6,852 bytes contain the revert strings `Sell limit per block exceeded`, `Max wallet size exceeded`, `Max transaction amount exceeded`, `Cannot rescue own token`, plus an `enableTrading()` selector. The real SNOWBALL is 3,248 bytes with none of that. **`owner()` returns 0x0 and that is NOT reassurance: renouncing freezes those limits in place permanently.** A per-block sell limit is the soft honeypot: buys are free, exits are throttled, and it is invisible to every flow-based measure.
- Fix: **`detect.contract_risk(token)`**, one `eth_getCode`. Scores revert-string fingerprints (a solidity `require()` message survives optimisation and names the restriction in the author's own words) plus dangerous selectors. **An unreadable contract scores 25, never 0** (fail closed, same doctrine as the `code()` bug).
- Calibration: copycat **100 pts**; real SNOWBALL, DELTA, OPTIMUS, GG, PONS, PAIR all **0 pts**.

**2. NO IMPERSONATION CHECK.** The real coin launched **51 minutes earlier with 4.6x the 24h volume**. rugscan printed "SNOWBALL" with no caveat because it only ever looks at the one address it is handed.
- Fix: **`detect.impersonation(token)`**. **v1 of this was wrong and the failure is worth keeping: it scored 90 on GG, OPTIMUS and the REAL SNOWBALL, because ticker squatters register a name weeks early and never trade.** A dormant address is not a rival. v2 requires a sibling to have **24h volume >= $50k** to count, and scores **only the single worst sibling, never the sum**. After that: copycat 75, everything else 0.

**3. "HISTORY IS NOT SCORED" WAS APPLIED TO A 2.9-HOUR-OLD TOKEN.** This is the subtle one. The judge() v2 doctrine came from a backtest of tokens **weeks** past a pump and dump, where "are they out NOW" is genuinely a different question from "were they in at launch". **Under 24 hours those are the same question.** The alert literally printed `11 near-identical opening buy sizes - farm wallets` and `the opening cluster dumped 80% of its buy early` and scored both at zero, then printed "the launch cluster is OUT, that overhang is gone" as a POSITIVE. At hour three, "out" means they dumped on whoever bought.
- Fix: **young-token gate in judge().** Under 24h, uniform buys (+15), an early dump >=60% (+15) and a same-block bundle (+12) are scored, and the "cluster is OUT" positive is replaced by `the launch cluster took X% and was already gone Nh in - that is not an exit, that is the dump` (+10).

### Deployment and regression, all verified live
- Patch `/opt/rugscan/patch_contract.py`, idempotent, exact-string replacement with `assert OLD in s`, **11 self-checks, 0 failed**, `ast.parse` before write, backups `detect.py.bak-20260907-060402` and `rugscan.py.bak-20260907-060402`. Written on the Mac in `~/rugscan-deploy/`, scp'd, run on the droplet.
- **copycat CLEAN 0 -> AVOID 100/100.** Names the sell limit, the max-wallet cap, the max-tx cap and the 51-minute-earlier original.
- **real SNOWBALL (4.6h old, genuinely bundled, 15 near-identical buys, cluster took 76% and left) -> CAUTION 37.** Correct and honest: no contract flags, no impersonation flag, but the young-token gate fires. This is the gate working, not a false positive.
- **DELTA 0, OPTIMUS 0, GG 0, PAIR 0.** No regression on mature tokens.
- The miss case and its thresholds are written into `/opt/rugscan/README.md` under "MISS CASE, 7 Sep 2026 ... Do not loosen these back."

### THE GENERALISABLE LESSON, worth more than the three fixes
**rugscan was a flow detector calling itself a rug detector.** It could see what wallets did and was structurally blind to (a) what the code permits, and (b) whether the token is even the one it claims to be. Both are single cheap calls. **Before trusting any "clean" verdict, ask what class of evidence the detector cannot see at all** — that is where the miss will be, not in a threshold.
Corollary now in code: **a CLEAN verdict on a token under 24 hours old is a much weaker statement than a CLEAN on a mature one**, because most of the scored evidence has not had time to exist.
