---
name: alert-bot-fleet-2
description: Continuation of the alert bot fleet change log from Sep 2026 onward, after alert-bot-fleet.md hit its size cap. Read both whenever a bot, alert, tracker or watcher comes up.
sources: [cowork]
aliases: [alert bots part 2, mastalert, Must Alert]
---
Continues [[alert-bot-fleet]]. Same droplet, same rules.

- NEW BOT 2 Sep 2026: **mastalert** ("Must Alert"), unit mastalert.service at /opt/mast-alert, own bot **@Mastalert_bot**, chat 357464289. Watches **@mastbond** on X (Mast Bond / MAST on Robinhood Chain, see [[mast-bond]]) - originals, replies, quotes and retweets all on. Cloned from /opt/x-alert as mastalert.py with "x-alert" replaced by "Must Alert" in all user-facing strings. [stated] Ben asked for a new bot named Must Alert for this account, and created the BotFather token himself
- **THE CONSTRAINT THAT SHAPED IT, and it applies to every future X bot: twitterapi.io allows ONE websocket slot and ONE monitor list PER API KEY, and the fleet has one key (xalert's).** A second stock xalert clone would have called reconcile_accounts() at boot and on every config change, stripping brozinkerbell and forginvestments from the shared monitor list; xalert's own config_watcher would add them straight back; the two would fight forever and both go intermittently blind. xalert.py's own comment at the poll branch states the one-socket rule explicitly
- Fix: mastalert.py carries an override defining `reconcile_accounts()` as a no-op, inserted just before `def main():` (a module-level redefinition wins for every runtime caller). It returns ok AND sets `_last_remote` to the wanted handles - that second part is load-bearing, because config_watcher retries the sync every 60s whenever `want` is not a subset of `_last_remote`, and on success re-announces "Must Alert is live" every minute. Config sets `transport: "poll"` and `poll_seconds: 20`, so it never opens a socket. **Never set transport to auto or websocket in this bot.** Latency is 0-20s versus xalert's ~1s
- Verified after the change: the twitterapi.io monitor list still reads exactly [forginvestments, brozinkerbell], and xalert is still active on its websocket
- state.json was pre-seeded with all 17 existing @mastbond tweet ids before first start, so the first poll cycle did not fire ten historical posts. Reusable pattern for any new poll-based watcher
- Two test alerts were sent through the bot's own `format_alert` (imported via sys.path, not hand-rolled), one original (Bearings DAO v1, 2095101833635058062) and one reply (the dox commitment, 2095119002531967008)
- Registered in ~/botctl/registry.json and in watchdog.py UNITS with max_silence 300 - unlike the wallet trackers it logs every poll cycle, so it CAN legitimately be judged by log age. Watchdog now covers 13 units; botctl reports **14 bots, all healthy**
- Drift noted, not fixed: the droplet is also running coinbench, fillwatch and rhtop, and holds /opt/digitalocean, /opt/rbt3k, /opt/rhfee and /opt/patch_pricing.py, none of which appear in ~/botctl/registry.json or watchdog UNITS. The two-registry diff check is still not automated

## crashbot noise fix, 2 Sep 2026 - buys and sells only
- [stated] Ben, on a screenshot of repeated "Crash W5 position UP 52,121 DoqcRK" messages: "somebody keeps sending him tokens. fix it so that i only see active buys and sells." This is the airdrop noise that was recorded as known-but-unfixed on 28 Aug; he has now decided he wants it gone
- **ROOT CAUSE, and it is structural, not a threshold problem.** crashbot's `attribute()` scans `getSignaturesForAddress(OWNER)`. An incoming SPL transfer to a wallet's existing ATA has the ATA as an account key, not the owner, so it never appears in that list. attribute() therefore finds nothing, returns `"unknown"`, and the alert renders as "position UP / position DOWN" with the "balance changed but I could not match it to a recent transaction" line. **Every airdrop spray lands in the unknown bucket, and conversely a genuine buy or sell is always signed by the wallet and so can never land there.** That asymmetry is what makes suppressing `unknown` safe
- Second leak found in the same read: the `min_usd` floor is written `if mn and usd is not None and usd < mn`, so a token DexScreener cannot price bypasses the $1,000 floor entirely. Spam tokens are exactly the unpriced ones. DoqcRKovaMLvnpgFB1K5HNLDSUquwSDwJKJpirJA7dBC returns **0 DexScreener pairs** - no pool, no price, nothing to buy or sell with
- THE PATCH (crashbot.py.bak-20260903-010* and config.json.bak alongside): (1) new config key **`alert_unknown`, default False**, now gates the unknown branch, which previously piggybacked on `alert_transfers`; (2) a structural guard `if kind != "trade" and price is None: return`, so a non-trade event on a token with no market can never alert even if transfers get switched back on; (3) `/unknown` added to the Telegram toggle dict and to /help and /status
- Config now: alert_buys true, alert_sells true, **alert_transfers false, alert_unknown false**, alert_funding true, min_usd 1000. Funding was deliberately left ON - a dormant wallet being funded is the loudest signal the bot sends and is not a token transfer
- **WHAT HE LOSES, told to him at the time:** outbound "sent out" alerts. A wallet moving tokens to a CEX deposit address is a sell precursor, and that is now silent. One Telegram command, `/transfers`, brings it back without touching the airdrop noise
- Verified with the importlib self-test harness (CrashBot() constructed directly, tg.send monkeypatched, attribute() stubbed per case): unmatched-unpriced, unmatched-priced, incoming transfer and outgoing transfer all **suppressed**; real buy, real sell, and a real buy of a brand-new UNPRICED token all still **SENT**. That last case is the one that matters - `ignore_unpriced_tokens` was deliberately left false so a fresh pump.fun entry is never missed. RESULT: PASS
- **RPC GOTCHA while diagnosing:** the Helius key in /opt/crash-bot/config.json is shared with the live 12s poll loop, so an ad-hoc diagnostic script hitting `getSignaturesForAddress` with limit 100 returns 429 within seconds and also makes the bot's own loop log 429s. Keep any diagnostic to small limits with sleeps between calls, and expect a few 429 lines in crashbot.log that are your own fault, not the bot's
- SAME DAY REFINEMENT, [stated] Ben: "sending out is ok to get an alert." The single `alert_transfers` flag was split by DIRECTION into **`alert_transfers_in` (false)** and **`alert_transfers_out` (true)**, both falling back to `alert_transfers` if absent so nothing else that reads the old key changes behaviour. Inbound is the airdrop spray; outbound is a sell precursor and he wants it. New Telegram toggles **`/in` and `/out`**, /status now reports "In off · Out on" instead of one Transfers flag
- Note that interacts with this: the `kind != "trade" and price is None` guard still drops an outbound send of a token with NO DexScreener pair, so dumping a worthless airdrop stays silent. That is intended, but it is also the one case where an outbound move could be missed
- Re-verified with the harness, 9 cases: inbound spam (priced and unpriced), both unmatched cases, and a sub-$1,000 outbound all suppressed; outbound $4,000, real buy, real sell, and a buy of a brand-new unpriced token all SENT. RESULT: PASS
- [stated] **Ben's alert doctrine, now explicit across two bots:** he wants events he can act on, not a ledger of everything that touches a wallet. Inbound noise off, outbound and trades on. Same instinct as the USDG suppression on dinerodom on 1 Sep - filter what other people do TO the wallet, keep everything the owner chooses to do

## Uber bots, 3 Sep 2026 - blocked by a locked account
- [stated] Ben asked for a separate bot covering ALL of @0xuberM's activity: posts, replies, retweets and follows. He created the BotFather bot; it resolves as **@OxUberM_bot**, display name "0xUberM"
- **@0xuberM IS A PROTECTED (LOCKED) ACCOUNT.** `user/info` reports protected=true, 17,624 followers, 692 following, 11,762 tweets, created 3 Jun 2025, bio "These violent delights have violent ends.". `last_tweets` returns `tweets: []` and `followings` returns `[]`, both with status success - **an empty success, not an error**, which is exactly how this hides. user/info works fine, so a naive build looks healthy and simply never alerts. Ben's own Twitter follow does not help: twitterapi.io reads with its own credentials, not his
- Built anyway and PARKED (stopped, still enabled) so they are one command from live: **uberalert** (/opt/uber-alert, clone of mastalert.py, poll 20s, reconcile no-op, all four post types on) and **uberfollow** (/opt/uber-follow, clone of the retired /opt/followwatch which was still intact on disk, 180s poll, $10k guess floor and chain-hint requirement preserved). Both share the @OxUberM_bot token and chat 357464289 so posts and follows land in one thread
- **uberlock** (/opt/uber-lock, uberlock.service) is the only one running: it polls user/info every 600s and, when protected flips false, seeds uber-alert's state.json with his existing tweets, starts both units, messages Ben and exits. Written because the alternative was handing him a dead bot and a to-do
- **TRAP TO NOT REPEAT: uberfollow's state.json currently holds a baseline of ZERO followings**, because the API returned none. If it were started while still locked and the account later unlocked, it would alert on all 692 follows at once. Only uberlock should start it. Same shape as the gramwatch cold-start problem
- Only **uberlock** was added to watchdog UNITS. uberalert and uberfollow are deliberately NOT there - the watchdog would read a parked unit as a failure and restart it every 300s, fighting the park. **Add them to watchdog when they go live.** All three are in ~/botctl/registry.json (17 entries), which now shows "DOWN: uberalert, uberfollow" by design
- OPEN QUESTION for Ben: whether @0xuberM is the handle he meant, given it is locked and he presumably cannot read it either. If it is wrong, delete all three units
- Standing check worth adding to any future X bot: **read `protected` from user/info before building anything.** A locked account cannot be watched by this key at all, and every endpoint reports success while returning nothing

## twitterapi.io cost and capability, measured 3 Sep 2026 (not guessed)
- Balance on the shared key: **2,826,772 recharge credits** (`GET /oapi/my/info` returns recharge_credits and total_bonus_credits, no auth beyond the key)
- **Measured cost per `last_tweets` call, by difference before/after over 10 calls: 37.5 credits for a PUBLIC account (mastbond), 288 credits for the LOCKED account (0xuberM).** A locked account costs ~7.7x MORE and returns nothing - it evidently pages/retries internally before giving up. `followings` page 1 measured **0 credits**, which is why follow-watching is effectively free
- **1s polling is not viable and the numbers settle it:** public account = 3,240,000 credits/day (more than the whole balance, daily); locked account = **24,883,200 credits/day, 8.8x the entire balance, to receive nothing**. Even 20s polling of a locked account is 1,244,160/day, i.e. the balance gone in 2.3 days
- **STANDING BURN TO WATCH: mastalert at 20s poll = 4,320 calls/day x 37.5 = ~162,000 credits/day, which empties the shared balance in about 17 days.** This was not visible when it was built. Dropping it to 60s is ~54,000/day (~52 days). The real fix is the websocket, which costs nothing per tweet
- **LIKES ARE NOT AVAILABLE AT ALL.** Probed `/twitter/user/last_likes`, `/liked_tweets`, `/favorites` - all return `status: error, msg: "user not found"`. `/twitter/user/likes` is a false positive: it is the user/info route matching `userName=likes` and returns the "Like Button" account. Underlying reason: X made likes private in 2024, so no vendor can supply them. Do not go looking again
- **`/twitter/user/mentions` DOES work on a locked account** - it returns other people's tweets mentioning @0xuberM, because those tweets are public even though his are not. It cannot show what he posts, but it is the only readable signal around a locked account. Not built; offered
- ARCHITECTURE CONCLUSION, not yet implemented: the correct way to get ~1s latency for more than one account is NOT faster polling but **per-account bot-token routing inside xalert's existing websocket** - one socket, push delivery, zero credits per tweet, each watched account's alerts sent to its own BotFather bot. That would let mastalert and any future X bot drop polling entirely. It requires editing xalert.py, the bot that holds the single socket, so it is real surgery and should be done in daylight with a backup and the importlib harness
- **UBER BOTS FULLY DELETED 3 Sep 2026.** [stated] Ben: "we dont need the watcher, he will never unlock." All three units (uberalert, uberfollow, uberlock) stopped, disabled, unit files removed, /opt/uber-alert /opt/uber-follow /opt/uber-lock deleted from disk, uberlock dropped from watchdog UNITS (14 -> 13) and all three removed from ~/botctl/registry.json (17 -> 14). Fleet back to **14 bots, all healthy**
- Recovery path if he ever changes his mind: **/root/retired/uber-bots-20260903-033047.tar.gz** (17 entries, verified to contain the configs before anything was deleted). The BotFather bot **@OxUberM_bot** still exists and its token is unique to these three, so deleting it in BotFather is safe and is a step only Ben can do
- [stated] He confirmed he follows @0xuberM personally and can read the account himself. The blocker was never the code: twitterapi.io reads with its own Twitter identity, which he has not approved, so a locked account is invisible to the fleet no matter what is built

## COINBENCH SWITCHED OFF, 5 Sep 2026 ~09:00 UTC
- [stated] Ben, on a screenshot of the 14:22 COINBENCH card (FATCOIN SELL, DINO SELL, PAIR BUY, plus the four-panel Memestock arena board): **"stop and delete these alerts. remove the entire alert for this."**
- Done: `systemctl stop coinbench` + `systemctl disable coinbench`. Verified `is-active inactive`, `is-enabled disabled`, and `pgrep -af "coinben[c]h"` returns nothing. **No coinbench alert can fire again, including after a droplet reboot.**
- Code, config and history left on disk at /opt/coinbench (bench.py, verdict.py, guards.py, board.py, charts.py, backfill.py, config.json, refcurves.json, bench.db). Nothing was deleted, so `systemctl enable --now coinbench` restores it exactly. **Ask before deleting the directory; the sqlite history is the only holder-series record the fleet has.**
- Context that led here: the service had been stopped since 05:40 the same day and was restarted at 07:40 only to watch COMMOTITTES (see [[screen-log-outcomes-3]]). COMMOTITTES was dropped from config.json at 08:55 when the screen closed. So the tracker's last state is: 16 coins tracked, service off.
- **Read for any future recurring output: this is the second time Ben has killed a recurring alert whose per-tick message was a table of coins he was not acting on.** The crashbot doctrine in this file applies to dashboards too: he wants events he can act on, not a repeating status board. A tracker that posts the same verdict card every 10 minutes is noise regardless of how correct the verdicts are. Build alerts that fire on a CHANGE worth an action, not on a schedule.

## MASTALERT ("Must Alert") FULLY DELETED, 6 Sep 2026 ~04:18 UTC
- [stated] Ben, right after killing coinbench: **"Also remove the must alert. Delete it completely."**
- Removed for real, not parked: `systemctl stop mastalert` + `disable`, unit file `/etc/systemd/system/mastalert.service` deleted, `daemon-reload` + `reset-failed`, `/opt/mast-alert` deleted from disk. Verified `is-active inactive`, `is-enabled not-found`, `/opt/mast-alert` no such file, and mastalert absent from `systemctl list-units --type=service --state=running`.
- Watchdog cleaned: `/opt/watchdog/watchdog.py` line 38 `"mastalert": ("/opt/mast-alert/mastalert.log", 300, None),` deleted, file compile-checked (SYNTAX OK), watchdog-bots restarted and active. **Without this the watchdog would have alarmed on a missing log forever.**
- Recovery tarball: **`/root/retired/mastalert-20260906-111751.tar.gz`** (97,156 bytes, holds /opt/mast-alert and the unit file). Same pattern as the uber-bots retirement.
- **CORRECTION to this file's 2 Sep note.** It records mastalert as registered "in ~/botctl/registry.json and in watchdog.py UNITS". The registry path was wrong: **there is no `~/botctl/` on the droplet. The real paths are `/opt/botctl/registry.json` and `/opt/watchdog/watchdog.py`**, and mastalert was in watchdog.py only, never in the botctl registry. Every `~/botctl/...` path written in [[alert-bot-fleet]] and in this file is wrong and should be read as `/opt/botctl/...`.
- **This also means the fleet counts in both files ("14 bots, all healthy") were never verifiable from the path they cite.** Recount from `/opt/botctl/registry.json` before quoting a number again.
- Services actually running after this change: ansem-rh, ansemtracker, crashbot, crashton, dinerodom, earlybirds, gramwatch, kookwatch, lwserve, rhtracker, rhtracker-base, rugwatch, strongtrader, watchdog-bots, xalert. **coinbench and fillwatch are NOT running** (coinbench deliberately off since 5 Sep, fillwatch state unexplained and worth a look next time the fleet comes up).
- The BotFather bot **@Mastalert_bot** still exists and its token was unique to this service. Deleting it in BotFather is safe and is the one step only Ben can do.

## NEW BOT 6 Sep 2026: "DELTA alert", unit deltaalert.service at /opt/delta-alert
- [stated] Ben: "create a separate delta alert fee switch for that. also report key on chain metrics daily 8am bangkok time. always make sure that they are correct so run them with 3 iterations before they get sent out as a daily update. call the whole thing: DELTA alert"
- One service, two jobs. **Job 1, fee-switch watcher, polls every 60s** and fires once, loudly, on any change to: `feeBps()` on DeltaLadderManager v3 0x5cA6214227D1195c4b7b4B96847b8966c688295D (currently 100 = 1%), `treasury()` on all three fee-bearing contracts (all point at 0x266311a44e70363b3d168bcb38cf38cbf2ccc51f), `owner()` (0xb1c2bbf86e557ecdc1812f75ae3fe973e5ec9e69), the treasury's CODE (a real payout contract appearing is the loudest case), DELTA arriving at the burn address 0x000000000000000000000000000000000000dEaD above 100,000 tokens, or the treasury accumulating more than 500,000 DELTA. **Job 2, daily card at 01:00 UTC = 08:00 Asia/Bangkok.**
- **THE 3-ITERATION REQUIREMENT IS IMPLEMENTED AS `triple_check()`**: the entire metric set is collected three times, 20s apart, the MEDIAN of each field is used, and any field whose three passes spread beyond a per-field tolerance is NAMED in the message rather than silently averaged. Tolerances live in the `TOLERANCE` dict (mcap 6%, fees 2%, holders 1%, revenue 15%, cap/rev 18%); `EXACT` fields (feeBps, position counts) must match exactly. If ALL passes fail the bot sends "daily report FAILED" and no numbers, deliberately.
- **Plus a fourth, independent check**: `cross_check()` recomputes revenue a second way, as 1% of the change in Delta's own lifetime fee counter since yesterday's card, and flags the line if the two methods disagree by more than 35%. This is the manual cross-check from the 6 Sep screen, automated.
- Revenue is measured, never assumed: chunked `eth_getLogs` on Transfer with topic2 = the treasury, every inbound token priced via DexScreener, and **the window is derived from real block timestamps, never from an assumed block time** (Robinhood Chain measured at 9.92 blocks/sec, but the code does not rely on it).
- First live run, 6 Sep 06:55 UTC: **3 passes in 30s, 25s, 23s, ZERO flags**, card sent 06:57:26. Values: mcap $11,010,990, revenue $1,131,604/yr, cap/rev 9.71x, holders 15,364, positions 13,965, feeBps 100.
- **TRAP FOUND AND WORKED AROUND: robinhoodchain.blockscout.com Cloudflare-challenges the droplet's IP** ("Just a moment..." HTML on every call, v1 and v2 alike), so the holder count could not come from there. **GeckoTerminal `/api/v2/networks/robinhood/tokens/{ca}/info` works from the droplet and is better**: it returns `holders.count` AND `holders.distribution_percentage.top_10`, so the card now also shows top-10 concentration. Blockscout is kept as a fallback only. Reusable for any RH-chain bot on this droplet.
- **TELEGRAM TOKEN NOTE: the retired @Mastalert_bot token from /root/retired/mastalert-20260906-111751.tar.gz is DEAD (401 Unauthorized)** — Ben evidently deleted it in BotFather after the 6 Sep removal. Do not try to reuse it. DELTA alert therefore borrows **rhtracker's @bk_rhwatch_bot token**, which is thematically right (both are Robinhood Chain watchers) and lands in the same chat 357464289. Swapping in a dedicated BotFather bot is a one-line edit of `bot_token` in /opt/delta-alert/config.json plus a restart.
- Registered in **/opt/botctl/registry.json (now 14 bots)** and in **/opt/watchdog/watchdog.py UNITS (now 13 units)**, watchdog-bots restarted and active. max_silence 0 because the service only logs on events and on the daily run.
- Config knobs: poll_seconds 60, daily_hour_utc 1, iterations 3, iteration_gap_s 20, revenue_window_blocks 6,048,000 (~7 days), burn_alert_tokens 100,000, treasury_buy_alert_tokens 500,000.
- CLI for manual use: `python3 deltaalert.py --switch` (one fee-switch pass), `--daily` (send a card now), `--test` (one collection pass printed as JSON, sends nothing).
- **The card leads with a plain action word** (STRONG BUY / BUY / HOLD / TRIM / SELL) per Ben's 2 Sep doctrine, driven by cap/revenue plus whether the route to holders is live. It is NOT a repeating status board of coins he is not acting on: the fee-switch half only speaks when something changes, and the daily half is one token he holds.
- Backups on disk: deltaalert.py.bak-20260906-064857, /opt/botctl/registry.json.bak-*, /opt/watchdog/watchdog.py.bak-*.

## NEW BOT 6 Sep 2026: "0XUBERM ALERT", unit ubermwatch.service at /opt/ubermwatch
- [stated] Ben: "set up an alert for him. The alert will be called OX Uber M alert, and it will be a separate Telegram channel where you do that. I want to know about his buys and sells immediately"
- NOT the deleted 3 Sep uber bots. Those watched his X account and died because @0xuberM is a protected account twitterapi.io cannot read. **This one watches his WALLETS on chain, so the lock is irrelevant.**
- Cloned from **/opt/dinerodom/tracker.py**, the mature generic rh-tracker (38KB, Transfer logs plus v2/v3/v4 Swap topics, DexScreener pricing, buy/sell classification, resulting position, Telegram command handling). settoken.py cloned from /opt/earlybirds with the paths and strings swapped. Only code change: the hardcoded startup line "Dinero Dom bot starting" became "0XUBERM ALERT starting"
- **THREE WALLETS.** PRIMARY **0xdce001e4ec11ab58650c51ae9658accbe0f49c48**, the trading wallet found the same day (see [[uberm-wallets]]). FOMO 1 **0xcf6cdf01acb524d8e53d1d13b1a89fcadbd7034c**. FOMO 2 **0xc3a76958cb75bd3070462515916fa6ff2564c405**
- Config: poll_seconds 4, min_usd_buy 100, min_usd_sell 100, **alert_transfers TRUE**. Transfers are ON here, against the crashbot doctrine of suppressing them, and deliberately: this man's whole pattern is moving size between wallets (10M FIRE on 23 Aug, the IMD forward on 2 Sep), so a transfer out is the single most informative event he generates. Revisit only if airdrop spray becomes a problem, and if it does, split by direction as crashbot did rather than switching it off
- Registered in BOTH registries and under the watchdog: the Mac's /Users/bk/botctl/registry.json (15 bots, this is what `botctl status` actually reads), the droplet's /opt/botctl/registry.json (15 bots), and /opt/watchdog/watchdog.py UNITS with max_silence 0 (14 units). Watchdog restarted and active
- **PATH CORRECTION CONFIRMED, extending this file's 6 Sep note.** BOTH registries exist and both matter. `~/botctl/botctl status` on the Mac reads **/Users/bk/botctl/registry.json**, verified by editing it and watching the new row appear. /opt/botctl/registry.json is the droplet's own copy. Keep them in sync; editing only one leaves botctl and the droplet disagreeing
- chat_id is set to 357464289 (his usual chat) so the moment a token lands it works. **bot_token is deliberately EMPTY and alerts land in /opt/ubermwatch/ubermwatch.log until it is filled**, the same pattern as earlybirds
- **THE ONE STEP ONLY BEN CAN DO**, and it was given to him as a single command so no token passes through Claude: create the bot in BotFather, then run on his Mac
  `ssh -i ~/kookwatch/id_kookwatch root@159.223.88.168 "echo 'TOKEN' > /opt/ubermwatch/token.txt && python3 /opt/ubermwatch/settoken.py"`
  settoken.py verifies the token against getMe, writes it to config, deletes the token file, restarts the unit and sends a proof-of-life listing the watched wallets
- Verified live: unit active and enabled, state.json advancing (last_block moved 55,950,604 to 55,950,727 in 20 seconds, about 6.2 blocks/sec), zero seen events at start so no historical backfire
- Note for reuse: **/opt/dinerodom/tracker.py is now the standard template for any new Robinhood Chain wallet watcher.** Clone it, write a fresh config.json, clone earlybirds' settoken.py with paths swapped, add a unit file, register in both registries, add to watchdog. About ten minutes end to end.
- **0XUBERM ALERT WENT LIVE 6 Sep 2026 11:47 UTC.** [stated] Ben created the BotFather bot and handed the token over with "build it". It authenticates as **@OxUberMendes_bot**. settoken.py verified it against getMe, wrote it to config, deleted the token file, restarted the unit and sent the proof-of-life listing all three wallets. commands_enabled is now true, chat 357464289
- Token handling: written to a temp file on the Mac, scp'd to /opt/ubermwatch/token.txt, consumed and deleted by settoken.py, and the Mac copy removed in the same command. It was never echoed to a log or to chat. It DID pass through the chat transcript when Ben pasted it, so /revoke in BotFather is available if he ever wants it rotated
- Both registries now show `@OxUberMendes_bot` for ubermwatch. `botctl status` reads 15 bots; mastalert still shows DOWN and is the expected leftover from its 6 Sep deletion (its row was never removed from the Mac registry, worth cleaning next time the fleet is touched)

## 0XUBERM ALERT: two wallets, and a real P&L engine, 6 Sep 2026
- [stated] Ben: "Remove the third one from the alert, and only give me the first one, which we call trading wallet, and then the second one, which we call former wallet." Then, correcting the names: "Not former wallet. One is called Uber M Trading, and one is called Uber M Fomo."
- Final config: TWO wallets only, labels **UBER M TRADING** = 0xdce001e4ec11ab58650c51ae9658accbe0f49c48 and **UBER M FOMO** = 0xcf6cdf01acb524d8e53d1d13b1a89fcadbd7034c. 0xc3a76958cb75bd3070462515916fa6ff2564c405 removed. poll_seconds dropped 4 -> 2.
- [stated] Ben then asked for P&L on every alert: "What is his profit or loss percentage? He probably has some average entry, and if he sells, is he above or below entry."
- **NEW FILES on the droplet: /opt/ubermwatch/pnl.py (the book) and /opt/ubermwatch/backfill.py (history), plus basis.json and legs.json.** tracker.py gained a `pnl_block()` method and an action headline; the old `update_basis` "Avg entry since tracking" line is gone.
- **THE PRICING METHOD, reusable on any EVM chain and the key insight: never ask a price API what something cost in the past.** Every trade is a Uniswap v4 swap, and the PoolManager Swap event (topic0 0x40e9cecb9f5f1f1c5b9c97dec2917b7ee92e57ba5563708daca94dd84ad7112f, data = int128 amount0, int128 amount1) carries BOTH sides. Match the wallet's raw token delta against |amount0| or |amount1| (15% tolerance covers relayer fees), and the other side is exactly what was paid. No GeckoTerminal, no rate limits, no wrong historical price.
- **THREE BUGS FOUND AND FIXED IN ONE SESSION, all worth remembering:**
  1. **Never accumulate a cost basis in raw quote units.** The same coin is traded against different quote assets over time (AI was NVDA-quoted, then USDG-quoted), and summing raw units across pools produced an average entry of $149,000,000 for AI. The book must be denominated in DOLLARS, converted at the moment each leg is priced.
  2. **Refuse memecoin-quoted pools in a backfill.** Converting an eight-week-old trade using a quote COIN's price today can be wrong by orders of magnitude (AGI/AI, SIT/AI, CLANKER/AI pools). Only stables, ETH, and tokenised stocks (quoteToken.name contains "Robinhood Token") are accepted; 166 of 968 legs were dropped for this and that is correct behaviour.
  3. **An accumulated token count ALWAYS drifts from the wallet's real balance**, because airdrops, wallet-to-wallet moves and unpriceable trades move tokens without moving cost. Before reconciliation the book claimed he still held BONER, SPACEHOOD and SHROOM that the chain says are at zero, and 61% too much AI. **Fix: `Book.reconcile(wallet, token, balance)` anchors SIZE to the on-chain balance while keeping the average entry from the priced legs.** It runs at the end of every backfill and on every live alert. 95 phantom positions closed, 7 resized, 106 "open" became 11 real ones.
- Backfill result, verified against DeBank's $589,443: **11 open positions worth about $570k**, realised $95,974 across 143 coins ever traded. AI 2,055,429 tokens, avg entry $0.0239, open +693%. FIRE 12,471,300 in the FOMO wallet matches his public profile exactly, which is an independent proof the pricing is right.
- **The alert now leads with an ACTION word** per Ben's 2 Sep doctrine: NEW POSITION / ADDING · stack up N% / TRIMMING · sold N% of his stack / FULLY OUT. Then: sold-at vs avg entry, IN PROFIT or AT A LOSS with % and dollars, realised on that coin so far, and what is left with its open %.
- Ops gotchas hit while building this: `pkill -f backfill.py` over ssh **kills your own ssh session** because the pattern matches the remote command line (use a partial pattern like "python3 -u backfil"). The droplet has **961MB RAM**, so holding 3,673 receipts in a dict gets the process OOM-killed silently with no traceback: stream them. And `decimals()` on 1,034 spam tokens with 3 retries and backoff hangs for an hour, so resolve decimals only for legs that will actually be priced.
- `bookreport.py` on the droplet prints the whole reconstructed book for eyeballing. `legs.json` caches pass 1, so a re-run of backfill.py is instant instead of 15 minutes.
