copybot: push mode — Alchemy webhook + heartbeat + backstop poll

Setting ALCHEMY_SIGNING_KEY on the worker switches start.sh to webhook
mode: instant copies on POST /alchemy (signature-verified), a 60s
housekeeping heartbeat (settle, feed, summary), and a full backstop poll
every 5 min so a dropped push costs minutes, not a silent miss. Webhook
mode now baselines at boot and publishes an initial feed like poll mode.
No signing key -> classic 60s poll, unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
jaxperro
2026-07-06 00:07:23 -04:00
parent 104534eef0
commit 0c7ee62ed7
3 changed files with 50 additions and 10 deletions
+29 -2
View File
@@ -914,8 +914,35 @@ def main():
signing_key = (os.environ.get("ALCHEMY_SIGNING_KEY")
or cfg.get("alchemy_signing_key", ""))
port = int(os.environ.get("PORT", 8080))
log(f"listening on :{port} · POST /alchemy · "
f"signature-verify {'ON' if signing_key else 'OFF'}")
bot.baseline()
# webhook mode is event-driven, but the book must not depend on the next
# push arriving: a heartbeat thread settles resolved positions, refreshes
# the feed, and logs the summary every 60s — plus a FULL backstop poll of
# every wallet each 5th tick, so a dropped/misconfigured push costs at most
# ~5 minutes of lag instead of a silent miss.
def _heartbeat():
cycle = 0
while True:
time.sleep(60)
cycle += 1
try:
bot.settle_resolved()
if cycle % 5 == 0:
for w in cfg.get("watchlist", []):
bot.on_wallet_activity(w)
bot.summary(cycle)
bot.write_feed()
bot.publish_feed()
except Exception as e:
log(f"heartbeat error: {e}")
threading.Thread(target=_heartbeat, daemon=True).start()
log(f"push mode · listening on :{port} · POST /alchemy · "
f"signature-verify {'ON' if signing_key else 'OFF'} · "
f"heartbeat 60s · backstop poll 300s")
bot.write_feed()
bot.publish_feed()
ThreadingHTTPServer(("0.0.0.0", port), make_handler(bot, signing_key)).serve_forever()
+19 -8
View File
@@ -11,9 +11,13 @@
# GITHUB_TOKEN fine-grained PAT with Contents: read+write on
# jaxperro/winning-wallet-finder (the push credential)
# Optional env:
# DISCORD_WEBHOOK ping on every placement
# POLL_SECONDS poll cadence (default 60 — keep well under the bot's
# 600s stale window so no trade is skipped)
# ALCHEMY_SIGNING_KEY presence switches the bot to PUSH mode: it serves
# POST /alchemy for the Alchemy address-activity
# webhook (~2-5s detection) with a 60s housekeeping
# heartbeat + 5min backstop poll. Requires the service
# to have a public domain. Absent -> classic poll mode.
# POLL_SECONDS poll cadence in poll mode (default 60 — keep well
# under the bot's 600s stale window)
#
# Railway setup (one time):
# 1. New service -> Deploy from GitHub repo -> jaxperro/winning-wallet-finder
@@ -34,8 +38,15 @@ cd "$DIR"
git config user.name "copybot[bot]"
git config user.email "copybot@users.noreply.github.com"
# paper config is committed (no secrets); state resumes from the last commit
exec python3 copybot.py \
--config live/copybot.paper.json \
--state copybot_state.json \
--poll "${POLL_SECONDS:-60}"
# paper config is committed (no secrets); state resumes from the last commit.
# ALCHEMY_SIGNING_KEY set -> push mode (webhook server); else 60s poll mode.
if [ -n "${ALCHEMY_SIGNING_KEY:-}" ]; then
exec python3 copybot.py \
--config live/copybot.paper.json \
--state copybot_state.json
else
exec python3 copybot.py \
--config live/copybot.paper.json \
--state copybot_state.json \
--poll "${POLL_SECONDS:-60}"
fi
+2
View File
@@ -58,6 +58,8 @@ for i in $(seq 1 15); do
railway logs --service copybot 2>/dev/null | grep -E "watching|floor\[" | tail -12
echo "$tail5" | tail -1
echo "✅ bot rebooted with the new follow set"
echo " (push mode? remember: the Alchemy webhook's address list must match"
echo " the follow set — update it at dashboard.alchemy.com → Webhooks)"
exit 0
fi
done