You have read "Smart Money is buying" in forty tweets this month. Could you point at a Smart Money wallet if one were on your screen — no label, just the numbers?
I couldn't. So I built a card game to find out how much of a wallet's label is actually visible in its numbers, and then I wrote the dumbest possible reader — one page of thresholds, no model, no LLM — to see how far "reading a wallet" goes before it stops being a skill and starts being a lookup.
Play it: labelme.edycu.dev · code: github.com/edycutjong/labelme
The game, in one card
Ten real ethereum wallets. Each card shows four things Nansen computes for the address over the last 30 days — PnL, top trades, balance profile, counterparty mix — and five chips: Smart Money · Exchange · Whale · Contract / Pool · Regular. You pick one. The reveal is Nansen's own label group for that wallet, plus a one-line tell written from the card's numbers.
Here is card 1 of the round I recorded, straight from the CLI (npm run labelme -- play --seed meridian1933 --answers):
Wallet 1 of 10 · ethereum
PnL 30d realised +$12K · win rate 67% · 472 trades · 6 tokens · top: GIVE 4%, GULD 3%, ETH 3%, WIN -10%, ELMT -13%
Trades GIVE +$6.6K (113b/44s) · GULD +$5.4K (67b/113s) · ETH +$2 (2b/5s) · WIN −$68 (48b/9s) · ELMT −$87 (22b/48s)
Balance 10 tokens · $14K · top GULD 86% · stables 0%
Counterp. 9 in 30 d · 472 interactions · top outflow 51% · DEX 97% · wealth-tagged 0% · contracts 0% · unlabelled 3%
→ Smart Money
tell: 472 trades in 30 d · win rate 67% · realised $12K across 6 tokens · 97% of flow through DEX pools and routers — a trader Nansen tracks as Smart Money
The important design decision is that the answer key is not my opinion. A card is Smart Money because the address came back from tgm/holders with label_type: "smart_money", or from smart-money/dex-trades. It is an exchange because it came back from the exchange filter. It is Regular because it came back from tgm/who-bought-sold with all seventeen of Nansen's label groups excluded — an honest negative, "none of the groups", not a positive identification. Every clue on the face is a field from one of four profiler calls. If Nansen is wrong, the card is wrong, and the reveal says exactly which call decided it.
The house rule: one page of thresholds
Once the deck existed, the obvious question was: how much of this can a rule read? Not a classifier — a rule a player can read, argue with, and beat. This is the whole thing, from packages/core/src/reader.ts:
export const READER = {
/** a pool: traffic without a trader, and few tokens (a two-sided pool holds two) — exchange hot wallets have traffic AND 20–100 tokens */
contract: { minInteractions: 2000, interactionsPerCounterparty: 20, maxTokens: 20 },
/** an exchange: big, many tokens, no DEX trades; either its counterparties are wealth-tagged/exchange wallets or it simply holds 50+ tokens */
exchange: { minUsd: 10_000_000, maxTrades: 5, minTokens: 15, minWealthMix: 0.5, manyTokens: 50 },
/** a whale: one position is most of a $1M+ balance and it barely trades */
whale: { minUsd: 1_000_000, minTopShare: 0.8, maxTrades: 10 },
/** Smart Money (active): many trades across many tokens in 30 days — win rate and sign of PnL do NOT separate it from a regular buyer on this deck */
smartMoney: { minTrades: 25, minTokensTraded: 6 },
} as const;
The reader runs those in order — contract, exchange, whale, Smart Money, else Regular — and returns a guess with a sentence:
const traffic = k.interactions >= R.contract.minInteractions || (k.countCapped && perCp >= R.contract.interactionsPerCounterparty);
if (traffic && b.tokens < R.contract.maxTokens)
return {
guess: "contract",
because: `${k.interactions} interactions from ${k.count}${k.countCapped ? "+" : ""} counterparties and only ${b.tokens} tokens — traffic, not trading`,
};
On the 62-card deck it reads 50/62 — 81%. On ten fresh draws it had never seen (the benchmark, docs/BENCH.md), it read 7/10. After every round the score screen shows the house rule's score next to yours. That's the bar.
What the thresholds taught me
Two things I believed going in were wrong, and the deck said so.
Win rate does not identify Smart Money. I assumed the tell would be "wins more". It isn't. On this deck, a Smart Money wallet's win rate and the sign of its PnL do not separate it from a regular buyer who got lucky on one token. What separates it is breadth and tempo: 25+ trades across 6+ tokens in 30 days. Smart Money, as Nansen labels it, looks like a job, not a streak.
Exchanges are not "big wallets". Whales are big wallets. An exchange hot wallet is big and holds 15–100 tokens and barely trades on DEXes and — the clue that surprised me — its counterparties are themselves wealth-tagged or exchange-labelled wallets. Money moves in and out; nobody is trading. Card 2 of the recorded round is a $9.4B Binance wallet whose 30-day PnL is exactly $0.
Where the rule fails — and why that's the game
The confusion matrix on the deck, rows are truth:
| truth \ guess | smart-money | exchange | whale | contract | regular |
|---|---|---|---|---|---|
| smart-money | 10 | 0 | 0 | 0 | 2 |
| exchange | 0 | 14 | 1 | 0 | 1 |
| whale | 0 | 0 | 11 | 1 | 0 |
| contract | 1 | 0 | 0 | 3 | 6 |
| regular | 0 | 0 | 0 | 0 | 12 |
The rule is nearly perfect on Regular and good on Smart Money, exchanges and whales. It is bad at contracts: 6 of 10 read as Regular. A dormant proxy or multisig — zero counterparties in 30 days, a handful of tokens, no trades — has no numbers, and "no numbers" is what Regular looks like too. A Bybit wallet holding eight tokens reads as a whale. A "Token Millionaire" with 5,019 trades reads as a contract, because that much traffic from few counterparties is what a pool looks like.
Those twelve cards are the hard ones, and a human can beat the rule on them — that's what makes it a game rather than a demo. The rule doesn't know that a $165K balance in one MOG position with zero activity is more likely a proxy than a person, because the rule has no priors. You do.
Building it so it can't break on camera
The deck was recorded once, live, by scripts/seed.ts: 163 candidate addresses from the sourcing calls, 101 dropped with a reason (100 of them for being in Nansen's Public Figure group — a person label is not a wallet behaviour, and the class turned out unreadable from clues), 62 dealt. Every raw response is committed. Each card carries a sha256 of {address, chain, class, nansenLabel, entity, clues, tell}, so npm run verify replays all 62 offline and fails if a clue, a tell or an answer drifts. The default round makes zero network calls and costs zero credits. Only "Draw fresh" is live — one sourcing page and four clue calls, about 12 credits, cold p50 2.1 s, streamed to the page as each call lands.
134 tests, 100% statement/branch/function/line coverage on the engine, 12,000 property-based cases on the reader (it must be total over any clues, the tell must be one line, the hash must ignore time).
Honest limits
- Free-tier labels are wealth and structural tags (
Token Millionaire,MultiSig), not entity names. An entity name (🏦 Binance) appears only where a 1-credit transfer lookup found one — 23 of 62 cards. - "Regular" is a negative. It means Nansen filed the wallet in none of its groups, not that it's a retail trader.
- A dormant Smart Money wallet is unreadable from cheap clues; the deck keeps only active ones. A live draw can still deal one, and says so.
- Ethereum only. The 30-day window is a snapshot from 2026-09-18.
- The house rule's thresholds were set by looking at this deck. The 7/10 out-of-sample number is the honest one; the 81% is not.
The round in this post is shareable: labelme.edycu.dev/r/meridian1933 — same ten cards for everyone. I got 7/10, which is exactly what the rule got. If you beat it, I'd genuinely like to know which of the hard cards you read right.
Top comments (0)