---
name: coinbench
description: COINBENCH, Ben's 10-minute holder and market-cap benchmark tracker on the kookwatch droplet. Read whenever Ben pastes a new coin to track, asks "is this still filling", asks about the four rooms (cheap/expensive x filling/emptying), or asks to add/remove a token from the tracker.
sources: [cowork]
aliases: [coin-bench, holder-tracker, four-rooms, party-model, holder-velocity-tracker]
---

# COINBENCH

Built 2 Sep 2026 at Ben's request. Companion to [[rh-memestock-arena]], [[flow-model]], [[alert-bot-fleet]].

- [stated] Ben's brief: track holder count every 10 minutes for a coin he pastes, benchmark it against coins that reached $10M/$50M/$100M over their first hours, and classify into four rooms: cheap and filling, cheap and emptying, expensive and filling, expensive and emptying.
- [stated] Ben's four questions, in his own words: (1) are people still arriving, count the holders; (2) is the money staying or just passing through, volume against pool; (3) is the door cheap, market cap divided by holders; (4) how big can the room get, distance to the sector's largest.

## Where it lives
- Droplet **kookwatch 159.223.88.168**, service **coinbench.service**, code **/opt/coinbench/bench.py**, config **/opt/coinbench/config.json**, data **/opt/coinbench/bench.db** (sqlite, table `snap`).
- Pushes to the same Telegram chat as the rest of the fleet, every 10 minutes, plus a room-change alert line at the top when a token flips state.
- Add a coin: `python3 /opt/coinbench/bench.py add 0x<address> LABEL`. Remove: `... drop 0x<address>`. Mute: set `"muted": true` in config.json.
- Backfill helper for a token's pre-tracking history: `/opt/coinbench/backfill_dino.py` is the template (Dune holder curve plus GeckoTerminal minute OHLCV).

## The mechanism it runs on, derived from first principles
- Identity: **marketcap = holders x door**, where door = mcap/holders. So **dlog(mcap) = dlog(holders) + dlog(door)**. Every price move decomposes into people arriving and price per person, and those are independent.
- Rooms, on 60-minute deltas:
  - holders up + door up = **EXPANDING**, people arriving and paying more, real
  - holders up + door down = **ABSORBING**, crowd growing while price falls. Bargain if turnover is low, distribution into retail if turnover is high
  - holders down + door up = **THINNING**, fewer richer holders, squeeze or illiquid drift
  - holders down + door down = **DYING**
- Third gate: turnover = vol24 / aggregate liquidity. Under 10x people sat down, 10 to 20x mixed, over 20x front door to back door.
- Correction this build forced: **door price is a stage metric, not a value metric.** Low $/head correlates with the arena's failures, not its winners, because price appreciation is what raises $/head. What matters is the direction of the door, not its level.

## The benchmark curves, from Dune query 8562361, holders at age
Reconstructed from `erc20_robinhood.evt_Transfer` running balances in 10-minute buckets.

| age | AI $225M | BONER $46.7M | MOO | SPACEHOOD | MICRODUCK $29.6M | GG $6.0M | FATCOIN $1.2M | DINO |
|---|---|---|---|---|---|---|---|---|
| 10m | 92 | 40 | 2 | 4 | 13 | 167 | 1,872 | 466 |
| 60m | 333 | 372 | 2 | 4 | 998 | 635 | 3,132 | 2,367 |
| 120m | 667 | 321 | 2 | 135 | 1,319 | 1,255 | 3,008 | 2,913 |
| 240m | 577 | 268 | 2 | 222 | 2,280 | 1,424 | 2,737 | 3,519 |
| 1440m | 493 | 321 | 5 | 347 | 4,557 | 4,612 | 3,172 | (n/a) |

### The three findings
1. **Day-one holder count does not predict the ceiling.** AI ended day one with 493 holders and is $225M today. FATCOIN ended day one with 3,172 and is $1.2M.
2. **The cohorts must be split by launch platform or the comparison is meaningless.** Doppler tokens (AI, BONER, MOO, SPACEHOOD) hold supply on a bonding curve, so their day-one holder counts start near zero by construction. MOO shows 5 holders at 24 hours and is a $29M coin. Only instant-distribution platforms (PONS v2, LaunchHook) are valid comps for each other.
3. **Shape beats level.** The winners' curves keep climbing through day one (MICRODUCK 998 to 4,557, GG 635 to 4,612). The failure front-loads then flatlines (FATCOIN 1,872 in the first 10 minutes, then flat for 23 hours). **A first-hour holder spike followed by a flat line is the failure signature.**

## First tracked set, 2 Sep 2026 12:00 UTC
- **DINO 0x000b2164a76560323163343431Db8bE550F164B8** (the subject), plus two permanent controls: **microduck 0xD5f1afEA47b1A9eab414D2ee740cF1d6d039E725** as the winner control and **FATCOIN 0x12D5ee7917cA430073C3A638ee1e6f0648A98a01** as the failure control. Keeping both live means every reading has a same-chain, same-sector reference at the same timestamp.

## DINO first call off the system, 2 Sep 2026 12:00 UTC
- Age 4.5h. mcap $881,075, holders 3,643, door $240/head, turnover 17.4x, aggregate liquidity $204,648, h1 813 buys / 689 sells, 81% of volume still in the GOOGL pool.
- 60-minute deltas: **holders +3.7%, door -41.8%. Room = ABSORBING.**
- Peak door was $501/head at 180 minutes on a $1.75M cap. Since then the crowd grew 4% and the door fell 52%.
- Against the same-platform winners at 4 hours DINO looks ahead (1.60x MICRODUCK, 2.56x GG) but its holder slope has already decayed: +2,367 in hour one, +546 hour two, +576 hour three, +154 hour four.

## 2 Sep 2026 12:20 UTC, rebuilt after Ben's feedback
- [stated] Ben rejected the first alert format: too many numbers, and words like "seeding", "absorbing" and "bargain watch" meant nothing to him. He wants a plain recommendation: is this a buy, a strong buy, a hold, a sell. Recorded in [[preferences]].
- New output: one emoji, one action word (**STRONG BUY / BUY / HOLD / TRIM / SELL**), one sentence saying what to do, then at most five receipt lines. Code in `/opt/coinbench/verdict.py`.
- Score is five terms, each measurable: holder growth over the last hour; whether the arrival RATE is holding or decaying (this hour vs last hour); direction of the door price; turnover; holder count against the median of the winners' curve at the same age. Cuts at +6 STRONG BUY, +4 BUY, +2 HOLD, 0 TRIM, below that SELL.
- [stated] Ben also asked for visualisations in Telegram, one dot per 10 minutes.
- `/opt/coinbench/charts.py` renders a four-panel PNG per tracked coin under 48 hours old and Telegram `sendPhoto` pushes it every 10 minutes. Panels: (1) holders vs age, (2) market cap vs age log, (3) **holders added per hour**, which is the panel that actually decides the verdict, (4) door price vs age log.
- Reference curves in `/opt/coinbench/refcurves.json`: holders at 10-minute resolution from Dune query 8562361, market cap hourly from GeckoTerminal.

### Data traps hit building the charts, all worth remembering
- **GeckoTerminal `currency=usd` silently returns quote-token units on some Robinhood pools.** GG's top-liquidity pool read a price 65,000x too high. Fix: anchor by fetching the latest candle for the same pool and scaling to DexScreener's `priceUsd`; if the implied scale is more than 0.35 in log10 away from 1, discard that pool and try the next one by volume.
- **Pick the pool by 24h VOLUME, not by liquidity, for price history.** The deepest pool is often not the one that traded.
- GeckoTerminal 5-minute candles do not reach back far enough for a coin older than about two days. Use hourly for the reference coins and 5-minute only for the live one.
- Blockscout on Robinhood Chain 403s the cloud container intermittently and needs a browser User-Agent plus a retry with backoff. It is reliable from the droplet.
- Do not derive total supply from Blockscout when it may 403. Derive it as `marketCap / priceUsd` from DexScreener, which is always consistent with the cap being displayed.

## 2 Sep 2026 12:30 UTC, second round of Ben's feedback
- [stated] Ben: any coin that appears on the chart must also get a verdict in the alert. GG was drawn but never scored. **Rule: the tracked set and the chart's reference set are the same list.** GG 0xcaCB0e9caCcee63ec4d82952E561a291c68Bcb68 added.
- [stated] Ben: when a coin is not clearly one thing, say so, for example "between trim and hold", rather than forcing a single label.
- Implemented three hedge modes in `verdict.py`:
  - **edge**, the score lands exactly on a bucket boundary, prints "HOLD / TRIM" with "Borderline, could go either way on the next reading."
  - **conflict**, positive terms total +3 or more AND negative terms total -3 or more, prints the same slash label with "Not clean either way: real buying and real selling at the same time. Half position, and let the next two readings decide."
  - **partial**, less than one hour of history on that coin, prints "(partial read)" and calls the verdict provisional. Clears about an hour after a coin is added.
- Room-change alerts fire on the base bucket, not the hedged label, so a coin oscillating on a boundary does not spam.
- First hedged read: DINO **HOLD / TRIM** at 5.0h, $898k cap, 3,627 holders, $247/head, holders +3.1%/h with the arrival rate holding but the door at -40%/h. The two sides genuinely disagree, which is the case the hedge exists for.
- GG at the same moment: $6.24M cap, 6,191 holders, $1,008/head, 108h old, turnover 9x, 1.4x the winners' pace. HOLD.

## 2 Sep 2026 12:45 UTC, sectors, 16 coins, and pool hygiene
- [stated] Ben pasted eleven addresses and asked for them all in the alert, grouped by sector, with benchmarks, and asked whether it matters that a coin trades across several pools.
- Tracked set is now **16 coins in two sectors**, config at `/opt/coinbench/config.json` with a `sector` field per token.
  - **Memestock arena**: DINO, FATCOIN, microduck, GG, MOO 0xD9dB30BB0D2b8d2eae3826A1372117E058791e18, SAYLORMOON 0xD18528b39dA6464B3662c331a52181ecB15b1E18, **CHIPS 0xbcd09284dFfe06f868a4650546233e3E9AbE4d97** (Chips Party Pack, quotes against MU 35% / NVDA 26% / INTC 23%, the only basket-of-semis meme found so far, $344k), plus AI and BONER as benchmarks.
  - **RH Chain protocols**: PONS 0x39dBED3a2bd333467115dE45665cC57F813C4571 (ceiling, $281M), DELTA 0xe8ffd7e24187F72afB08d75B1bb13088A989a791, OPTIMUS 0x0fF9072a1EAD154d92C2d2Fef16AFba6028Ce2B2, HOOKR 0x18E674231A58c239Dc7DaeDcffE15Ec3A24cff5c, BOW 0x451b42A15100C340CA12F7c66DE06fac5EA2D751, PAIR 0x6b1d42927B1a84eC28Fa88d4fC6FA7AF404966be, TWO 0x2A4a33A2163D005d8E7f1D9aC08d14c98db288d5.
- **New chart type `/opt/coinbench/board.py`, one PNG per sector, cross-sectional rather than launch-curve**, because the first-24-hours benchmark is meaningless for a coin that is 100 to 1,200 hours old. Panels: (1) holders vs market cap scatter with $/head iso-lines, so cheap is bottom-right; (2) holder growth per hour, ranked; (3) turnover, ranked, with the 10x and 20x lines; (4) multiple to the sector ceiling. Launch-curve charts still fire for any coin under 48 hours old.
- **PENDING fix.** With no hour of history the score was collecting only the turnover point and landing in TRIM, so seven mature protocol tokens were told to "sell half" on their first tick. A coin with no history now returns **NO READ YET** with a white dot and no instruction. Rule this teaches: a scoring model must have an explicit "not enough data" state, or a partial score silently becomes a recommendation.

### Pool hygiene, the answer to Ben's multi-pool question, now enforced in code
- **Holder count is chain-level and already covers every pool**, so the single most important metric is unaffected by pool structure. Price is arbitraged, so market cap is nearly pool-independent too.
- **What pools DO poison is liquidity, volume and therefore turnover**, because summing across all pairs sums junk. Measured 2 Sep 12:40 UTC, spread between the highest and lowest priced pool holding over $5k: MOO 3.1%, PONS 1.8%, FATCOIN 5.6%, **DINO 68.9%, GG 956%, OPTIMUS 3,104,067%** (OPTIMUS has a pool quoting against OPTIMUS itself).
- Fix shipped in `snapshot()`: **keep only pools priced within 25% of the highest-VOLUME pool**, aggregate liquidity and volume over those, and print a warning naming how many were excluded. First run excluded 7 pools on BONER, 1 each on AI, GG, DINO, OPTIMUS, HOOKR and BOW. This is the fake-liquidity impersonator pattern from [[rh-chain-radar]], now handled automatically.
- Price for market cap comes from the highest-**volume** pool, never the highest-liquidity pool.
- The pool split that IS a real signal is the share of volume in the origin pool. Falling share means the coin has graduated into general USDG and WETH liquidity, which the arena base rates show is a maturity marker.
- DINO at 12:46 UTC after the filter: **TRIM / SELL**, $723k cap, 3,625 holders, $199/head, holders +3.0%/h with the rate holding, door **-52%/h**, turnover 21x. Removing the off-price pool is what pushed turnover over the 20x line.

## 2 Sep 2026 13:10 UTC, guards, adaptive cadence, history backfill
- [stated] Ben: hardcode the mistakes so they cannot happen again; why are so many coins NO READ YET when the history exists; and new coins deserve a 10-minute cadence while old ones do not.

### `/opt/coinbench/guards.py` — every past mistake is now a runtime check, not a note
Nine guards, each named for the wrong call it already produced. `G.check()` runs before any verdict and a violation prints **DATA CHECK FAILED** with the reason instead of a recommendation.
- G1 no history means no verdict. G2 off-price pool filter. G3 price from the highest-VOLUME pool. G4 benchmark only within the same launch platform, and only under 30 hours of age. G5 GeckoTerminal usd anchor check. G6 supply from marketCap/priceUsd. G7 a spliced holder series must be scaled at the join. G8 Blockscout browser User-Agent plus backoff. G9 a jump too large to be real is a data error: holders over 60%/h, door over 300%/h, turnover over 400x all block the verdict.

### Adaptive cadence
Under 24h old, every 10 minutes. 1 to 7 days, hourly. Over 7 days, every 4 hours. **All rates are normalised to per-hour against the previous reading, whenever it was, so cadence never changes the score.** This replaced the fixed 60-minute lookback, which was the real reason mature coins read NO READ YET.

### History backfill, `/opt/coinbench/backfill.py`
- 30 hours of holder history for all 16 coins reconstructed from `erc20_robinhood.evt_Transfer` running balances (Dune query 8562361, ~9 credits), plus GeckoTerminal hourly prices, **2,515 rows**. Every coin now has a real verdict from the first tick.
- **The Dune reconstruction runs 0 to 12% BELOW Blockscout** (measured: AI -0.0%, PONS +0.3%, DELTA -1.0%, DINO -1.4%, MICRODUCK -2.9%, MOO -3.9%, GG -6.7%, TWO -6.8%, SAYLORMOON -9.5%, CHIPS -9.3%, FATCOIN -11.8%). Splicing raw would have printed a fake double-digit arrival spike, so each series is scaled by `blockscout_now / dune_now` and a scale outside 0.5 to 2.0 is refused.
- Known residual: GeckoTerminal 429s under bursts, and on a miss the backfill falls back to the current market cap for that coin's whole history, which flattens its door series. DINO, BOW, MOO and HOOKR were affected. Only the first tick or two is exposed, after which all readings are live.

### Not reachable from this session
- [stated] Ben has a parallel Cowork session called **Party Framework** with related code he wants reviewed. **It is not reachable from here**: no other Claude session is running on this machine, no artifact by that name exists in his gallery, and a Mac-wide filename search for "party" returned nothing. Its container is separate. He has to paste the code or publish it as an artifact.

## 2 Sep 2026 14:05 UTC, fillwatch merged into coinbench
- [stated] Ben pasted the **fillwatch** handover from his parallel Cowork session. Its own recommendation: one service, coinbench as the shell, fillwatch's holder engine and cohort layer dropped in.

### Overlap settled first
- fillwatch was live on the SAME droplet with its own `*/10 * * * *` cron, pushing to the SAME Telegram chat via the rhtracker bot. **Its cron is removed.** `/opt/fillwatch/` stays as reference; nothing there runs.
- Load on the box had hit 29.98 earlier in the day with both running. 961MB total, ten alert bots, so only one holder engine may run at a time.

### What was adopted, in `/opt/coinbench/ledger.py`
Exact on-chain holder ledger from ERC-20 Transfer logs plus size cohorts, with fillwatch's four paid-for bugs preserved as named guards L1 to L4:
- **L1 an eth_getLogs timeout is NOT an empty range.** The RH RPC answers a wide window on a busy token with "log query timed out"; treating that as no logs inflated DINO balances by 22.5%. `_getlogs` halves the window on any failure and never skips one.
- **L2 duplicate logs pass a sum check.** BAWSAQ reconciled to exactly 100.0% of totalSupply with 7 negative balances. `scan()` dedupes on (txHash, logIndex) and `reconcile()` requires BOTH the sum AND zero negatives.
- **L3 EIP-7702 wallets have code and are real users.** The test is code that does not start with `0xef0100`, never code length.
- **L4** robinhood-rpc.publicnode.com 403s after sustained use, so an endpoint is benched after 6 consecutive failures.
- Cohorts: whale above 0.5% of float, mid 0.05 to 0.5%, shrimp below 0.05%, measured as SUPPLY not head count. **Absorption = whale supply lost / shrimp supply gained; near 1.0 is distribution wearing the costume of adoption.**
- `distribution()` in bench.py needs **6 consecutive readings and a 0.5 percentage point noise floor** before it declares anything, and a CONFIRMED verdict forces SELL regardless of the other four terms.

### fillwatch's own gaps, fixed by coinbench and now moot
It summed liquidity and volume across every pair, so its turnover for GG (956% price spread) and OPTIMUS (3,104,067%) was junk. coinbench's off-price pool filter, highest-volume-pool pricing, supply from marketCap/priceUsd and platform-aware benchmarking are the shell, so those are inherited automatically.

### STATUS: merged but GATED OFF. `"ledger": false` in config.json.
- The cohort layer is in the code path and the tables exist, but the first live tick **hung for over eight minutes** inside the ledger update for DINO and stopped every alert. A `ledger: false` flag now guards the call and alerts are back to normal across all 16 coins.
- **Root cause not yet proven.** The copied fillwatch state seeded balances but no `codeclass`, so the first run also faces up to 400 `eth_getCode` calls; that alone should be one to three minutes, not eight. The cold backfill is definitely expensive: fillwatch's own seed had only reached 2 of 18 coins.
- **Next step, do this before re-enabling:** run `/opt/coinbench/seedledger.py` under nice/ionice with the service STOPPED, time each coin, confirm the incremental path is under about 30 seconds per coin with a warm `codeclass` cache, then set `"ledger": true`. Never let a cold backfill run inside the service loop; `L.update` is already called with `seed_if_missing=False` so the service can only do incremental work.
- Rule this teaches: **an enrichment layer must never be able to block the alert loop.** Gate it behind a flag, cap it with a budget, and prove its cost offline first.

### Open item inherited
**FATCOIN 0x12D5ee7917cA430073C3A638ee1e6f0648A98a01 fails fillwatch's reconcile gate at all three window sizes** and cannot be ledger-tracked yet. Not a scanner bug at this point. The gate is working as designed: it refuses to store a ledger it cannot prove.

## 2 Sep 2026 14:15 UTC, output simplified to a single table
- [stated] Ben re-pasted the eleven addresses and asked for them all in, reported "simply as the table" via Telegram. **All eleven were already tracked; nothing was missing.** The tracked set stays at 16.
- Telegram output is now **one monospace table per sector** inside `<pre>`, columns: coin, verdict, cap, holders, $/head, turnover. The per-coin receipt blocks are gone. Level-change lines still appear above the table, and the sector board PNGs still go out hourly or on a change.
- New command: **`python3 /opt/coinbench/bench.py now`** forces a poll of every coin regardless of its cadence and pushes the table immediately. Use this whenever Ben asks for the board on demand.
- Lesson: he asked twice for less. When in doubt, the alert is a table and nothing else; detail is available on request, not pushed.

## 2 Sep 2026 14:20 UTC, CORRECTION. Ben caught a false signal flip on GG.
- [stated] Ben: "GG lost its buy signal when its door stopped climbing. what do you mean? within 15min you changed your signal while GG price went up?"
- **He was right and the flip was my fault, not the market's.** I ran `bench.py now` twice while checking the table formatting, at 14:12:54 and 14:13:37, **43 seconds apart**. Both wrote snap rows.
- Every rate in this system is normalised per hour by dividing by dt. A 43-second window divides by 0.012, so it multiplies any move by about 84x. The 8-minute window before it multiplies by 7.5x. GG's door going $1,195 to $1,136 (a 4.9% dip) was reported as **-36%/h**, which flipped it from BUY to HOLD / TRIM.
- The real GG series: door **$995 at 12:56 → $1,195 at 14:04 → $1,136 at 14:12 → $1,123 at 14:20**. Over the proper window that is **+12.9% in 1.4 hours**, and market cap went $6.16M to $6.98M. GG never lost the signal. It reads **BUY** again now that the window is correct.
- **GUARD G10 shipped:** a comparison row must be at least **20 minutes old AND at least 60% of that coin's own cadence old** before any rate is computed. A forced off-schedule poll can no longer manufacture a rate out of a two-minute move. A forced poll within 120 seconds of the last reading now deletes the near-duplicate row instead of stacking it.
- Standing lesson, general: **any metric produced by dividing by elapsed time needs a minimum elapsed time.** Adding an on-demand refresh to a rate-based system is not a display change, it is a change to the maths.
- Second-order note: the correction pass itself printed eight level-change alerts. Those were the model returning to correct values, not eight market events. When a guard is added mid-session, say so, because the alert burst that follows looks like signal.

## 2 Sep 2026 14:35 UTC, Telegram control, and three more guards hard-coded
- [stated] Ben wants to run the watchlist from the chat: "remove FATCOIN", and "add 0x..." with the sector chosen automatically and the coin reported in the next table like all the others. He also asked that the G10 window fix be hard-coded.

### Guards now in `/opt/coinbench/guards.py` as numbered entries, not prose elsewhere
- **G10 never compute a rate over a short window** (the GG false flip). `prevs()` requires a comparison row at least 20 minutes old AND at least 60% of that coin's cadence old; a forced poll inside 120 seconds deletes the near-duplicate row.
- **G11 one getUpdates consumer per bot token.** `/opt/x-alert/xalert.py` already long-polls this token with `timeout=25`. A second poller silently steals half the messages, which would have made "remove FATCOIN" work only about half the time.
- **G12 an enrichment layer must never block the alert loop** (the ledger hang).

### How chat control actually works, and why it is built this way
- **x-alert is the single consumer.** Its `dispatch()` used to silently drop anything that was not one of its own slash commands; it now appends that text to **`/opt/coinbench/inbox.txt`**. Backup written as `xalert.py.bak-<timestamp>`. This was the only change to a live fleet bot.
- coinbench drains and truncates that file at the top of every tick, before polling coins, so an add or a remove takes effect on the same cycle. Code in `/opt/coinbench/commands.py`.
- **Ben must type the commands WITHOUT a leading slash.** `/add` and `/remove` already belong to x-alert (watch and unwatch an X account). Bare `add`, `remove`, `list`, `mute`, `unmute`, `now` route to coinbench. `rm`, `drop` and `del` are accepted synonyms for remove; matching is by ticker, label or contract address, case-insensitive.
- **Automatic sector rule, mechanical:** take the pools carrying real volume; if the highest-volume pool quotes against something that is not money (WETH, ETH, USDG, USDC, USDT, DAI, USDS, FRAX), or 20% or more of total volume sits in equity-quoted pools, the coin is **Memestock arena**, otherwise **RH Chain protocols**. Verified live: GG classified as arena ("top pool quotes WETH, 23% of volume in equity-quoted pools"), PONS as protocols ("0%"), and a live `add` of UBIK 0x812486EAea648819853F8E372dc9f1516C7868Bd landed correctly in the arena before being removed again by `remove UBIK`.
- `now` from the chat forces a full poll on that tick.

## 2 Sep 2026 14:45 UTC, tap-to-run menu
- [stated] Ben does not want to type the commands, he wants a button that shows the options.
- **Two layers, both live.**
  1. **The "/" menu button** on the bot, set with `setMyCommands` plus `setChatMenuButton` (script `/opt/coinbench/setmenu.py`, re-runnable). The bot had NO commands registered before this, so nothing was overwritten. coinbench's entries are namespaced **`/menu`, `/coins`, `/coinnow`, `/coinadd`, `/coinremove`, `/coinmute`, `/coinunmute`** because `/add`, `/remove`, `/list`, `/mute`, `/unmute` already belong to x-alert on the same token. x-alert's `/list`, `/status` and `/help` are listed too so the one menu is honest about the whole bot.
  2. **A ReplyKeyboard** under the text box: `list | now`, `remove a coin`, `mute | unmute`. `remove a coin` replies with a one-time keyboard carrying a button per tracked coin ("remove DINO", "remove GG", …) plus cancel.
- **Why a ReplyKeyboard and not inline buttons:** tapping a ReplyKeyboard button sends its label as an ordinary message, which x-alert already forwards to `/opt/coinbench/inbox.txt`. Inline buttons arrive as `callback_query` updates and x-alert asks Telegram for `["message"]` only, so they would be silently dropped. This is guard G11 showing up a second time: one consumer per token dictates the UI, not just the plumbing.
- `/coinremove` and a bare `remove` with no argument both open the picker rather than erroring.

## 2 Sep 2026 14:42 UTC, the menu was slow, not broken
- [stated] Ben tapped /menu twice and nothing happened, and the menu labels were too long.
- **Diagnosis: it was not broken, it was ten minutes late.** x-alert spooled both taps correctly to `inbox.txt`, but only the main loop drained the spool, and that loop wakes every 600 seconds. A button that answers in up to ten minutes is a broken button as far as the user is concerned.
- **Fix: a third service, `coinbench-cmd.service`** (`/opt/coinbench/cmdloop.py`), drains the spool **every 4 seconds**. It is deliberately tiny: config edits and replies only, no market calls, no database. A `now` from the chat launches `flock -n /opt/coinbench/.lock python3 bench.py now` immediately, so `-n` drops a second request instead of stacking two full sweeps on a 961MB box, and the main loop also honours a `.force` flag as a fallback.
- **Three services now: `coinbench` (the 10-minute loop), `coinbench-cmd` (the 4-second command drain), `xalert` (the single getUpdates consumer).**
- Menu labels stripped of the "coinbench:" prefix and shortened: buttons · tracked coins · poll now, post table · add coin: /coinadd 0x... · remove a coin · mute pushes · resume pushes, plus x-alert's X accounts · bot status · x-alert help. Keyboard header is now "Pick one, or send add 0x..." and the remove picker asks only "Which one?".
- Verified live end to end: list, remove picker, cancel, now (forced sweep ran under the lock and posted the table), menu registration read back from getMyCommands. Watchlist still 16 coins.
- Design rule worth keeping: **an interactive command path needs its own fast loop.** Attaching it to a slow scheduled job makes every button feel dead, and the user cannot tell "queued" from "failed".

## 5 Sep 2026 — TURNED OFF at Ben's request
- [stated] Ben: "Stop the CoinBench alert stuff in the XBot, in the XAlert Telegram. All those 'move from sell to hold', 'meme stock arena', and 'cheapest door' alerts, remove them." Those three are, respectively, the verdict level-change lines, the per-sector table plus board PNG, and the $/head (door) column.
- **`coinbench.service` and `coinbench-cmd.service` both stopped AND disabled.** Not muted: disabled, so they do not come back on reboot. Verified inactive + disabled.
- Safe to do without side effects because **coinbench was never in watchdog UNITS** (watchdog covers 13 other units and would otherwise have restarted it every 300s) and it had **no cron entry** and no systemd timer. Nothing else on the box pushes those alerts; fillwatch's cron was already removed on 2 Sep.
- Everything else still runs: xalert, mastalert, crashbot, crashton, rhtracker, rhtracker-base, strongtrader, dinerodom, earlybirds, kookwatch, ansemtracker, ansem-rh, gramwatch, watchdog-bots all active. Cron still has rhtop (04:20 UTC) and the new [[pullback-comeback]] scanner (22:00 UTC).
- `/opt/botctl/registry.json` never listed coinbench, so nothing to remove there (backup taken anyway).
- **Left in place deliberately, and this matters if it is ever revived:** x-alert's `dispatch()` still appends unrecognised chat text to `/opt/coinbench/inbox.txt`. Harmless while coinbench is off (the file just grows slowly), but it means the bare `add` / `remove` / `list` / `now` words and the ReplyKeyboard buttons now go nowhere. **The "/" menu still advertises `/menu /coins /coinnow /coinadd /coinremove /coinmute /coinunmute`; those commands are now dead.** Re-run `/opt/coinbench/setmenu.py` with the coinbench entries removed if the stale menu becomes annoying.
- All code, config and `bench.db` are untouched on disk. To bring it back: `systemctl enable --now coinbench coinbench-cmd`.

## 16 Sep 2026 — found RUNNING again, turned off a second time
- Despite the 5 Sep disable, `coinbench.service` was **active** on 16 Sep with `ActiveEnterTimestamp = 15 Sep 19:14 UTC`. Enabled was still `disabled`, so something started it by hand. **`is-enabled` says nothing about whether it is running right now.**
- [stated] Ben retired the whole `@xalertben_bot` stream (see [[alert-bot-fleet-10]]). coinbench and coinbench-cmd disabled again, and the **bot token blanked in `/opt/coinbench/config.json`** (backup `.bak-20260916-*`) so the watchdog's blocked-bot check stops naming it.
- To revive it now takes two steps, not one: restore the token from the backup config, then `systemctl enable --now coinbench coinbench-cmd`.
