|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""wQFLOP engine liveness monitor — polls dexpaprika, posts heartbeat EVTs to DN. |
| 3 | +Cron: */15 * * * * (via crontab) |
| 4 | +On high-severity alerts (volume > $100): also posts to news.genesisconductor.io. |
| 5 | +""" |
| 6 | +import json, sys, os, datetime, subprocess |
| 7 | +import requests |
| 8 | + |
| 9 | +DN_INGEST = "https://dn.genesisconductor.io/api/rpsi/ingest" |
| 10 | +NEWS_BASE = "https://news.genesisconductor.io" |
| 11 | +DEXPAPRIKA = "https://api.dexpaprika.com/networks/base/pools/0x4abc6d796cd036b6f1e433a97f9784a00f90c53e" |
| 12 | +POOL_ID = "base-aerodrome-weth-wqflop-0x4abc6d796cd036b6f1e433a97f9784a00f90c53e" |
| 13 | +ENV_FILE = os.path.expanduser("~/.env") |
| 14 | + |
| 15 | +ALERT_VOL_USD = 100.0 |
| 16 | +UA = "Mozilla/5.0 (compatible; DiamondNode-wQFLOP-Monitor/1.0)" |
| 17 | + |
| 18 | +def load_env(): |
| 19 | + env = {} |
| 20 | + try: |
| 21 | + with open(ENV_FILE) as f: |
| 22 | + for line in f: |
| 23 | + line = line.strip() |
| 24 | + if '=' in line and not line.startswith('#'): |
| 25 | + k, _, v = line.partition('=') |
| 26 | + env[k.strip()] = v.strip().strip('"').strip("'") |
| 27 | + except Exception: |
| 28 | + pass |
| 29 | + return env |
| 30 | + |
| 31 | +def main(): |
| 32 | + env = load_env() |
| 33 | + ts = datetime.datetime.now(datetime.timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ") |
| 34 | + |
| 35 | + pool = None |
| 36 | + try: |
| 37 | + pool = requests.get(DEXPAPRIKA, headers={"User-Agent": UA}, timeout=10).json() |
| 38 | + except Exception as e: |
| 39 | + print(f"dexpaprika error: {e}", file=sys.stderr) |
| 40 | + |
| 41 | + tvl_usd = vol_24h_usd = 0.0 |
| 42 | + buys_24h = txns_24h = 0 |
| 43 | + last_price_usd = 0.0 |
| 44 | + severity = "low" |
| 45 | + alert_msgs = [] |
| 46 | + |
| 47 | + if pool and isinstance(pool, dict): |
| 48 | + for r in pool.get("token_reserves", []): |
| 49 | + tvl_usd += float(r.get("reserve_usd") or 0) |
| 50 | + s = pool.get("24h", {}) |
| 51 | + vol_24h_usd = float(s.get("volume_usd", 0) or 0) |
| 52 | + buys_24h = int(s.get("buys", 0) or 0) |
| 53 | + txns_24h = int(s.get("txns", 0) or 0) |
| 54 | + last_price_usd = float(pool.get("last_price_usd") or 0) |
| 55 | + if vol_24h_usd > ALERT_VOL_USD: |
| 56 | + severity = "high" |
| 57 | + alert_msgs.append(f"volume spike: ${vol_24h_usd:.4f} > ${ALERT_VOL_USD}") |
| 58 | + |
| 59 | + hb = f"evt-wqflop-hb-{ts[:16].replace(':','-').replace('T','-')}" |
| 60 | + evts = [{ |
| 61 | + "evt_id": hb, |
| 62 | + "record_type": "monitoring_hook", |
| 63 | + "timestamp": ts, |
| 64 | + "payload": { |
| 65 | + "target_asset_id": POOL_ID, |
| 66 | + "monitor_type": "heartbeat", |
| 67 | + "pool_state": { |
| 68 | + "tvl_usd": round(tvl_usd, 6), |
| 69 | + "volume_24h_usd": round(vol_24h_usd, 6), |
| 70 | + "buys_24h": buys_24h, |
| 71 | + "txns_24h": txns_24h, |
| 72 | + "last_price_usd": round(last_price_usd, 4), |
| 73 | + "data_source": "dexpaprika" |
| 74 | + }, |
| 75 | + "severity": severity, |
| 76 | + "alerts": alert_msgs, |
| 77 | + "engine_alive": True, |
| 78 | + "crystal_score": {"passed": True, "meets_target": True, |
| 79 | + "value": 1.0, "target": 1.0, "metric": "heartbeat_ok"} |
| 80 | + } |
| 81 | + }] |
| 82 | + |
| 83 | + if severity == "high": |
| 84 | + evts.append({ |
| 85 | + "evt_id": hb + "-qubo", |
| 86 | + "record_type": "qubo_modeling_request", |
| 87 | + "timestamp": ts, |
| 88 | + "payload": { |
| 89 | + "trigger": "volume_spike", "asset_id": POOL_ID, |
| 90 | + "volume_usd": round(vol_24h_usd, 4), |
| 91 | + "modeling_objective": "Re-evaluate allocation on volume spike", |
| 92 | + "constraints": {"max_allocation_usd": 50, "requires_maru_guard": True}, |
| 93 | + "crystal_score": {"passed": True, "meets_target": True, "value": 1.0, "target": 1.0} |
| 94 | + } |
| 95 | + }) |
| 96 | + |
| 97 | + dn_ok = False |
| 98 | + try: |
| 99 | + resp = requests.post(DN_INGEST, json=evts, timeout=10) |
| 100 | + dn_ok = resp.json().get("ok", False) |
| 101 | + except Exception as e: |
| 102 | + print(f"DN error: {e}", file=sys.stderr) |
| 103 | + |
| 104 | + # Post to news on high-severity alert |
| 105 | + news_ok = False |
| 106 | + if severity == "high": |
| 107 | + secret = env.get("NEWS_PUBLISH_SECRET", "") |
| 108 | + if secret: |
| 109 | + date_str = ts[:10] |
| 110 | + slug = f"wqflop-alert-{date_str}-vol-spike" |
| 111 | + md = f"""# wQFLOP Alert — Volume Spike Detected |
| 112 | +
|
| 113 | +**Time**: {ts} |
| 114 | +**Pool**: wQFLOP/WETH · Aerodrome · Base |
| 115 | +**Severity**: HIGH |
| 116 | +
|
| 117 | +## Alert Details |
| 118 | +
|
| 119 | +{chr(10).join(f'- {a}' for a in alert_msgs)} |
| 120 | +
|
| 121 | +## Pool State |
| 122 | +
|
| 123 | +| Metric | Value | |
| 124 | +|---|---| |
| 125 | +| TVL USD | ${tvl_usd:.4f} | |
| 126 | +| 24h Volume | ${vol_24h_usd:.4f} | |
| 127 | +| 24h Txns | {txns_24h} | |
| 128 | +| 24h Buys | {buys_24h} | |
| 129 | +| Last Price | ${last_price_usd:.4f} WETH/wQFLOP | |
| 130 | +
|
| 131 | +## Actions Triggered |
| 132 | +
|
| 133 | +- QUBO modeling request queued (maru guard required) |
| 134 | +- monitoring_hook EVT posted to DN pipeline |
| 135 | +- News alert published |
| 136 | +
|
| 137 | +*Automated alert from wQFLOP engine monitor (*/15 cron)* |
| 138 | +""" |
| 139 | + try: |
| 140 | + nr = requests.post(f"{NEWS_BASE}/api/publish", |
| 141 | + json={"title": f"wQFLOP Alert — {date_str}", "md": md, "slug": slug}, |
| 142 | + headers={"Authorization": f"Bearer {secret}"}, timeout=15) |
| 143 | + news_ok = nr.json().get("ok", False) |
| 144 | + except Exception as e: |
| 145 | + print(f"news error: {e}", file=sys.stderr) |
| 146 | + |
| 147 | + out = { |
| 148 | + "ts": ts, "tvl_usd": round(tvl_usd, 6), |
| 149 | + "vol_24h_usd": round(vol_24h_usd, 6), "txns_24h": txns_24h, |
| 150 | + "severity": severity, "alerts": alert_msgs, |
| 151 | + "dn_ok": dn_ok, "news_ok": news_ok |
| 152 | + } |
| 153 | + print(json.dumps(out)) |
| 154 | + |
| 155 | +if __name__ == "__main__": |
| 156 | + main() |
0 commit comments