Skip to main content
Glama
akashnaren

kalshi-readonly

by akashnaren

kalshi-readonly

Kalshi Trade API v2 MCP over stdio (kalshi-mcp-v1-readonly-fleet). Read tools cover exchange status, markets, cash, positions, and a ranked list of small-stake ideas. Trade tools place, cancel, amend, or decrease one order. They are gated behind KALSHI_SAFE_MODE. They are not absent from v1.

Fleet default is KALSHI_SAFE_MODE=1. tools/list then omits place_order, cancel_order, amend_order, and decrease_order, and those handlers still refuse the call. The Finance Engineer harness (harness/SKILL.md) is the only policy that sets KALSHI_SAFE_MODE=0, and only on the Finance Engineer host. There is no separate Kalshi role harness. There is no deposit tool, no withdraw tool, and no auto-trading tool. find_best_bets never places an order.

Run

Python 3.11+ and cryptography==41.0.7 (see requirements.txt).

pip install -r requirements.txt
python3 server.py

server.py speaks MCP on stdin/stdout and waits for a host. Do not type at it.

pip install -r requirements-dev.txt
pytest

Related MCP server: Kalshi MCP Server

Environment

Copy .env.example. Portfolio and order calls need a key id and a private key. Public market calls do not.

Variable

Required

Purpose

KALSHI_API_KEY_ID

portfolio and trade calls

API key id. Or set KALSHI_API_KEY_ID_PATH

KALSHI_API_KEY_ID_PATH

alternative to the key id

File containing the key id. If both id env vars are unset, ~/.secrets/kalshi/key_id is used when that file exists

KALSHI_PRIVATE_KEY_PATH

one of these

PEM file path. If PEM env vars are unset, ~/.secrets/kalshi/private.pem is used when that file exists

KALSHI_PRIVATE_KEY_PEM

one of these

PEM contents

KALSHI_API_BASE

no

Default https://api.elections.kalshi.com/trade-api/v2

KALSHI_SAFE_MODE

no

Defaults to on. See Trade.

Missing auth fails before any portfolio or order request. The private key is used only to sign. It is not logged and not returned. If a response or error would contain key material, that text is dropped.

Signing matches the cash CLI for GET, POST, and DELETE: timestamp_ms + METHOD + path (path includes /trade-api/v2, query string excluded, body excluded), RSA-PSS with SHA-256 and a digest-length salt, base64 signature. Headers are KALSHI-ACCESS-KEY, KALSHI-ACCESS-TIMESTAMP, and KALSHI-ACCESS-SIGNATURE.

Prove

No-network fleet check. The printed names must include exchange_status, cash_or_positions, find_best_bets, append_decision, summarize_decisions, and fe_routine, and must omit place_order, cancel_order, amend_order, and decrease_order.

KALSHI_SAFE_MODE=1 python3 -c 'from kalshi_readonly.tools import registered_tools; print([t["name"] for t in registered_tools()])'
printf '%s\n' '{"id":1,"op":"list"}' | KALSHI_SAFE_MODE=1 python3 -m kalshi_readonly.dispatch

On the box, after the host is up: call exchange_status, then cash_or_positions with include=both. The same cash read from the CLI, which does not place:

export KALSHI_API_KEY_ID="$(tr -d '[:space:]' < /path/to/key_id)"
export KALSHI_PRIVATE_KEY_PATH=/path/to/private.pem
python3 -c 'import json; from kalshi_readonly.tools import cash_or_positions; print(json.dumps(cash_or_positions({"include":"both","limit":5}), indent=2))'

~/.secrets/kalshi/key_id and private.pem are enough when those env vars are unset. A host that says Not connected needs npm run build and a restart so node dist/index.js is the live process. Then tools/list is the check.

exchange_status includes build_sha. That value is the first 12 characters of <install root>/DEPLOYED_SHA, read once when the process starts. A missing or invalid file reports unknown.

Refresh

The box host tree is a git archive with no .git. Refresh it from a git checkout of this repo. The script writes DEPLOYED_SHA, then runs npm ci, npm run build, and python3 -m pytest -q. It does not restart the host and it does not call Kalshi. Restart the host after a successful run so the new process reads the stamp.

After scripts/refresh-host.sh, the stdio MCP has to be fully respawned: stop the old node and python processes, then reconnect. The python child's cwd otherwise follows the renamed .previous tree.

scripts/refresh-host.sh --help
scripts/refresh-host.sh --dry-run --dest /home/box/mcp/kalshi-mcp-main 1ce6d72
scripts/refresh-host.sh --dest /home/box/mcp/kalshi-mcp-main 1ce6d72

A second run of the same commit prints already installed and does not rebuild. --dry-run resolves the commit and writes nothing. --dest must be an absolute path outside this checkout. An existing dest must be empty or already this install. A directory that contains .git is not this install. The script refuses any other directory.

Install as a Cursor plugin

Cursor hosts read .cursor-plugin/plugin.json (kalshi-readonly, version 0.3.0). Root plugin.json and mcp.json stay in place. Skills are under skills/. The Finance Engineer checklist is harness/SKILL.md. Stdio launch for the Cursor plugin is root mcp.json: command python3, args ${PLUGIN_ROOT}/server.py, env KALSHI_API_KEY_ID, KALSHI_PRIVATE_KEY_PATH, KALSHI_PRIVATE_KEY_PEM, and KALSHI_SAFE_MODE (default on). No secrets are committed.

This repository is not published to the Cursor Marketplace or cursor.directory. Publishing is out of scope. Until then, use the IDE mcp.json entry below, or the Grok Bot command in the next section.

server.py puts its own directory on sys.path before importing the package, so a host can launch it from another working directory. That includes Python's safe path (python3 -P). Do not paste the private key into chat. Do not set confirm unless Akash has confirmed that order.

Grok Bot

Use the Node entry. It speaks MCP stdio with @modelcontextprotocol/sdk (the same transport bambu-mcp uses) and keeps one Python worker for the tool calls.

npm install
npm run build
node /absolute/path/to/kalshi-mcp/dist/index.js

Set these in the host env, not on the command line:

  • KALSHI_API_KEY_ID or KALSHI_API_KEY_ID_PATH (default ~/.secrets/kalshi/key_id)

  • KALSHI_PRIVATE_KEY_PATH or KALSHI_PRIVATE_KEY_PEM (default ~/.secrets/kalshi/private.pem)

  • KALSHI_SAFE_MODE=1 on every fleet host

KALSHI_SAFE_MODE=1 is the fleet default. Missing or empty stays on. tools/list omits place_order, cancel_order, amend_order, and decrease_order. find_best_bets only reads. Do not commit the key file.

{
  "mcpServers": {
    "kalshi-readonly": {
      "command": "node",
      "args": ["/absolute/path/to/kalshi-mcp/dist/index.js"],
      "env": {
        "KALSHI_API_KEY_ID_PATH": "/absolute/path/to/.secrets/kalshi/key_id",
        "KALSHI_PRIVATE_KEY_PATH": "/absolute/path/to/.secrets/kalshi/private.pem",
        "KALSHI_SAFE_MODE": "1"
      }
    }
  }
}

Omit the path env vars when ~/.secrets/kalshi/key_id and private.pem already exist for that user. Do not commit those files.

Finance Engineer override, and only that host. harness/SKILL.md owns the decision. Do not add a second Kalshi role harness. On the box, use Refresh above instead of a hand-run npm install. Install, in order:

  1. From a git checkout, scripts/refresh-host.sh --dest /home/box/mcp/kalshi-mcp-main <sha> so that tree matches the commit. --dry-run prints the plan and writes nothing. A checkout you are editing still uses npm run build in that checkout.

  2. Point the host at node /absolute/path/to/kalshi-mcp/dist/index.js with KALSHI_SAFE_MODE=0.

  3. Restart the host. The Node process answers MCP initialize before the Python worker finishes tools/list, then tools/list waits for that list. The list is fixed for the life of the process (listChanged is false). A restart that leaves no live dist/index.js shows up as Not connected. Rebuild and restart so the host spawns the process again.

  4. Prove the new process with tools/list. It must include fe_routine, find_best_bets, place_order, cancel_order, amend_order, and decrease_order. stderr from the entry includes safeMode=off and those names. exchange_status is the cheap public call that the worker can reach Kalshi. It does not place.

The Finance Engineer sleeve sets KALSHI_SAFE_MODE=0 and restarts the host. place_order, cancel_order, amend_order, and decrease_order then show up. Every one of those calls still needs confirm: true. Opening risk still has to fit the caps: about $2 until Kalshi fill history shows the sleeve is profitable, hard max $15, at most 15% of the sleeve in one market, and at most 30% in one corr_group. There is no withdraw tool and no deposit tool. The daily and end-of-day prompt is fe_routine and harness/fe-grok-bot-routine.md.

{
  "mcpServers": {
    "kalshi-readonly": {
      "command": "node",
      "args": ["/absolute/path/to/kalshi-mcp/dist/index.js"],
      "env": {
        "KALSHI_API_KEY_ID": "your-key-id",
        "KALSHI_PRIVATE_KEY_PATH": "/absolute/path/to/private.pem",
        "KALSHI_SAFE_MODE": "0"
      }
    }
  }
}

"KALSHI_SAFE_MODE": "0" in the block above is the Finance Engineer host only. Every other host stays at "1". Restart after you change it so the tool list reloads. listChanged stays false for the life of the process.

Cursor

{
  "mcpServers": {
    "kalshi-readonly": {
      "command": "python3",
      "args": ["/absolute/path/to/kalshi-mcp/server.py"],
      "env": {
        "KALSHI_API_KEY_ID": "your-key-id",
        "KALSHI_PRIVATE_KEY_PATH": "/absolute/path/to/private.pem",
        "KALSHI_SAFE_MODE": "1"
      }
    }
  }
}

mcp.json in this repo is the plugin-host launch (python3 and ${PLUGIN_ROOT}/server.py). Grok Bot should use the Node command above instead.

Tools

Tool

What it does

exchange_status

Public exchange status, plus Washington region attestation (expires_at, expires_in_hours, expired) from the note on disk. washington_attestation is null with a reason when that note is missing. build_sha is the first 12 characters of DEPLOYED_SHA at the install root, or unknown when that file is missing or invalid. The process reads the file once at startup

list_markets

Public markets for liquid singles. Default limit 25, status=open, mve_filter=exclude, liquid=true (drop null yes bid and yes ask). Follows an empty page up to 4 times. Optional cursor, series_ticker, event_ticker. ticker is one market via GET /markets/{ticker} and includes result plus settlement fields Kalshi sent. List rows include those fields when present. category is a client prefix filter (sports, weather, politics, crypto, macro, other), not a Kalshi parameter. mve_filter=all and liquid=false and status=any widen the page

append_decision

Validate one place, skip, or outcome and append it to the JSONL ledger. Does not trade. Does not rewrite history. flb_band allowed values: <10¢, 10–25¢, 25–75¢, 75–90¢, ≥90¢ (>=90¢ accepted). rank_source allowed values: find_best_bets, fe_manual

summarize_decisions

Week and life-to-date stats from that ledger only. Does not invent N

find_best_bets

Read-only rank. See Bets. Does not place orders. Rows include edge_net_cents, flb_band, kelly_frac, stake_mode, side_exec, days_to_res, spread_cents, depth_at_ask, fee_cents_est, corr_group_hint, and hold_to_res_default. rate_limited is true when a later scan page hit the retry budget

fe_routine

Daily and end-of-day prompt for the Finance Engineer. Does not trade

cash_or_positions

Cash and positions. include: balance, cash, positions, both (default), or fills. Optional limit (default 50)

list_open_orders

Resting orders only (status=resting). Optional ticker, limit (default 100), cursor, subaccount. Read. No confirm

place_order

One order. Hidden until safe mode is off. Requires confirm: true

cancel_order

One cancel. Hidden until safe mode is off. Requires confirm: true

amend_order

Price and total fillable count. Hidden until safe mode is off. Requires confirm: true

decrease_order

Reduce resting size. Hidden until safe mode is off. Requires confirm: true

cash_or_positions returns balance_cents, balance_dollars, and cash (the same official dollar string). portfolio_value is included only when the balance endpoint sends it. market_positions and event_positions each include ticker and qty when the API sends a quantity. Market side is the official sign of that quantity: positive YES, negative NO. avg and mark are copied only when the API sends them. include=fills reads recent fills (ticker, side, qty, official prices). This server does not compute remaining-to-recover.

Bets

find_best_bets is a read. The dry-run command is python3 -m kalshi_readonly.recommend --fixture tests/fixtures/recommend_scan.json. That command uses the file only. It does not call Kalshi and it does not place orders.

The score is estimated_confidence * payout_ratio / stake_needed. stake_needed is the ask for one contract. payout_ratio is the profit per dollar you stake if that side wins. A 15 cent contract with high confidence sorts ahead of an 82 cent contract, because the 82 cent contract ties up more cash for a smaller payout.

Pass beliefs when you can name the evidence: ticker, side (yes or no), confidence (0 to 1), and an optional evidence string. At most 20. Those tickers are one GET /markets?tickers=... request. The server does not fetch each ticker on its own, and it does not pull an order book per market. Without beliefs, you get a research_queue and no recommendations. A queue row is not a bet.

Defaults, all overridable inside a fixed range:

Knob

Default

Range

min_confidence

0.55

0.50 to 0.99

min_edge

0.08

0 to 0.90

max_price

0.84

0.05 to 0.84

max_risk_dollars

5

1 to 25

min_volume

20 contracts of 24h volume (weaker proxy)

0 to 1000000

min_lifetime_volume

1000 contracts lifetime

0 to 100000000

min_ask_size

1

0 to 100000

min_hours_to_expiry

2

0 to 168

max_hours_to_expiry

1440 (60 days)

above the minimum, up to 8760

max_pages

1

1 to 4

page_size

100

1 to 200

limit

5 rows

1 to 10

Prices at or above 0.85 are never recommended. The scan also skips multivariate combos (mve_filter=exclude) so a page is not spent on them. Public GET /markets counts against the same IP budget as signed calls, so the default scan is one page. A successful list is cached for about 5 minutes in the process. Concurrent scans of the same page share one in-flight request. Separate processes share markets-get.lock so those GETs start at least 1.5 seconds apart. The file is under KALSHI_STATE_DIR, or /tmp/kalshi-mcp when that variable is unset. Hosts that should share the limit need the same directory. The Node entry keeps that process alive across tool calls.

rate_limited is false on a normal read. If a later scan page still gets HTTP 429 or 503 after the retry budget, the tool returns the pages already fetched, sets rate_limited to true, and does not place. That partial list is reused for about 60 seconds. A rate limit on the first page fails the tool with an error that starts with rate_limited. Do not scan again immediately.

HTTP 429 and 503 are tried at most 5 times. A Retry-After value is honored up to 30 seconds. Without that header, or when it is not a delay or a date, the waits are 2s, 4s, 8s, then 16s, plus up to 0.25s of jitter. The next step of that schedule is capped at 30 seconds. That replaces the 0.25s, 0.5s, 1s, 2s schedule. The error is rate_limited only after that budget is spent.

fee_cents_est is the official schedule for the suggested role, looked up by series prefix before the score: taker ceil(M_taker * 0.07 * C * P * (1-P) * 100) cents, maker ceil(M_maker * 0.0175 * C * P * (1-P) * 100). At P=0.50, C=1, M_taker=1 the raw taker amount is 1.75 cents and the round-up is 2 cents. NFL, MLB, Fed, and CPI are usually M_taker=1 and M_maker=1. Weather (KXHIGH) is M_taker=1 and M_maker=0 by inference, because those series are absent from the Non-Standard table. Some crypto year-end series (KXBTCY, KXETHY) are 0/0. An unlisted series stays taker 1 and maker 0. A payload fee_multiplier (also series_fee_multiplier or fee_multiplier_fp) overrides both sides. edge_net_cents subtracts that taker dome, half the spread, and a depth haircut (0 when the ask shows at least 3 contracts, otherwise the shortfall capped at 2 cents). maker_flag is true when side_exec is maker. hold_to_res_default is true when days to resolution are at most 7 and the round-trip fee is larger than the remaining edge. There is no settlement fee and no fixed take-profit percent. wx_gap_pp is copied onto the row only when a belief passes it. It is not an entry gate.

Every response includes places_orders: false and the sentence Finance Engineer may place under the caps with confirm true. The tool still does not place. Each ranked row reports gates_passed and gates_failed (empty on a pass), plus edge_net_cents after the taker fee dome (ceil(M_taker * 0.07 * contracts * price * (1 - price)) cents), flb_band, kelly_frac of 0.25, stake_mode, side_exec / SIDE_EXEC, days_to_res / DAYS_TO_RES, spread_cents, depth_at_ask, fee_cents_est, m_taker, m_maker, fee_m_source, corr_group_hint, and hold_to_res_default / HOLD_TO_RES_DEFAULT. min_edge (default 0.08) is probability points and is separate from edge_net_cents. A row with edge_net_cents at or below 0 is dropped after the fee, the spread, and the depth haircut. A taker quote in the <10¢ band is dropped unless the belief sets allow_longshot true, edge_net_cents is at least 8, and the suggested stake is forced to $2. A 10–25¢ quote uses stake_mode fixed_2 and caps suggested_max_dollars_risked at $2. Lifetime volume of at least 1000 passes; 24h min_volume is the weaker proxy, and both numbers are on the row. A maker quote whose taker fee is within 2 cents sorts 0.0001 ahead of an equal score. The displayed score stays the formula. max_price defaults to 0.84, the hard ceiling, so a 50¢–84¢ contract with edge is eligible. The previous default of 0.50 hid that band; pass max_price 0.50 to restore it. The Finance Engineer routine is fe_routine and harness/fe-grok-bot-routine.md.

Decision ledger

append_decision and summarize_decisions are reads of local state. They stay on tools/list while KALSHI_SAFE_MODE=1. They do not place orders.

The ledger is append-only JSONL. A later settlement is a new line with action outcome and parent_id. The server never rewrites an earlier line. The latest outcome for a parent_id is the one that counts.

Plugin path: KALSHI_STATE_DIR (default ~/.local/state/kalshi) plus ledger/decisions.jsonl. KALSHI_LEDGER_PATH overrides that file. Finance Engineer mirrors it to /workspace/state/kalshi/ledger/decisions.jsonl. summarize_decisions regenerates stats-weekly-YYYY-Www.json and stats-ltd.json beside the ledger. Those two files are rewritten. The JSONL is not.

summarize_decisions reports n_places, n_skips, and n_resolved from the lines on disk. hit_rate is wins divided by wins plus losses, and it is null when that denominator is 0. roi_after_fees is realized pnl divided by parent stake, and it is null when stake is missing. Brier and log-score are null until 20 binary outcomes. N under 10 is counts and P&L only (claim_level counts_and_pnl_only). N under 30 labels Bürgi maker/taker and the fee dome as external priors and does not treat them as this sleeve's track record. Concurrent corr_group percent is null because sleeve cash is not on the ledger.

find_best_bets puts gates_passed and gates_failed on each recommendation. A scored skip is a row on skipped with failed_gate. The research queue is unchanged: no beliefs is not a decision tree.

Catalog size

KALSHI_SAFE_MODE=1 advertises 8 tools: exchange_status, list_markets, find_best_bets, append_decision, summarize_decisions, cash_or_positions, fe_routine, list_open_orders.

KALSHI_SAFE_MODE=0 adds place_order, cancel_order, amend_order, and decrease_order (12 tools). Mutations stay behind safe mode, the same as before. Fleet hosts stay at 8. The Finance Engineer host is the one that carries 12. A host that caps a server near 10 tools should keep safe mode on everywhere except that host.

Trade

Opening risk is capped in process. The server does not set confirm for you.

Cap

Default

Stake

$2 until fill history shows the sleeve is profitable. Prices under 25 cents stay at $2

Hard max

$15 per trade. A setting above 15 is ignored

One market

15% of the $71 sleeve

One corr_group

30% of the sleeve. corr_group is required on opening risk (place_order, and amend_order because an amend is checked as new risk). Missing, empty, and none are refused. Mutually exclusive children share one group. Examples: nfl_week_N, city_weather_YYYYMMDD, fed_meeting_YYYYMM. A full co-resolution matrix is later

New notional per UTC day

$10

Cancels and decreases do not add risk and are not size-capped. An amend that would increase risk is capped. Size above the $2 default requires at least three closing trades and a positive net on this account.

KALSHI_SAFE_MODE defaults to on. Missing, empty, 1, true, yes, on, and any other value keep it on. Only 0, false, no, and off turn it off. While it is on:

  • place_order, cancel_order, amend_order, and decrease_order are omitted from tools/list.

  • Calling one of those handlers anyway returns an error and does not hit the network.

  • list_open_orders stays registered. It is a signed GET of resting orders.

The Finance Engineer harness is the only unlock. After that host sets KALSHI_SAFE_MODE=0, restart it so tools/list reloads. Every mutating call still requires confirm to be the boolean true. false, "true", 1, and a missing confirm are refused before auth and before HTTP. The confirm flag is not copied into the Kalshi body. Fleet hosts do not set 0.

Tool

Kalshi route

Required arguments

place_order

POST /portfolio/events/orders

ticker, side (bid or ask), count, price, time_in_force, self_trade_prevention_type, corr_group, confirm

cancel_order

DELETE /portfolio/events/orders/{order_id}

order_id, confirm. Pass market_ticker so Kalshi can auto-route

amend_order

POST /portfolio/events/orders/{order_id}/amend

order_id, ticker, side, price, count, corr_group, confirm

decrease_order

POST /portfolio/events/orders/{order_id}/decrease

order_id, confirm, and exactly one of reduce_by or reduce_to

list_open_orders

GET /portfolio/orders?status=resting

none

bid buys YES. ask sells YES. Prices are YES-book dollar strings strictly between 0 and 1, such as 0.4200. count is a contract string such as 1.00 (a positive integer is accepted). Amend count is already filled plus the desired resting remainder, not a reduce-by amount. Decreasing size keeps queue position. A price change or a larger size does not. Use decrease_order to reduce.

time_in_force is fill_or_kill, good_till_canceled, or immediate_or_cancel. The maker path is time_in_force=good_till_canceled and post_only=true. post_only with fill_or_kill or immediate_or_cancel is refused here. Kalshi also rejects a post_only order that would cross, so a maker flag cannot silently take. self_trade_prevention_type is taker_at_cross or maker. expiration_time is only valid with good_till_canceled. reduce_only is only valid with immediate_or_cancel and does not require corr_group. Pass client_order_id yourself when you want dedup. This server does not invent one.

A successful place_order echoes role (maker when post_only is true, otherwise taker), fee_cents_est from the series catalog on the ticker (unlisted maker M is 0; NFL, Fed, and CPI maker M is 1), flb_band for the entry price (price on a bid, 1 - price on an ask), edge_net_cents when the caller passes it (the server does not invent one), corr_group, order_id, and day_spend_remaining from the live book against the soft $10 UTC-day cap. fe_routine does not invent that remaining figure. Size above $2 still needs a profitable Kalshi fill history. If the ack already shows a fill, one fill line is appended to the decision ledger for that audit. The line does not raise the $2 cap. There is no withdraw and no deposit.

Optional place fields: client_order_id, expiration_time, post_only, reduce_only, cancel_order_on_pause, subaccount, order_group_id, exchange_index.

Demo host, if you use one: KALSHI_API_BASE=https://demo-api.kalshi.co/trade-api/v2.

There is no batch place, no batch cancel, no withdraw, and no deposit.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables interaction with the Kalshi prediction market platform through their REST API. Supports trading and market operations with secure RSA key authentication for both production and demo environments.
    32
    MIT
  • A
    license
    Not graded
    quality
    C
    maintenance
    Enables users to search, analyze, and trade on Kalshi prediction markets directly through MCP-compatible clients. It supports portfolio management, market data retrieval, and limit order execution with production-grade authentication and rate limiting.
    20 PyPI
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Provides access to US-regulated prediction-market data from Kalshi, enabling queries on market events without authentication.
    321 npm
    MIT
  • A
    license
    B
    quality
    D
    maintenance
    Enables AI assistants to trade on Kalshi through natural language, including placing orders, market data access, portfolio management, and paper trading with safety guardrails.
    60
    8 npm
    3
    MIT