diff --git a/.gitignore b/.gitignore index 315285f3..0fef0e9a 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,5 @@ lp_paper_state.json # cross-venue scanner output xarb_hits.csv hunt.log +recv.log +watcher_state.json diff --git a/Procfile b/Procfile new file mode 100644 index 00000000..e5d7bfd1 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: python3 webhook_receiver.py diff --git a/watch.json b/watch.json new file mode 100644 index 00000000..a69040ea --- /dev/null +++ b/watch.json @@ -0,0 +1,34 @@ +[ + { + "wallet": "0x0b0f92507bbc340762d38eca43eba1e11ee37af1", + "name": "Famecesgoal" + }, + { + "wallet": "0xe8c4d68aff65b38cac46987b9b65e01eb47d395d", + "name": "JAMJAMJAM4" + }, + { + "wallet": "0xf4ad6aedf5475f2023912cd191eff5ec90ead00b", + "name": "Domerina" + }, + { + "wallet": "0x419f32f30814030554f5df3e9f508ad7394ce853", + "name": "MyLastStand" + }, + { + "wallet": "0x71528368bdff0a0b58493b71832c34c373c5faac", + "name": "qqqq88888" + }, + { + "wallet": "0x3dd2db2d398ff72c2c71c9ad6473368b941e9ab1", + "name": "paspor" + }, + { + "wallet": "0x03805a13a0b3e058f55f6c6af95389d4f431073d", + "name": "donthackme" + }, + { + "wallet": "0xf6099a9eacfef24a4161bd857fbfbcd36cdfe68b", + "name": "Xixihaha008" + } +] \ No newline at end of file diff --git a/webhook_receiver.py b/webhook_receiver.py new file mode 100644 index 00000000..5734309e --- /dev/null +++ b/webhook_receiver.py @@ -0,0 +1,162 @@ +#!/usr/bin/env python3 +"""Alchemy webhook receiver — push-based wallet trade alerts (no polling). + +Alchemy's Address-Activity webhook POSTs here the moment a watched wallet +transacts on Polygon. We confirm/enrich the trade via Polymarket's data-API +(so the alert has market, side, outcome, price, size) and push it to Discord. + +Zero dependencies (stdlib http server). Binds to $PORT (Render/Railway/Fly) or +8080. Endpoints: + POST /alchemy ← Alchemy points here + GET /health ← uptime check + +Config (config.json): + "discord_webhook": "...", + "watch": [{"wallet":"0x...","name":"Famecesgoal"}, ...], + "alchemy_signing_key": "whsec_..." # optional; verifies the POST is genuine +""" + +import hashlib +import hmac +import json +import os +import ssl +import time +import urllib.error +import urllib.parse +import urllib.request +from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer + +DATA_API = "https://data-api.polymarket.com" +SSL_CTX = ssl._create_unverified_context() + + +def _load_cfg(): + for p in ("config.json",): + if os.path.exists(p): + try: + return json.load(open(p)) + except Exception: + pass + return {} + + +CFG = _load_cfg() +# secrets come from env in the cloud (never committed); config.json is local fallback +WEBHOOK = os.environ.get("DISCORD_WEBHOOK") or CFG.get("discord_webhook", "") +SIGNING_KEY = os.environ.get("ALCHEMY_SIGNING_KEY") or CFG.get("alchemy_signing_key", "") +# watch list is non-secret → committed watch.json (so it deploys), config fallback +if os.path.exists("watch.json"): + WATCH = json.load(open("watch.json")) +else: + WATCH = CFG.get("watch", []) +NAMES = {w["wallet"].lower(): w["name"] for w in WATCH} +SEEN = set() # tx hashes already alerted (in-memory; dedups retries) + + +def log(m): + print(f"{time.strftime('%H:%M:%S')} {m}", flush=True) + + +def get_json(path, params): + url = DATA_API + path + "?" + urllib.parse.urlencode(params) + try: + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + with urllib.request.urlopen(req, timeout=12, context=SSL_CTX) as r: + return json.loads(r.read().decode()) + except (urllib.error.URLError, TimeoutError, json.JSONDecodeError): + return None + + +def post_discord(content): + if not WEBHOOK: + return + try: + body = json.dumps({"content": content}).encode() + req = urllib.request.Request(WEBHOOK, data=body, method="POST", + headers={"Content-Type": "application/json"}) + urllib.request.urlopen(req, timeout=10, context=SSL_CTX).read() + except (urllib.error.URLError, TimeoutError): + log("⚠ Discord post failed") + + +def alert_for_wallet(wallet): + """Pull the wallet's latest trades; alert on any we haven't seen.""" + name = NAMES.get(wallet.lower(), wallet[:10] + "…") + acts = get_json("/activity", {"user": wallet, "type": "TRADE", "limit": 10}) or [] + for t in sorted(acts, key=lambda x: x.get("timestamp", 0)): + tx = t.get("transactionHash") + if not tx or tx in SEEN: + continue + # only fire on very recent trades (the webhook just told us one happened) + if time.time() - t.get("timestamp", 0) > 600: + SEEN.add(tx) + continue + SEEN.add(tx) + side = t.get("side", "?") + emoji = "🟢" if side == "BUY" else "🔴" + msg = (f"{emoji} **{name}** {side} **{t.get('outcome', '?')}** " + f"@ {t.get('price', 0):.3f} (${t.get('usdcSize', 0):,.0f})\n" + f"{t.get('title', '?')}\n" + f"") + log(f"ALERT {name}: {side} {t.get('outcome')} @ {t.get('price')} " + f"${t.get('usdcSize', 0):,.0f}") + post_discord(msg) + + +def addresses_in_payload(payload): + out = set() + for a in payload.get("event", {}).get("activity", []): + for k in ("fromAddress", "toAddress"): + v = a.get(k) + if v and v.lower() in NAMES: + out.add(v.lower()) + return out + + +def verify(raw, sig): + if not SIGNING_KEY: + return True # verification off if no key configured + digest = hmac.new(SIGNING_KEY.encode(), raw, hashlib.sha256).hexdigest() + return hmac.compare_digest(digest, sig or "") + + +class Handler(BaseHTTPRequestHandler): + def _send(self, code, body="ok"): + self.send_response(code) + self.send_header("Content-Type", "text/plain") + self.end_headers() + self.wfile.write(body.encode()) + + def do_GET(self): + self._send(200, "alive" if self.path == "/health" else "polymarket watcher") + + def do_POST(self): + raw = self.rfile.read(int(self.headers.get("Content-Length", 0))) + if not verify(raw, self.headers.get("x-alchemy-signature")): + log("⚠ bad signature — rejected") + return self._send(401, "bad signature") + self._send(200) # ack fast; Alchemy retries on non-2xx + try: + payload = json.loads(raw or b"{}") + for w in addresses_in_payload(payload): + alert_for_wallet(w) + except Exception as e: + log(f"handler error: {e}") + + def log_message(self, *a): + pass + + +def main(): + port = int(os.environ.get("PORT", 8080)) + if WEBHOOK: + post_discord(f"👀 **Webhook watcher online** · push alerts for " + f"{len(NAMES)} wallets (fires only on a real trade).") + log(f"listening on :{port} · {len(NAMES)} wallets · " + f"signature-verify {'ON' if SIGNING_KEY else 'OFF'}") + ThreadingHTTPServer(("0.0.0.0", port), Handler).serve_forever() + + +if __name__ == "__main__": + main()