chain seed: the code for 5e97b13 — fills_from_tx + hint_txs funnel (autostash dropped it from the prior commit)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+26
-1
@@ -1,4 +1,4 @@
|
||||
# Session handoff — 2026-07-13 (rev 11: RTDS-seed lag fix, tick-conform prices, floor_pin audit)
|
||||
# Session handoff — 2026-07-14 (rev 12: chain-seed — fills decoded from the tx receipt)
|
||||
|
||||
## Operating boundary set by the user (2026-07-13, "away for a while")
|
||||
**Full autonomy on BOTH bots**; real-money bot **stays ARMED**. Never touch
|
||||
@@ -7,6 +7,31 @@ the private key, never raise/loosen caps, never rotate the Discord webhook
|
||||
DISARM the live bot (`flyctl secrets unset LIVE_CONFIRM`) rather than push
|
||||
through it.
|
||||
|
||||
## Shipped since rev 11 (2026-07-14)
|
||||
- **CHAIN SEED (T0b)**: the missed-bets review found the one remaining
|
||||
detection leak — RTDS doesn't emit every market (the "Credible public
|
||||
sale" sweep was missed on BOTH books at 20:18Z despite the rev-11 fix:
|
||||
RTDS silent + data-api indexer past the 600s window; badaf's detection
|
||||
misses are the only +EV ones, +$34.60/10 on paper). Now the Alchemy push
|
||||
carries its tx hashes into `on_wallet_activity(hint_txs=…)`; any hash the
|
||||
data-api fetch didn't return is decoded straight from the receipt
|
||||
(`fills_from_tx`: OrderFilled logs, maker==wallet row; gamma lookup for
|
||||
title/outcome/cond; block ts). Validated 200/200 against shadow-ledger
|
||||
fills and end-to-end on the actual missed Credible txs. Fail-open on any
|
||||
RPC/gamma error (backstops unchanged); forged-event-safe (only the two
|
||||
new-stack exchange contracts 0xe111…996b / 0xe2222…0f59 are trusted
|
||||
emitters — a dust transfer can trigger the webhook, so this matters).
|
||||
Tests: tests/test_chainseed.py (6 stub paths). Both boxes have
|
||||
ALCHEMY_RPC_URL, so the path is live on deploy. Watch for the first
|
||||
`chainseed:` line — expected on badaf niche markets during RTDS flaps.
|
||||
- **Missed-bets audit verdict** (what prompted this): price guard skips are
|
||||
net-NEGATIVE would-be P&L on both books (validated again — leave at 0.05);
|
||||
old detection misses were -EV and all from since-dropped wallets; conform/
|
||||
sub-$1/sub-penny categories all fixed pre-rev-11. Remaining candidates NOT
|
||||
built: thin-book FAK no-match → resting limit via the pending registry
|
||||
(small now, scales with stakes), paper thin-book modeling parity (biases the
|
||||
live-vs-paper ratio optimistic).
|
||||
|
||||
## Shipped since rev 10 (all deployed + verified)
|
||||
- **RTDS-seed lag fix** (the big one): RTDS detected trades at ~0.3s but the
|
||||
copy landed 130-305s later — `on_wallet_activity` threw the RTDS payload
|
||||
|
||||
+128
-12
@@ -84,15 +84,19 @@ _RPC_URL = None # resolved once in main() (env/config), stays None wi
|
||||
_PAYOUTS = {} # cond -> [p0, p1], cached once resolved (immutable)
|
||||
|
||||
|
||||
def _eth_call(data):
|
||||
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": "eth_call",
|
||||
"params": [{"to": CTF_ADDR, "data": data}, "latest"]}).encode()
|
||||
def _rpc(method, params):
|
||||
body = json.dumps({"jsonrpc": "2.0", "id": 1, "method": method,
|
||||
"params": params}).encode()
|
||||
req = urllib.request.Request(_RPC_URL, data=body,
|
||||
headers={"Content-Type": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=15, context=SSL_CTX) as r:
|
||||
return json.loads(r.read())["result"]
|
||||
|
||||
|
||||
def _eth_call(data):
|
||||
return _rpc("eth_call", [{"to": CTF_ADDR, "data": data}, "latest"])
|
||||
|
||||
|
||||
def onchain_payouts(cond):
|
||||
"""[payout_outcome0, payout_outcome1] for a resolved condition (order matches
|
||||
the CLOB market's tokens[] order), or None if unresolved / no RPC configured."""
|
||||
@@ -112,6 +116,100 @@ def onchain_payouts(cond):
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
# ── T0b: fills straight from the tx receipt (Alchemy push) ──────────────────
|
||||
# RTDS doesn't emit every market (2026-07-13: badaf's "Credible public sale"
|
||||
# entry was missed on BOTH books — RTDS silent, and the data-api indexer
|
||||
# lagged past the 600s window, so the ~3s Alchemy push detected a trade it
|
||||
# couldn't fetch). The push carries the tx hash, and the fill is fully
|
||||
# decodable from the receipt — so no market's copy speed depends on RTDS
|
||||
# coverage or indexer freshness.
|
||||
_ORDER_FILLED = "0xd543adfd945773f1a62f74f0ee55a5e3b9b1a28262980ba90b1a89f2ea84d8ee"
|
||||
_EXCHANGES = { # new-stack (pUSD) exchange contracts, the only trusted
|
||||
# OrderFilled emitters — an attacker CAN trigger the webhook with a dust
|
||||
# transfer, so never decode fills from arbitrary contracts' events.
|
||||
# Both observed across 200/200 shadow-ledger fills (2026-07-14).
|
||||
"0xe111180000d2663c0091e4f400237545b87b996b",
|
||||
"0xe2222d279d744050d28e00520010520000310f59",
|
||||
}
|
||||
_TOK_MKT = {} # token id -> (title, outcome, cond), immutable
|
||||
_TOK_LOCK = threading.Lock()
|
||||
|
||||
|
||||
def _token_market(token):
|
||||
"""token id -> (title, outcome, conditionId) via the gamma API, or None.
|
||||
A failed lookup means the caller drops the seed — the backstop poll still
|
||||
copies the trade once the indexer catches up (honest, never guessy)."""
|
||||
with _TOK_LOCK:
|
||||
if token in _TOK_MKT:
|
||||
return _TOK_MKT[token]
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"https://gamma-api.polymarket.com/markets?clob_token_ids={token}",
|
||||
headers={"User-Agent": "Mozilla/5.0"})
|
||||
with urllib.request.urlopen(req, timeout=12, context=SSL_CTX) as r:
|
||||
m = (json.loads(r.read().decode()) or [{}])[0]
|
||||
toks = json.loads(m["clobTokenIds"])
|
||||
got = (m.get("question") or "",
|
||||
json.loads(m["outcomes"])[toks.index(token)],
|
||||
m.get("conditionId"))
|
||||
except Exception:
|
||||
return None
|
||||
with _TOK_LOCK:
|
||||
_TOK_MKT[token] = got
|
||||
return got
|
||||
|
||||
|
||||
def fills_from_tx(tx, wallet):
|
||||
"""The watched wallet's fills decoded from the receipt, as data-api-shaped
|
||||
trade dicts ([] without an RPC / on any failure — backstops still cover).
|
||||
OrderFilled topics: (sig, orderHash, maker, taker); the wallet's own order
|
||||
is the maker==wallet row (aggregate when it took, per-order when it made).
|
||||
Data words: (assetClassGiven, tokenId, amountGiven, amountTaken, fee…) in
|
||||
6dp base units; class 0 = gave cash = BUY, else gave tokens = SELL.
|
||||
Validated 200/200 against shadow-ledger fills — side, size, price exact."""
|
||||
if not _RPC_URL:
|
||||
return []
|
||||
try:
|
||||
rcpt = _rpc("eth_getTransactionReceipt", [tx])
|
||||
except Exception:
|
||||
return []
|
||||
if not rcpt:
|
||||
return []
|
||||
try: # block ts keeps the staleness gate + lag stat honest
|
||||
ts = int(_rpc("eth_getBlockByNumber",
|
||||
[rcpt["blockNumber"], False])["timestamp"], 16)
|
||||
except Exception:
|
||||
ts = int(time.time())
|
||||
w40, out = wallet[2:].lower(), []
|
||||
for lg in rcpt.get("logs", []):
|
||||
t = lg.get("topics") or []
|
||||
if (len(t) < 4 or t[0].lower() != _ORDER_FILLED
|
||||
or (lg.get("address") or "").lower() not in _EXCHANGES
|
||||
or t[2][-40:].lower() != w40):
|
||||
continue
|
||||
d = lg.get("data", "0x")[2:]
|
||||
wd = [int(d[i:i + 64], 16) for i in range(0, len(d), 64)]
|
||||
if len(wd) < 4:
|
||||
continue
|
||||
side = "BUY" if wd[0] == 0 else "SELL"
|
||||
token, given, taken = str(wd[1]), wd[2] / 1e6, wd[3] / 1e6
|
||||
usd, size = (given, taken) if side == "BUY" else (taken, given)
|
||||
if not size:
|
||||
continue
|
||||
meta = _token_market(token)
|
||||
if not meta:
|
||||
log(f"chainseed: no metadata for token …{token[-8:]} "
|
||||
f"({tx[:10]}…) — left to the backstop poll")
|
||||
continue
|
||||
out.append({"transactionHash": tx, "asset": token, "side": side,
|
||||
"size": size, "price": round(usd / size, 6),
|
||||
"usdcSize": round(usd, 2), "title": meta[0],
|
||||
"outcome": meta[1], "conditionId": meta[2],
|
||||
"timestamp": ts})
|
||||
return out
|
||||
|
||||
|
||||
# follow-filter defaults — merged under cfg["follow"]; permissive so nothing is
|
||||
# silently dropped until you opt in. The engine's risk caps bound everything
|
||||
# regardless of these.
|
||||
@@ -1643,16 +1741,29 @@ class Copybot:
|
||||
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, seed_trade=None):
|
||||
def on_wallet_activity(self, wallet, ignore_stale=False, seed_trade=None,
|
||||
hint_txs=None):
|
||||
"""A watched wallet just transacted — pull its latest trades and route any
|
||||
new, recent one through the filter and (if it passes) the engine.
|
||||
`seed_trade` (from the RTDS push) is merged into the fetched set so a
|
||||
lagging data-api indexer can't delay the copy — deduped by tx."""
|
||||
lagging data-api indexer can't delay the copy — deduped by tx.
|
||||
`hint_txs` (from the Alchemy push) are receipt hashes: any hash the
|
||||
fetch didn't return is decoded straight from the chain (fills_from_tx),
|
||||
so an RTDS-silent market still copies at push speed."""
|
||||
name = self.names.get(wallet.lower(), wallet[:10] + "…")
|
||||
trades = self._fetch_since_cursor(wallet)
|
||||
if seed_trade and seed_trade.get("transactionHash") and seed_trade.get("asset"):
|
||||
if seed_trade["transactionHash"] not in {t.get("transactionHash") for t in trades}:
|
||||
trades = trades + [seed_trade]
|
||||
have = {t.get("transactionHash") for t in trades}
|
||||
for tx in (hint_txs or []):
|
||||
if tx in have or tx in self.engine.seen or tx in self.skipped:
|
||||
continue
|
||||
have.add(tx)
|
||||
for f in fills_from_tx(tx, wallet):
|
||||
log(f"chainseed: {name} {f['side']} {f['size']:g} @ "
|
||||
f"{f['price']:.3f} · {str(f['title'])[:40]}")
|
||||
trades = trades + [f]
|
||||
# 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 =
|
||||
# 3×$204 same-second rows, every clip sub-floor while the backtest's
|
||||
@@ -2065,13 +2176,18 @@ def verify(raw, sig, signing_key):
|
||||
|
||||
|
||||
def addresses_in_payload(payload, watched):
|
||||
out = set()
|
||||
"""-> {watched wallet: sorted tx hashes}. The hashes ride along so a
|
||||
trade the data-api can't return yet is decoded from its receipt instead
|
||||
of waiting on the indexer (fills_from_tx)."""
|
||||
out = {}
|
||||
for a in payload.get("event", {}).get("activity", []):
|
||||
for k in ("fromAddress", "toAddress"):
|
||||
v = a.get(k)
|
||||
if v and v.lower() in watched:
|
||||
out.add(v.lower())
|
||||
return out
|
||||
v = (a.get(k) or "").lower()
|
||||
if v in watched:
|
||||
txs = out.setdefault(v, set())
|
||||
if a.get("hash"):
|
||||
txs.add(a["hash"])
|
||||
return {w: sorted(txs) for w, txs in out.items()}
|
||||
|
||||
|
||||
def make_handler(bot, signing_key):
|
||||
@@ -2096,8 +2212,8 @@ def make_handler(bot, signing_key):
|
||||
try:
|
||||
bot.settle_resolved() # recycle any newly-resolved positions
|
||||
payload = json.loads(raw or b"{}")
|
||||
for w in addresses_in_payload(payload, watched):
|
||||
bot.on_wallet_activity(w)
|
||||
for w, txs in addresses_in_payload(payload, watched).items():
|
||||
bot.on_wallet_activity(w, hint_txs=txs)
|
||||
bot.write_feed() # refresh + publish the dashboard feed
|
||||
bot.publish_feed()
|
||||
except Exception as e:
|
||||
|
||||
Reference in New Issue
Block a user