Files
rm-circle-team-router/tools/server/README.md
T
martbost 09e9d469dc Watch #220, and say who actually caught the money
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>
2026-09-24 16:25:27 -05:00

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

  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