$npx -y skills add agiprolabs/claude-trading-skills --skill kalshi-apiKalshi exchange mechanics — RSA-PSS auth, order schema, YES/NO orderbook convention, WebSocket, and endpoint surface. Market-type-agnostic shared layer for all Kalshi skills.
| 1 | # Kalshi API |
| 2 | |
| 3 | CFTC-regulated US event exchange. USD-denominated binary contracts settle at $1.00 (YES wins) or $0.00 (NO wins). REST + WebSocket, RSA-PSS authentication on every request. |
| 4 | |
| 5 | For contract semantics and settlement rules, see the `kalshi-weather-markets` and `kalshi-crypto-index-markets` skills. For strategy, sizing, and backtesting, see `prediction-market-strategy`. |
| 6 | |
| 7 | --- |
| 8 | |
| 9 | > **VERIFY BEFORE CODING.** |
| 10 | > The Kalshi API has broken backward compatibility before: the host changed (old `trading-api.kalshi.com` → dead), and the order schema changed (integer cents → dollar strings). Always smoke-test signing and order bodies against a live response before shipping. |
| 11 | > |
| 12 | > Canonical sources: |
| 13 | > - API reference: <https://docs.kalshi.com> (legacy mirror: <https://trading-api.readme.io>) |
| 14 | > - Official Python starter: <https://github.com/Kalshi/kalshi-starter-code-python> |
| 15 | |
| 16 | --- |
| 17 | |
| 18 | ## Overview |
| 19 | |
| 20 | - **Base URL:** `https://api.elections.kalshi.com/trade-api/v2` |
| 21 | - **Auth:** RSA-PSS on every request — there are no public/unauthenticated endpoints |
| 22 | - **No demo parity:** the demo environment (`demo-api.kalshi.co`) has a near-empty book; use production even for read-only pulls |
| 23 | - **Contracts:** $0.01–$0.99 per contract; pay price if YES wins, lose price if NO wins; max payout = $1.00 |
| 24 | |
| 25 | --- |
| 26 | |
| 27 | ## Quick Start |
| 28 | |
| 29 | ### 1. Credentials |
| 30 | |
| 31 | ``` |
| 32 | KALSHI_KEY_ID=<your-key-uuid> |
| 33 | KALSHI_PRIVATE_KEY_PATH=~/.kalshi/private.pem |
| 34 | ``` |
| 35 | |
| 36 | Generate the key in the Kalshi dashboard. Store secrets in environment variables or a secrets manager — never in code. |
| 37 | |
| 38 | ### 2. Install |
| 39 | |
| 40 | ```bash |
| 41 | pip install httpx cryptography |
| 42 | ``` |
| 43 | |
| 44 | ### 3. Host + auth (the part everyone gets wrong) |
| 45 | |
| 46 | The host and signature format are where implementations break. Three common failures: |
| 47 | 1. Using the old `trading-api.kalshi.com` host → 401 |
| 48 | 2. Including the query string in the signed path → 401 |
| 49 | 3. Signing with seconds instead of milliseconds → 401 |
| 50 | |
| 51 | ```python |
| 52 | import os, time, base64, httpx |
| 53 | from cryptography.hazmat.primitives import hashes, serialization |
| 54 | from cryptography.hazmat.primitives.asymmetric import padding |
| 55 | |
| 56 | BASE = "https://api.elections.kalshi.com/trade-api/v2" |
| 57 | KEY_ID = os.environ["KALSHI_KEY_ID"] |
| 58 | with open(os.environ["KALSHI_PRIVATE_KEY_PATH"], "rb") as f: |
| 59 | PRIV = serialization.load_pem_private_key(f.read(), password=None) |
| 60 | |
| 61 | def _headers(method: str, path: str) -> dict: |
| 62 | """path must include /trade-api/v2 prefix and exclude query string.""" |
| 63 | ts = str(int(time.time() * 1000)) # milliseconds |
| 64 | msg = f"{ts}{method}{path}".encode() |
| 65 | sig = PRIV.sign( |
| 66 | msg, |
| 67 | padding.PSS(mgf=padding.MGF1(hashes.SHA256()), |
| 68 | salt_length=padding.PSS.DIGEST_LENGTH), |
| 69 | hashes.SHA256(), |
| 70 | ) |
| 71 | return { |
| 72 | "KALSHI-ACCESS-KEY": KEY_ID, |
| 73 | "KALSHI-ACCESS-TIMESTAMP": ts, |
| 74 | "KALSHI-ACCESS-SIGNATURE": base64.b64encode(sig).decode(), |
| 75 | } |
| 76 | |
| 77 | def get(path: str, params=None): |
| 78 | # Sign the path only — query goes into params, not the signature |
| 79 | r = httpx.get(BASE + path, params=params, |
| 80 | headers=_headers("GET", "/trade-api/v2" + path)) |
| 81 | r.raise_for_status() |
| 82 | return r.json() |
| 83 | |
| 84 | def post(path: str, body: dict): |
| 85 | r = httpx.post(BASE + path, json=body, |
| 86 | headers=_headers("POST", "/trade-api/v2" + path)) |
| 87 | r.raise_for_status() |
| 88 | return r.json() |
| 89 | |
| 90 | # Example: open markets in a series |
| 91 | markets = get("/markets", params={"series_ticker": "KXHIGHNY", "status": "open"}) |
| 92 | ``` |
| 93 | |
| 94 | **Signature spec:** RSA-PSS, MGF1 over SHA-256, salt length = `PSS.DIGEST_LENGTH`. String to sign: `{timestamp_ms}{METHOD}{path}` where path includes `/trade-api/v2` and excludes the query string. |
| 95 | |
| 96 | Headers: `KALSHI-ACCESS-KEY` (UUID), `KALSHI-ACCESS-TIMESTAMP` (ms), `KALSHI-ACCESS-SIGNATURE` (base64). |
| 97 | |
| 98 | ### 4. Candlestick history |
| 99 | |
| 100 | ```python |
| 101 | # Returns OHLC for yes_bid / yes_ask + volume + open_interest |
| 102 | # Values are dollar strings: {"close": "0.42"} |
| 103 | candles = get( |
| 104 | f"/series/KXHIGHNY/markets/{ticker}/candlesticks", |
| 105 | params={"start_ts": start_epoch, "end_ts": end_epoch, "period_interval": 60}, |
| 106 | ) |
| 107 | ``` |
| 108 | |
| 109 | `period_interval` is in **minutes**: `1`, `60`, or `1440`. |
| 110 | |
| 111 | --- |
| 112 | |
| 113 | ## YES/NO Order-Book Convention |
| 114 | |
| 115 | On Kalshi, **`yes` and `no` are both resting BID ladders** — there is no separate ask book. To take the other side you lift the opposing bid: |
| 116 | |
| 117 | ``` |
| 118 | no_ask = 1 − best_yes_bid # cost to buy NO right now (lift YES bids) |
| 119 | yes_ask = 1 − best_no_bid # cost to buy YES right now (lift NO bids) |
| 120 | P(YES) mid = (best_yes_bid + (1 − best_no_bid)) / 2 |
| 121 | ``` |
| 122 | |
| 123 | Getting this backwards silently inverts every signal. Use the helpers in `scripts/kalshi_orderbook.py`. |
| 124 | |
| 125 | **Orderbook response** comes in two variants depending on API tier — normalize before using: |
| 126 | ```json |
| 127 | {"orderbook": {"yes": [[price, size], ...], "no": [[price, size], ...]}} |
| 128 | ``` |
| 129 | If a price value |