A monetizable remote MCP server that sells prediction-market intelligence (Polymarket, Kalshi) as tools other AI agents call — and pay for — per call. Not another bot: rails. Normalized data, mispricing detection, and honest realizable edge (after fees/gas/slippage), packaged as tools an agent can lean on instead of building itself.
The server is a thin wrapper over a core/ engine (matcher, signals,
realizable-edge, storage). It returns intelligence only — it never executes
trades or holds funds.
Engine status. Two interchangeable engines share one surface, selected by
CORE_ENGINE(the MCP layer never changes either way):
mock(default) — realistic, same-signature stubs (core/mock.py) so the server works end-to-end offline.live— real Polymarket + Kalshi adapters (core/adapters/) feeding a live engine (core/live.py). Needs network access to the venue APIs; falls back gracefully (empty results) if a venue is unreachable.The shared intelligence (matcher, signals, realizable-edge) lives in
core/algorithms.pyand is used by both engines — not duplicated.
Descriptions are the agent's only documentation, so they're written as copy.
Every response carries freshness (as_of / data_age_seconds) and cost
(tier / price_usd).
| Tool | What it answers |
|---|---|
search_markets(query, category?, venue?) |
Discover markets by keyword. |
list_venues() |
Which venues exist, their status and coverage. |
track_record() |
Verifiable performance — how flagged opportunities actually did at resolution (hit-rate, edge slippage, Brier). The reason to trust the paid tools. |
evaluate_market(venue, market_id) |
Prices, implied prob, depth for one market. Data delayed ~60s on the free tier. |
| Tool | What it answers | Price/call |
|---|---|---|
find_mispricing(min_edge, kind?, category?) |
Flagship. Live cross_venue / bundle / dutch_book opportunities above a realizable edge threshold; each risk-adjusted (annualized edge, holding days). Signed. | $0.05 |
compare_across_venues(event) |
Same event across venues: spread, direction, match confidence. | $0.02 |
estimate_execution(legs, size_usd) |
Realizable edge at your size from current depth, with per-venue cost breakdown. | $0.01 |
get_market_history(venue, market_id, from_ts, to_ts) |
Historical price/spread series. | $0.01 |
watch(min_edge, kind?, category?, event?) |
Subscribe to opportunities instead of polling — register once, get alerts. | $0.02 |
poll_alerts() |
Retrieve opportunities that fired against your watches (delivered once). | $0.005 |
Prices live in pricing.yaml, never hardcoded.
- Resources:
market://{venue}/{market_id}(snapshot),house://{venue}/{market_id}(the server's own fused probability),maker://{venue}/{market_id}(market-maker quote advice),scenario://scan/{spec}(what-if stress test),leaderboard://top(public proof-of-alpha ranking),portfolio://me(your own paper P&L),alerts://me(your queued alerts), plusevent://,conditional://,distribution://,outcomes://— for agents that prefer resources over tools. - Identity: personal resources are addressed as
meand resolved from a derived identity — an OAuth subject, or the payer address of a verified x402 payment.x-client-idis only a namespace hint inside an already-proven identity; it can never select a principal. There is deliberately no URI that names another caller, so cross-tenant reads are impossible by construction rather than by check. SetREQUIRE_PROVEN_IDENTITY=onto refuse personal resources to unauthenticated callers entirely. - Prompt:
arbitrage_scan_workflow(min_edge)— guides an agent scan → confirm → estimate execution → rank.
uv sync # Python 3.12, deps
uv run pytest # 22 tests, all green
uv run python -m predmarket_mcp.server # streamable-http on http://0.0.0.0:8000/mcp
curl -s http://127.0.0.1:8000/health # {"status":"ok",...}
# live data from Polymarket + Kalshi (needs network egress to the venue APIs):
CORE_ENGINE=live uv run python -m predmarket_mcp.serverVerify with the official MCP Inspector (see tests/test_inspector.md):
npx @modelcontextprotocol/inspector # UI → Streamable HTTP → http://127.0.0.1:8000/mcpDirect HTTP agent (Cursor, LangGraph, any MCP client that speaks Streamable HTTP):
{
"mcpServers": {
"predmarket": { "url": "https://your-host/mcp", "transport": "streamable-http" }
}
}stdio-only hosts (Claude Desktop / Claude Code) — bridge to the remote server
with mcp-remote:
{
"mcpServers": {
"predmarket": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://your-host/mcp"]
}
}
}Two rails; x402 is primary, API-key/metering is the fallback. Both are inert until you flip the flag — the server can take money, but doesn't gate at launch (usage first, billing later).
PAID_ENABLED=false # default: paid tools run free, metering still records usage
PAID_ENABLED=true # enforce the gate on paid tools
PAYMENT_RAIL=x402 # or "apikey" for the OAuth/metering fallbackA paid tool call without a signed X-PAYMENT header gets a real HTTP 402
with an x402 challenge (scheme, network, amount, pay-to). Retry with a valid
base64-JSON X-PAYMENT header → the Facilitator verifies it, a receipt is
logged, and the call is forwarded. Settlement in USDC.
The default
MockFacilitatordoes structural verification and stubs settlement (# TODO: real facilitator/settlement). The 402 flow, gating, and receipt log are real.
Every paid call writes exactly one usage record via a pluggable
MeteringBackend. Default is local SQLite (zero infra); StripeBackend /
MoesifBackend are typed stubs behind the same interface. OAuth 2.1 for the
API-key rail is wired via FastMCP helpers (auth.py), enabled by env.
| Var | Default | Purpose |
|---|---|---|
CORE_ENGINE |
mock |
mock (offline stubs) or live (Polymarket/Kalshi adapters). Live mode needs outbound egress to the venue hosts (gamma-api.polymarket.com, clob.polymarket.com, api.elections.kalshi.com); if the network policy blocks them the live engine degrades gracefully to empty results and the per-venue circuit breaker opens (verified). |
PAID_ENABLED |
false |
Master gate switch. |
PAYMENT_RAIL |
x402 |
x402 or apikey. |
FREE_TIER_DELAY_SECONDS |
60 |
Free-tier data delay. |
METERING_BACKEND |
local |
local | stripe | moesif. |
METERING_DB_URL |
sqlite:///metering.db |
Usage/receipt store. |
HISTORY_DB_URL |
sqlite:///history.db |
Price-history store. A postgresql:// DSN engages the real asyncpg Timescale/Postgres path (degrades to SQLite if the driver/DB is unavailable). |
MATCHER |
lexical |
Cross-venue matcher tier: lexical (offline), semantic (embeddings), hybrid. |
MATCH_MIN_CONFIDENCE |
0.45 |
Match threshold — tune when using semantic/hybrid (cosine is on a different scale). |
EMBED_BACKEND / EMBED_MODEL |
fastembed / BAAI/bge-small-en-v1.5 |
Embedder for the semantic tier. EMBED_BACKEND=voyage uses a hosted embedder (no model in the image — serverless-friendly). |
VOYAGE_API_KEY |
— | Required for EMBED_BACKEND=voyage (Voyage AI — Claude has no embeddings endpoint). |
RECON_DB_URL |
sqlite:///reconciliation.db |
Track-record store (flagged → resolved → realized). |
WATCH_DB_URL |
sqlite:///watches.db |
Watch/alert subscription store. |
SIGNING_KEY |
— | Operator secret; when set, responses carry an HMAC-SHA256 provenance signature. |
SIGNING_KEY_ED25519 |
— | 32-byte Ed25519 seed (hex/base64). Preferred over HMAC — asymmetric signatures anyone can verify against the public key at GET /pubkey. |
X402_OPERATOR_WALLET |
— | Payee address for x402. |
X402_NETWORK |
base-sepolia |
Settlement network. |
X402_FACILITATOR_URL |
— | Set to enable real settlement: a Coinbase/self-hosted x402 facilitator (/verify + /settle). Unset → the structural MockFacilitator. |
KALSHI_API_KEY_ID / KALSHI_PRIVATE_KEY (or _PATH) |
— | Live Kalshi private endpoints (orderbook) — RSA-PSS request signing. |
AUTH_JWKS_URI / AUTH_ISSUER / AUTH_AUDIENCE |
— | OAuth 2.1 fallback. |
HOST / PORT |
0.0.0.0 / 8000 |
Bind address. |
| v2 — Provable Alpha Rails | (all off/degrading by default) | |
STREAMING |
off |
Real-time WSS ingestion of venue price ticks into the history store. Degrades to the TTL-fetch path if unset / no websockets / dropped connection. |
ALERT_ENGINE / ALERT_ENGINE_INTERVAL |
off / 30 |
Background thread that fires watches off the hot path; poll_alerts becomes drain-only. Off → firing happens inline (synchronous, serverless-safe). |
ALERT_WEBHOOK_URL / ALERT_WEBHOOK_SECRET |
— | Push fired alerts to a webhook (best-effort; alert stays queued for poll_alerts on failure). |
WATCH_MAX_PER_CLIENT |
50 |
Per-client active-watch cap. |
ANALYST_ENABLED / ANTHROPIC_API_KEY |
off / — |
LLM resolution-risk analyst (Claude claude-opus-4-8, structured tool output, adaptive thinking). Off/no-key → offline heuristic. ANALYST_MODEL, ANALYST_MAX_ENRICH (3) tune it. |
KELLY_FRACTION |
0.5 |
Fractional-Kelly multiplier for estimate_execution sizing (half-Kelly). |
FAIRVALUE_MIN_LIQUIDITY_USD |
1.0 |
Liquidity floor for the cross-venue consensus weighting. |
MANIFOLD_ENABLED |
off |
Add Manifold as a third market/consensus venue (live engine). |
RATE_LIMIT_RPM / RATE_LIMIT_BURST |
0 / =RPM |
Per-client token-bucket rate limit (429 on exceed). 0 = disabled. |
CIRCUIT_FAIL_MAX / CIRCUIT_RESET_TIMEOUT |
5 / 30 |
Per-venue circuit breaker: open after N failures, half-open probe after cooldown. |
HISTORY_RETENTION_DAYS |
90 |
Prune history older than this (throttled). 0 = keep everything. |
LOG_FORMAT / LOG_LEVEL |
text / INFO |
LOG_FORMAT=json for structured logs. |
| v3 — Provable Alpha (the moat) | (intelligence built on our own history/logic) | |
CALIBRATION_MIN_SAMPLES / CALIBRATION_BINS |
30 / 10 |
Online probability calibration from resolution history (identity until enough outcomes). |
ANALYST_ENABLED / ANTHROPIC_API_KEY |
off / — |
Also powers the resolution-basis LLM diff (settlement-rule comparison); heuristic otherwise. |
RESOLUTION_ENGINE / RESOLUTION_INTERVAL |
off / 3600 |
Autonomous resolver: settles pending flagged opportunities against venue resolution APIs so the track record / calibration / Merkle anchor grow with no operator (needs CORE_ENGINE=live). |
OPTIONS_ENABLED / DERIBIT_API_URL |
off / deribit.com |
Options-implied probability cross-check (Breeden–Litzenberger) in evaluate_market. |
MICROSTRUCTURE_WINDOW |
20 |
Lookback for the informed-flow / momentum read. |
ANCHOR_ENABLED / ANCHOR_RPC_URL / ANCHOR_NETWORK / ANCHOR_MIN_INTERVAL |
off / — / base-sepolia / 3600 |
On-chain anchoring of the track-record Merkle root (mock chain until an RPC is set). |
KELLY_FRACTION (reuse) |
0.5 |
Kelly can be fed the calibrated probability, not the raw price. |
Entailment & term-structure arbitrage (C1) and value-based pricing for
get_market_history (scales with range, capped) need no flags — always on.
| v4 — autonomy & product depth | | |
| PERSISTENCE_MIN_SAMPLES / EXEC_HORIZON_SECONDS | 20 / 60 | Edge-survival model: ranks find_mispricing by expected value (edge × P(survives the execution window)), learned from opportunity lifespans. |
| QUALITY_GATE / QUALITY_MIN | off / 0.5 | Quote-quality guard: skip scans on stale/thin/crossed quotes. Advisory (surfaced in evaluate_market) unless the gate is on. |
| ADMIN_TOKEN | — | Enables the operator-only GET /revenue (charged-vs-settled reconciliation); sent as X-Admin-Token. |
| ALERT_WEBHOOK_SECRET (reuse) | — | Now also HMAC-signs the webhook body (X-Signature: sha256=…). |
New surfaces: free event://{entity} resource (the whole market structure for a
subject: related markets, implies/same_event/term_neighbor relations, transitive
entailment violations, term structures); paid assess_portfolio tool (net
exposure, correlation, portfolio Kelly, hedges); simulate_strategy now includes
an out-of-sample walk-forward check.
Quality & provable evidence (J): every find_mispricing opportunity carries
an adverse block — a trap_score (informed flow against the position +
historical hit-rate of edges of that kind + quote quality) and a
trust_adjusted_edge. Pass audit=true for a signed audit bundle per
opportunity: inputs, prices, book depth and timestamp, Ed25519-signed and
fetchable at public GET /audit/{id} — a buyer independently verifies the edge
existed, without trusting our word (the deepest, least-copyable trust primitive).
Capital-velocity strategy (I): find_mispricing prices TIME, not just edge —
every opportunity carries velocity metrics (capital_efficiency = EV per locked
day, compound_annual_growth under recycling, early-exit liquidity from book
depth), rank="velocity" puts the fastest capital turnover first, and passing
bankroll_usd + horizon_days returns a rotation_plan: allocations across
short-cycle opportunities with a projected compounded bankroll as capital
recycles (VELOCITY_DEFAULT_DAYS, ROTATION_MAX_POSITIONS).
Implied distribution & dependency graph (H/J): free distribution://{entity}
resource reconstructs the market's whole implied distribution from a threshold
ladder (survival curve, percentiles, implied mean, prob at any strike — the
prediction-market vol surface) plus a continuous lognormal fit (J2:
P(X in [a,b]) for any range, moments, quantiles); free conditional://{event}
resource returns market-implied P(A|B) (Gaussian copula from co-movement + exact
logical overrides) plus Bayesian belief updates (J3: multi-hop P(A | B,C…) by
naive-Bayes log-odds). compare_across_venues adds a calibration-weighted
meta_consensus (J4): a best-estimate + credible interval that weights each venue
by how accurate it has historically been (Brier), learned as markets resolve —
not just liquidity. The cross-venue matcher learns from
resolution ground truth — compare_across_venues reports a history-calibrated
match confidence, tuned by which past matches actually resolved the same way.
House probability, scenarios & client-sampled rulebooks (K): the server
publishes its OWN forecast, not just the market's. evaluate_market and the free
house://{venue}/{market_id} resource return a house_probability (K1) that
fuses calibration, options-implied, accuracy-weighted consensus and microstructure
into one number with a confidence and its edge_vs_market; every forecast is
recorded and, when the market resolves, Brier-scored — track_record's
house_forecast block reports brier_edge (>0 means the house beats the raw
market), a claim no competitor can fake. The free scenario://scan/{spec} resource
(K2, e.g. scenario://scan/pm-btc-150k-2026:yes) pins markets to a hypothetical
outcome and propagates the consequences across the entity graph — prior → posterior
→ shift per related market (logical edges exact, copula edges estimated); pass
scenario= to assess_portfolio to stress a book under the what-if. And with
RULEBOOK_SAMPLING=on, compare_across_venues uses MCP sampling (K3) to ask
the calling agent's own model to judge settlement basis risk — the deepest
rulebook analysis, run at the client's expense; degrades to the heuristic when the
client can't sample.
Agent leaderboard — proof-of-alpha as a service (K4): an agent doesn't just
find edge here, it can prove it. Call estimate_execution(commit=true) and the
position is logged as a paper trade tagged to you; when the markets resolve it's
P&L-scored by the same model as the track record. The free, server-signed
leaderboard://top resource ranks agents by REAL, resolution-verified return
(handles anonymized; a single lucky trade stays provisional until
LEADERBOARD_MIN_RESOLVED), and the caller-scoped portfolio://me
resource shows your own trades, rank and realized P&L. The server becomes an
independent notary of an agent's performance — a portable, tamper-evident record
to show a third party, and the network effect the product was missing.
Market-maker advisor (K5): the other side of the book. Every other tool serves
a taker (lift someone's mispricing); the free maker://{venue}/{market_id} resource
serves a maker — where to post a two-sided limit quote to earn the spread
(recommended_bid/ask, half_spread, skew) and a pull_quotes guard that says
step aside when the flow looks informed. It prices adverse-selection risk from
microstructure (C8) + quote quality (F7), widening and skewing quotes against
informed flow. Intelligence only — it never posts orders.
Reality feeds — the market vs the world (K7): every other signal is
intramarket; K7 adds an EXTERNAL anchor. With REALITY_ENABLED=on and a
REALITY_NOWCAST_URL feed (a poll aggregator, a macro nowcast, an internal model),
evaluate_market reports a reality block — the market's probability vs the
real-world nowcast and their divergence ("market says 60%, the data says 45%") —
and the reality-implied probability feeds the house forecast (K1) as an
independent, heavily-weighted component. The divergence math is pure/offline; the
feed is a flag-gated provider (like the Deribit options cross-check) that degrades
to nothing when unconfigured.
Native multi-outcome markets (G): free outcomes://{event} resource
reconstructs an N-outcome event (election, bracketed number) from its binary
markets — de-vigged fair probabilities (margin removed so they sum to 1), the
overround, the favorite, and a completeness-aware full dutch book (an
over-round set is always arbitrage; an under-round is only free money if the
outcome set is complete — flagging otherwise is a false arb the naive path emits).
Adapters populate event_group from venue grouping (Polymarket negRisk, Kalshi
event ticker).
| Horizontal scaling | | (single-instance defaults need nothing) |
| REDIS_URL | — | Shared backend for the x402 replay guard (atomic SET NX) and rate limiter (global fixed-window). Required for correctness behind a load balancer — without it each instance has its own replay set, so one signed payment could buy a call per instance. Degrades to local SQLite/memory if unset or redis isn't installed. |
| NONCE_TTL_SECONDS | 2592000 | TTL for consumed-payment fingerprints in Redis (30d). |
| PG_POOL_MIN / PG_POOL_MAX | 1 / 10 | asyncpg pool bounds for the Postgres history path. |
Multi-instance deploy: point every instance at the same REDIS_URL (replay +
rate limits) and a postgresql:// HISTORY_DB_URL (shared time series). The
async asyncpg path runs on a dedicated loop thread so the sync stores are
unchanged. Install the extras: --extra redis --extra postgres.
Every automated test runs against mocks — the venue adapters normalize a
response shape assumed from docs. Real APIs differ, and that gap is the single
biggest launch risk. deploy/preflight.py is the check the test suite
structurally cannot do: it calls the real venues, pushes what comes back through
the actual adapters and the whole pipeline (normalization → matching → claim
parsing → scan → execution estimate), and names the link that breaks.
docker compose exec server python deploy/preflight.py # all venues
docker compose exec server python deploy/preflight.py kalshi # one venue
docker compose exec server python deploy/preflight.py --json # for monitoringRead-only (writes to no store), exits non-zero when the live path is broken, and
also audits production config — unsigned provenance, PAID_ENABLED=true with no
real facilitator (accepting payments that never settle), a missing
RESOLUTION_ENGINE (nothing ever resolves, so calibration / house Brier /
leaderboard stay empty forever). Run it before serving traffic.
docker build -t predmarket-mcp .
docker run -p 8000:8000 -e PAID_ENABLED=false predmarket-mcp
# with optional features baked in (extras + pre-downloaded embedding weights):
docker build -t predmarket-mcp \
--build-arg EXTRAS="--extra semantic --extra kalshi --extra postgres" \
--build-arg WITH_SEMANTIC=true .Runs on Cloud Run / Container Apps / any container host. Streamable HTTP is
serverless-compatible. Terminate TLS and rate-limit at the proxy; use /health
for liveness. The container starts via python -m predmarket_mcp.server so the
x402 ASGI middleware is wired in (equivalent to fastmcp run + payment gating).
WITH_SEMANTIC=true pre-fetches the bge-small weights into the image so the
semantic matcher runs with no Hugging Face egress at runtime.
src/predmarket_mcp/
server.py FastMCP app; /health, /metrics, /pubkey, /track-record; HTTP app + middleware
tools.py the 11 tools (call core/, format for agents — no logic here)
resources.py market:// · house:// · maker:// · scenario:// · leaderboard:// · portfolio://me · alerts://me · event:// · outcomes:// · distribution:// · conditional:// resources
identity.py caller identity DERIVED from OAuth / verified x402 payer, never from a header
multioutcome.py native N-outcome markets: de-vig + complete-set dutch book (G)
distribution.py implied distribution from a threshold ladder — vol surface (H1)
conditional.py market-implied P(A|B): Gaussian copula + logical overrides (H2)
matchlearn.py matcher calibrated from resolution ground truth (H5)
prompts.py arbitrage_scan_workflow
config.py env-driven settings (PAID_ENABLED flag)
deps.py the ONLY seam into core/
provenance.py signed provenance (Ed25519 preferred, HMAC fallback)
ops.py metrics registry, token-bucket rate limit, structured logging
auth.py OAuth 2.1 fallback wiring
billing/ tiers.py · metering.py · x402.py · middleware.py
core/
models.py canonical pydantic models
algorithms.py shared matcher / signals / realizable-edge + risk finalize (mock + live reuse)
entailment.py logical-implication + probability term-structure arbitrage (C1)
basis.py resolution-criteria diff / settlement basis risk (C2)
calibration.py isotonic online probability calibration from history (C3)
backtest.py mean-reversion backtester over recorded history (C4)
options.py Breeden-Litzenberger options-implied probability, Deribit (C7)
microstructure.py informed-flow / momentum read from the tape (C8)
anchor.py on-chain anchoring of the track-record Merkle root (C6)
fairvalue.py liquidity-weighted cross-venue consensus + deviation (B3)
sizing.py fractional-Kelly stake + market-impact curve (B4)
analyst.py LLM resolution-risk + basis analyst + offline heuristic (B5/C2)
merkle.py Merkle commitments/proofs for the track record (B6)
circuit.py per-venue circuit breaker (B7)
streaming.py WSS price ingestion (B1) · notifier.py webhook alert push (B2)
embeddings.py Embedder backends for the semantic matcher tier (fastembed default)
mock.py realistic offline engine (default)
live.py live engine: adapters + algorithms, TTL-cached, history ingest
storage.py price-history store (SQLite default; asyncpg Timescale/PG via DSN)
pg.py asyncpg loop-thread bridge for the sync stores (D2)
reconciliation.py flag → resolve → realized-edge / hit-rate / Brier + Merkle root
houseview.py fuse calibration+options+consensus+microstructure -> house probability (K1)
houseforecast.py record + Brier-grade the server's own forecasts vs the market (K1)
scenario.py what-if propagation across the market graph + portfolio stress (K2)
rulesample.py client-sampled (MCP sampling) settlement-basis analysis (K3)
leaderboard.py agent proof-of-alpha: paper trades, P&L, public signed ranking (K4)
maker.py market-maker quote advisor + adverse-selection guard (K5)
reality.py external nowcast anchor + market-vs-world divergence (K7)
watches.py watch/alert store + background AlertEngine (push computed, pull drained)
adapters/ base.py · polymarket.py · kalshi.py · manifold.py (fetch + normalize only)
tests/ 40+ offline tests: tools · billing · adapters · storage · matcher · hardening · streaming · push · fairvalue · sizing · analyst · trust · ops · manifold
- ≤ 15 tools (7 here) — agent tool-selection degrades past ~25–30.
- Tools are shaped around agent questions, not 1:1 API endpoints.
- Realizable edge, never gross. Every response marks data staleness.
- No custody, no auto-execution — intelligence only.
core/logic is not duplicated — tools call the engine.- Secrets via env only.
The spec referenced "FastMCP 3.x"; this builds on the current
fastmcp 3.x (decorator API, Streamable
HTTP, OAuth helpers). SSE is intentionally unused (deprecated).