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 (1)
The honest negative class and the out-of-sample score make this especially useful. One extension I would test is temporal stability: calculate the same features over 1 day, 7 days and 30 days, then measure how often the label changes as the window rolls forward. Showing label confidence together with the feature window and last activity would keep a behavioral label from becoming a permanent identity claim.