Marty asked for a watchdog on position #220: tell him when they buy their upgrade and whether Orlando's #30 gets paid. The chain says it should. #220 sits at Apex with four uplines between them and #30, and a level-6 upgrade skips exactly those four, so #30 is the first candidate the contract tests and it passes both gates (level 5 > 4, three directs). That is a prediction, though, and the watcher does not report predictions: it reads MemberUpgraded and UplineRewarded off the logs and says who was actually paid and how much. If somebody else catches it, it says so. Tested by replaying real history (BACK=600 over a live #319 upgrade) on both a wide-range endpoint and a chunked 50-block one, then the dedupe guard, then two outage cases. The outage cases mattered. A watchdog that dies quietly is worse than no watchdog, because silence gets read as "nothing happened", so: - a failed scan never advances the block pointer; unread blocks are read on the next run rather than skipped - four consecutive failures sends a warning instead of going quiet, and both failure paths feed one counter. The first version only counted the log scan, so a total outage -- where even the block number is unreachable -- raised straight past the counter, and the one outage most worth shouting about was the one that would have stayed silent. Also brings rmc-depth-watch.py into the repo. Both watchers existed only on core, with no copy anywhere, so a rebuilt box lost them silently. The README records the restore steps and which Polygon RPCs actually serve eth_getLogs (publicnode and tenderly take 2000 blocks; drpc and 1rpc cap at 50; four others refuse outright), measured from core rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
4.3 KiB
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:
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.
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.
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
- 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".
- 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