# Chain watchers (they run on core, not in the app) Two standalone scripts that read the RM Circle contract and message Marty's Hermes chat. They are deliberately **not** part of the site: they need no database, no config volume and no deploy, and they must keep working even when the app is down. They live at `/root/` on core (`ssh root@coolify.saasy.top`) with their cron entries in `/etc/cron.d/`. Copies are kept here so a rebuilt box can be put straight back. | file | on the server | |---|---| | `rmc-depth-watch.py` | `/root/rmc-depth-watch.py` | | `cron.d-rmc-depth-watch` | `/etc/cron.d/rmc-depth-watch` | | `rmc-member-watch.py` | `/root/rmc-member-watch.py` | | `cron.d-rmc-member-watch` | `/etc/cron.d/rmc-member-watch` | Restoring one: ```sh scp tools/server/rmc-member-watch.py root@coolify.saasy.top:/root/rmc-member-watch.py ssh root@coolify.saasy.top 'chmod +x /root/rmc-member-watch.py' scp tools/server/cron.d-rmc-member-watch root@coolify.saasy.top:/etc/cron.d/rmc-member-watch ssh root@coolify.saasy.top 'chmod 644 /etc/cron.d/rmc-member-watch' ``` Both read the Telegram token from `/root/.mbhermes-telegram-token` (never in this repo) and keep their own state file under `/root/.rmc-*.json`. Debian cron **ignores `CRON_TZ`**, so every schedule in those files is UTC. ## rmc-depth-watch.py Warns when an organisation grows deeper than its apex's level can collect from. A member at depth *d* pays the apex on their upgrade to level *d+1*, and only when `apex.level >= d`. Upgrades stop at level 8, so `levelIndex` never exceeds 6 and **depth 7 is a hard ceiling** — nothing deeper pays an apex at any level, so that is not a miss and must never alert. Speaks only when the verdict changes, so a steady state stays silent. ```sh APEX=1154 python3 rmc-depth-watch.py # normal DRY=1 APEX=21 python3 rmc-depth-watch.py # print, never send, never write state ``` ## rmc-member-watch.py Watches one position and reports every upgrade it makes together with who caught the pass-up payment and how much. Added 24 September 2026 for `WATCH=220`: #220 sits at Apex, and a Fastigium upgrade skips the four uplines between them and #30, so Orlando's #30 should catch 9,942.44 POL. The message reports **what the contract did**, read off the logs — if somebody else catches it, that is what it says. ```sh WATCH=220 python3 rmc-member-watch.py # normal (cron, every 10 min) DRY=1 WATCH=220 python3 rmc-member-watch.py # print only, no send, no state write DRY=1 WATCH=319 BACK=600 python3 rmc-member-watch.py # rehearse against real history ``` `BACK` is how it was tested before going live: replayed over a real #319 upgrade, on both a wide-range endpoint and a chunked 50-block one. ## Two rules both scripts follow 1. **Never advance the block pointer on a failed scan.** A watchdog that quietly skips the blocks it could not read is worse than none: the event passes, nothing is said, and silence gets read as "nothing happened". 2. **Going blind is itself news.** Four consecutive failures sends a warning. Both failure paths — the block number and the log scan — feed one counter, because an earlier version counted only the log scan, and a total outage raised straight past it: the single outage most worth shouting about was the one that would have stayed silent. ## Polygon RPCs, measured from core rather than assumed | endpoint | `eth_getLogs` | |---|---| | `polygon-bor-rpc.publicnode.com` | 2000-block span | | `polygon.gateway.tenderly.co` | 2000-block span | | `polygon.drpc.org` | **50 blocks max** | | `1rpc.io/matic` | **50 blocks max** | | polygon-rpc.com, ankr, blastapi, blockpi | refuse (401 / 401 / 403 / 521) | Any client must chunk to the endpoint's own limit. **publicnode rate-limits core's IP routinely**, because the site already polls it from the same address, so a 429 there is ordinary rather than a fault — the watcher walks the whole endpoint list twice, 20 seconds apart, before it will call a run failed. All of these also need a `User-Agent` header: publicnode 403s the default Python-urllib one. Event shapes, verified against live logs: ``` MemberUpgraded(uint48 id, uint8 newLevel, ...) topics[1]=id, data[0]=new level UplineRewarded(uint48 to, uint48 from, uint8, uint256) topics[1]=to, topics[2]=from, data[0]=level, data[1]=amount wei ```