MCP server for Kalshi prediction markets: native RSA-PSS auth, rate limiting, demo/prod safety.
π¦ PyPI Β Β·Β ποΈ MCP Registry Β Β·Β π³ Container image Β Β·Β π Deploy guide
A Model Context Protocol server for Kalshi prediction markets. Native RSA-PSS auth, async token-bucket rate limiting, two-step prepare/confirm order flow with safety caps, optional bundled OAuth proxy for remote-MCP deployments, 26 tools + 4 resources across REST and WebSocket. MIT, designed to be forked.
Works with any MCP client β locally via stdio (Claude Desktop, Claude Code, Cursor, Zed, Continue, Cline, Goose, etc.) or remotely as a self-hosted HTTP server (claude.ai custom connectors today, any OAuth-capable MCP client in the future).
β οΈ This software lets an LLM place trades. Read DISCLAIMER.md before deploying. Trading prediction markets involves substantial risk of loss. AI agents make mistakes β sometimes confidently. The authors are not liable for any losses. Test in demo (
KALSHI_ENV=demo,KALSHI_TRADING_ENABLED=0) until you understand the failure modes.
Status β alpha. Auth (REST + WS), rate limiting, safety controls, 26 tools across REST + live channels, and 4 resources are in place. A long-lived multiplexed WebSocket session and
kalshi://markets/{ticker}/orderbooklive resource are planned for v0.2.
Read-only against Kalshi's demo environment β no real money, no trading flag. This is the safe way to try it.
pipx install kalshi-mcp-server # or: pip install kalshi-mcp-server
Point any MCP client at it (this is the Claude Desktop / Claude Code shape β see the full client matrix for others):
{
"mcpServers": {
"kalshi": {
"command": "kalshi-mcp",
"args": ["--env-file", "/Users/you/.kalshi/.env"]
}
}
}
Minimal ~/.kalshi/.env (get a demo key at
demo.kalshi.co β it's shown once):
KALSHI_API_KEY_ID=<your-demo-key-id>
KALSHI_PRIVATE_KEY_PATH=/absolute/path/to/demo_private_key.pem
KALSHI_ENV=demo
Restart the client and ask it to run a Kalshi tool. Enabling prod and
trading is a deliberate opt-in β a few more flags (KALSHI_ALLOW_PROD=1,
KALSHI_TRADING_ENABLED=1) β see Configure and the
safety model.
Ask the agent for tradeable markets and kalshi_find_liquid_markets returns a
volume-ranked, combo-excluded shortlist (trimmed, illustrative):
{
"scanned": 300,
"markets": [
{
"ticker": "KXNBAGAME-25JUL12BOSLAL-BOS",
"title": "Will the Celtics beat the Lakers?",
"yes_bid_dollars": 0.58, "yes_ask_dollars": 0.60,
"volume_24h_fp": 41230, "open_interest_fp": 88400,
"status": "active", "close_time": "2026-07-12T23:30:00Z"
},
{
"ticker": "KXHIGHNY-26JUL12-B90.5",
"title": "Will NYC's high temp exceed 90.5Β°F today?",
"yes_bid_dollars": 0.31, "yes_ask_dollars": 0.34,
"volume_24h_fp": 12760, "open_interest_fp": 23110,
"status": "active", "close_time": "2026-07-13T04:00:00Z"
}
]
}
Placing a trade is a deliberate two step β kalshi_prepare_order runs the
local safety checks and hands back a confirmation_id; nothing reaches Kalshi
until you call kalshi_confirm_order with that token. An LLM can't place an
order in a single call.
Most existing Kalshi MCPs are thin wrappers around a handful of REST endpoints. This one aims to be:
main without ever triggering a production deploy β only
tagged releases (v*) do. Your fork's deployment stays decoupled from
this repo's, and your fork's contributors can't affect what you run.Published as kalshi-mcp-server.
pipx installs the kalshi-mcp entrypoint into its own
isolated environment:
pipx install kalshi-mcp-server # or: pip install kalshi-mcp-server
git clone https://github.com/cejor6/kalshi-mcp-server.git
cd kalshi-mcp-server
uv sync
Multi-arch (amd64 + arm64) images are published to GHCR on every tagged
release, tagged :latest and :vX.Y.Z:
docker pull ghcr.io/cejor6/kalshi-mcp-server:latest
See DEPLOY.md for hosted deployment.
Generate a Kalshi API key at https://kalshi.com/account/profile (or the demo equivalent at https://demo.kalshi.co/account/profile). Save the private key β it is shown ONCE.
Put your secrets in one .env file. A good location for the
MCP-client use case is ~/.kalshi/.env (outside any repo). For local
dev, the repo's own .env (gitignored) works too.
cp .env.example ~/.kalshi/.env
# edit ~/.kalshi/.env
KALSHI_API_KEY_ID=<your-key-id>
KALSHI_PRIVATE_KEY_PATH=/absolute/path/to/your_kalshi_private_key.pem
KALSHI_ENV=demo
For prod, also set:
KALSHI_ENV=prod
KALSHI_ALLOW_PROD=1
KALSHI_TRADING_ENABLED=1 # only if you want writes
On startup, the server resolves config in this order (highest wins):
env: block, or exported in your shell..env file β loaded from --env-file PATH if you pass that flag,
otherwise from ./.env in the current working directory if it exists.
Variables already in the environment from step 1 are not overridden.So you can put secrets either inline in the MCP config (env:) or in a
file the config points at (--env-file). You don't need to do both.
Every MCP stdio client uses the same shape: a command to launch the
server, optional args, optional env. The differences are just the
file/UI where you put the config.
Three install patterns work β pick whichever fits your environment.
pipx install (cleanest, recommended)Installs kalshi-mcp to a globally-available, isolated environment.
pipx is the modern Python tool for this:
pipx install kalshi-mcp-server
MCP client config then collapses to:
{
"mcpServers": {
"kalshi": {
"command": "kalshi-mcp",
"args": ["--env-file", "/Users/you/.kalshi/.env"]
}
}
}
Update with pipx upgrade kalshi-mcp-server when you want the latest.
uv run against a local cloneBest if you've cloned the repo and have uv
installed. Point the MCP client at uv with --directory:
{
"mcpServers": {
"kalshi": {
"command": "uv",
"args": [
"run",
"--directory", "/absolute/path/to/kalshi-mcp-server",
"kalshi-mcp",
"--env-file", "/Users/you/.kalshi/.env"
]
}
}
}
uv run activates the project's venv automatically. Update with
git pull + restart the MCP client. Useful for development /
hacking on the server itself.
Best for users without Python installed, or who prefer container isolation:
{
"mcpServers": {
"kalshi": {
"command": "docker",
"args": [
"run", "--rm", "-i",
"-v", "/Users/you/.kalshi/demo.pem:/secrets/demo.pem:ro",
"-e", "KALSHI_API_KEY_ID=<your-key-id>",
"-e", "KALSHI_PRIVATE_KEY_PATH=/secrets/demo.pem",
"-e", "KALSHI_ENV=demo",
"ghcr.io/cejor6/kalshi-mcp-server:latest"
]
}
}
}
The -v mount bind-mounts your PEM file read-only into the
container; KALSHI_PRIVATE_KEY_PATH points at that path. Secrets
live in the JSON config β fine for a single-user machine.
| Client | Config location |
|---|---|
| Claude Desktop | claude_desktop_config.json (Settings β Developer) |
| Claude Code | project .mcp.json or ~/.claude/mcp.json |
| Cursor | Settings β MCP β Add new MCP Server (UI fills the same JSON) |
| Zed | ~/.config/zed/settings.json under context_servers |
| Continue | ~/.continue/config.json under experimental.modelContextProtocolServers |
| Cline | Cline settings β MCP Servers β Edit JSON |
| Goose | ~/.config/goose/config.yaml under extensions |
If you'd rather inline secrets in the MCP config (acceptable for local dev where the config file is on your own machine):
{
"mcpServers": {
"kalshi": {
"command": "kalshi-mcp",
"env": {
"KALSHI_API_KEY_ID": "your-key-id",
"KALSHI_PRIVATE_KEY_PATH": "/path/to/your/private_key.pem",
"KALSHI_ENV": "demo"
}
}
}
}
Why not just
.envin the project dir? MCP clients spawn the server as a subprocess from their own working directory (typically your home dir on macOS/Linux, the client's install dir on Windows), so a.envsitting in this repo wouldn't get found. Hence--env-fileto point at it explicitly. Running the server directly from the project dir (no client) still works without flags β the CLI auto-loads./.envwhen launched there.
For clients that don't speak local stdio β currently the main one being claude.ai's custom connector form, which only supports OAuth-protected HTTP β host the server somewhere reachable and point the client at it. The OAuth proxy is bundled with the server; you just need to configure it.
See DEPLOY.md for an end-to-end walkthrough using Render + GitHub OAuth + Upstash Redis. Other image-deploy hosts (Fly.io, Cloud Run, ECS, Railway) work the same way β Render is just the worked example.
| Group | Tools |
|---|---|
| Exchange / account | kalshi_get_exchange_status, kalshi_get_exchange_schedule, kalshi_get_api_limits, kalshi_get_environment, kalshi_set_safety_limits |
| Discovery | kalshi_get_markets, kalshi_find_liquid_markets, kalshi_get_market, kalshi_get_event, kalshi_get_events, kalshi_get_series, kalshi_get_series_list, kalshi_get_series_summary, kalshi_get_milestones, kalshi_get_trades |
| Market data | kalshi_get_orderbook, kalshi_get_orderbooks, kalshi_get_market_candlesticks, kalshi_get_event_candlesticks, kalshi_get_batch_candlesticks, kalshi_get_event_forecast_history, kalshi_get_market_trades |
| Combos / parlays | kalshi_get_combo_collections, kalshi_get_combo_collection, kalshi_get_combo_events, kalshi_get_combo_legs, kalshi_create_combo_market (write β off by default, see below) |
| Portfolio | kalshi_get_balance, kalshi_get_positions, kalshi_get_orders, kalshi_get_fills, kalshi_get_settlements |
| Orders (write) | kalshi_prepare_order, kalshi_confirm_order, kalshi_cancel_order, kalshi_decrease_order, kalshi_get_order |
| Live (WebSocket) | kalshi_get_live_orderbook, kalshi_sample_trades |
| External data (read-only) | kalshi_fetch_external_data β host-allowlisted, GET-only, https-only fetch of public data feeds (Polymarket gamma/clob, NWS api.weather.gov, Open-Meteo incl. ensemble, Tennis Abstract, Deribit public). No credentials attached (trust_env=False), redirects not followed, body size- and wall-clock-capped and returned wrapped in UNTRUSTED-EXTERNAL-DATA delimiters. Exists so clients whose own egress is restricted (e.g. claude.ai cloud routines) can reach the public feeds their read-only research needs; the allowlist is enforced at runtime, additions are a code change, and the boundary rationale lives in AGENTS.md. |
Write tools require KALSHI_TRADING_ENABLED=1. kalshi_prepare_order runs
local safety checks and returns a confirmation_id; nothing is sent to
Kalshi until you call kalshi_confirm_order with that token. Cancel and
decrease bypass the trading-enabled flag β they only reduce exposure.
Listing markets for an LLM: kalshi_get_markets / kalshi_get_market
accept minimal=true to project each market down to a small whitelist of
triage fields (ticker, prices, sizes, volume, status, close time). Prefer
this over compact=true for scanning β compact is a blacklist and barely
shrinks multivariate (KXMVEβ¦) combo markets, whose bulk lives in
custom_strike / mve_selected_legs / long sub-titles. Pass a custom
fields="ticker,yes_bid_dollars,β¦" to override the default whitelist.
View precedence is fields > minimal > compact > full. kalshi_get_event
/ kalshi_get_events accept the same minimal / fields for their nested
markets (the event objects themselves only have the compact view).
Don't gate on liquidity_dollars: Kalshi currently returns it as
0.0000 on every market, even deep books β measure liquidity from the
orderbook (best bid/ask + resting size) plus volume_24h_fp /
open_interest_fp. It is stripped from compact and minimal views.
Finding tradeable markets: the default open listing is dominated by
multivariate (KXMVEβ¦) combo markets with empty/one-sided books. Pass
mve_filter="exclude" to kalshi_get_markets to drop them server-side, or
use kalshi_find_liquid_markets β it excludes combos, ranks by 24h volume,
and returns a short minimal-projection shortlist. (Kalshi has no server-side
sort, so the helper's ranking is over a bounded scan window, reported as
scanned in the result.) Pass scan_all=true to sweep the FULL open listing
before ranking β that turns the shortlist into a genuine exchange-wide top-N
rather than the top of an arbitrary slice. The sweep is bounded by internal
request / wall-clock / market caps, and the result reports complete plus
stopped_by so a partial scan is never mistaken for an exhaustive one.
Scanning wide without burning context: three tools exist for scan/anomaly workloads that would otherwise cost one call per market.
kalshi_get_orderbooks(tickers=[β¦], depth=5) fetches up to 25 books in
one request, with per-ticker error isolation β a bad or event-level
ticker becomes an error entry for that ticker instead of failing the
batch. Kalshi's batch endpoint has no depth parameter, so depth is
applied server-side by this MCP; it keeps the best levels (Kalshi
returns levels ascending by price and both sides are bids).kalshi_get_series_summary() rolls the whole listing up to one row per
series (market count, event count, 24h volume, tightest spread, soonest
close). Cheap in context, not free in reads β it's the daily "new supply
census" that spots a new event class listing without paging thousands of
markets. Note series_ticker is derived from the ticker prefix: Kalshi
does not return it on market objects.kalshi_get_batch_candlesticks(market_tickers=[β¦]) fetches OHLC bars for
up to 100 markets in one request β a momentum lens over a whole shortlist.
Its budget shape differs from the single-market tool: Kalshi returns at
most 10,000 candles total across all tickers, so window cost multiplies
by ticker count. Validated locally, with a message naming how many markets
the requested window actually affords.Combos / parlays. The multivariate surface lives in its own module:
kalshi_get_combo_collections / kalshi_get_combo_collection β the parlay
families on offer and the rules each imposes (size_min / size_max
legs, is_all_yes, the eligible-event universe).kalshi_get_combo_events β combos already listed against a collection, i.e.
live parlay supply. Defaults minimal=True for nested markets, because
combo markets are the largest objects Kalshi returns.kalshi_get_combo_legs(ticker) resolves a KXMVE⦠combo into its
underlying legs (market ticker, side, title) from mve_selected_legs β
strictly better than splitting the combo's title string on commas, which
drops the leg tickers and mis-splits on titles that contain a comma. If
Kalshi published no leg breakdown, it returns a structured
resolvable: false rather than guessing. The legs it returns feed
straight back into the create tool, so you can round-trip an existing combo
and vary one leg.kalshi_create_combo_market β write. Materializes a combo market
ticker for a chosen leg set. It places no order and commits no money, so it
has its own gate rather than riding on KALSHI_TRADING_ENABLED: set
MCP_ALLOW_COMBO_CREATION=1 to register it at all (default off, and when
off the tool isn't advertised to the model). Kalshi allows 5000 creations
per week per account, so the server also enforces
MCP_MAX_COMBO_CREATIONS_PER_DAY (default 100, in-process, resets at UTC
midnight and on restart) to stop a retry loop burning the weekly quota.
Before the POST it pre-flights your leg set against the collection's own
size_min/size_max/is_all_yes rules, which Kalshi otherwise rejects
with an opaque 400; that check fails open if the collection can't be read.Two env vars come with the combo write surface β add them to your .env:
MCP_ALLOW_COMBO_CREATION=0 # 1 registers kalshi_create_combo_market
MCP_MAX_COMBO_CREATIONS_PER_DAY=100
Event ticker vs market ticker: a market ticker carries an outcome
suffix (β¦PITHOU-HOU); an event ticker (β¦PITHOU) does not. Passing an
event ticker to kalshi_get_market / kalshi_get_orderbook / kalshi_get_markets
used to fail silently (404, or an empty book/list read as "no liquidity").
These tools now detect that case and raise an actionable hint naming the
real market tickers instead.
| URI | Description |
|---|---|
kalshi://environment | Current env, safety limits in force + their env ceilings, rate-limit headroom (no API call) |
kalshi://balance | Cash + buying power |
kalshi://positions | Open positions (unsettled) |
kalshi://orders | Resting orders (open / partially filled) |
A WebSocket-backed live-orderbook resource (kalshi://markets/{ticker}/orderbook)
is planned β for now, use the kalshi_get_live_orderbook tool which
opens a transient WS, samples the book, and returns the current
snapshot + delta arrival rate.
This server is deliberately conservative for the same reason your bank's ATM is β small mistakes shouldn't cost large amounts.
KALSHI_ENV=prod requires KALSHI_ALLOW_PROD=1. The server
refuses to start without both.KALSHI_TRADING_ENABLED=1. The default is
read-only.MCP_MAX_ORDER_SIZE_USD, MCP_DAILY_LIMIT_USD,
MCP_MAX_CONTRACTS_PER_ORDER, MCP_CASH_RESERVE_USD) are checked
before the request reaches Kalshi.kalshi_set_safety_limits tool can tighten any limit
on a running server (e.g. a fast clamp-down) but can never loosen one
past its env ceiling β the three caps only go down, the cash reserve
only goes up. Raising a ceiling still requires changing the env var and
redeploying. The limits in force vs. their ceilings show up in
kalshi_get_environment and kalshi://environment. Set MCP_REDIS_URL
to make runtime changes survive a restart (otherwise they reset to the
env ceilings on reboot).See AGENTS.md for the full design.
Use it locally as a stdio server with any MCP client, or run it as a remote HTTP MCP behind an OAuth proxy.
For remote deployment, the recommended setup is image-deploy: a
production host (Render, Fly.io, Cloud Run, ECS, anything that supports
pulling container images) pulls the image that's built and pushed when
you tag a release (git tag v0.1.0). This decouples deployments from
PR merges β PRs to main only ever run tests, never push a new image β
so a malicious or careless PR cannot affect what's running in your
container.
See DEPLOY.md for the rationale and a worked example with Render.
PRs welcome. Read CONTRIBUTING.md first β there are a few rules around auth changes, secret hygiene, and test conventions.
MIT. See also DISCLAIMER.md β the MIT license disclaims warranty; DISCLAIMER.md spells out the trading- and AI-specific risks you're accepting by using this software.
Source-derived launch command. Check the maintainerβs required arguments and credentials before running:
uvx kalshi-mcp-serverMerge this template into ~/Library/Application Support/Claude/claude_desktop_config.json. Keep existing servers. Add any arguments, credentials, and permissions required by the maintainer; this template has not been install-tested.
{
"mcpServers": {
"io-github-cejor6-kalshi-mcp-server": {
"command": "uvx",
"args": [
"kalshi-mcp-server"
]
}
}
}Restart Claude Desktop completely for changes to take effect. Confirm the server appears connected in the clientβs tool list, then try a read-only example from its documentation.
Claude Desktop setup referencekalshi-mcp-serverpypiKalshi Prediction Markets works with any MCP-compatible client. Copy the config snippet from the Configuration section above and add it to the file shown for your client, then restart the application.
~/Library/Application Support/Claude/claude_desktop_config.jsonRestart Claude Desktop completely for changes to take effect.~/.cursor/mcp.jsonRestart Cursor for changes to take effect..vscode/mcp.jsonReload VS Code window for changes to take effect.~/.codeium/windsurf/mcp_config.jsonRestart Windsurf for changes to take effect..mcp.jsonSave at the project root, then start Claude Code in that project and review the MCP server approval prompt. Keep real credentials out of shared files.