---
name: alert-bot-fleet-4
description: Continuation of the alert bot fleet change log from 10 Sep 2026, after alert-bot-fleet-3.md hit its size cap. Read all four whenever a bot, alert, tracker or watcher comes up.
sources: [cowork]
aliases: [alert bots part 4, prmwatch, PRM WATCH]
---
Continues [[alert-bot-fleet-3]], [[alert-bot-fleet-2]] and [[alert-bot-fleet]]. Same droplet `159.223.88.168`, same rules: everything runs on the droplet, nothing on the Mac.

## 10 Sep 2026: PRM WATCH, a token-scoped bot rather than a wallet-scoped one
- [stated] Ben: "build a bot that alerts me when he sells from any of the wallets. and that says how much he has left all in USD. make it a separate PRM bot."
- **`/opt/prmwatch` on kookwatch, unit `prmwatch.service`, log `/opt/prmwatch/prmwatch.log`, bot @bk_prmwatch_bot, chat 357464289.** Registered in the Mac `~/botctl/registry.json` (17 bots) so `botctl status` shows it. **NOT a clone of the ubermwatch family**: it watches ONE token across 38 wallets instead of one wallet across all tokens, so it is ~250 lines written fresh, not a copy.
- Watches PRM `0xf24f8F6b08fE87CF062E833a732AD7F636064BC8` and the 38 bonding-curve wallets from the 10 Sep forensics in [[screen-log-4]]. Poll 20s, one `eth_getLogs` per cycle on the token filtered to Transfer, then classify only the legs where a cohort wallet is on either side. Far cheaper than 38 per-wallet scans.
- **Four alert kinds, classified by the venue test (counterparty code size above 100 bytes), the same discriminator validated to 0.9% against DexScreener volume that day:** SELL (into a router or pool) · **CONSOLIDATION (a transfer BETWEEN two cohort wallets, which has never happened once and is the real signal that he is gathering the bag before a bigger exit)** · TRANSFER_OUT (to an outside wallet, explicitly labelled "not a sale") · BUY / IN. Thresholds `min_usd` 100 and `min_usd_buy` 250.
- **Every alert carries the answer to "how much is left": a live `balanceOf` sweep of all 38 wallets, printed as tokens, percent of supply, USD at the DexScreener price, and as a multiple of the pool.** Verified live at 406,546,817 PRM = 40.65% of supply = $1.58M against a $232,731 pool = 6.8x the pool, 25 of 38 wallets still holding. Also a daily report at 01:00 UTC with the day-over-day change and the five biggest wallets, and `/prm` (or `/status`) on demand, plus `/mute` and `/unmute`.
- **DESIGN DETAIL WORTH REUSING: the bot queues alerts to `queue.json` when no bot token is set and flushes them the moment one lands, prefixed "Held while no bot token was set".** It was deployed and watching ~40 minutes before the token existed and lost nothing. Any new bot in this fleet should do this instead of dying on a missing token. Helper `/opt/prmwatch/settoken.sh <token>` writes the token and restarts in one call.
- **THE BOTFATHER LESSON, and it cost three failed attempts: NEVER read a bot token off a screenshot.** I misread two capital O's as zeros (`EEKOq` as `EEK0q`, `orOY` as `or0Y`) and got 401 Unauthorized twice. **The right method: click the token in the BotFather message (Telegram copies code blocks on click), then read it with `pbpaste` over Desktop Commander.** Exact characters, no OCR.
- **Telegram DESKTOP driving notes, different from the Telegram Web notes in [[alert-bot-fleet-3]]:** the background `app_*` tools could open chats and type but **could not send** (both `app_key return` and AXPress on the Send button reported ok and did nothing); `osascript` keystrokes are refused ("osascript is not allowed to send keystrokes"). What worked: `computer_request_full_control`, then click the input and **`cmd+return`** (plain `return` does not send in this install). Also **display-scope click coordinates are in the reported full-resolution frame (e.g. 1372x887), NOT the scaled image's own pixels**; using image pixels opened two of Ben's private chats before I caught it.
- A new bot cannot message a user who has never started it, so after `/newbot` the flow must also open `t.me/<username>` and press START before the first alert can land. Done here.
- Falsifier the bot exists to catch: any transfer between two of `0x05091872e42636580057a91a2512bddda6f044e1`, `0x0bdd3b00ca5202cc1e8022b02b84cf46377c9577`, `0xde51ecd9193d934c1c9159ad0e22c63cdcf8b9f5` and the other 35 cohort wallets. Zero so far.
- Fleet count after this: 17 registered in botctl, 16 active (`mastalert` still DOWN / not-found, `earlybirds` still needs its own BotFather bot and logs alerts in the meantime).

## 10 Sep 2026: NOSTRADAMUS ALERT, two wallets of @NeetOlympics
- [stated] Ben: "obviously I want the alert as soon as he buys something." Built after the wallet hunt in [[neetolympics-wallet]].
- **`/opt/neetwatch` on kookwatch, unit `neetwatch.service`, log `/opt/neetwatch/neetwatch.log`, bot @bk_nostradamus_bot, chat 357464289.** Clone of /opt/chjokkawatch (tracker.py, pnl.py, backfill.py, report_pnl.py, bookreport.py, settoken.py, paths sed-replaced, log line "NOSTRADAMUS ALERT starting"). Config: tag NOSTRADAMUS, poll 2s, buys and sells only, transfers off, min $50, two wallets labelled **NOSTRADAMUS BIG** 0xfd6e86cf9aeca787cffb198bca859d459a44a847 and **NOSTRADAMUS DAILY** 0xd048558edbf485f56e2410ba9e20b7d458c9b1fb. Registered in /opt/botctl/registry.json (17 bots there, prmwatch missing from the droplet copy) and Mac ~/botctl/registry.json (18), watchdog UNITS line added and watchdog-bots restarted. Proof-of-life sent 12:10 UTC. basis.json empty, so P&L lines start thin until backfill.py is run.
- **TOKEN HANDLING THAT WORKED WITHOUT PRINTING IT:** in Telegram Web, click the token code block (Telegram copies it), then on the Mac via Desktop Commander `pbpaste | ssh ... 'cat > /opt/neetwatch/token.txt && python3 settoken.py'`, then `pbcopy < /dev/null` to clear the clipboard. The `find` tool still echoed the token in its result once, so prefer the JS `querySelectorAll('code')` length check plus a physical click, not `find` on the token text.
- Telegram Web notes reconfirmed: `type` works in the global search box but not for chat input; `key` with spaced characters works for chat input ("/ n e w b o t Return"); clicking the t.me link in BotFather's message does NOT open the bot, search the username, click the Global Search row, then the START button.
- Wallet-behaviour insight that shaped the config: zero transfers between his two wallets ever; both buy straight from routers; of 84 coins both wallets hold, the BIG wallet bought first in 63. So two wallets in one bot, no transfer alerts, BIG label is the one to act on.

## 10 Sep 2026: GHOST ALERT, one wallet of @ghostdotenv
- [stated] Ben: "okay build an alert for him, separate bot", after the wallet hunt and hit-rate scoring in [[ghostdotenv-wallet]].
- **`/opt/ghostwatch` on kookwatch, unit `ghostwatch.service`, log `/opt/ghostwatch/ghostwatch.log`, bot @bk_ghostenv_bot (display name GHOST ALERT), chat 357464289.** Clone of /opt/neetwatch (sed: paths, unit, tag GHOST). Config: poll 2s, buys and sells only, transfers off, min $50, one wallet **GHOST `0x5c54382981bdf5276912a68c1d1a15a5354cc6a0`**. Registered in /opt/botctl/registry.json (19 on the droplet), Mac ~/botctl/registry.json (19) and the watchdog UNITS table (watchdog-bots restarted). Proof-of-life sent 12:25 UTC. backfill.py started detached for the P&L book.
- Started on the shared @bk_rhwatch_bot token with commands off (the neetwatch/chjokka pattern), then cut loose with settoken.py once BotFather issued the token. Token moved by clicking the code block in Telegram Web and `pbpaste | ssh ... 'cat > token.txt && python3 settoken.py'`, then `pbcopy < /dev/null`. Never printed.
- **Two things that went wrong in the UI, both fixed from the Bot API instead:** (1) the `key` sequence for the bot NAME misfired (capital letters plus `space` sent a second `/newbot` and the name ended up "bk_ghostenv_bot/newbot"); **`setMyName` and `setMyDescription` over the Bot API fixed it in one call, so never fight BotFather for the display name again.** (2) settoken.py's proof-of-life returned HTTP 400 because Ben had not pressed START on the new bot yet; after START it sent fine. Order for next time: /newbot, copy token, search the username, START, then settoken.py. Also `/cancel` sent to BotFather afterwards to clear a stray `/setname`.
- Fleet count after this: 19 registered, 18 active.

## 10 Sep 2026: LINKSCAN, the first bot that screens LAUNCHES rather than wallets
- [stated] Ben asked for a tool that scans new launches whose token description carries links (website, X), tests whatever is there, and decides whether it is original and working, cheaply, hundreds of times a day. Then: "yes run it."
- **`/opt/linkscan` on kookwatch, unit `linkscan.service`, log `/opt/linkscan/linkscan.log`, bot @bk_linkscan_bot (display name LINKSCAN), chat 357464289.** Four files: `core.py` (feed, link splitting, simhash, fingerprint, copy detection), `checks.py` (RDAP domain age, twitterapi.io handle info, API endpoint discovery and probing, contract footprint via eth_getCode), `scan.py` (the funnel and the verdict), `linkscan.py` (service loop, Telegram card, queue-when-no-token).
- **Feed is free: DexScreener `token-profiles/latest/v1` plus `token-boosts/latest/v1`, merged and deduped by (chain, ca.lower()), keeping only link-bearing tokens.** That is the whole discovery layer, no key, no Dune, no browser.
- **Five verdicts: DEAD (no site, parked, empty, SSL dead) · COPY (site is a near-duplicate of another token's site) · FAKE (website is someone else's domain, or named contracts do not exist) · THIN · LOOK.** Only LOOK, COPY and FAKE above a $20,000 cap floor are alerted; DEAD and THIN are counted in `stats.json` and never sent.
- Measured cost and speed: **3.6 seconds per token, about $0.10 a day at 300 launches.** First full day on Robinhood Chain: 145 tokens screened, 74 THIN, 33 DEAD, 17 LOOK, 12 FAKE, 3 COPY, 24 alerts sent.
- **Two real catches on the first pass, both of which a human eye would have missed:** a token listing `huggingface.co` as its own website (caught by a THIRD_PARTY domain list, verdict FAKE), and the library self-matching every token against itself on re-scan (fixed by storing `ca` in each library entry and skipping same-ca comparisons).
- **DUPLICATE-ALERT BUG AND THE GATE THAT FIXES IT: two overlapping runs both alerted SANTACOIN four seconds apart, because each had loaded `seen.json` before the other wrote it.** Fix is two-layer and both layers matter: the service is a single systemd unit with an internal 600s loop (never cron, which can overlap), AND before every send the code re-reads `seen.json` from disk and skips any ca already carrying `alerted: 1`. In-memory dedupe alone is not enough when two processes share a state file.
- **WATCHDOG COMPATIBILITY IS A DESIGN CONSTRAINT, not an afterthought.** It was built as a `*/10` cron entry first, which works fine but is invisible to `/opt/watchdog/watchdog.py`, since that only knows `systemctl is-active` on its UNITS table. Converted to `linkscan.service` with a `loop()` (and a `once` argv for manual runs), cron entry removed, UNITS line added as `("/opt/linkscan/linkscan.log", 2400, None)` because it logs at least one line every pass. **Any future bot in this fleet should be a systemd unit from the start for this reason.**
- BotFather went cleanly this time using the method recorded above: display-scope control, click the input, `cmd+return` to send, `/newbot` then name then username, then **click the token code block and read it with `pbpaste`, never off a screenshot**, then search the username and press START before the first alert. Token piped straight into the droplet with `pbpaste | ssh ... python3` so it was never printed.
- Registered in the Mac `~/botctl/registry.json` (20 bots) and the watchdog UNITS table.
- Next stage Ben agreed to: run a week of fingerprints into `library.json` first, then bolt an AI judgement layer on once the base rates show which verdicts actually survive.

### LINKSCAN, same day: positive-only alerting and the first real benchmark
- [stated] Ben: "link scan should only send positive hits, bad stuff should not get a notification. keep collecting data and run through existing coins to get a benchmark."
- **`config.json` now carries `alert_on`, set to `["LOOK"]`.** COPY, FAKE, DEAD and THIN are still computed, still written to `bench.json`, `stats.json` and `library.json`, and never sent. The gate line is `if r["v"] in tuple(cfg.get("alert_on") or ["LOOK"])`, so widening it later is a config edit, not a code edit. **This retires the earlier "LOOK, COPY, FAKE are alerted" note above.**
- **New file `/opt/linkscan/backfill.py`: scans the existing coin universe silently and never sends anything.** Resumable via `backfill_done.json`, writes `bench.json`, and marks every backfilled coin `alerted: 1` in `seen.json` so the live service can never re-alert history. Cron `35 */6 * * *` with `flock -n /tmp/lsbf.lock`.
- **UNIVERSE ASSEMBLY, and the trap in it: `/opt/launchwatch/registry.json` holds 2,404 Robinhood Chain tokens but DexScreener `tokens/v1` only returns pairs for about 390 of them.** The other 2,000 are launches with no indexed pair. So the registry alone is a bad universe; it is unioned with GeckoTerminal `/networks/robinhood/pools` paged 1 to 10 on both `h24_volume_usd_desc` and `h24_tx_count_desc` (the live traded set), plus the current feed. **`tokens/v1` is also the cheapest link source there is: its `info.websites` and `info.socials` come back in the same 30-address batch as market cap, liquidity and volume, so discovery and enrichment are one call.**
- **THE BENCHMARK, 10 Sep 2026, 386 indexed Robinhood Chain tokens: only 51 carry any link at all, so 87% of the chain has nothing to screen.** Of the 51 link-bearing: **LOOK 6 (11.8%) · THIN 19 (37.3%) · DEAD 8 (15.7%) · FAKE 8 (15.7%) · COPY 10 (19.6%).** That is the base rate a LOOK now sits against: roughly one in eight link-bearing coins has a site that is original, live and not borrowed.
- **THE COPY RULE THAT THE BENCHMARK BOUGHT, and it is the single most valuable change of the session.** The first backfill returned zero COPY and 14 LOOK, which looked clean until three Orus clones (`orusagent.fun`, `orusagent.lol`, `orusagent.xyz`, three separate contracts) all sat in LOOK together. Measured: **DOM simhash Hamming distance 0, text distance 19, different asset bundles.** The site factory reuses the page skeleton and swaps the copy and the images, so every text-based and asset-based test misses it. **Fix: `dd <= 2` on the DOM simhash alone is a COPY, independent of text distance and asset hash.** Rerun: COPY went 0 to 10 and LOOK went 14 to 6, and the ten it caught are real families (Orus x3; `shielded.wtf` / `stonkchad.com` / `pea.family`; `bullions-rh.xyz` / `steppercpu.tech`; `app.netnet.capital` / `up33.xyz` / `chainscout.net`; `scraps.bot` / `nasdex.app`; `sieve.computer` / `gaia.finance`). **Generalisable lesson: page STRUCTURE is the fingerprint that survives a rebrand, text and images are not.**
- `library.json` was also deduping badly (156 rows for 129 real sites, same ca and domain repeated on every rescan). Deduped on `(ca, key)`.
- Standing caveat to check as the library grows: `dd <= 2` will eventually fire on off-the-shelf templates (a stock Next.js landing page) rather than a deliberate clone family. It costs nothing today because COPY does not alert, but if COPY is ever turned back on, require a second signal (shared asset hash, shared OG image, or the same creator wallet) before calling it.

## 11 Sep 2026: CRASH ACCUMULATION BOT rewritten to report ROTATIONS, not legs
- [stated] Ben, after a screenshot of four alerts: "fix this. selling or buying stable coins is not what i'm interested in. what coins did he rotate into or out of."
- `/opt/crash-bot/crashbot.py` on kookwatch, unit `crashbot.service`. Patched in place by `/tmp/patch_crash.py` (parse, byte-compile, import a temp copy, assert symbols, THEN write, backup `crashbot.py.bak-20260911-022728`). Live and cycling clean.
- **THE HEADLINE BUG WAS NOT THE STABLECOINS, IT WAS PHANTOM EVENTS.** `snapshot()` swallowed a failing `getTokenAccountsByOwner` with `continue`, so one RPC timeout produced a snapshot missing an entire token program. The next diff read as "sold everything", the cycle after as "bought it all back". That is the whole 11 Sep alert set: W6 "sold $222,862 USDC" 01:25 then "bought $222,862 USDC" 01:33, and W5 "sold $147,115 Cupsey" 02:14 then "bought $145,153 Cupsey" 02:24. **Verified against chain: neither wallet had ANY transaction that day. All four alerts were fabricated by the bot.** Fix: the snapshot is atomic, a failed read raises and the wallet is skipped with its old snapshot kept. **Generalisable rule for every bot in this fleet that diffs state: a partial read is not data. Never `continue` past a failed sub-read inside a snapshot.**
- Second bug: `attribute()` accepted any recent tx whose delta merely pointed the same way, no magnitude and no recency check, so a phantom event got a real old tx pinned to it and was labelled a trade. Now the snapshot carries `t`, and only txs at or after the previous snapshot (minus 90s slack) are candidates, and of those the one whose own delta is closest to the observed delta wins. No tx in the window means kind `unknown`, which is already suppressed by config.
- Third bug, the one Ben saw: one alert per LEG. Every swap fired twice and the quote side was treated as a position with its own market cap (USDC's $9.35B "market cap" in that alert is the giveaway). Now `check()` groups legs by transaction signature and `alert_group()` sends **one message per transaction**. New module constant `QUOTE_MINTS` (SOL, USDC, USDT, PYUSD, USD1, JupUSD, USDG, USDS, USDY, USDCet) marks the quote side, which is **never the subject of a message, only the price tag**. Shapes: 🔁 "rotated Cupsey → JOTCHUA" (real token out, real token in) · 🟢 "bought $X of TOKEN / Paid with 147,115 USDC" · 🔴 "sold $X of TOKEN / Received 693 SOL". A move touching only quote mints (SOL→USDC) is logged and never sent.
- Also: the SOL leg of a swap no longer fires its own funding alert, only the residual the swaps do not explain. And each wallet's `check()` is wrapped in its own try, so one wallet erroring no longer skips the rest of the cycle.
- Verified with a seven-case fixture run against the live module on the droplet (`/tmp/t_crash.py`): the exact SOL→USDC alert Ben complained about is silent; USDC→Cupsey renders as a buy; Cupsey→JOTCHUA as a rotation; sells, SOL-funded buys, unattributed moves and sub-min_usd dust all behave.
- State of the wallets as of the fix, checked on chain: see [[crash-wallet-activity]]. He has rotated into nothing.

## 11 Sep 2026: DOUG ALERT, three units on one bot for @cryptoklotz (doug funnie)
- [stated] Ben: "set up a new alert, remote access my desktop and set it all up, including the fomo wallet." Built after the wallet hunt in [[cryptoklotz-wallet]].
- **Bot @bk_dougalert_bot (display name DOUG ALERT, set via setMyName), chat 357464289. Three systemd units share the token:** `dougwatch.service` (/opt/dougwatch, clone of ghostwatch tracker.py, RH chain, commands ON, wallets **DOUG RH 0xd42fcea321b8b0093fd78e76182dd92f588c0a9b** and **DOUG fomo-RH 0x5795f2390e9e4c1c2e4664a9271339cacca5b658**, buys+sells+transfers, min $50; Relay fills classify as trades through settlement contract 0xb92fe925dc43a0ecde6c8b1a2709c170ec4fff4f already in the config) · `dougsol.service` (/opt/dougsol/dougsol.py, clone of the 11 Sep rotation-aware crash-bot, wallet **DOUG SOL HkgewTTgx9kY5CnfEX9ozwtAbnUod2xMtr5Sgb3Dw7ew**, min $50, poll 12s, Helius RPC inherited; **patched so handle_commands() only runs when `commands_enabled` is true, set false**, because two getUpdates pollers on one token would 409) · `dougfomo.service` (/opt/dougfomo/dougfomo.py, new ~150 lines, polls fomo `/v2/users/{id}/swaps?limit=50` every 20s, dedupes by swap id, primes on first run, queues to queue.json when no token).
- **fomo API facts that make dougfomo possible:** prod-api.fomo.family answers 431 `{"error":"unauthorized"}` to unauthenticated calls from the droplet AND the Mac, so the old "Cloudflare WAF bans the range" note is wrong for the droplet, it just needs the bearer. **Privy refresh token is REUSABLE:** `POST https://auth.privy.io/api/v1/sessions` body `{"refresh_token"}` headers `privy-app-id cm6h485o300n3zj9yl6vpedq7`, `privy-client react-auth:2.13.0`, `privy-ca-id <localStorage privy:caid>` returns a fresh 1h `token` with `refresh_token: null` and `session_update_action: "ignore"`, and works without a bearer. So the bot can mint its own access tokens forever without logging Ben's browser out. Needs `/opt/dougfomo/privy.json` = `{"app_id","refresh_token","caid","user_id":"9e9abf29-fa2f-5af4-845b-417612ea4de8"}`.
- **BLOCKED STEP: the auto-mode classifier refused to let page JS copy the refresh token to the clipboard ("Credential Materialization").** Handed Ben a DevTools `copy(...)` one-liner to run himself on fomo.family, after which `pbpaste | ssh ... 'cat > /opt/dougfomo/privy.json'` finishes it. Until then dougfomo logs "no privy.json yet; waiting" every 30s and alerts nothing. The fomo-RH EVM wallet in dougwatch covers the RH half of the challenge account on-chain regardless.
- Registered: Mac ~/botctl/registry.json (23 bots), droplet /opt/botctl/registry.json (21), watchdog UNITS lines for all three (log_age 0), watchdog-bots restarted. Proof-of-life sent 07:31 UTC (message_id 4).
- BotFather via Telegram Web again: the `key` tool types the words "slash" and "underscore" literally; use the actual `/` and `_` characters ("/ n e w b o t Return", "b k _ d o u g a l e r t _ b o t Return"). Token copied by clicking the code block (coords from getBoundingClientRect scaled by screenshot/CSS ratio 0.817), `pbpaste | ssh` into three token.txt files, `pbcopy < /dev/null`. **A screenshot of the BotFather chat exposed the token in the image once; never screenshot that chat after /newbot.**
- Solana watcher baseline: 157 tokens, 0.1126 SOL. Startup notice reads "DOUG ALERT (Solana) restarted".

## 11 Sep 2026: UNIPCS STALKER, the first bot that watches an EXCHANGE DOOR instead of a person
- [stated] Ben, after the EMBER trace in [[unipcs-wallets]]: "how do we find new KuCoin-funded burners before everybody else?" then "go for it, we call that the Unipcs Stalker. Build a new Telegram alert bot for that."
- **`/opt/unistalk/unistalk.py` on kookwatch, unit `unistalk.service`, log `/opt/unistalk/unistalk.log`, bot @bk_unipcs_stalker_bot (display name UNIPCS STALKER via setMyName), chat 357464289.** ~270 lines, written fresh. Mechanism: poll KuCoin Hot Wallet `BmFdpraQhkiDQE6SnfG5omcA1VwzqfXrwtNYBwWTymy6` signatures every 8s (Helius first, public RPC fallback, same rpc_urls as crash-bot), parse top-level system transfers OUT ≥ `min_sol` 200; recipient with ≤ `fresh_max_sigs` 2 prior signatures = tracked burner (👀 funding alert); burners polled for `track_hours` 6 (kept longer once they buy); first token inflow with SOL out = 🚨 LOOK alert (coin, clip size, mcap, pair age, running total), repeat alerts at clip counts 3/5/10/20/50 or any clip ≥ 100 SOL; 🔴 sells, 📦 token moved out (names fomo's Solana deposit wallet `2heJbC32Tpfcb3nbUb5ER61K11FGZVfVGtVnDm6LDogF` as the unipcs confirmation), ↗️ SOL forwarded. /status /mute /unmute. Queue-when-no-token pattern. Registered in Mac ~/botctl/registry.json (24) and droplet /opt/botctl/registry.json (22), watchdog UNITS line added, watchdog-bots restarted. Live 11:24 UTC, baselined (no history replay).
- Would have fired on today's case at 08:49 UTC (500 SOL into 5Yyz7b37bzoL8rdeFxpVL539BURt8AMsxPEzw9HaE7Xr) and again 16 seconds later on the first 200 SOL EMBER clip at $7.3M, versus fomo showing it at 10:57 at $14.4M.
- Known limits, stated to Ben: cannot prove a burner is unipcs until the bag moves to fomo; KuCoin has other hot wallets (only BmFdp is in `hot_wallets`, config-extendable); a burst of >100 hot-wallet txs inside one 8s poll would drop the overflow.
- **BotFather token copy: click-to-copy on the code block did NOT reach the Mac clipboard this time (three tries, coords and ref), even with Chrome activated via osascript. What worked: a real click on empty chat area for user activation, then page JS `navigator.clipboard.writeText(code.innerText)` in the same browser_batch, then `pbpaste | ssh`.** The `find` tool and two BotFather screenshots exposed the token in context again; never screenshot the BotFather chat after /newbot, and query `find` for the START button or search rows only.

## 11 Sep 2026: UNIPCS STALKER, RH module + noise tiers
- [stated] Ben: "make sure that it's really him ... I want to make sure that I don't get bombarded, while at the same time I want to get as many of his early buys notified early."
- **`unistalk-rh.service`** (/opt/unistalk/rhstalk.py, config_rh.json, state_rh.json, rhstalk.log, same @bk_unipcs_stalker_bot token, commands off) watches exchange hot wallet 0x88D25C861938a91AF4ad57aD964a8fCc6c6351d3 + treasury 0xD0A1F16bf2010985D861671293420D61F1d9130a (nonce watch, then an 8-thread block scan capped at 1,500 blocks) and the Relay API (destinationChainId=4663, paged on `continuation`, ETH out ≥ $5k) for fresh recipients; burners tracked by ERC20 Transfer logs; outflow to fomo omnibus 0x0a6ebed0155edb4b21d92ad02897a626cd90119e = CONFIRMED, appended to /opt/unistalk/confirmed.json (the Solana module does the same on moves to 2heJbC32Tpfcb3nbUb5ER61K11FGZVfVGtVnDm6LDogF). Registered on both registries (Mac 25 bots, droplet 23) and in watchdog UNITS.
- **Measured base rate before setting thresholds:** the RH hot wallet sent ≥2 ETH only twice in the last 3 days (15 ETH and 4 ETH on 11 Sep, both to OLD wallets, so the fresh filter drops them). So the RH door is quiet by nature. KuCoin Solana is busier (~5 tx/min) but fresh + ≥200 SOL is rare.
- **Tiers (patch3, both modules):** funding ping only when fresh AND ≥ 5 ETH / ≥ 300 SOL (`fund_alert_min_*`); first-buy LOOK only when the burner is < 6 h (Sol) / 12 h (RH) old AND the clip is ≥ 20% of its funding or ≥ $5k (`first_buy_min_frac`, `first_buy_min_usd`, `first_buy_max_hours`); 🔥 STRONG once when ≥ 3 clips in one coin add up to ≥ 50% of funding (`strong_clips`, `strong_frac`); otherwise buys are logged "(quiet)". Clip milestones 10/20 still ping. CONFIRMED always pings.
- Bot backups: unistalk.py.bak-*, rhstalk.py.bak-* in /opt/unistalk.
- **Verified 11 Sep 12:32 UTC by replaying real chain history through the live modules with a stubbed Telegram (/opt/unistalk/stalk_test.py):** Solana replay of the EMBER burner produced funding ping → LOOK on the first 200 SOL clip → STRONG at clip 3 → MOVE-to-fomo (7 messages for the whole play, 30+ buys stayed quiet); RH replay of the MEME burner 0x66ec4733ccf97502e6553297cfb9147c514f69dd produced LOOK → STRONG at clip 4 → CONFIRMED (3 messages). Test appends to confirmed.json were removed afterwards. Live, the Solana module had already caught a fresh 351 SOL KuCoin wallet (8PnknvAuWMuk7m31syZmGt8tMoDaqyFLpPPZyU3MT95W, 12:23 UTC) and skipped a 512.8 SOL send to an old wallet.
- **patch4: RH hot wallet nonce moves every ~40s (0.01 ETH dust sends), and each full-block scan took 27-69s, so the scan is now gated on the hot wallet's ETH balance dropping by ≥ 0.75 × min_eth between polls (`scan_drop_factor`), 12 workers.** Known blind spot: an inflow masking an outflow inside one 8s poll.
- **First live false positive, 11 Sep 20:33 Bangkok: "LOOK: fresh RH burner is aping WETH" (0xef61267bf1548c32319f39bca64ca15fe17eb10a, 2.9 ETH via Relay from Ethereum, then WETH inflow $7,243).** WETH is just wrapped ETH, money not a coin; the RH module had no quote-token exclusion (the Solana module already had QUOTE_MINTS). Fix: `quote_tokens` in config_rh.json (WETH 0x0bd7d308f8e1639fab988df18a8011f41eacad73, USDG 0x5fc5360d0400a0fd4f2af552add042d716f1d168, 0x4200000000000000000000000000000000000006) skipped on both inflow and outflow; the bogus buy was scrubbed from state so the wallet is still watched for a real coin.

## 11 Sep 2026: GRAIL ALERT, two linked wallets of @graildoteth
- [stated] Ben: "okay build a separate alert for him. Grail Alert." Built after the wallet hunt and double-check in [[graildoteth-wallet]].
- **`/opt/grailwatch` on kookwatch, unit `grailwatch.service`, log `/opt/grailwatch/grailwatch.log`, bot @bk_grailalert_bot (display name GRAIL ALERT via setMyName), chat 357464289.** Clone of /opt/ghostwatch (tracker.py, pnl.py, backfill.py, report_pnl.py, bookreport.py, settoken.py, sed paths and tag GRAIL). Config: poll 3s, buys and sells only, transfers OFF (the two wallets shuffle tokens between each other and stake into vaults, so transfer alerts would be noise), min $50, commands on, wallets **GRAIL A 0xdc0aa7c3205fe9c6b077522c5ca1acc4599af0d2** and **GRAIL B 0xa988002cdde8101d214bd3be92364f07dc11ad8b**. Started on the shared @bk_rhwatch_bot token with commands off, then settoken.py once the new token landed. Proof-of-life 16:49 UTC. Registered droplet /opt/botctl/registry.json (24) and Mac ~/botctl/registry.json (26), watchdog UNITS line added, watchdog-bots restarted. backfill.py NOT run yet (droplet memory too tight, see below), so P&L lines start thin.
- **NEW TRAP, worse than the screenshot one: `get_page_text` on the BotFather chat dumps EVERY token ever issued into context (eight of them this time).** Never call get_page_text, read_page or find-by-token-text on the BotFather chat. The clean flow that worked end to end: search box, type "BotFather", click the search row, click the message box, `key` "/ n e w b o t Return", short name, username, then a real click on empty chat area plus page JS that regex-matches `^\d{8,}:[A-Za-z0-9_-]{30,}$` over `code` elements and `navigator.clipboard.writeText` on the LAST match, then `pbpaste | ssh ... 'cat > token.txt'`, getMe + setMyName + setMyDescription over the Bot API in the same ssh, `pbcopy < /dev/null`. Then search the username, click the Global Search row, START, then settoken.py.
- **DROPLET IS AT ITS MEMORY CEILING: 961 MB total, 22 to 100 MB available, load average hit 47.** Any research script over ~100 MB RSS is OOM-killed (three times today), and `unistalk-rh` rhstalk.py had grown to 95 MB RSS (restarted to reclaim it, state persists in state_rh.json). 26 bots now. Before adding the next bot, either drop a dead one (mastalert is still registered but not-found) or resize the droplet. Research pulls should run on the Mac, not the droplet, while it is this full.

## 11 Sep 2026: UNIPCS STALKER RH, patch5 after two false alerts Ben pasted
- [stated] Ben pasted the 🔥 STRONG "RH burner clipping ROBINHOOD" (0xc686ab2aa8dd2146057c32f0f9b990ce6463070e) and 🚨 LOOK "fresh RH burner is aping PLTR" (0xac610aacb4d2ff1c615e414066fb6aaef288206e) alerts: "double check both of these".
- **Both wrong, for different reasons.** (1) 0xc686ab2a is not a burner: 21 Relay requests since 7 Aug hopping Solana / BNB / Ethereum / RH in $1k to $4k lots, came into RH with 4.5 ETH on 7 Sep and left again 8 Sep (nonce 1 at funding, so it passed `fresh_max_nonce 1`). It bought $10k of ROBINHOOD (PonsV2 memecoin 0x008df4b3e857d06c4603aeb11f267ccd32ce2005) in 11 clips and still holds 5.33M, but it is a small multichain trader, not unipcs shape. The "154% of funding" was a bot bug: the second Relay fill (1.596 ETH from BNB, $4,144) was below `min_usd_relay` 5000 so it was never counted as a top-up; real funding 3.97 ETH, buys 97%. (2) 0xac610aac is genuinely fresh (4.98 ETH from Base) but "PLTR" 0x894e1ec2d74ffe5aef8dc8a9e84686accb964f2a is **"Palantir Technologies • Robinhood Token", the tokenized STOCK**, supply 12,966: it bought 63 shares (~$10.6k) and deposited 48 into contract 0x56D1BbBC3da0dFe141bd4dAEf9cF10492E50fA07. Not a coin, not unipcs.
- **patch5 (rhstalk.py, backup .bak-20260911-18*):** `dex()` now stores baseToken name and a `stock` flag (name contains "robinhood token", or "•" plus "robinhood", or ends with "stock"); `scan_burner()` skips stock inflows as "tokenized stock (not a coin)". `fresh()` now also queries `api.relay.link/requests/v2?user=` and refuses any wallet with a Relay request older than `fresh_relay_hours` 2, so cross-chain hoppers with a clean RH nonce no longer pass (verified: fresh(0xc686ab2a) = False). Top-ups: `relay_one()` and `withdrawal()` accept ANY size for an already-tracked burner (only new burners need `min_usd_relay` / `min_eth`), so funding totals and `frac` are right. Both false burners scrubbed from state_rh.json (10 tracked left). Service restarted 18:46 UTC, active.
- Standing lesson for every RH bot: Robinhood's own tokenized stocks trade in the same v4 pools as memecoins and DexScreener lists them with a market cap, so any "fresh wallet bought X" logic must check the token NAME for "Robinhood Token" before calling it a coin.
- **patch6, same evening, after Ben pasted "↗️ RH burner sent 53 NVDA ($11,725) to 0x8366a39cc670b4001a1121b8f6a443a643e40951": "this isn't really relevant".** Two more holes: patch5's stock skip only covered INFLOWS, and an outflow to the Uniswap v4 PoolManager 0x8366a39cc670b4001a1121b8f6a443a643e40951 was being called "sent to" when it is a sell. Now: outflow of a tokenized stock is ignored; an outflow whose destination has code (pool, router, vault, cached in `self.codec` via eth_getCode) is a 🔴 SOLD and only pings if the burner had an alerted buy; an outflow to an EOA only pings if the burner was alerted or the amount is ≥ `first_buy_min_usd`; the fomo omnibus CONFIRMED path is unchanged. First attempt of this patch broke the file (a `\\n` inside a heredoc inside single-quoted ssh became a real newline, SyntaxError at line 280, service down ~90s): **ship patch scripts to the droplet as base64, never as nested heredocs**, and the patch script now restores the newest compiling backup before applying. Burner 0x28acaa9bee1b0bac48f17445290a8137be5937ba (5.68 ETH from Base, bought and sold 53 NVDA stock) scrubbed. 9 burners tracked, service active 19:02 UTC.

## 12 Sep 2026: ALFIE ALERT, two linked wallets of @enjoy0r
- [stated] Ben: "okay build an alert, new one, use laptop to set it up." Built after the wallet hunt in [[enjoy0r-wallet]].
- **`/opt/alfiewatch` on kookwatch, unit `alfiewatch.service`, log `/opt/alfiewatch/alfiewatch.log`, bot @bk_alfiealert_bot (display name ALFIE ALERT via setMyName), chat 357464289.** Clone of /opt/grailwatch (same six files, sed paths and tag ALFIE). Config: poll 3s, buys and sells only, transfers OFF (the two wallets pass ETH between each other constantly), min $50, commands on, wallets **ALFIE BAG 0x04a9c71faff1f6b1fbd8844d83450f8c04ceee09** and **ALFIE HOT 0x10490b4560d6c6956510842c0312fb6c9543fd09**. Started on the shared @bk_rhwatch_bot token with commands off, then settoken.py. Proof-of-life 01:49 UTC. Registered droplet /opt/botctl/registry.json (25) and Mac ~/botctl/registry.json (27), watchdog UNITS line added, watchdog-bots restarted. backfill.py not run.
- **DROPLET MEMORY CEILING FIXED THE CHEAP WAY: 1 GB swapfile added (`/swapfile`, fstab entry, swappiness 10).** Available went from 49 MB to 332 MB with 348 MB paged out. No more OOM kills expected for bots; research scripts still belong on the Mac.
- **Telegram Web K (`/k/`) went blank and stopped sending this session (local echo of /newbot without delivery, then a fully white page after reload). The `/a/` client at `web.telegram.org/a/#93372553` (BotFather's id) worked instead.** In `/a/`: `#editable-message-text` is the input; `execCommand('selectAll')`, `delete`, `insertText`, then click the `.Button.send` sends reliably (a synced draft from `/k/` was still in the box, so ALWAYS clear the input first, otherwise "/newbot" plus the draft goes out as one message).
- **Clipboard rule learned: `navigator.clipboard.writeText` silently times out when the tab's `document.visibilityState` is "hidden".** The MCP tab was behind another tab. Fix: osascript to activate Chrome AND select the tab by URL (`set active tab index of w`), then a real click plus the JS write in the same browser_batch. `clipboard.readText` opens a permission prompt, never call it. Token then went `pbpaste | ssh ... 'cat > token.txt'`, getMe + setMyName + setMyDescription in the same ssh, `pbcopy < /dev/null`, never printed. Opening the new bot: type the username in the global search with the `type` action (JS insertText does not update the search state), `find` the "ALFIE ALERT @bk_alfiealert_bot" row, click, click START.
- Nested-heredoc-in-ssh SyntaxError bit again on the registry patch; shipped as base64 as the rule says. Fleet count after this: 27 registered on the Mac.

## 12 Sep 2026: XALERT and IGNITION DELETED. LAUNCHWATCH KEPT.
- [stated] Ben: "Also remove the X alert stuff completely. All this stuff with ignition, they're too much. Delete them all." Then, mid-task: "dont turn off the launchwatch, let it run."
- **xalert (`/opt/x-alert`, @xalertben_bot, watched brozinkerbell and forginvestments): stopped, disabled, unit file `/etc/systemd/system/xalert.service` DELETED, daemon-reloaded.** `xalert.py`, its backups, `state.json`, `install.sh`, `uninstall.sh` moved to `/opt/x-alert/RETIRED-<ts>/`. Row removed from `/opt/watchdog/watchdog.py` UNITS, from the droplet `/opt/botctl/registry.json` (25 -> 24) and the Mac `~/botctl/registry.json` (27 -> 26). watchdog-bots restarted and active.
- **`/opt/x-alert/config.json` was deliberately LEFT IN PLACE and must stay.** `/opt/xq.py` reads the live twitterapi.io key from exactly that path (`for p in ("/opt/x-alert/config.json","/opt/followwatch/config.json")`), and that is the documented fallback in the research-sources skill when the hardcoded key rotates. Deleting the directory would silently break X search for the whole stack. A `RETIRED.txt` in the directory says so.
- **ignition (`/opt/launchwatch/ignition.py`, the `*/2` cron ignition-hour scanner): cron line removed** (crontab backed up to `/tmp/cron.<ts>.bak`), `ignition.py`, `ignition.py.bak-asquote`, `ignition_state.json` and `ignition_ages.json` moved to `/opt/launchwatch/RETIRED-ignition-<ts>/`. The filter study itself stays in [[alert-bot-fleet-3]] and the artifact; only the alerting is gone.
- **LAUNCHWATCH IS STILL RUNNING AND MUST STAY:** cron `*/3` on `/opt/launchwatch/launchwatch.py` and `0 3` on `deptab.py`, plus `lwserve.service`. Verified no code path in launchwatch.py, serve.py or deptab.py references ignition, so removing ignition cannot break it. `linkscan` backfill cron untouched.
- Verified after the change: zero xalert units, zero ignition cron lines, no xalert or ignition processes, watchdog-bots active, launchwatch run.log still cycling.
- Fleet count: 26 registered on the Mac. This follows the 9 Sep removal of ceofollow in [[alert-bot-fleet-3]]. **Pattern to note: Ben is pruning bots that produce volume without a decision. When a bot is cut, always remove it from BOTH registries and the watchdog UNITS table in the same pass, or `botctl status` and the watchdog will report a dead unit forever (mastalert has been doing exactly that for a week).**

## 12 Sep 2026: GHOST ALERT gets a SOLANA module, because the wallet it watched went empty
- [stated] Ben asked to check the GHOST wallet against @ghostdotenv's latest posts, then "Yes, find his other wallets." Full forensics in [[ghostdotenv-wallet]].
- The RH wallet `0x5c54382981bdf5276912a68c1d1a15a5354cc6a0` is still CORRECT (re-proved out of sample: it bought MOTH `0x7db66154fbe0f046d0b3be719758d97dea52d90c` at 11 Sep 19:20:22 UTC, 89 seconds before he replied to @mothinterface, on a $2,618 cap coin) but he drained it to **$138** and moved to Solana. `ghostwatch.service` alone could only ever fire on dust.
- **NEW UNIT `ghostsol.service`, `/opt/ghostsol/ghostsol.py`, log `/opt/ghostsol/ghostsol.log`, config `/opt/ghostsol/config.json`.** Clone of `/opt/dougsol` (the 11 Sep rotation-aware crash-bot lineage), code copied and sed-renamed, **state deliberately NOT copied**. Shares the @bk_ghostenv_bot token with ghostwatch, so `commands_enabled` is **false** here (two getUpdates pollers on one token 409, same rule as dougsol). Poll 10s, min_usd 50, buys and sells, transfers_in off, transfers_out on, alert_unknown off.
- Wallets: **GHOST SOL (main book) `Hz8535i7KtnUPFLQxXAJRvwVgbCeLi2sxnKhDbTXxvVo`** and **GHOST FOMO (new 4.5k acct) `A3o9MC2cAsCvm56jKq3QcyF4452YLCMqjjCrkajSsku`**. Live 04:58 UTC, baselined 100 tokens / 4 tokens, zero errors, first correct suppression at 05:01 ("unattributed move suppressed ... WETH", the hourly stonk.fun holder-reward stream). Registered droplet `/opt/botctl/registry.json` (25) and Mac `~/botctl/registry.json` (27), watchdog UNITS line added, watchdog-bots restarted. Proof-of-life message_id 10.
- **THE METHOD THAT FOUND THE WALLETS, and it belongs in every future KOL hunt: the Relay bridge API indexes by sender and hands back the address on the other chain.** `https://api.relay.link/requests/v2?user=<address>&limit=50`, top-level `recipient` is the destination, `data.inTxs[0].chainId` / `outTxs[0].chainId` the route, **Solana is chainId 792703809**. One unauthenticated call replaced a full session of timestamp fingerprinting. The unistalk-rh module already calls this endpoint for freshness; it is just as good for identity.
- Droplet after this: 961 MB RAM with the 1 GB swapfile, ~192 MB available with ghostsol running. Fine, but it is still the binding constraint on new bots.

## 12 Sep 2026: which bot caught PAIREX (Ben asked "which of the alerts had found pairex token?")
- Real PAIREX is `0xb8cf4ad387cfd607c66a207cfffc46498aacd9d6` (pairex.market, X @pairexmarket, USDG pool created 05:55:30 UTC).
- **CORRECTION, made the same hour: LINKSCAN FOUND it but NEVER SENT it.** I first read `"alerted": 1` in seen.json as "an alert went out". It means the opposite: `backfill.py` writes that flag so the live service can never alert the coin later, and backfill itself calls no send function at all ("Sends nothing" in its own docstring). **Ben got no PAIREX message.** The flag name is what misled me; any future audit must check for a `send()` call, not a state flag. Details of the find: 🟢 WORTH A LOOK computed 06:36:33 UTC at $234,589 cap, $39,360 liq, $437,155 vol, domain 0.3 days old, X account 3 days old, 4 of 6 named contracts real, zero flags. Came through the `backfill.py` path (`src: backfill`), not the live feed, which is worth noting: the backfill scan of the existing universe is producing live alerts, not just benchmarks.
- **RUGSCAN also scanned it, 06:09:07 UTC, but only because `/opt/rugscan/watch.json` watches Ben's own fomo wallet `0x01c2E8cA848486166e3D08fa987d69b65df0D00F` and that wallet bought it.** So that alert was "fomo wallet bought PAIREX ... rug check running", verdict **AVOID, risk 77** (launch cluster still holds 35% of supply, 41 wallets paid no launch tax while 853 paid, 10 wallets bought in one block taking 13%, 8 near-identical opening buy sizes, 68.3% of buyers had already sold in 2h). Note rugscan's `config.json` "watchlist" (DELTA/OPTIMUS/CHIPS/PAIR) is a separate token list; the wallet trigger lives in `watch.json`.
- **LAUNCHWATCH saw the pool and never alerted** (no BUY alert, nothing in blocked.jsonl). Its registry also shows **seven different PAIREX-named tokens created on 12 Sep alone** (0x93fee56d1039202904bc2210438434477762b986 fxEUR 04:02, 0xee20b95ab3f80cfcb29183e379132ee55bb9122a 05:29, 0xf713e7aa6265675de2e235222f4edbdb5a7c5c69 05:44, the real one 05:55, 0x6406e42586d53223ca5a0b8cb4b8e7323c7ad9d6 06:28, 0x061d2c3959ef9afeabd37bd8a3a88a3d6bbcd9d6 06:49, 0x93d6e67bd9b499b0af7776ef5c109b1457543907 06:58), all but the real one at sub-$10k caps in linkscan's bench as NO LINKS. **Ticker-only search on a hot RH launch is useless within the hour; match on the CA or the domain.**
- No other bot in the fleet touched the CA. By 07:05 UTC it was $675,393 cap, $67,378 liq, +577% in an hour.

## 12 Sep 2026: LINKSCAN kept, and the muted-discovery bug fixed
- [stated] Ben: "I like the feature that found it, so let's keep it. If Link Scan was deleted, then bring it back."
- **Nothing to restore: linkscan was never deleted.** The 12 Sep prune removed xalert and ignition only. `linkscan.service` active and enabled since 06:15 UTC, 10-minute passes logging clean, watchdog row present. It WAS missing from the droplet `/opt/botctl/registry.json` (Mac copy had it), now added: 26 bots there.
- **THE REAL BUG, and it is why PAIREX never reached him: the live service and the discovery scan look at different universes, and only the live one could talk.** Live `linkscan.py` screens the DexScreener token-profiles/boosts feed, which is only tokens whose team submits a profile. `backfill.py` screens the REAL universe (launchwatch registry + GeckoTerminal top traded pools + feed) and that is what catches organic launches like PAIREX, but it was written as a silent benchmarking job.
- **PATCH (backup `/opt/linkscan/backfill.py.bak-20260912-073213`, shipped as base64 per the standing rule):** `enrich()` now keeps `pairCreatedAt` and takes the EARLIEST pool of a token as its true age; `main()` gained an alert branch that sends the normal LOOK card plus a "found by the universe scan, pool X.Xh old" footer. Gated by four config keys so it cannot spam history: `backfill_alert` true, `backfill_max_age_hours` 24, `backfill_max_alerts` 6 per run, plus the existing `min_mcap` 20000 and `alert_on` ["LOOK"]. Anything older than 24h stays silent and benchmark-only exactly as before, and keeps `src: backfill`; a sent one is tagged `src: backfill-alert`.
- **Cron 6-hourly -> every 30 minutes (`5,35 * * * *`, flock unchanged).** A 6-hour discovery loop is useless on a chain where a launch does 10x in 90 minutes; PAIREX was screened 41 minutes after its pool opened purely by luck of timing. Runtime is 30s to 6 min and the run is resumable, so 30 minutes is affordable. Droplet after a live run: 205 MB available.
- **Verified with a fixture replay before trusting it (`/tmp/test_backfill.py` on the droplet, `linkscan.send` stubbed, state snapshotted and restored):** TEST1 enrich captured created=1789192530000, age 1.64h; TEST2 fresh PAIREX produced exactly one card ("WORTH A LOOK PAIREX ... found by the universe scan, pool 1.6h old") and tagged `backfill-alert`; TEST3 same coin with the age gate at 0.001h produced zero messages and tagged `backfill`. Then a real live run: 1,187 to enrich, 3 link-bearing, zero alerts, no spam.
- Standing lesson, wider than this bot: **a state flag named "alerted" is not evidence that anything was sent.** Two paths wrote the same flag for opposite reasons. When auditing whether Ben actually received something, trace the send call, and give the suppressed path a different tag so the log tells the truth.
- NOTE: this file is at ~46KB of its 49KB cap. The next fleet entry starts alert-bot-fleet-5.
