---
name: "onchain-forensics"
description: "Wallet and token forensics on EVM chains (Robinhood Chain, Uniswap v4) and on Solana. Opens with a runnable seller-ledger script that must be executed rather than hand-rolled, then a hard pre-delivery gate, then the full trap ledger: LP concentration, JIT volume, PoolKey encoding, staking vs selling, cost basis, tx.origin vs token sender, Solana tx versions, shared exchange deposit addresses, and the tools that structurally cannot see them."
---

# READ THIS FIRST. THE RULES BELOW DID NOT WORK AS PROSE.

On 2 Sep 2026 the rule "an unclassified counterparty is not a sale" was written into this file in the morning and **broken the same afternoon**, producing two wallets reported to Ben as 83% and 75% sellers that had sold almost nothing. They were transferring wallet to wallet.

**The lesson is not "try harder". A rule buried in a long file loses to the convenience of `sum(outbound)`. So the method is now a script you RUN, and a gate that BLOCKS delivery.**

---

# STEP 0: RUN THE SCRIPT. DO NOT HAND-ROLL THE QUERY.

Any question of the form "who is selling", "who is dumping", "what is the overhang", "how much do they have left", "seller analysis", "who is capping this" is answered by running this. Writing a fresh `sum(value) group by "from"` query is the error, every time.

```python
#!/usr/bin/env python3
"""seller_ledger.py -- the ONLY approved way to answer "who is selling and what is left".
  python3 seller_ledger.py <TOKEN_CA> [--stake <STAKING_CA>] [--chain robinhood] [--min-sold 1000000]
Enforced definitions, each one a past reported error:
  SELL      = transfer into a VERIFIED SWAP VENUE only. Never any other outbound.
  VENUE SET = auto-derived {net-flow ~0, >300 legs, empty balance} + every DexScreener pool.
  LEFT      = balanceOf(token) + balanceOf(staking). A zero wallet balance is NOT an exit.
  ACQUIRED  = inbound excluding the staking contract. Unstake returns are not buys.
  A stake is read from the staking contract's own accessor, never inferred from a transfer to it.
"""
import json,urllib.request,time,argparse
K="<DUNE_KEY from research-sources>"; QID=8562361
RPC="https://rpc.mainnet.chain.robinhood.com"
UA={"Content-Type":"application/json","User-Agent":"Mozilla/5.0 Chrome/128.0"}

def rpcb(calls):
    for _ in range(4):
        try: return json.load(urllib.request.urlopen(urllib.request.Request(RPC,data=json.dumps(calls).encode(),headers=UA),timeout=55))
        except Exception: time.sleep(2)
    return []

def dune(sql,limit=400):
    urllib.request.urlopen(urllib.request.Request(f"https://api.dune.com/api/v1/query/{QID}",
        data=json.dumps({"query_sql":sql}).encode(),
        headers={"X-Dune-API-Key":K,"Content-Type":"application/json"},method="PATCH"),timeout=40).read()
    # performance "large" is REJECTED as "Invalid performance tier" on this plan. medium only.
    eid=json.load(urllib.request.urlopen(urllib.request.Request(f"https://api.dune.com/api/v1/query/{QID}/execute",
        data=json.dumps({"performance":"medium"}).encode(),
        headers={"X-Dune-API-Key":K,"Content-Type":"application/json"}),timeout=40))["execution_id"]
    for _ in range(40):
        st=json.load(urllib.request.urlopen(urllib.request.Request(
            f"https://api.dune.com/api/v1/execution/{eid}/status",headers={"X-Dune-API-Key":K}),timeout=30))
        if st["state"]=="QUERY_STATE_COMPLETED": break
        if st["state"]=="QUERY_STATE_FAILED": raise RuntimeError(st.get("error"))
        time.sleep(7)
    return json.load(urllib.request.urlopen(urllib.request.Request(
        f"https://api.dune.com/api/v1/execution/{eid}/results?limit={limit}",
        headers={"X-Dune-API-Key":K}),timeout=60))["result"]["rows"]

def main():
    ap=argparse.ArgumentParser(); ap.add_argument("token"); ap.add_argument("--stake",default=None)
    ap.add_argument("--chain",default="robinhood"); ap.add_argument("--min-sold",type=float,default=1_000_000)
    a=ap.parse_args()
    T=a.token.lower(); SK=(a.stake or "0x"+"0"*40).lower(); tbl=f"erc20_{a.chain}"
    px=json.load(urllib.request.urlopen(urllib.request.Request(
        f"https://api.dexscreener.com/latest/dex/tokens/{a.token}",headers={"User-Agent":"Mozilla/5.0"}),timeout=25))
    pairs=sorted(px["pairs"],key=lambda p:-(p.get("liquidity",{}).get("usd") or 0))
    P=float(pairs[0]["priceUsd"])
    pools=[p["pairAddress"].lower() for p in pairs if p["pairAddress"].startswith("0x") and len(p["pairAddress"])==42]
    print(f"price ${P}  mcap ${pairs[0].get('marketCap'):,}  pairs {len(pairs)}")

    print("\n[1/4] deriving swap-venue set ...")
    rows=dune(f"""
      select a, sum(inv) i, sum(outv) o, count(*) legs from (
        select \"to\" a, value/1e18 inv, cast(0.0 as double) outv from {tbl}.evt_Transfer where contract_address={T}
        union all
        select \"from\" a, cast(0.0 as double) inv, value/1e18 outv from {tbl}.evt_Transfer where contract_address={T})
      group by 1 having count(*)>300 order by o desc limit 200""",200)
    cand=[r["a"] for r in rows if abs(r["i"]-r["o"])<max(1.0,r["o"]*1e-9)]
    res={}
    for i in range(0,len(cand),20):
        cl=[{"jsonrpc":"2.0","id":f"b{i+j}","method":"eth_call",
             "params":[{"to":a.token,"data":"0x70a08231"+"0"*24+x[2:]},"latest"]} for j,x in enumerate(cand[i:i+20])]
        for o in rpcb(cl): res[o["id"]]=o.get("result")
    venue={x for k,x in enumerate(cand)
           if (int(res.get(f"b{k}") or "0x0",16)/1e18) < 1000}
    venue |= set(pools); venue.discard(SK); venue.discard(T)
    print(f"    venue set: {len(venue)} addresses")

    print("[2/4] building seller ledger ...")
    vlist=",".join(f"({v})" for v in sorted(venue))
    rows=dune(f"""
      with venue as (select * from (values {vlist}) as t(v)),
      s as (select \"from\" a, value/1e18 sold, evt_block_time t from {tbl}.evt_Transfer
            where contract_address={T} and \"to\" in (select v from venue)),
      q as (select \"to\" a, sum(value/1e18) acq from {tbl}.evt_Transfer
            where contract_address={T} and \"from\" != {SK} group by 1)
      select s.a addr, coalesce(max(q.acq),0) acquired, sum(s.sold) sold,
             sum(case when s.t>now()-interval '24' hour then s.sold else 0 end) s24,
             sum(case when s.t>now()-interval '6' hour then s.sold else 0 end) s6,
             max(s.t) last_sell, count(*) legs
      from s left join q on q.a=s.a
      where s.a not in (select v from venue) and s.a != {SK} and s.a != {T}
        and s.a != 0x0000000000000000000000000000000000000000
      group by 1 having sum(s.sold) > {a.min_sold} order by sold desc limit 400""")

    print("[3/4] classifying and reading live balances ...")
    ad=[r["addr"] for r in rows]; res={}
    for i in range(0,len(ad),12):
        cl=[]
        for j,x in enumerate(ad[i:i+12]):
            k=i+j
            cl+=[{"jsonrpc":"2.0","id":f"c{k}","method":"eth_getCode","params":[x,"latest"]},
                 {"jsonrpc":"2.0","id":f"b{k}","method":"eth_call","params":[{"to":a.token,"data":"0x70a08231"+"0"*24+x[2:]},"latest"]}]
            if a.stake: cl.append({"jsonrpc":"2.0","id":f"s{k}","method":"eth_call",
                                   "params":[{"to":a.stake,"data":"0x70a08231"+"0"*24+x[2:]},"latest"]})
        for o in rpcb(cl): res[o["id"]]=o.get("result")
        time.sleep(0.2)
    out=[]
    for k,r in enumerate(rows):
        c=res.get(f"c{k}") or "0x"
        kind="EOA" if c=="0x" else ("7702" if c.startswith("0xef0100") else "CTR")
        bal=int(res.get(f"b{k}") or "0x0",16)/1e18
        st=int(res.get(f"s{k}") or "0x0",16)/1e18
        out.append(dict(a=r["addr"],kind=kind,bal=bal,st=st,left=bal+st,acq=r["acquired"],sold=r["sold"],
                        s24=r["s24"],s6=r["s6"],last=r["last_sell"],
                        pct=(r["sold"]/r["acquired"]*100) if r["acquired"]>0 else 0))
    print("[4/4] done\n")

    fin=[x for x in out if x["left"]<50000 and x["sold"]>1_000_000]
    act=sorted([x for x in out if x["left"]>50000 and x["pct"]>=50 and x["s24"]>0],key=lambda y:-y["left"])
    pau=sorted([x for x in out if x["left"]>50000 and x["pct"]>=50 and x["s24"]==0],key=lambda y:-y["left"])
    def tbl_(t,rr):
        print(f"=== {t} ===")
        print(f"{'wallet':44s}{'kind':>6}{'acquired':>14}{'SOLD':>14}{'%':>5}{'24h':>11}{'6h':>10}{'LEFT':>13}{'$ LEFT':>10}")
        tot=0
        for x in rr:
            tot+=x["left"]
            print(f"{x['a']:44s}{x['kind']:>6}{x['acq']:>14,.0f}{x['sold']:>14,.0f}{x['pct']:>4.0f}%"
                  f"{x['s24']:>11,.0f}{x['s6']:>10,.0f}{x['left']:>13,.0f}{x['left']*P:>10,.0f}")
        print(f"{'TOTAL '+str(len(rr)):44s}{'':>54}{tot:>13,.0f}{tot*P:>10,.0f}\n"); return tot
    print(f"FINISHED: {len(fin)} wallets dumped {sum(x['sold'] for x in fin):,.0f}, "
          f"hold {sum(x['left'] for x in fin):,.0f} (${sum(x['left'] for x in fin)*P:,.0f})\n")
    ta=tbl_("ACTIVELY SELLING (>=50% sold into a venue, sold again in 24h)",act)
    tp=tbl_("PAUSED MID-EXIT (>=50% sold, nothing in 24h)",pau)
    liq=sum(x['bal'] for x in act+pau); stk=sum(x['st'] for x in act+pau)
    print(f"IN DISTRIBUTION: {len(act)+len(pau)} wallets, {ta+tp:,.0f} tokens = ${(ta+tp)*P:,.0f}")
    print(f"  liquid now ${liq*P:,.0f} | staked ${stk*P:,.0f}")
    print(f"\nGATE: full addresses [ok] | venue-only sells [ok] | "
          f"LEFT includes staking [{'ok' if a.stake else 'NO STAKING CA PASSED'}] | price ${P} stamped [ok]")
    json.dump(out,open("seller_ledger_out.json","w"))

if __name__ == "__main__":
    main()
```

**Verified 2 Sep 2026 on BOW 0x451b42A15100C340CA12F7c66DE06fac5EA2D751 with `--stake 0xeba502e1177f5fad9432c2deb273569d8381e01a`.** It derived a 53-address venue set, correctly EXCLUDED the two wallet-to-wallet transferrers a hand-rolled query had reported as sellers, and INCLUDED two real sellers the manual pass missed. The script is more accurate than the hand pass. That is the whole point.

**Do not forget `if __name__ == "__main__": main()`.** Without it the script exits 0 and prints nothing, which reads exactly like a silent failure.

---

# STEP 1: THE DELIVERY GATE. Every line must pass or the answer does not ship.

1. **Did a sale get counted only for transfers into the derived venue set?** If any outbound to an unclassified address was counted, STOP and re-derive.
2. **Was every destination that carries >5% of a wallet's outflow classified with `eth_getCode`?** An 83-byte contract and two zero-balance EOAs were once reported as sale venues.
3. **Does LEFT include the staking balance, read from the staking contract's own accessor?** A zero `balanceOf` is not an exit.
4. **Was every stake read from the contract, never inferred from a transfer into it?**
5. **Is there a cost basis and a realised average beside every sold%?** No basis means no behavioural claim.
6. **Is every address printed in full, never truncated?** No ellipsis anywhere, ever, EVM or Solana.
7. **Is the price, market cap and chain-head timestamp stamped on the output?** Bangkok time in anything Ben reads.
8. **Is every behavioural label timestamped?** "Actively selling" goes stale in three hours.
9. **Is every source that failed named in the output?**
10. **If a prior claim in this thread is now wrong, is it corrected at the TOP of the reply, naming the number that misled?**
11. **SOLANA ONLY: did the follow stop at the first shared exchange deposit address, and does the ledger reconcile to live balances?** See the Solana section.
12. **Was every native-asset to dollar conversion done with a price fetched in the SAME script, and was one converted figure cross-checked against a dollar figure the data already printed?** A ten-pass PAID screen used ~$229 for SOL when it was $111.53 and doubled every figure in it.

---

# THE SEVEN DEFINITIONS THAT KEEP BEING GOT WRONG

| Thing | WRONG way it was done | RIGHT way |
|---|---|---|
| A sale | any outbound transfer | only a transfer into the derived venue set |
| A stake | a transfer into the staking contract | `balanceOf`/`staked` on that contract |
| An exit | `balanceOf` == 0 | `balanceOf` + staking + pending == 0 |
| Acquired | all inbound | inbound excluding unstake returns |
| A trader | anything with outbound flow | `eth_getCode` first; net-zero + thousands of legs = infrastructure |
| Proceeds | WETH transfers | check WETH **and** native ETH traces joined to the token's own txs |
| Sold% | sold / acquired | sold / acquired **plus** cost basis and realised average |

**Per-user staking accessors are contract-specific and do NOT carry across tokens.** TWO's `StakingVaultV2` uses `staked(address)` and `pending(address)`. BOW's Synthetix-style `StakingRewards` uses plain `balanceOf(address)` and reverts on `staked`. **Pull the ABI from the explorer for each token. Never reuse a selector from the last one.**

**Classify the pasted address before analysing it.** 0x451b42A15100C340CA12F7c66DE06fac5EA2D751 was handed over as a wallet and is a token contract (`PonsV2LauncherToken`). `eth_getCode` plus `name()`/`symbol()` settles it in one call.

---

# SOLANA: following money through a wallet chain

Added 23 Sep 2026 after the PAID / UsePaid creator-fee trace. Four traps, all of which produced a wrong statement to Ben.

## 1. `maxSupportedTransactionVersion` must be 2, never 0

**Version-1 transactions now exist on mainnet.** `getTransaction` with `maxSupportedTransactionVersion: 0` does not return them and does not error at the top level: it returns `{"error": {"code": -32015}}`, which a `.get("result")` wrapper turns into a silent `None`. A live watcher ran for two days with 0 and **dropped every one of the target wallet's memecoin trades**. Always pass 2. On any Solana job, count the transactions you failed to parse and print that count.

## 2. THE CHAIN STOPS AT THE FIRST EXCHANGE DEPOSIT ADDRESS

A deposit address is **shared**: many unrelated people deposit into the same one, and it sweeps to a hot wallet. Anything that leaves it is not your subject's money.

Signature of a shared deposit address: near-zero balance, 1,000 signatures over a short span, **many unrelated senders in, one destination out**. Contrast with a *personal* deposit address: the same one-destination-out shape, but a low signature count over a long span (169 signatures over 200 days) and a small sender set.

**Receipt:** an alert reported 141.48 SOL as the dev's because that was the total that landed at `GcqwXjKzQXXYL4hBdNv6iSbZb71rqbUn5JjjioF3EaV7`. Only 90.00 SOL of it was his. Follow one hop to identify the address, then stop, and say in the output that the trail ends there.

## 3. A wrapped-SOL token account's lamports are the same money as its WSOL balance

In a swap, the pool's WSOL vault shows up **twice**: once as a native lamport delta on the vault address, and once as a `So1111...112` token-balance delta owned by the pool. Summing both double-counts the trade. Decide one convention and hold it.

This also settles "is this a transfer or a swap": read `getAccountInfo` on the destination. Owner `TokenkegQfe...` with `mint == So1111...112` means **a pool's SOL vault, so this is a swap, not a payment**. Owner `1111...1111` means a real wallet.

## 4. Prove a buyback, do not infer one

A real buyback is two things in sequence, both visible: SOL out to a pool vault with the token balance going UP, then a transaction whose `logMessages` contain `Instruction: Burn` or `BurnChecked` with the token balance going DOWN. **Price every burn in dollars and separate it from the launch or graduation burn**, which is not a buyback (on PAID, 15,065,204 tokens burned on day one was the allocation burn; only 332,259 was bought back).

**A buyback can be real and still be a rounding error.** Report it as a percentage of fees claimed, not as a yes/no. PAID: 1.7% of fees to buyback, 92.2% to exchanges.

## 5. Solana endpoint notes

- `api.mainnet-beta.solana.com` handles 8 to 10 threads. `getTokenLargestAccounts` 429s instantly: never use it, and never wrap it in a sleep/retry loop. Use `advanced-api-v2.pump.fun/coins/top-holders/{mint}` instead.
- `getTokenAccountsByOwner` with only `TokenkegQfe...` **misses Token-2022 balances**. Query both program ids before saying a wallet holds nothing.
- `getSignaturesForAddress` pages with `before`; 1,000 per call is the cap.
- Reconcile every wallet: inflows minus outflows minus trade PnL must equal the live `getBalance`. If it does not, a leg is missing and the answer is not ready. On the PAID mid-hop this reconciled to 0.004 SOL and that is what made the ledger trustworthy.
- Market cap comes from DexScreener, never from an on-chain supply read: DexScreener nets out burns (COPPERINU read 1,000,000,000 on chain against 919,545,414 circulating, a 9% error).

---

# Uniswap v4 measurement suite

## MANDATORY on any v4 token: the LP concentration check

**GoPlus structurally cannot see v4 LP concentration.** On TWO it reported 2 LP holders and missed a position that was 98.41% of the real pool. `is_locked: 0` from GoPlus is neither a pass nor a fail on v4.

1. Pull the operator EOA's ERC-721 list: `/api/v2/addresses/{addr}/nft?type=ERC-721`
2. For each `UNI-V4-POSM` tokenId on the PositionManager: `positionInfo(uint256)` (top 25 bytes are the poolId prefix), `getPoolAndPositionInfo(uint256)` (the full PoolKey), `getPositionLiquidity(uint256)`
3. Read the pool's in-range L from the PoolManager singleton: `extsload(bytes32)` at `keccak256(poolId . uint256(6)) + 3`
4. Divide

**Cross-validation:** two independent reads landing within a few percent confirms the slot layout. **State the caveat:** slot+3 is IN-RANGE liquidity, so the honest claim is "N% of active liquidity backing the current price".

**A deployer-owned position in a bare EOA with no locker is the fatal column.** No position size survives it.

**Proving a locker is fee-only: use `eth_simulateV1`, not bytecode reading.** Call `claim(tokenId)` as the owner then re-read state in the same bundle. `liquidityDelta = 0`, byte-identical `getPositionLiquidity`, unchanged `ownerOf` means the principal cannot leave.

**A locker ABI with no withdraw, release, unlock, sweep or transfer function makes that supply structurally immobile.** BOW's `V2LaunchLocker` 0x267444D099b10fB5Ed7c3Cc7B7c767AdcA574952 holds 8.16% of supply and has no ERC-20 exit path at all. No aggregator shows this. Check it, and credit it.

## MANDATORY: solve tickSpacing before any PoolKey call

**A wrong PoolKey returns zeros indistinguishable from an empty pool.** Brute force `tickSpacing` until `keccak256(abi.encode(PoolKey)) == poolId`. Candidates: 1, 10, 20, 30, 50, 60, 100, 120, 200, 500, 1000, 2000. Fee 3000 on Twofold solved to **60**, not the 200 that `getPoolAndPositionInfo` returned for a different pool. **v4 decouples fee from tickSpacing. Never guess.**

## MANDATORY on a JIT hook: volume and TVL

- **Zero in-range liquidity is the EXPECTED state, not a finding.**
- **TVL comes from the hook's own `getReserves(PoolKey)` per pool**, not raw balances and not an aggregator.
- **Size every pool individually.** Uniform pool sizes are an operator-seeding signature; the one pool that breaks the pattern is the only real user.
- **NEVER read swap volume off vault withdrawal amounts.** Overstated by 25x. Only the leg touching the PoolManager is swap volume: $3,399.53 withdrawn, **$641.52 to the PoolManager**, $2,758.01 returned.

## MANDATORY: the toll booth test, read from bytecode

1. `eth_call` every fee selector on hook and token: `feeBps()`, `fee()`, `protocolFee()`, `protocolFeeBps()`, `claimFeeBps()`, `MAX_FEE()`, `maxFee()`, `treasury()`, `feeRecipient()`, `protocolFeeRecipient()`
2. **Check whether a distribution setter is secretly a fee split.** TWO's `getDistribution(bytes32)` looked like one and returns int24 tick bounds.
3. Read the pool's own `protocolFee` from slot0.
4. All empty means **there is no fee switch to flip**. Extraction needs a redeploy, which is structural and permanent.
5. **A locked LP whose fees pay a bare EOA is not value accrual.** Check `feeRecipient()` for code, and whether `setFeeRecipient` is still operator-controlled.

---

# Infrastructure and identity

- **`tx.origin` is not the token sender.** A `0x4337`-prefixed EOA calling `handleOps` (**selector 0x765e827f**) on a verified `EntryPoint` is an **ERC-4337 bundler**: it is tx.origin on thousands of other people's swaps while its own `balanceOf` never moves. On Robinhood Chain most retail swaps arrive this way.
- **EIP-7702 delegated EOAs read as 24-byte contracts**, code `0xef0100` + 20-byte implementation. They are people, not infrastructure. Decode rather than lumping them with contracts.
- **ERC-7760 proxies of `TradingVaultImplementation` are retail positions in a wrapper**, not whales and not one entity. On BOW they are most of the top-30 holders.
- **Net flow of exactly zero plus a leg count in the thousands plus an empty balance is the infrastructure signature.** That triple is what the script uses to derive the venue set.
- **A wallet receiving dozens of unrelated tokens in 50M-plus lots is a rotator, not a holder.** Its selling of your token is portfolio rotation, not a verdict on the project. Check its non-target inbound before calling it a distributor.
- **A pump.fun profile hands over both chains' wallets.** `frontend-api-v3.pump.fun/users/{address}` returns `canonical_svm_wallet` AND `canonical_evm_wallet`. There is no username-to-address endpoint, but the server-rendered HTML of `pump.fun/profile/{username}` carries the address: curl it, because the rendered page is blank without a sign-in.

---

# Aggregator and pricing traps, all live

- **DexScreener and GeckoTerminal miss v4 hook pools they do not list.** DexScreener showed 3 of 20 Twofold pools and missed the flagship.
- **CORRECTED 2 Sep 2026: a v4 pool DexScreener DOES list can be indexed completely.** Measured on TWO/WETH: 711 on-chain swaps in 8.30 hours against a DexScreener h6 of 508, a match on scaling. **The blind spot is pools it never lists. Test before asserting either way.**
- **DexScreener HAS a wallet filter**: a "Filter by Trader" modal and a `?maker=0x...` URL parameter. **The maker it indexes is `tx.origin`, not the v4 `Swap` event's `sender`** (that field is the router). **Never claim a tool lacks a feature without opening it.**
- **A pool fee tier is not a transfer tax.** Read the fee from the v4 `Swap` event's `fee` field and always state the round trip.
- **Any honeypot scanner that only knows the v3 factory prints price impact as a tax on a v4-native token.** Fomo's "91.34% buy fees" on TWO was a ~$1,030 simulated buy into a $118 v3 dust pool.
- **Never take liquidity from fomo.** It read $152.2K against a real $281K, and $11.2K against $39.3K.
- **Hardcode known stablecoins at $1.** DexScreener priced USDe at $11,476,674 per unit off a thin pool.
- **Equity-quoted pools price in garbage, and it hits OHLCV too, not just the price field.** GeckoTerminal's daily candles on BOW's deepest v4 pool return ~775 per BOW because the pair is BOW/SPY. **Always use a USDG or stable-quoted pool for a series, and take mcap from the highest-VOLUME pair.**
- **ERC-4626 balances: use `convertToAssets(balanceOf(x))`.**
- **DefiLlama's chain key is the display name** (`Robinhood Chain`), and its `mcap` field is often null or wrong.
- **Dune: `performance:"large"` is rejected as "Invalid performance tier". Use `"medium"`.** A datapoint-cap error is a billing limit, not a query bug: say so and route through Blockscout holders plus per-address `tokentx` plus direct RPC, then name the gap in the output.

---

# Interpretation discipline: the number was right, the story was wrong

- **Do not attribute a drift to a cause you cannot separate.** An operator's LP share rose 97.76% to 98.41% with his position fixed while price fell 25%; out-of-range explains it as well as withdrawals. **Report the level, say the cause is unresolved.**
- **Two reads are not a slope.** 1,416 to 1,397 holders is explorer noise.
- **Read the transfer legs before naming a cause.** "Vault TVL falling means adoption reversed" was the AMM absorbing sellers.
- **Distinguish trend from sampling noise by sampling.** Six reads across ~1,000 blocks; zero variance means the decline is real.
- **A cluster is an inference, not a fact.** Name the evidence, never report it as one wallet.
- **A collapsing clip size is a dribble, not a dump.** 1,266,071 down to 180,753 against pool depth may be noise.
- **Cost basis predicts behaviour; sold% does not.** 57.5x banked on a $484 basis means no price forces the holder out and no price is a loss. A 2x to 10x cohort still has a loss point and is shakeable both ways.
- **Never extrapolate a rate from a short window.** A 28-minute sample of PAID burns landed in a trough and produced "$9.7k/day" when the real hourly batches were 345k to 385k tokens. Sample a full cycle or say the rate is unmeasured.
- **Money SENT is not money RECEIVED.** "$856k paid out" counted transfers to X handles that may never be claimed and return to the treasury after ~14 days. Say "sent", and say what happens if it is not claimed.
- **A label in your own watcher's KNOWN map is a claim like any other.** `PaidybYx1q4xTYXMgHq1PTAsFkPsdJf6XG7KC1NUSJ8` was labelled "the official burn wallet" and is the project's payout wallet, which drips SOL into an exchange deposit. Re-verify the map whenever it drives a verdict.