overnight batch: H3 cursor fetch, Bearer clone-guard, SDK preflight, 5c drift alarm
- H3 (burst overflow): per-wallet trade cursor + paginated activity fetch (up to 5×100, walks back to the last processed timestamp) — a gkmg-scale clip burst can no longer scroll past a single newest-100 page. Cursor only advances after a fetch; 600s overlap re-serves boundary rows into the seen-tx dedupe. offset pagination verified against the data-api. - clone-guard: Bearer auth (fine-grained PATs reject the 'token' scheme) with unauthenticated fallback + 40-hex sanity on the answer — Fly's shared egress IPs rate-limit the anonymous path. - preflight_live.py rewritten for the unified SDK: deposit-wallet auth, pUSD collateral, order-book reach, RTDS stream delivery, geo verdict. - ledger-drift ALARM floor 1c → 5c (4dp fill rounding accumulates pennies across 60+ copies; the check stays for real bugs — it caught $15.45 and $7.24. The self-heal path keeps its own threshold.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+27
-3
@@ -987,7 +987,7 @@ class Copybot:
|
|||||||
fixed = True
|
fixed = True
|
||||||
if fixed:
|
if fixed:
|
||||||
self.engine.persist()
|
self.engine.persist()
|
||||||
if abs(drift) > 0.01:
|
if abs(drift) > 0.05: # pennies = 4dp fill rounding, not a bug
|
||||||
log(f"⚠ LEDGER DRIFT ${drift:+.2f} — check_book could not attribute it")
|
log(f"⚠ LEDGER DRIFT ${drift:+.2f} — check_book could not attribute it")
|
||||||
return drift
|
return drift
|
||||||
|
|
||||||
@@ -1426,7 +1426,7 @@ class Copybot:
|
|||||||
f"slip {lag['sum_slip_pct']/lag['n']:+.1%}")
|
f"slip {lag['sum_slip_pct']/lag['n']:+.1%}")
|
||||||
bankstr = f" · banked ${reserve:,.0f}" if reserve else ""
|
bankstr = f" · banked ${reserve:,.0f}" if reserve else ""
|
||||||
drift = self.ledger_drift()
|
drift = self.ledger_drift()
|
||||||
driftstr = f" · ⚠ LEDGER DRIFT ${drift:+.2f}" if abs(drift) > 0.01 else ""
|
driftstr = f" · ⚠ LEDGER DRIFT ${drift:+.2f}" if abs(drift) > 0.05 else ""
|
||||||
gap = self.chain_cash_gap()
|
gap = self.chain_cash_gap()
|
||||||
if gap is not None and abs(gap) > 1.0: # ≥$1: beyond fee/rounding float
|
if gap is not None and abs(gap) > 1.0: # ≥$1: beyond fee/rounding float
|
||||||
driftstr += f" · ⚠ CASH≠CHAIN ${gap:+.2f}"
|
driftstr += f" · ⚠ CASH≠CHAIN ${gap:+.2f}"
|
||||||
@@ -1440,11 +1440,35 @@ class Copybot:
|
|||||||
+ (f" · CAN'T OPEN (free < ${stake:,.0f} stake — bets missed)"
|
+ (f" · CAN'T OPEN (free < ${stake:,.0f} stake — bets missed)"
|
||||||
if cash < stake else ""))
|
if cash < stake else ""))
|
||||||
|
|
||||||
|
def _fetch_since_cursor(self, wallet):
|
||||||
|
"""H3 fix (2026-07-13): a burst trader can push unprocessed rows past
|
||||||
|
a single newest-100 fetch (gkmg: 30+ clips in 15 min). Paginate the
|
||||||
|
activity feed back to the last processed timestamp per wallet, capped
|
||||||
|
at 5 pages — the cursor makes every fetch lossless regardless of
|
||||||
|
burst size; the cap bounds a cold start. Cursor moves only forward
|
||||||
|
and only after a fetch, so a failed page just re-reads next time."""
|
||||||
|
cur = self.engine.state.setdefault("trade_cursor", {})
|
||||||
|
since = cur.get(wallet.lower(), 0)
|
||||||
|
rows, offset = [], 0
|
||||||
|
for _ in range(5):
|
||||||
|
page = recent_trades(wallet, limit=100, offset=offset)
|
||||||
|
if not page:
|
||||||
|
break
|
||||||
|
rows.extend(page)
|
||||||
|
oldest = min((t.get("timestamp") or 0) for t in page)
|
||||||
|
if oldest <= since or len(page) < 100:
|
||||||
|
break
|
||||||
|
offset += 100
|
||||||
|
if rows:
|
||||||
|
newest = max((t.get("timestamp") or 0) for t in rows)
|
||||||
|
cur[wallet.lower()] = max(since, newest)
|
||||||
|
return [t for t in rows if (t.get("timestamp") or 0) > since - 600]
|
||||||
|
|
||||||
def on_wallet_activity(self, wallet, ignore_stale=False):
|
def on_wallet_activity(self, wallet, ignore_stale=False):
|
||||||
"""A watched wallet just transacted — pull its latest trades and route any
|
"""A watched wallet just transacted — pull its latest trades and route any
|
||||||
new, recent one through the filter and (if it passes) the engine."""
|
new, recent one through the filter and (if it passes) the engine."""
|
||||||
name = self.names.get(wallet.lower(), wallet[:10] + "…")
|
name = self.names.get(wallet.lower(), wallet[:10] + "…")
|
||||||
trades = recent_trades(wallet)
|
trades = self._fetch_since_cursor(wallet)
|
||||||
# ONE conviction bet often arrives as several fills — a sweep through
|
# ONE conviction bet often arrives as several fills — a sweep through
|
||||||
# the book or rapid clip entries (gkmg 2026-07-09: a $612 MOUZ entry =
|
# the book or rapid clip entries (gkmg 2026-07-09: a $612 MOUZ entry =
|
||||||
# 3×$204 same-second rows, every clip sub-floor while the backtest's
|
# 3×$204 same-second rows, every clip sub-floor while the backtest's
|
||||||
|
|||||||
+6
-3
@@ -167,10 +167,13 @@ def their_positions(wallet):
|
|||||||
return pos
|
return pos
|
||||||
|
|
||||||
|
|
||||||
def recent_trades(wallet, limit=100):
|
def recent_trades(wallet, limit=100, offset=0):
|
||||||
"""Newest-first TRADE activity for a wallet."""
|
"""Newest-first TRADE activity for a wallet. `offset` pages deeper —
|
||||||
|
the copybot's per-wallet cursor fetch (H3) walks back through bursts
|
||||||
|
that overflow a single page."""
|
||||||
return get_json("/activity",
|
return get_json("/activity",
|
||||||
{"user": wallet, "type": "TRADE", "limit": limit}) or []
|
{"user": wallet, "type": "TRADE", "limit": limit,
|
||||||
|
"offset": offset}) or []
|
||||||
|
|
||||||
|
|
||||||
def event_key(t):
|
def event_key(t):
|
||||||
|
|||||||
+9
-2
@@ -69,12 +69,19 @@ cd "$DIR"
|
|||||||
# API is unreachable — never blocks the boot forever).
|
# API is unreachable — never blocks the boot forever).
|
||||||
for try in 1 2 3 4; do
|
for try in 1 2 3 4; do
|
||||||
LOCAL_SHA=$(git rev-parse HEAD)
|
LOCAL_SHA=$(git rev-parse HEAD)
|
||||||
# public repo — unauthenticated works and avoids fine-grained-PAT header
|
# Bearer auth first (fine-grained PATs reject the legacy "token" scheme —
|
||||||
# quirks (the token'd call 401'd on the box, 2026-07-11 06:04 boot)
|
# the 06:04 401), falling back to unauthenticated — which itself rate-limits
|
||||||
|
# on Fly's SHARED egress IPs (60/hr across tenants; bit at 01:49).
|
||||||
REMOTE_SHA=$(curl -sf --max-time 10 \
|
REMOTE_SHA=$(curl -sf --max-time 10 \
|
||||||
|
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||||
|
-H "Accept: application/vnd.github.sha" \
|
||||||
|
https://api.github.com/repos/jaxperro/winning-wallet-finder/commits/main \
|
||||||
|
|| curl -sf --max-time 10 \
|
||||||
-H "Accept: application/vnd.github.sha" \
|
-H "Accept: application/vnd.github.sha" \
|
||||||
https://api.github.com/repos/jaxperro/winning-wallet-finder/commits/main \
|
https://api.github.com/repos/jaxperro/winning-wallet-finder/commits/main \
|
||||||
|| true)
|
|| true)
|
||||||
|
# a sha is 40 hex chars — anything else (error JSON, rate-limit body) = unusable
|
||||||
|
echo "$REMOTE_SHA" | grep -qE '^[0-9a-f]{40}$' || REMOTE_SHA=""
|
||||||
if [ -z "$REMOTE_SHA" ]; then
|
if [ -z "$REMOTE_SHA" ]; then
|
||||||
echo "[clone-guard] GitHub API unreachable — proceeding with $LOCAL_SHA"
|
echo "[clone-guard] GitHub API unreachable — proceeding with $LOCAL_SHA"
|
||||||
break
|
break
|
||||||
|
|||||||
+83
-101
@@ -1,28 +1,31 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""Read-only preflight for live trading — verifies credentials, balance, and
|
"""Read-only preflight for live trading — verifies credentials, balance, and
|
||||||
market access WITHOUT placing any order.
|
market access on the UNIFIED SDK (polymarket-client) WITHOUT placing any
|
||||||
|
order. (Rewritten 2026-07-13: the original validated the archived
|
||||||
|
py-clob-client stack, whose orders the CLOB rejects — gotcha 16.)
|
||||||
|
|
||||||
python3 preflight_live.py # uses config.live.json
|
python3 preflight_live.py # uses config.live.json
|
||||||
|
python3 preflight_live.py --config config.live.example.json # Fly path
|
||||||
|
|
||||||
Checks, in order:
|
Checks, in order:
|
||||||
1. config.live.json parses; private key + funder present
|
1. config parses; private key present (env LIVE_PRIVATE_KEY wins)
|
||||||
2. CLOB auth: derive L2 API creds from the key (proves the key signs)
|
2. unified-SDK auth: SecureClient.create resolves the Deposit Wallet
|
||||||
3. USDC balance + allowance on the funder (proves deposits landed and the
|
3. pUSD collateral via get_balance_allowance — the balance the exchange
|
||||||
proxy can spend — the balance the bot will actually trade with)
|
will actually let the bot trade with (raw USDC reads 0 — gotcha 16)
|
||||||
4. live order book fetch for one of the followed wallets' recent markets
|
4. live order book fetch for a followed wallet's recent market
|
||||||
(proves market-data access end to end)
|
5. RTDS trade stream: connect + subscribe + first message (T0 detection)
|
||||||
5. Polygon RPC + EOA POL gas balance (needed by auto-redeem)
|
6. geo-gate verdict (informational on a blocked box; the bot enforces
|
||||||
|
fatally at boot)
|
||||||
|
|
||||||
Exit code 0 = every check passed; anything else prints what to fix.
|
Exit code 0 = every check passed; anything else prints what to fix.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import sys
|
import os
|
||||||
import time
|
|
||||||
import urllib.request
|
|
||||||
import ssl
|
import ssl
|
||||||
|
import sys
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
CLOB = "https://clob.polymarket.com"
|
|
||||||
_SSL = ssl._create_unverified_context()
|
_SSL = ssl._create_unverified_context()
|
||||||
OK, BAD = " ✓", " ✗"
|
OK, BAD = " ✓", " ✗"
|
||||||
failures = []
|
failures = []
|
||||||
@@ -34,113 +37,92 @@ def check(name, fn):
|
|||||||
print(f"{OK} {name}" + (f" — {msg}" if msg else ""))
|
print(f"{OK} {name}" + (f" — {msg}" if msg else ""))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
failures.append(name)
|
failures.append(name)
|
||||||
print(f"{BAD} {name} — {e}")
|
print(f"{BAD} {name} — {type(e).__name__}: {e}")
|
||||||
|
|
||||||
|
|
||||||
|
def get(url):
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
return json.load(urllib.request.urlopen(req, timeout=15, context=_SSL))
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
# config: --config PATH, else config.live.json. Secrets: env wins (the Fly
|
|
||||||
# worker path — LIVE_PRIVATE_KEY / LIVE_FUNDER_ADDRESS / LIVE_SIGNATURE_TYPE
|
|
||||||
# / ALCHEMY_RPC_URL — same 1.2 override chain as copybot; no key file needs
|
|
||||||
# to exist anywhere on the box).
|
|
||||||
import os
|
|
||||||
path = "config.live.json"
|
path = "config.live.json"
|
||||||
if "--config" in sys.argv:
|
if "--config" in sys.argv:
|
||||||
path = sys.argv[sys.argv.index("--config") + 1]
|
path = sys.argv[sys.argv.index("--config") + 1]
|
||||||
cfg = json.load(open(path))
|
cfg = json.load(open(path))
|
||||||
live = cfg.setdefault("live", {})
|
live = cfg.setdefault("live", {})
|
||||||
for env, key in (("LIVE_PRIVATE_KEY", "private_key"),
|
if (os.environ.get("LIVE_PRIVATE_KEY") or "").strip():
|
||||||
("LIVE_FUNDER_ADDRESS", "funder_address"),
|
live["private_key"] = os.environ["LIVE_PRIVATE_KEY"].strip()
|
||||||
("LIVE_SIGNATURE_TYPE", "signature_type"),
|
pk = live.get("private_key")
|
||||||
("ALCHEMY_RPC_URL", "rpc_url")):
|
if not pk:
|
||||||
if (os.environ.get(env) or "").strip():
|
sys.exit(f"{BAD} no live.private_key in {path} and no LIVE_PRIVATE_KEY env")
|
||||||
live[key] = os.environ[env].strip() # tolerate pasted newlines
|
print(f"{OK} config parses — key present, pct {cfg.get('bankroll_pct')}, "
|
||||||
if "watchlist" not in cfg and cfg.get("wallets"):
|
f"band [{cfg['risk'].get('min_price')},{cfg['risk'].get('max_price')}]")
|
||||||
cfg["watchlist"] = [w["wallet"] for w in cfg["wallets"]]
|
|
||||||
pk, funder = live.get("private_key"), live.get("funder_address")
|
|
||||||
if not pk or not funder:
|
|
||||||
sys.exit("fill live.private_key and live.funder_address (config or env) first\n"
|
|
||||||
"(Polymarket profile -> the deposit/profile address is the funder;\n"
|
|
||||||
" email-login accounts export the key in Settings)")
|
|
||||||
|
|
||||||
from py_clob_client.client import ClobClient
|
from polymarket import SecureClient
|
||||||
from py_clob_client.clob_types import BalanceAllowanceParams, AssetType
|
client = SecureClient.create(private_key=pk)
|
||||||
|
|
||||||
client = ClobClient(host=CLOB, key=pk, chain_id=137,
|
|
||||||
signature_type=int(live.get("signature_type") or 1), funder=funder)
|
|
||||||
|
|
||||||
def auth():
|
def auth():
|
||||||
creds = client.create_or_derive_api_creds()
|
return f"deposit wallet {client.wallet}"
|
||||||
client.set_api_creds(creds)
|
check("unified-SDK auth (SecureClient.create)", auth)
|
||||||
return f"L2 api key {creds.api_key[:8]}… derived (signer {client.get_address()[:10]}…)"
|
|
||||||
check("CLOB auth (key signs, creds derive)", auth)
|
|
||||||
|
|
||||||
def balance():
|
def collateral():
|
||||||
r = client.get_balance_allowance(
|
b = client.get_balance_allowance(asset_type="COLLATERAL")
|
||||||
BalanceAllowanceParams(asset_type=AssetType.COLLATERAL))
|
usd = b.balance / 1e6
|
||||||
bal = int(r.get("balance", 0)) / 1e6
|
if usd <= 0:
|
||||||
if bal <= 0:
|
raise RuntimeError("exchange-view collateral is $0 — bankroll "
|
||||||
raise RuntimeError("USDC balance is $0 — deposit test funds to the funder address first")
|
"not wrapped to pUSD? (gotcha 16)")
|
||||||
return f"${bal:,.2f} USDC spendable on {funder[:10]}…"
|
return f"${usd:,.2f} pUSD, {len(b.allowances)} spender allowances"
|
||||||
check("USDC balance on funder", balance)
|
check("exchange-view collateral (pUSD)", collateral)
|
||||||
|
|
||||||
def book():
|
def book():
|
||||||
# a sharp's LATEST trade is often an in-play market that has already
|
for w in [x["wallet"] for x in cfg.get("wallets", [])][:3]:
|
||||||
# closed (404 = "no orderbook", which itself proves connectivity) —
|
for t in get(f"https://data-api.polymarket.com/activity?user={w}"
|
||||||
# walk recent trades until one still has a live book
|
f"&type=TRADE&limit=5"):
|
||||||
last = None
|
tok = t.get("asset")
|
||||||
for w in cfg["watchlist"]:
|
if not tok:
|
||||||
req = urllib.request.Request(
|
|
||||||
"https://data-api.polymarket.com/activity?user=" + w
|
|
||||||
+ "&type=TRADE&limit=10",
|
|
||||||
headers={"User-Agent": "Mozilla/5.0"})
|
|
||||||
for t in json.loads(urllib.request.urlopen(req, timeout=15, context=_SSL).read()):
|
|
||||||
try:
|
|
||||||
ob = client.get_order_book(t["asset"])
|
|
||||||
except Exception as e:
|
|
||||||
last = e
|
|
||||||
continue
|
continue
|
||||||
bid = ob.bids[-1].price if ob.bids else "—"
|
ob = get(f"https://clob.polymarket.com/book?token_id={tok}")
|
||||||
ask = ob.asks[-1].price if ob.asks else "—"
|
bids, asks = ob.get("bids") or [], ob.get("asks") or []
|
||||||
return f"{(t.get('title') or '')[:40]}… bid {bid} / ask {ask}"
|
return (f"{len(bids)} bids / {len(asks)} asks on "
|
||||||
# every followed wallet is fully in-play right now — prove book access
|
f"{(t.get('title') or '?')[:40]}")
|
||||||
# against any active market instead
|
raise RuntimeError("no recent market found across the follow set")
|
||||||
req = urllib.request.Request(
|
check("order-book access (followed wallet's market)", book)
|
||||||
"https://gamma-api.polymarket.com/markets?active=true&closed=false"
|
|
||||||
"&limit=1&order=volume24hr&ascending=false",
|
|
||||||
headers={"User-Agent": "Mozilla/5.0"})
|
|
||||||
gm = json.loads(urllib.request.urlopen(req, timeout=15, context=_SSL).read())[0]
|
|
||||||
tok = json.loads(gm["clobTokenIds"])[0]
|
|
||||||
ob = client.get_order_book(tok)
|
|
||||||
bid = ob.bids[-1].price if ob.bids else "—"
|
|
||||||
ask = ob.asks[-1].price if ob.asks else "—"
|
|
||||||
return f"[fallback: {(gm.get('question') or '')[:36]}…] bid {bid} / ask {ask}"
|
|
||||||
check("order book fetch (market access)", book)
|
|
||||||
|
|
||||||
def gas():
|
def rtds():
|
||||||
if live.get("auto_redeem") is False:
|
import threading
|
||||||
return "auto_redeem OFF — manual UI redeems; no POL needed (CASH≠CHAIN will nag after wins until redeemed)"
|
import websocket
|
||||||
from web3 import Web3
|
got = []
|
||||||
rpc = live.get("rpc_url") or (
|
|
||||||
f"https://polygon-mainnet.g.alchemy.com/v2/{cfg.get('alchemy_key')}"
|
|
||||||
if cfg.get("alchemy_key") else None)
|
|
||||||
if not rpc:
|
|
||||||
raise RuntimeError("no live.rpc_url and no alchemy_key — auto-redeem needs a Polygon RPC")
|
|
||||||
w3 = Web3(Web3.HTTPProvider(rpc))
|
|
||||||
acct = w3.eth.account.from_key(pk)
|
|
||||||
pol = w3.eth.get_balance(acct.address) / 1e18
|
|
||||||
if pol < 0.05:
|
|
||||||
raise RuntimeError(f"EOA {acct.address[:10]}… holds {pol:.3f} POL — "
|
|
||||||
"send ~1 POL for redeem gas (or set live.auto_redeem false)")
|
|
||||||
return f"{pol:.2f} POL gas on EOA {acct.address[:10]}…"
|
|
||||||
check("Polygon RPC + redeem gas", gas)
|
|
||||||
|
|
||||||
print()
|
def on_open(ws):
|
||||||
|
ws.send(json.dumps({"action": "subscribe", "subscriptions": [
|
||||||
|
{"topic": "activity", "type": "trades", "filters": ""}]}))
|
||||||
|
|
||||||
|
def on_message(ws, raw):
|
||||||
|
got.append(raw)
|
||||||
|
ws.close()
|
||||||
|
app = websocket.WebSocketApp("wss://ws-live-data.polymarket.com",
|
||||||
|
on_open=on_open, on_message=on_message)
|
||||||
|
t = threading.Thread(target=lambda: app.run_forever(
|
||||||
|
sslopt={"cert_reqs": ssl.CERT_NONE}), daemon=True)
|
||||||
|
t.start()
|
||||||
|
t.join(timeout=15)
|
||||||
|
if not got:
|
||||||
|
raise RuntimeError("no message within 15s")
|
||||||
|
return "stream delivers (T0 detection reachable)"
|
||||||
|
check("RTDS trade stream", rtds)
|
||||||
|
|
||||||
|
def geo():
|
||||||
|
r = get("https://polymarket.com/api/geoblock")
|
||||||
|
return ("TRADABLE from here" if not r.get("blocked")
|
||||||
|
else f"BLOCKED here ({r.get('country')}) — bot must run on "
|
||||||
|
"the Fly box (informational)")
|
||||||
|
check("geo-gate", geo)
|
||||||
|
|
||||||
|
client.close()
|
||||||
if failures:
|
if failures:
|
||||||
sys.exit(f"NOT ready: fix {len(failures)} item(s) above.")
|
sys.exit(f"\n{len(failures)} check(s) failed: {', '.join(failures)}")
|
||||||
print("ALL CHECKS PASSED — ready for the supervised live test:")
|
print("\nall checks passed — safe to arm")
|
||||||
print(" python3 copybot.py --config config.live.json "
|
|
||||||
"--state copybot_state.live.json --poll 60 --live")
|
|
||||||
print(' (it will ask you to type the confirmation phrase before anything is placed)')
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
Reference in New Issue
Block a user