#!/usr/bin/env python3 """Spreads reference quoter. An AI agent's entry point to market making on tokenized stock pairs on Robinhood Chain: python quoter.py init create the agent wallet (works now) python quoter.py status ETH + USDG balances, venue status (works now) python quoter.py doctor self check the environment (works now) python quoter.py scan pair universe + session regime (works now) python quoter.py demo simulated quoting session with PnL (works now) python quoter.py run join the live book (needs the Book) Configuration comes from environment variables (see skill.md): SPREADS_RPC Robinhood Chain JSON-RPC endpoint SPREADS_SITE desk base URL (pair registry, session clock, telemetry) SPREADS_KEYFILE where the wallet keyfile lives SPREADS_PRIVATE_KEY use an existing key instead of a keyfile (optional) SPREADS_BOOK Book contract address (published at launch) Dependencies: pip install eth-account requests """ import json import os import random import sys import time import requests from eth_account import Account RPC = os.environ.get("SPREADS_RPC", "https://rpc.mainnet.chain.robinhood.com") SITE = os.environ.get("SPREADS_SITE", "https://spreads.money") KEYFILE = os.environ.get("SPREADS_KEYFILE", "./spreads-wallet.json") BOOK = os.environ.get("SPREADS_BOOK", "") USDG = "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168" CHAIN_ID = 4663 EXPLORER = "https://robinhoodchain.blockscout.com" # Windows consoles hand Python a legacy codepage by default, which mangles any # UTF-8 the wrapper writes. Force UTF-8 so output survives intact. for _stream in ("stdin", "stdout"): try: getattr(sys, _stream).reconfigure(encoding="utf-8", errors="replace") except Exception: pass # ---- chain plumbing ------------------------------------------------------- def rpc_call(method, params): r = requests.post(RPC, json={"jsonrpc": "2.0", "id": 1, "method": method, "params": params}, timeout=15) r.raise_for_status() j = r.json() if "error" in j: raise RuntimeError(j["error"].get("message", "rpc error")) return j["result"] def eth_balance(address): return int(rpc_call("eth_getBalance", [address, "latest"]), 16) / 1e18 def token_balance(token, holder, decimals=18): data = "0x70a08231" + holder.lower().replace("0x", "").rjust(64, "0") raw = rpc_call("eth_call", [{"to": token, "data": data}, "latest"]) return (int(raw, 16) if raw and raw != "0x" else 0) / (10 ** decimals) def is_deployed(address): code = rpc_call("eth_getCode", [address, "latest"]) return bool(code) and code != "0x" and len(code) > 4 # ---- desk API ------------------------------------------------------------- def desk(path): if not SITE: return None try: r = requests.get(f"{SITE}{path}", timeout=10) return r.json() if r.ok else None except Exception: return None def beacon(address): """Register this agent with the fleet counter on the desk. Best effort and anonymous beyond the public wallet address: powers the live 'agents online' count. Set SPREADS_SITE="" to opt out. """ if not SITE: return try: requests.post(f"{SITE}/v1/ping", json={"operator": address}, timeout=5) except Exception: pass # never let telemetry break quoting # ---- session clock (local fallback when the desk is unreachable) ---------- def market_session(): s = desk("/v1/session") if s: return s try: from datetime import datetime, timezone, timedelta try: from zoneinfo import ZoneInfo now = datetime.now(ZoneInfo("America/New_York")) except Exception: # no tz database (Windows without tzdata): approximate ET by month utc = datetime.now(timezone.utc) offset = -4 if 3 <= utc.month <= 11 else -5 # rough DST guess now = utc + timedelta(hours=offset) minutes, day = now.hour * 60 + now.minute, now.weekday() # Mon=0 if day >= 5: name = "weekend" elif 9 * 60 + 30 <= minutes < 16 * 60: name = "primary" elif 16 * 60 <= minutes < 20 * 60: name = "after_hours" elif 4 * 60 <= minutes < 9 * 60 + 30: name = "pre_market" else: name = "overnight" if day == 4 and minutes >= 16 * 60: name = "weekend" return {"session": name, "primary_open": name == "primary", "clock": "America/New_York", "source": "local"} except Exception: return {"session": "unknown", "primary_open": False, "source": "none"} # ---- wallet --------------------------------------------------------------- def load_or_none(): pk = os.environ.get("SPREADS_PRIVATE_KEY") if pk: return Account.from_key(pk) if os.path.isfile(KEYFILE): with open(KEYFILE) as f: return Account.from_key(json.load(f)["private_key"]) return None def cmd_init(): if load_or_none() and os.path.isfile(KEYFILE): acct = load_or_none() print(f"wallet already exists: {acct.address}") return acct = Account.create() with open(KEYFILE, "w") as f: json.dump({"address": acct.address, "private_key": acct.key.hex()}, f) print("agent wallet created") print(f" address : {acct.address}") print(f" keyfile : {KEYFILE} (keep it private, never commit it)") print(f" explorer: {EXPLORER}/address/{acct.address}") print("Fund this address with a small amount of ETH for gas, and USDG for") print("quoting capital once the Book is live. Then: python quoter.py status") beacon(acct.address) def cmd_status(): acct = load_or_none() if not acct: sys.exit("no wallet. run: python quoter.py init") beacon(acct.address) print(f"agent : {acct.address}") print(f"chain : Robinhood Chain ({CHAIN_ID})") try: print(f"ETH : {eth_balance(acct.address):.6f}") print(f"USDG : {token_balance(USDG, acct.address):,.2f}") except Exception as e: print(f"balances : unreadable ({e})") s = market_session() print(f"session : {s.get('session')}") if BOOK: try: live = is_deployed(BOOK) print(f"the Book : {BOOK} ({'verified on chain' if live else 'NOT deployed — do not escrow'})") except Exception as e: print(f"the Book : unverifiable ({e})") else: print("the Book : published at launch (set SPREADS_BOOK when announced)") def ok(flag): return "ok " if flag else "FAIL" def cmd_doctor(): print("spreads doctor\n") acct = load_or_none() print(f"[{ok(acct is not None)}] wallet " f"{acct.address if acct else '(missing — run: python quoter.py init)'}") try: block = int(rpc_call("eth_blockNumber", []), 16) print(f"[{ok(True)}] Robinhood Chain RPC reachable, block {block:,}") except Exception as e: print(f"[{ok(False)}] Robinhood Chain RPC: {e}") pairs = desk("/v1/pairs") print(f"[{ok(bool(pairs))}] desk API " f"({SITE + '/v1/pairs' if SITE else 'opted out'}) " f"{'— ' + str(pairs['count']) + ' pairs' if pairs else ''}") s = market_session() print(f"[{ok(s.get('session') != 'unknown')}] session clock — {s.get('session')}") if BOOK: try: live = is_deployed(BOOK) print(f"[{ok(live)}] Book contract {'verified' if live else 'has no bytecode — DO NOT escrow'}") except Exception as e: print(f"[{ok(False)}] Book contract: {e}") else: print("[ -- ] Book contract not announced yet (live quoting disabled)") if acct: beacon(acct.address) def cmd_scan(): s = market_session() print(f"session : {s.get('session')} primary_open={s.get('primary_open')}") p = desk("/v1/pairs") if not p: sys.exit("pair registry unreachable — set SPREADS_SITE or check the desk") q = p.get("quote_asset", {}).get("symbol", "USDG") print(f"universe: {p['count']} pairs, quoted in {q}\n") print(f"{'PAIR':<12}{'TIER':<10}{'HOLDERS':>9} TOKEN") for t in p["pairs"][:30]: print(f"{t['symbol'] + '/' + q:<12}{t.get('tier', '-'):<10}" f"{t.get('holders', 0):>9,} {t['address']}") if p["count"] > 30: print(f"...and {p['count'] - 30} more via {SITE}/v1/pairs") # ---- the estimate hook ---------------------------------------------------- def estimate(pair, context): """Return (fair_price, confidence 0..1) or None to pull all quotes. THIS is where the intelligence goes. When run inside an agent harness (Claude Code, MCP, or any loop that can read news), the driving model should answer using every signal it can see: the primary close, correlated tickers, headlines, and the fill tape in `context`. Returning None is a first-class answer meaning "I cannot price this" — the kit pulls quotes. Standalone fallback: trust the last observed reference, decay confidence when the tape turns one-sided, and refuse to quote right after a shock. """ ref = context["reference"] if context.get("shock_unpriced"): return None # news we have not priced: stand down onesided = abs(context.get("tape_imbalance", 0.0)) confidence = max(0.2, 1.0 - onesided) return ref, confidence # ---- demo: a full simulated after-hours session --------------------------- def cmd_demo(seed=None): rng = random.Random(seed if seed is not None else 2026) print("spreads demo — simulated after-hours session on NVDA/USDG") print("mechanics are identical to the live loop; fills are synthetic\n") true_px = 100.0 # what the market "really" knows ref_px = 100.0 # what the agent can observe (lags on news) base_half_bps = 40.0 # starting half-spread, in bps size = 10.0 # tokens per quote inventory = 0.0 max_inventory = 5 * size cash = 0.0 spread_income = 0.0 adverse = 0.0 fills = tape = 0 shock_ticks = 0 STEPS = 240 # ~two simulated hours, 30s per step for step in range(STEPS): # world evolves: drift + occasional news shock the agent sees late true_px *= 1 + rng.gauss(0, 0.0004) shock = rng.random() < 0.02 if shock: jump = rng.choice([-1, 1]) * rng.uniform(0.01, 0.04) true_px *= 1 + jump shock_ticks = 4 # reference catches up after 4 ticks if shock_ticks > 0: shock_ticks -= 1 if shock_ticks == 0: ref_px = true_px # the agent's feeds finally reprice else: ref_px = true_px * (1 + rng.gauss(0, 0.0002)) ctx = { "reference": ref_px, "tape_imbalance": max(-1.0, min(1.0, inventory / max_inventory)), # the agent's feeds flag the shock one tick late: like real life, # you always eat the first print — the skill is capping it there "shock_unpriced": shock_ticks > 0 and not shock, } est = estimate("NVDA/USDG", ctx) if est is None: continue # quotes pulled this tick — the safe state fair, conf = est half = fair * (base_half_bps / 10000.0) / conf skew = -inventory / max_inventory * half * 0.5 bid, ask = fair - half + skew, fair + half + skew # taker flow: mostly noise, always informed right after a shock if rng.random() < 0.45: tape += 1 informed = shock_ticks > 0 side = ("sell" if true_px < bid else "buy" if true_px > ask else None) if informed else rng.choice(["buy", "sell"]) if side == "buy" and inventory > -max_inventory: cash += ask * size; inventory -= size; fills += 1 spread_income += (ask - fair) * size adverse += max(0.0, (true_px - ask)) * size elif side == "sell" and inventory < max_inventory: cash -= bid * size; inventory += size; fills += 1 spread_income += (fair - bid) * size adverse += max(0.0, (bid - true_px)) * size pnl = cash + inventory * true_px print(f"ticks : {STEPS} (30s each)") print(f"taker arrivals : {tape}") print(f"fills : {fills}") print(f"ending inventory: {inventory:+.0f} tokens (target 0)") print(f"spread income : {spread_income:+,.2f} USDG") print(f"adverse select. : {-adverse:+,.2f} USDG") print(f"net PnL : {pnl:+,.2f} USDG\n") if pnl > 0: print("verdict: the estimator survived the night. Tune it, then wait for the Book.") else: print("verdict: run over. Widen quotes, pull faster on shocks, try again.") print(" (a losing demo is the kit doing its job — no capital was at risk)") def cmd_run(): if not BOOK: sys.exit("live quoting is not open yet: the Book contract is published at " "launch.\nWatch /v1/contracts (or the Desk) and set SPREADS_BOOK " "when it lands.\nUntil then: python quoter.py demo") if not is_deployed(BOOK): sys.exit(f"SPREADS_BOOK is set but no bytecode exists at {BOOK} on chain " f"{CHAIN_ID}.\nDO NOT escrow anything. Verify the address on " f"{EXPLORER}.") sys.exit("Book detected. The live loop ships with the venue — update the kit:\n" f" curl -L -o quoter.py {SITE}/quoter.py") # ---- entry ---------------------------------------------------------------- COMMANDS = { "init": cmd_init, "status": cmd_status, "doctor": cmd_doctor, "scan": cmd_scan, "demo": cmd_demo, "run": cmd_run, } if __name__ == "__main__": cmd = sys.argv[1] if len(sys.argv) > 1 else "doctor" fn = COMMANDS.get(cmd) if not fn: sys.exit(f"unknown command: {cmd}. one of: {', '.join(COMMANDS)}") fn()