x402 wire-level fix + live Polymarket/Kalshi adapters - #1
Open
johnsmithxy1mmm-sys wants to merge 74 commits into
Open
x402 wire-level fix + live Polymarket/Kalshi adapters#1johnsmithxy1mmm-sys wants to merge 74 commits into
johnsmithxy1mmm-sys wants to merge 74 commits into
Conversation
…eware The x402 ASGI middleware buffers the request body to read the tool name, then forwards to the streamable-HTTP handler. The replayed `receive` emitted synthetic `http.request` messages once the buffer drained, so the handler — which keeps calling `receive()` to detect client disconnect — never saw `http.disconnect` and hung on every forwarded (paid-with-payment / free-in-paid- mode) request. Only the 402 paths (which don't forward) worked. Fix: after replaying buffered messages, delegate to the original `receive` so `http.disconnect` flows. Strengthen the ASGI test so the downstream reads the body and asserts it observes the disconnect, catching this regression. Verified end-to-end over real HTTP with PAID_ENABLED=true: 402 without payment, 402 on underpayment, 200 + data with a valid signed X-PAYMENT, free tools bypass; one receipt + one usage record (paid_via=x402) logged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Replaces the mock engine with real venue data when CORE_ENGINE=live; mock stays the default so the offline suite is unchanged. The MCP/tools layer is untouched — deps.py picks the engine, both engines expose the same surface. - core/algorithms.py: shared, venue-agnostic intelligence (realizable-edge math, computed cross-venue matcher, bundle/cross-venue signal detectors). Mock now delegates its edge calc here, so the math is defined once, not duplicated. - core/adapters/: base (single HTTP boundary — every httpx failure maps to AdapterError), polymarket (Gamma markets + CLOB book), kalshi (public markets + orderbook, cents->prob). Fetch + normalize only; no intelligence. - core/live.py: LiveRepo + engine over the adapters, TTL-cached, degrades gracefully (empty results) when a venue is unreachable. - deps.py: CORE_ENGINE=mock|live switch (mock default). - tests/test_adapters.py: normalization + live-engine tests via httpx.MockTransport (no network). 29 tests green. - README: engine selection, CORE_ENGINE env, updated layout. Note: venue APIs aren't reachable from this sandbox (network policy), so live calls were verified against mocked HTTP, not real endpoints. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…istory Live mode returned an empty history series (get_market_history had no source). This adds a time-series store and ingestion so it returns real data. - core/storage.py: HistoryStore mirroring the metering pattern — SQLite by default (zero infra, HISTORY_DB_URL), Timescale/Postgres DSN seam for prod (# TODO real asyncpg writer). Indexed by (venue, market_id, ts). - core/live.py: LiveRepo records a price point for every observed market on each fresh fetch; get_history now reads the ingested series back (backfill depth grows with uptime / a Timescale DSN). - tests/test_storage.py: range/isolation/spread-from-market + a live ingest->read test with mocked adapters. 33 tests green. - README: HISTORY_DB_URL env + layout. Mock engine's synthetic history is unchanged; this only affects CORE_ENGINE=live. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…ined score) Replaces the token-Jaccard title matcher with a normalized, combined-similarity one that markedly improves cross-venue matching on real titles: - Normalize numbers/units ($100k == 100,000 == 100000), month names, and stopwords before comparing, so equivalent phrasings reinforce a match. - Score = 0.5*token Jaccard + 0.35*difflib ratio + 0.15*number overlap. - match_markets default threshold raised to 0.45 to favor PRECISION: a false pair is a fake arbitrage opportunity, worse for trust than a missed real one. True pairs now score ~0.66-0.70; cross-topic negatives ~0.08-0.12. Known limitation (documented in code): "shared entity + shared year" titles can still false-positive — that's the boundary of lexical matching and where embeddings would take over. tests/test_matcher.py added (normalization, ranking, cross-venue pairing, same-venue exclusion). 38 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Runs on every push and pull request: install uv (pinned 0.8.17), provision Python 3.12, `uv sync --extra postgres --frozen`, then `uv run pytest`. Caches the uv download. Verified the exact commands locally (38 tests pass, lockfile in sync). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Adds an embedding-based matcher tier that resolves the lexical tier's "shared entity + shared year" false positives (e.g. "Ethereum flips Bitcoin 2026" vs "Bitcoin above 100k 2026"). Lexical stays the default; nothing else in the pipeline changes. - core/embeddings.py: Embedder abstraction. Default FastEmbedEmbedder (local ONNX bge-small, CPU-only, no torch); Voyage stub for a hosted rail. Degrades to None (-> lexical fallback) if the package/weights are unavailable, so the server never crashes offline. - core/algorithms.py: semantic_similarity (cosine) + a `similarity` dispatcher selected by MATCHER=lexical|semantic|hybrid; match_markets/best_match use it; threshold is env-tunable (MATCH_MIN_CONFIDENCE) since cosine sits on a different scale than the lexical blend. - pyproject: optional `semantic` extra (fastembed). - tests/test_semantic_matcher.py: mocked concept-vector embedder proves the semantic tier keeps the true BTC pair and rejects the ETH false positive the lexical tier accepts; plus hybrid blend and no-embedder fallback. 44 green. - README: MATCHER / MATCH_MIN_CONFIDENCE / EMBED_* env + layout. Note: Anthropic has no embeddings endpoint; fastembed downloads weights from Hugging Face once (blocked in this sandbox), so the semantic path runs where that egress is allowed or with weights baked into the image. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…c tier) Makes the semantic matcher production-ready: - core/embeddings.py: `embed_texts` caches each string's vector, so matching embeds every unique title once (O(N)) instead of re-embedding per pair (O(N^2)) — essential for a paid/hosted backend. semantic_similarity routes through it. - VoyageEmbedder implemented for real (POST api.voyageai.com/v1/embeddings, bearer auth, index-ordered results, all failures -> EmbedderError -> lexical fallback). No model in the image, so it's serverless/cold-start friendly; EMBED_BACKEND=voyage + VOYAGE_API_KEY. get_embedder returns None without a key. - tests/test_embeddings.py: Voyage parse/ordering + HTTP/connection error mapping (mocked transport, no network); cache batches misses and serves repeats (embed-once proof); no-embedder path returns None. - conftest clears the vector cache between tests. README documents the hosted option. 50 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…k arb Deepens the actual intelligence (where non-obvious edge lives), folded into existing tools — no new tools, keeps the <=12 discipline. - Calibrated fee model: venue_fee() replaces the flat-bps approximation with Kalshi's price-dependent 0.07*notional*(1-price) (peaks at 50c, ~0 at the tails); estimate_realizable_edge now returns a per-venue cost_breakdown. - Risk-adjusted edge: annotate_risk() adds holding_days, annualized_edge (edge/days*365), and a resolution_risk haircut — a distant-resolution edge is worth less than the same edge next week. Surfaced on every opportunity. - Dutch-book detector: scan_dutch_book() finds combinatorial arb across mutually-exclusive outcome groups (YES sum != 1 -> buy all YES / all NO), wired into both engines and find_mispricing (kind=dutch_book). Mock seed gains a 2028-president group to demonstrate it. - Models: DUTCH_BOOK kind, Market.event_group, risk fields on Opportunity, cost_breakdown on ExecutionEstimate. - tests/test_intelligence.py: fee curve, cost breakdown, annualization, under/over-round Dutch book, singleton/fair-book rejection, engine integration. 58 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
The trust moat — turns "honest realizable edge" from a claim into evidence. - core/reconciliation.py: records every flagged opportunity (idempotent, captures per-leg entry price), resolves it once markets settle, and computes realized edge, hit-rate, predicted-vs-realized edge_slippage, and a Brier score on the implied probabilities surfaced. SQLite default (RECON_DB_URL). - find_mispricing now records what it flags; a new FREE tool track_record() exposes the aggregate performance — the funnel/trust surface an agent checks before paying. - src/predmarket_mcp/provenance.py: tamper-evident HMAC-SHA256 signature over the response data (SIGNING_KEY env; marked signed:false when unset). find_mispricing and track_record carry a provenance block. - deps: record_flagged / resolve_outcomes / track_record_metrics seams. - tests/test_reconciliation.py: idempotent record; arb realizes regardless of outcome; divergent resolution breaks it; hit-rate + Brier math; sign/verify + tamper detection; track_record tool reflects a resolution end-to-end. 8 tools now (still within the <=12 target). 66 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Changes the usage model from polling to subscribing, and the economics from per-scan to per-subscription + per-alert. - core/watches.py: WatchStore registers watches (edge threshold + optional kind/category/event filters) and queues alerts when live opportunities cross them. Alerts are computed server-side (push) and drained by the client (pull) — deduped per (watch, opportunity), delivered once, per-client. Serverless-friendly; no server-initiated SSE required (TODO: MCP resource notifications for true push). SQLite default (WATCH_DB_URL). - Tools: watch() (paid $0.02 subscription; returns an id + immediate matches) and poll_alerts() (paid $0.005; drains newly-fired alerts). Client identity from x-client-id / authorization header, else "anonymous". - deps: create_watch / poll_alerts / list_watches orchestration (scan -> fire -> drain). conftest isolates the watch DB. - tests/test_watches.py: threshold gating, dedupe + deliver-once, kind/event filters, per-client isolation, and the watch->poll tool flow. 10 tools now (~1.7k-token catalog, within target). 71 tests green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Turns the three production seams from env-only stubs into working code. - HttpFacilitator (billing/x402.py): speaks the x402 facilitator protocol (POST /verify + /settle) to a Coinbase/self-hosted facilitator that recovers the EIP-712 signature and settles USDC on-chain, returning a real tx hash into the receipt. Selected by build_facilitator() when X402_FACILITATOR_URL is set; MockFacilitator remains the default. HTTP client injectable for tests. - Kalshi RSA-PSS request signing (core/adapters/kalshi.py): signs timestamp+method+path with the operator private key (KALSHI_API_KEY_ID + KALSHI_PRIVATE_KEY[_PATH]) for private endpoints (orderbook); legacy bearer and no-auth fallbacks. `cryptography` added to the dev group + a `kalshi` extra. - Dockerfile: EXTRAS build-arg to bake optional extras, and WITH_SEMANTIC=true to pre-download bge-small weights at build time (no HF egress at runtime). - tests/test_facilitator.py (verify/settle happy path, rejection, settlement failure, HTTP error, config selection) and tests/test_kalshi_auth.py (signature verifies against the public key, tamper fails, bearer + no-auth fallbacks) — all offline via mocked HTTP / a local keypair. 80 tests green; lockfile updated; README + env docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…st free-tier delay, async IO
Part A (A1-A5) — closes the security/honesty bugs the audit surfaced.
A1 replay: NonceStore records each accepted payment fingerprint (signature +
EIP-3009 authorization); a reused X-PAYMENT is rejected with 402
"payment_reused" and never reaches /settle. One signed header = one paid call.
A2 batch: _paid_tool_calls collects EVERY paid call in a JSON-RPC batch;
check_x402 requires ONE payment covering their SUM. Paying for one tool no
longer unlocks the rest.
A3 privacy: metering stores a hashed client fingerprint (never the raw
Authorization token) and only the param SHAPE ({keys,count}), never argument
values. Shared "anonymous" client id replaced with per-caller ephemeral hash
so alert queues stay isolated (deliver-once can't leak across callers).
A4 honest delay: evaluate_market now serves a genuinely delayed price from the
history store with its TRUE as_of (delayed:true), instead of backdating live
data; no live orderbook leak on the free tier. Falls back honestly when no
delayed snapshot exists yet.
A5 async: payment verify/settle and metering writes run via anyio.to_thread so
SQLite + facilitator HTTP never block the event loop.
Tests: replay->402, batch pays the sum, client_fingerprint hides secrets,
metering stores shape-only params, evaluate_market price comes from history. 84 pass.
…-A9)
A6: LRU-bound the embedding vector cache (EMBED_CACHE_SIZE) and the live
TTL cache (LIVE_CACHE_MAX) so neither grows without limit.
A7: Kalshi volume/depth are contract counts -> convert to USD notional
(contracts * price). Polymarket fetch_orderbook resolves the YES clob
token via a persistent map + Gamma fallback, never sending a
conditionId as a token_id (which silently returns an empty book).
A8: per-client active-watch cap (WATCH_MAX_PER_CLIENT) and an opt-in
background AlertEngine (ALERT_ENGINE) that fires watches off the hot
path; poll_alerts becomes drain-only when it runs. Off by default so
offline/serverless stays synchronous.
A9: shared keep-alive httpx client pool per host; HistoryStore retention
(HISTORY_RETENTION_DAYS, throttled prune); finalize_opportunities
annotates risk from the EARLIEST close across ALL legs (not leg[0])
and caps max_size_usd by real top-of-book depth.
93 tests green (8 new hardening regressions).
B1: core/streaming.py — flag-gated (STREAMING) background WSS ingestor that
writes venue price ticks into the history store. Pure normalize_tick()
is unit-tested offline; the socket loop degrades gracefully (no flag /
no websockets pkg / dropped conn -> server runs on the fetch path).
B2: core/notifier.py — flag-gated (ALERT_WEBHOOK_URL) webhook push of newly
fired alerts, best-effort (never breaks firing; alert stays queued for
poll). New alerts:// resource peeks queued alerts WITHOUT draining, and
WatchStore.peek() backs it. fire() now collects + pushes new alerts.
104 tests green (12 new).
…e (B3, B4)
B3: core/fairvalue.py — liquidity-weighted cross-venue consensus probability,
per-venue deviation, and dispersion-based confidence. compare_across_venues
now reports fair_value + which venue is rich/cheap vs it; cross_venue
opportunities carry a fair_value anchor (new Opportunity.fair_value field).
B4: core/sizing.py — fractional-Kelly stake (KELLY_FRACTION, half-Kelly
default, clamped, never sizes a negative edge) + a market-impact curve
walking real book depth. estimate_execution takes optional bankroll_usd +
fair_value and returns a sizing block (kelly_fraction, recommended_size_usd,
impact_curve). Both pure/offline over the shared book_getter seam.
118 tests green (14 new).
B5: core/analyst.py — resolution-risk scoring. Offline heuristic
(category/liquidity/horizon) is always available; when ANALYST_ENABLED +
ANTHROPIC_API_KEY are set, an LLMAnalyst calls Claude (claude-opus-4-8)
with a structured tool (no free-text parsing) and adaptive extended
thinking, degrading to the heuristic on any error. find_mispricing
enriches the top N opportunities (ANALYST_MAX_ENRICH cost guard).
B6: Trust v2 — provenance now signs with Ed25519 when SIGNING_KEY_ED25519 is
set (asymmetric; verifiable via the new public GET /pubkey), HMAC
fallback otherwise. core/merkle.py commits the resolved track record to a
Merkle root (inclusion proofs); track_record + a public GET /track-record
expose it so the record is auditable and tamper-evident without an MCP
session.
130 tests green (12 new).
…g (B7)
core/circuit.py: per-host circuit breaker around fetch_json — after N
consecutive failures it opens and fast-fails for a cooldown, then
half-opens a probe (CIRCUIT_FAIL_MAX / CIRCUIT_RESET_TIMEOUT). A dead
venue no longer costs every call a full timeout.
ops.py: thread-safe Metrics registry + Prometheus-text GET /metrics (folds in
circuit states); token-bucket RateLimitMiddleware (per-client, 429 +
Retry-After, inert unless RATE_LIMIT_RPM>0); JSON structured logging via
LOG_FORMAT=json. Rate limiter sits outermost in the ASGI stack.
138 tests green (8 new).
core/adapters/manifold.py: normalizes Manifold binary markets (live
probability -> YES price) and synthesizes a shallow two-level book from
CPMM liquidity for the edge model (no central order book on Manifold).
Adds Venue.MANIFOLD; opt-in in the live engine via MANIFOLD_ENABLED. A
third venue turns the fair-value spread into a real consensus with a
dispersion signal.
141 tests green (3 new).
README: v2 config table (streaming, alert engine/webhook, analyst, Kelly, fair value, Manifold, rate limit, circuit breaker, retention, logging, Ed25519), updated layout + tool/resource counts + endpoints (/metrics, /pubkey, /track-record). Catalog stays 10 tools / ~1.8k schema tokens.
1. Circuit breaker no longer counts 4xx as venue failures (new AdapterClientError; 429 still counts as pushback) — repeated bad market ids could previously open the breaker and block good calls. 2. Breaker registry cleared between tests (state leaked across tests). 3. Stream sink invalidation throttled to half the market TTL — clearing on every tick defeated the cache and hammered venue APIs under streaming. 4. normalize_tick: a price of 0 is a real quote, no longer dropped by or-chaining. 5. embed_texts assembles results locally — one call with more unique texts than the cache bound could return None vectors after self-eviction. 6. RateLimiter bucket map LRU-bounded (RATE_LIMIT_MAX_CLIENTS) — was an unbounded per-client memory leak. 7. History prune uses a None sentinel — monotonic()~0 on fresh boots silently skipped pruning for the machine's first hour. 8. Holding period = LATEST close across legs (was earliest): a basket is realized only when its last leg resolves; earliest close overstated annualized edge, violating the never-overstate principle. 9. alerts:// resource enforces caller identity — anyone who guessed a client_id could peek that client's paid alerts. Plus consistency: single _http_middleware() shared by all entrypoints (the Docker __main__ path was missing the rate limiter + logging), server instructions copy updated to the real 10-tool catalog, /track-record DB reads offloaded from the event loop, hoisted hot-loop imports. 148 tests green (7 new audit regressions).
core/entailment.py: parses market titles into (entity, direction, threshold,
deadline, cumulative) claims and detects RISK-FREE arbitrage no spread scanner
can see:
* threshold monotonicity — P(BTC>150k) must be <= P(BTC>100k) at the same
date; a violation is banked by shorting the over-priced stronger claim and
longing the under-priced weaker one (whenever the strong one pays, the weak
one pays too).
* term structure — for cumulative 'reach X by DATE' claims, probability must
be non-decreasing in the deadline; point-in-time snapshots are deliberately
excluded to keep precision high.
New OpportunityKind.ENTAILMENT wired into find_mispricing (mock + live) and its
kind filter. Also exposes the implied probability term structure as a by-product.
157 tests green (9 new).
core/basis.py: scores whether two 'matching' markets actually resolve by the same rules. Offline heuristic reads resolution-sensitive wording (snapshot vs cumulative framing, explicit data source, venue) into a basis_risk score + same_contract flag; the LLM path (ANALYST_ENABLED) does a structured point-by-point diff via a shared analyst.call_tool() helper, degrading to the heuristic. compare_across_venues now returns basis_risk, same_contract, and resolution_differences — so a spread flagged same_contract=false is exposed as a bet on which rulebook wins, not free money. Refactored analyst.score() onto the reusable call_tool() boundary. 163 tests green (6 new).
core/calibration.py: turns the resolution history into a probability correction. Bins surfaced implied-probabilities, takes the empirical resolution rate per bin, enforces monotonicity with Pool-Adjacent-Violators isotonic regression, and interpolates — so 'we said 0.70, it resolved YES 64% of the time' becomes a calibrated forecast that differs from the raw market (edge, not just arbitrage). Identity below CALIBRATION_MIN_SAMPLES (never correct on thin evidence); cached by resolved-sample count and rebuilt as outcomes land. evaluate_market now returns a calibration block; the moat compounds with every resolution no competitor has. 169 tests green (6 new).
core/backtest.py: replays a mean-reversion rule (long YES on a dip below the trailing mean, exit on recovery) over the server's recorded price history, net of venue fees + slippage — same cost model as the live edge math. Returns trades, hit_rate, total_return, sharpe_per_trade, max_drawdown. New paid tool simulate_strategy (11th tool, under the 12 cap). Lets an agent validate a rule on real history before paying for live scans, and runs on price data no competitor starting today has. 175 tests green (6 new).
core/options.py: Breeden-Litzenberger — risk-neutral P(S>K) = -e^{rT}.dC/dK,
the negative slope of the call chain across strikes (pure, central-difference,
offline-testable). DeribitProvider fetches the live call chain (flag-gated
OPTIONS_ENABLED, injectable client, degrades to None). evaluate_market on a
crypto strike market now carries an 'options' block comparing the
prediction-market probability to what billions in options imply — a real-money
cross-check no prediction-market aggregator has. Reuses the C1 claim parser to
extract (currency, strike) from the title.
183 tests green (8 new).
core/anchor.py: periodically anchors the reconciliation Merkle root in a public chain so 'we didn't rewrite history' is provable against a ledger we don't control, not against our own endpoint. Pluggable submitter (x402-facilitator pattern): MockChainAnchor (deterministic, offline, tested) by default; EvmChainAnchor when ANCHOR_RPC_URL is set (structural, degrades to None). Anchors persist in SQLite, deduped by root and rate-limited by ANCHOR_MIN_INTERVAL. Public GET /track-record includes onchain_anchor when ANCHOR_ENABLED. Off by default. 190 tests green (7 new).
…ing (C8, C5)
C8 core/microstructure.py: tick-direction imbalance, realized vol and drift over
the recent price series -> signed momentum + informed_flow flag ('an edge is
ABOUT to appear', not just 'exists now'). Pure/offline; evaluate_market
surfaces it from recorded history.
C5 value-based pricing: get_market_history now charges by requested range
(+$0.005/30d, capped $0.10, floored at base) via Pricing.price_for(). The
x402 gate and metering both use the value-based amount, threaded through the
middleware as (tool, args) pairs. Money-safety pinned by tests: monotone,
capped, never below base, batch pays exactly the sum. Flat tools unchanged.
200 tests green (10 new).
README: v3 config table (calibration, basis LLM diff, options/Deribit, microstructure, on-chain anchor), core/ layout with the 8 new modules (entailment, basis, calibration, backtest, options, microstructure, anchor), tool/resource counts (11 tools). Catalog stays under budget (~2.26k schema tokens, 11 tools).
kv.py: lazy, optional Redis client from REDIS_URL (degrades to local backend if unset or redis-py missing). NonceStore gains a Redis path using atomic SET NX, so a signed X-PAYMENT consumed on one instance is globally consumed — the only correct replay guard behind a load balancer (SQLite-per-process would let the same header buy N calls across N instances). RateLimiter gains a shared fixed-window INCR counter so limits are global, not per-process, and degrades to the local token bucket on any Redis error. Both default to the existing single-instance backends; new optional [redis] extra. 206 tests green (6 new, with a self-contained fake Redis).
core/pg.py: an AsyncLoop runs a single asyncio loop on a daemon thread so the synchronous stores can drive async asyncpg via run_coroutine_threadsafe — a real Postgres/Timescale path without rewriting callers as async. storage.py: PgHistoryStore mirrors the SQLite store's surface (record/query/count/prune, server-side retention via DELETE ... WHERE ts < cutoff); HistoryStore() is now a factory that dispatches by DSN (postgresql:// -> PgHistoryStore, else SQLite) and degrades to SQLite if the driver/DB is unavailable. README documents the multi-instance deploy (shared REDIS_URL + postgresql:// HISTORY_DB_URL). 212 tests green (6 new, fake asyncpg pool — no live DB).
…lass strategy (I1)
Every ranking so far optimized edge quality; none priced TIME. For a compounding
bankroll a 1% edge recycled weekly (~68%/yr) beats a 5% edge locked for a year
(5%/yr). core/velocity.py makes turnover the strategy:
* velocity metrics on every opportunity — capital_efficiency (EV per
locked-capital day), an illustrative compound_annual_growth under recycling
(capped), and early-exit liquidity from the unwind side of the books (deep
bids = realize convergence without holding to resolution);
* rank=velocity mode in find_mispricing — fastest turnover first;
* a rotation_plan (bankroll_usd + horizon_days): greedy allocation across
short-cycle opportunities by efficiency, cycles = horizon // holding,
projected compounded bankroll with idle capital carried flat — explicitly
flagged as a plan, not a promise. Unknown holding periods use a conservative
default and are marked holding_known=false.
Mock markets gained real close_times so holding periods and cycles are real
(Fed ~2 months = the fast-turnover winner; BTC year-end). Trimmed tool copy to
hold the catalog near the ~2.5k-token budget (12 tools).
311 tests green (9 new).
…ot path Audit of the I1 velocity module found two real issues: 1. (crash) compound_annual_growth = (1+edge)^cycles - 1 applied the display cap AFTER exponentiating; a short holding + edge>1.63 raised OverflowError, crashing find_mispricing (annotate_velocity had no guard). Now computed in log-space with the cap applied BEFORE exp — verified no overflow at edge=1.7. 2. (perf) early-exit liquidity fetched an orderbook per leg for EVERY opportunity on EVERY find_mispricing call (O(opps x legs) book fetches in live mode, even when rank!=velocity). Split out into annotate_early_exit, applied only to the velocity-ranked shortlist (top N); the default EV path is now book-free. 314 tests green (4 new regressions).
core/adverse.py: scores P(this edge is a trap — you're the one being picked off) from three signals we already compute: informed flow moving AGAINST the position (microstructure momentum vs leg side), the historical hit-rate of edges of this KIND (reconciliation ground truth — new hit_rate_by_kind()), and quote quality flags. Returns trap_score, factors, and trust_adjusted_edge = edge*(1-trap). find_mispricing now carries an adverse block per opportunity. Never penalizes on thin history (ADVERSE_MIN_SAMPLES). Fewer bad fills -> better hit-rate, the strongest marketing asset. 322 tests green (8 new).
…(J6)
core/auditbundle.py: the track record proves the aggregate is honest; an audit
bundle proves a SPECIFIC edge existed at a specific moment. For a flagged
opportunity it assembles the evidence (each leg's market snapshot, top-of-book
depth, the edge, a timestamp), signs the canonical payload (Ed25519, via
provenance) and stores it. find_mispricing takes opt-in audit=true to attach an
audit_bundle_id per opportunity; public GET /audit/{id} serves the signed
bundle so a buyer independently verifies the edge was real — no trust required.
Added provenance.verify_block (Ed25519/HMAC) + tamper-detection tests. Off by
default -> zero overhead on the hot path. The deepest, least-copyable trust
primitive: cryptographic evidence for each call, not just aggregate stats.
329 tests green (7 new).
…is (J2)
Extends H1: the threshold ladder gives discrete survival points; J2 fits a
lognormal to them (regress ln(strike) on the probit of the CDF -> mu, sigma) so
the implied distribution becomes CONTINUOUS. Answers P(X in [a,b]) for ANY range
with principled tails, plus mean/median/mode/stdev and arbitrary quantiles, not
just the rungs the market happens to list. Verified the fit recovers known
lognormal params from a synthetic ladder. distribution://{entity} now carries a
continuous block; degrades to None on a non-monotone ladder.
333 tests green (4 new).
Extends H2 from pairwise P(A|B) to multi-hop P(A | B, C, ...). core/inference.py
combines each piece of evidence's weight of evidence in log-odds (naive Bayes):
start from the market's price as prior, then update for every conditioning event
— corroborating evidence compounds, contradictory evidence cancels, a
mutually-exclusive sibling observed true drives the posterior to ~0, certain
evidence to ~1 (sigmoid overflow-guarded). The naive-Bayes independence
assumption is flagged in the output. conditional://{event} now carries a
bayesian_updates section: each market's posterior given all the others as
evidence.
340 tests green (7 new).
Fair value (B3) weights venues by liquidity; J4 weights them by ACCURACY. Every time a market resolves, reconciliation.resolve now records the venue's surfaced YES-price vs the outcome; core/metaconsensus.py scores each venue by Brier over those samples and fuses venue prices into a best-estimate + credible interval, with historically-more-accurate venues getting more say. Neutral (equal) weighting until ACCURACY_MIN_SAMPLES, so no venue is over-trusted on thin evidence — meta-calibration that compounds with resolution history. compare_across_venues now returns meta_consensus. 347 tests green (7 new).
…ore growth Release-hardening pass across the money and hot paths: * rotation_plan OverflowError (release blocker): (1+ev)^cycles with a short holding over a long horizon overflowed and 500'd a paid find_mispricing call with bankroll_usd. The factor is now computed in log-space with the same cap as compound growth. * x402 middleware body cap: payment gating buffered the whole POST body unbounded; an oversized request now gets a 413 before parsing (X402_MAX_BODY_BYTES, default 2 MB). * Rate-limit bypass: rotating spoofable x-client-id values minted fresh buckets, sidestepping the per-client limit. Added a secondary per-IP cap at rpm x RATE_LIMIT_IP_MULT (default 5x; 0 disables). * SQLite alert queue cap: a client that never polls now keeps only the newest ALERT_QUEUE_MAX undelivered alerts (mirrors the Redis LTRIM path). * NonceStore TTL prune: the SQLite replay table never deleted expired fingerprints; now swept hourly past NONCE_TTL_SECONDS. * Live index bound: LiveRepo's (venue, id) market index grows with churn; now LRU-bounded (LIVE_INDEX_MAX, default 50k). * fit_lognormal sigma guard: an absurd strike span (garbage in a title) produced a sigma whose exp(sigma^2) overflowed; such fits are rejected. 7 new regression tests; 355 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…ooks (K1-K3)
Three uniqueness modules built on the existing signal stack — no new tools
(house folds into evaluate_market/track_record; scenario is a resource plus an
assess_portfolio arg), so the catalog stays at 12 tools.
K1 — house probability. core/houseview.fuse blends calibration (C3),
options-implied (C7), accuracy-weighted consensus (J4) and a bounded
microstructure tilt (C8) into one house_probability with a confidence and its
edge_vs_market. Every forecast is recorded (core/houseforecast) and Brier-graded
at resolution; track_record.house_forecast reports brier_edge (>0 = the server's
number beats the raw market) — a provable, un-fakeable calibration claim.
Surfaced in evaluate_market, a new house:// resource, and /track-record. The
autonomous resolver grades house forecasts alongside the track record.
K2 — scenario engine. core/scenario propagates hypothetical outcomes across the
entity graph: pin markets (logical entailment/exclusion edges exact, copula edges
from history) and read each related market's prior->posterior->shift. Exposed as
the free scenario://scan/{spec} resource and an optional `scenario=` arg on
assess_portfolio that stress-tests a book. Cluster-scoped so a stopword can't drag
unrelated markets in and manufacture spurious shifts.
K3 — client-sampled rulebooks (the deferred J5). With RULEBOOK_SAMPLING=on,
compare_across_venues uses MCP sampling to ask the CALLING agent's own model to
judge settlement basis risk (core/rulesample) — the deepest analysis at the
client's expense. Degrades to the C2 heuristic when the client can't sample.
28 new tests; 383 passing. Catalog ~2.65k tokens, 12 tools, 8 resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
The network effect the product was missing: an agent doesn't just find edge here,
it can prove it. estimate_execution(commit=true) logs the position as a paper
trade tagged to the caller; the autonomous resolver grades its P&L at resolution
with the same model as the track record (realized_edge = (payoff-cost)/cost,
pnl = size x edge). core/leaderboard.PaperTradeStore aggregates per client.
New surfaces, no new tools (stays at 12): a `commit` arg on estimate_execution,
a public server-signed `leaderboard://top` resource ranking agents by REAL,
resolution-verified return (handles anonymized via sha256; fewer than
LEADERBOARD_MIN_RESOLVED resolved trades stay provisional so one lucky trade
can't top a proven record), and an ownership-checked `portfolio://{client_id}`
resource for a caller's own trades, rank and realized P&L. The server becomes an
independent notary of agent performance — a portable, tamper-evident track record
to show a third party.
12 new tests; 395 passing. Catalog ~2.69k tokens, 12 tools, 10 resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Two new signal classes, no new tools (both are resource/response additions);
catalog stays at 12 tools.
K5 — market-maker advisor. Every other tool serves a taker; core/maker serves a
maker. The free maker://{venue}/{market_id} resource recommends a two-sided limit
quote (recommended_bid/ask, half_spread, skew) to earn the spread, and a
pull_quotes guard to step aside when flow looks informed. It prices
adverse-selection risk from microstructure (C8) + quote quality (F7): widening the
spread under choppy/informed conditions and skewing the center with informed flow
so you don't get run over. Intelligence only — never posts orders. Pure/offline.
K7 — reality feeds (market vs the world). core/reality adds an EXTERNAL anchor: a
real-world nowcast (polls / macro / on-chain) for the same event, and the market's
divergence from it. Structured like the Deribit options cross-check (C7): pure,
offline divergence math + a flag-gated HttpNowcastProvider that degrades to None
without a feed. The reality-implied probability feeds the house forecast (K1) as
an independent, weight-2 component, and evaluate_market surfaces a `reality` block.
Off by default (REALITY_ENABLED + REALITY_NOWCAST_URL).
17 new tests; 412 passing. 12 tools, 11 resources.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…rboard integrity, nowcast cache Findings and fixes from a line-by-line audit of the K-series before release: * K2 pin lookup broken on the live engine: find_market keyword-searched by internal market id, which venue text-search APIs never match. Now a direct per-venue id lookup (repo.get_market) first, search as fallback; and one lookup per pin instead of two. * K2 volume cap could evict the PINNED market itself (lowest-volume pin in a big cluster + small limit), silently producing zero-evidence shifts. Pinned markets are now always kept in the propagation set. * K4 leaderboard integrity: a self-declared paper size was unbounded, so "$1B on a coin flip" could top the proof-of-alpha ranking, and open trades per client were unlimited (disk growth). Size is clamped to LEADERBOARD_MAX_SIZE_USD (default 100k) and commits are refused past LEADERBOARD_MAX_OPEN (default 200); the tool path degrades gracefully (response without paper_trade_id). * K7 double fetch + no cache: one evaluate_market hit the nowcast feed twice (K1 fusion + reality block), every call paid a network round trip, and a downed feed added its timeout each time. Added a bounded per-entity TTL cache (REALITY_TTL_SECONDS, default 60) that also caches negative reads. * K5 cleanup: dead `* 0.0` fee expression replaced with an honest env-driven MAKER_FEE_PER_SIDE (default 0 — these venues fee takers, not makers); elevated realized volatility is now named in the adverse factors. 8 new regression tests; 420 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…rdening, backtest clamp Line-by-line pass over every module not covered by the two previous audit commits (algorithms, sizing, portfolio, multioutcome, entailment, persistence, matchlearn, merkle, backtest, embeddings, streaming, circuit, anchor, quality, adapters, deploy artifacts). Findings and fixes: * estimate_realizable_edge was dimensionally wrong (critical): every filled dollar was counted as profit, so a naked single-leg buy reported ~97% "realizable edge" and gross_edge assumed an N-payout on complementary pairs. Rewritten as contract-based basket math: per-leg average price + contract count, NO legs costed as the YES-complement, baskets executable only in complete units (units = thinnest leg), profit = units x (guaranteed payout - unit cost) - fees - gas. Payout is structure-aware (mixed pairs / all-YES exclusive sets pay 1; all-NO over N outcomes pays N-1). A naked leg reports the (negative) execution drag instead of invented profit, and a basket with a dead/unfillable leg reports 0 fillable/0 edge rather than selling the surviving leg as an arb. Sanity: cross-venue mock basket now ~+2.3% net vs 4% gross (depth and fees eat the paper edge), single leg ~-2.6%. * Adapter orderbook parsing hardened: one malformed depth level from a venue (bad tuple shape / non-numeric size) raised TypeError/ValueError past the AdapterError boundary and would 500 a paid call; per-level parses now skip bad rows (Kalshi + Polymarket). * Backtest net-return clamped at -100%: fees on top of a total wipeout could push net below -1, flipping equity negative and corrupting compounding. Verified clean: sizing (Kelly), portfolio, multioutcome, entailment ordering, persistence, matchlearn, merkle (domain-separated, odd-tail), circuit half-open logic, anchor (base-0 chain id), streaming normalization, embeddings LRU, quality flags, Dockerfile/compose/CI. 4 new regression tests; 424 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…mplate Closes the two mandatory pre-launch gaps identified in the code review, plus a launch-blocking packaging bug found while doing it. * Live path was never validated against real venues (all 430 tests run on mocks; the adapters normalize a response shape assumed from docs). Added deploy/preflight.py — the check the test suite structurally cannot do: it calls the real APIs and pushes what comes back through the actual adapters and the whole pipeline (normalization -> depth -> matching -> claim parsing -> scan -> execution estimate), naming the exact link that breaks. Verifies price normalization, yes/no coherence, titles, volume, close times, event_group, book sanity (crossed/empty/zero-size), scan latency and edge plausibility. Read-only, exits non-zero on failure, --json for monitoring, and audits prod config (unsigned provenance, PAID_ENABLED with no real facilitator = accepting payments that never settle, missing RESOLUTION_ENGINE = a moat that never grows). * Fee model carried a "TODO: source live fee schedules" while being the basis of the product's core claim. Now documented against the published schedules and env-overridable (KALSHI_FEE_RATE / POLYMARKET_FEE_RATE / GAS_USD_*): the Kalshi formula rounds UP to the cent per trade as the venue does (understating a real cost is the one error an "honest edge" product must not make), makers correctly pay no taker fee, and gas resolves at call time instead of freezing an import-time constant. The mock engine now shares this model instead of its own stale constants (gas 0.35 vs 0.05), which had edges evaporating the moment CORE_ENGINE=live. * .gitignore's `.env.*` swallowed deploy/.env.example, so the template was never committed and the documented first step (`cp deploy/.env.example deploy/.env`) failed on a fresh clone. Negated it; real .env files stay ignored. The template also gained the cost-model, RESOLUTION_ENGINE and ADMIN_TOKEN keys. 6 new regression tests; 430 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
* Gas was still frozen at import time despite the previous commit's claim: DEFAULT_GAS_USD / mock._GAS_USD were module-level env snapshots containing every venue, so the call-time gas_for() fallback was dead code and a runtime GAS_USD_* override never reached the edge estimate (proven empirically: override to 5.00 still produced 0.05). Removed both dicts; each leg now resolves gas_for() at call time unless the caller passes an explicit mapping. End-to-end regression tests cover the env override through both the shared estimator and the mock engine, plus explicit-override precedence. * Lint sweep (ruff F/B/PLE): removed 23 unused imports/variables across core, src and tests; zip(strict=True) where an embedder answering short must fail loudly instead of silently dropping vectors; B904 `from None` on the payment parse error; unused loop var renamed; AdapterClientError added to the adapters' __all__. Remaining B905s are equal-length-by-construction zips — left as is. 431 passing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
The MCP registry listing requires a manifest and none existed, blocking the first distribution step. Declares the remote streamable-http transport (so any agent connects with one line, no local install) and leads with the differentiators rather than "prediction market data", which is free at source. Two fields must be filled before publishing: the remote URL (your domain) and a re-check of the $schema/field names against the current registry docs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Findings are recorded only; nothing fixed, per audit protocol.
Critical, audit finding INV-001. check_x402 ordered its steps is_used -> verify_and_settle -> mark_used, with the irreversible step in the middle. check_x402 runs in the ASGI thread pool, so two concurrent requests carrying the SAME signed X-PAYMENT both passed is_used() and both reached the facilitator: the payer was charged twice, one call was served, and a single receipt was written — leaving /revenue reconciliation reporting zero discrepancy. The operator would never have seen it. Reordered so the atomic claim (SET NX on Redis, INSERT OR IGNORE on SQLite) happens BEFORE settlement. The claim is not released on settlement failure: a failed settlement moved no money, so the caller simply presents a freshly signed payment, whereas releasing would re-open the window whenever a facilitator error is ambiguous about whether the transfer went through. Two regression tests: concurrent same-payment settles at most once (kills the old ordering), and a failed settlement does not free the claim. Repro: docs/audit/repro/repro_double_settle.py — was 2 settlements, now 1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Critical, audit finding INV-004. A market whose title contained a very long number killed find_mispricing — the paid flagship — for every caller with an OverflowError, and took compare_across_venues with it through the matcher. On venues where anyone can create a market with an arbitrary question this is a remote, unauthenticated denial of service against the revenue path, using data the operator does not control. Two functions consume untrusted title text and neither was total: * _canon_number (algorithms) ran float() on an arbitrarily long digit string (yielding inf) and then int(round(...)) on it. Now bounded by _MAX_CANON, finiteness-checked before and after scaling, and it returns the span unchanged rather than raising — an unparseable span simply fails to match a threshold ladder. * _threshold (entailment) converted that possibly-unchanged span straight back with float(), and separately called int(digits) for the year check, which raises past Python's 4300-digit conversion limit. Split into _usable_threshold (total parse, None on absurd input) and _is_year (length-guarded before int). Regression tests: five hostile titles never raise, one poisoned market does not break a full scan, and the same input passes through the matcher. Repro: docs/audit/repro/repro_hostile_input.py — parser section now fully green. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
High, audit findings INV-005 and INV-008. One root cause: a venue response was treated as trusted. Its shape is documented, not guaranteed. INV-005 — Market.yes_price/no_price documented '[0, 1]' with no validation, so a venue returning 999 constructed a Market that poisoned every downstream number (edge, probabilities, house forecast, calibration) and was persisted to history. The range is now ENFORCED on the model, so an invalid Market cannot exist anywhere in the system. Rejection, not clamping: 999 is garbage, and clamping it to 1.0 would manufacture a fake certainty. INV-008 — _normalize_market assumed every row was a dict; the loops checked the outer type only. A None or a scalar inside the array raised AttributeError past the AdapterError boundary, i.e. a 500 on a paid call. Both are closed by one defensive boundary, base.normalize_rows, now shared by all three adapters: non-list payloads yield nothing, non-dict rows are skipped, and a row that fails normalization or model validation costs exactly that row. Regression tests: out-of-range price rejected while a healthy market in the same response survives; model raises on out-of-range; None/scalar/nested rows skip without raising; wrong top-level shape yields no markets. Repro: docs/audit/repro/repro_hostile_input.py — 0 failures (was 5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Critical, audit finding INV-002. Ownership of watches, alerts, portfolios and
leaderboard entries was keyed on the x-client-id header, and 'checked' by
comparing a caller-supplied URI parameter against that caller-supplied header.
Both sides were attacker-controlled, so the check was vacuous: setting
x-client-id to a victim's id and reading portfolio://<victim> returned their
positions, sides, sizes and P&L. OAuth did not help — the header outranked the
authorization header in the old _client_id().
Replaced with a derived-identity model (new identity.py):
* identity comes only from something the caller cannot forge — an OAuth subject
(signature verified upstream) or the payer address of a VERIFIED x402 payment
(EIP-712 signature checked by the facilitator before settling), falling back
to an ephemeral connection hash marked proven=False;
* x-client-id survives only as a namespace hint INSIDE an already-proven
identity, sanitized, and is ignored entirely for ephemeral callers — letting
an unproven caller pick a sub-namespace is the defect itself;
* personal resources are addressed as portfolio://me and alerts://me. No URI
names another principal, so this class of IDOR is impossible by construction
rather than by check;
* REQUIRE_PROVEN_IDENTITY=on additionally refuses personal resources to
unauthenticated callers.
X402Middleware now publishes the verified payer into the request context, so
'whoever paid owns it' becomes the natural identity for the paid tier.
BREAKING: alerts://{client_id} and portfolio://{client_id} are replaced by
alerts://me and portfolio://me. estimate_execution(commit=true) returns the new
link. README updated.
Regression tests (tests/test_identity.py): a header alone cannot choose an
identity, a verified payer is proven, the hint cannot forge another principal,
ephemeral callers are isolated, proven identity can be required, and the full
original exploit reads nothing.
Repro: docs/audit/repro/repro_idor.py — the foreign URI no longer exists.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…state Wave 2 of the audit. Four findings, one theme: a guard that only held for part of the sequence it was protecting. INV-003 (High) — the metering write sat in a bare "finally", so a storage failure escaped it, cancelled a successful "return result" and replaced any real tool exception with the storage error. Under the paid gate the payment has already settled by then: the caller paid, the tool ran, and they got an error with no usage record. The write is now best effort and its failure is NOT silent — it increments metering_write_failures_total, visible on /metrics. INV-007 (Medium) — AnchorStore.maybe_anchor spanned read-decide-submit-save with an irreversible chain write in the middle, so concurrent /track-record requests could burn gas twice on one root. Serialized on a dedicated lock held across the whole decision, separate from the row lock. INV-009 (Medium) — create_watch called count_active(), which took and released the lock before the insert took it again; concurrent requests all passed the check and overshot the cap. Since every watch is evaluated on every fire, that is per-scan CPU the caller controls. Count and insert now share one lock. INV-010 (Medium) — an unrecognized METERING_DB_URL silently relocated usage, receipts AND the x402 replay guard to cwd/metering.db, i.e. the container's ephemeral filesystem rather than the mounted volume: a one-character typo lost revenue records and made consumed payments reusable after every redeploy, with nothing in the logs. It now raises with a message naming the expected form. The same parsing was duplicated inline in three places and the first pass only fixed two — ReceiptStore now uses the shared resolver. Regression tests: metering failure preserves a successful call and does not mask a real tool error; 20 concurrent watch creations respect the cap; 8 concurrent anchors submit once; unrecognized DSNs raise across all three stores. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Wave 3, closing the remaining audit findings. INV-006 (Low) — gross_edge and realizable_edge sat side by side in one response in DIFFERENT units: gross was an absolute per-unit difference, realizable a fraction of deployed capital. They agreed only when unit cost happened to be near 1, so an agent comparing them (the obvious thing to do with two fields named *_edge) was misled on any cheap basket. Gross is now a return on capital too, which makes the difference between the two exactly the execution drag. INV-011 (Low) — de-vigging divided by the ROUNDED total, pushing the "fair" probabilities off 1 by up to ~0.2% on small numbers and breaking the single property that defines de-vigging. Divides by the unrounded sum. INV-012 (Low) — fillable_size_usd rounded to the nearest cent, so it could be reported ABOVE the requested size. Rounds down. TEST-01 (High, test quality) — three surgical mutations of the money and access paths previously survived the whole suite. Added the missing tests: a basket is limited by its thinnest leg (units = min, not max), and a replayed payment never reaches the facilitator at all (the old test could not tell which of the two barriers had caught it). Re-running the mutation set now kills 6/6 applicable mutants; the other two no longer apply because the code they broke was removed by the INV-001 and INV-002 fixes. Property-based suite: 10/10 invariants hold (was 8/10). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
…xed)
Phases 0-2 of an adversarial audit plus the full remediation.
Three Critical: one signed payment settled twice under concurrency while the
books showed a single receipt (invisible to /revenue); any caller could read any
client's alerts and portfolio by setting one header; a single market title
containing a long number killed the paid flagship for everyone.
Three High: a metering write failure destroyed an already-paid successful call;
an out-of-range venue price was ingested and poisoned every downstream number;
malformed venue rows crashed past the AdapterError boundary. Plus INV-009/010/007
(cap and DSN and anchor races), INV-006/011/012 (units and rounding), and
TEST-01 — three surgical mutations of the money and access paths that the whole
suite failed to notice.
Includes a breaking change: alerts://{client_id} and portfolio://{client_id} are
now alerts://me and portfolio://me, resolved from a derived identity (OAuth
subject or verified x402 payer) rather than a caller-supplied header.
Audit trail, invariants, findings and executable repros in docs/audit/.
461 tests green; every repro verified against this HEAD.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two follow-ups on top of the initial
predmarket-mcpserver (the base commit onmain):CORE_ENGINEflag (mock stays the default).The MCP surface (7 tools, 1 resource, 1 prompt) is unchanged — this is engine + billing plumbing only.
1. x402 middleware fix (
c49cdc7)The payment middleware buffers the request body to read the tool name, then forwards to the streamable-HTTP handler. The replayed
receiveemitted synthetichttp.requestmessages once the buffer drained, so the handler — which keeps callingreceive()to detect client disconnect — never sawhttp.disconnectand hung on every forwarded request (paid-with-payment, and free tools in paid mode). Only the402paths (which don't forward) worked, so unit tests missed it.receivesohttp.disconnectflows.PAID_ENABLED=true:402without payment,402on underpayment,200+ data with a valid signedX-PAYMENT, free tools bypass; one receipt + one usage record (paid_via=x402) logged.2. Live venue adapters (
bc95622)core/algorithms.py— shared, venue-agnostic intelligence: realizable-edge math (walk depth, apply fees + gas + slippage), a computed cross-venue matcher (token-Jaccard placeholder), and bundle/cross-venue signal detectors. Mock now delegates its edge calc here, so the math is defined once, not duplicated.core/adapters/—base.py(single HTTP boundary: every httpx failure maps toAdapterError),polymarket.py(Gamma markets + CLOB book),kalshi.py(public markets + orderbook, cents→prob). Fetch + normalize only; no intelligence.core/live.py—LiveRepo+ engine over the adapters, TTL-cached, degrades gracefully (empty results) when a venue is unreachable rather than crashing a tool.deps.py—CORE_ENGINE=mock|liveswitch (mock default). Nothing else in the MCP layer changes.Testing
uv run pytest→ 29 passed (22 prior + 7 new adapter/live tests).httpx.MockTransportwith recorded sample payloads (no network).Notes / caveats
gamma-api.polymarket.com,clob.polymarket.com,api.elections.kalshi.com) are not reachable from this sandbox (network policy denies them), soCORE_ENGINE=livewas exercised against mocked HTTP, not real endpoints. Run it where egress to those hosts is allowed to hit live data.# TODO: embeddings); live history returns empty until a time-series store is ingested.🤖 Generated with Claude Code
https://claude.ai/code/session_0195BXP7CY4CMy57uUsNuDTj
Generated by Claude Code