How to Build a Kalshi Trading Bot in Python (2026 API)
A step by step build with code that matches Kalshi's current API: market data, signed requests, a first order on the demo exchange, a live price stream, fees and the limits that keep a bot from hurting you.
TL;DR
A Kalshi trading bot is a Python script that reads prices from Kalshi's API, decides when a price is good enough, and sends orders. This guide builds one in seven steps: read markets and order books without a login, sign requests with your API key, place a first order on Kalshi's demo exchange, stream prices, account for fees, and add risk limits. Kalshi changed its price fields, its order endpoint and its hosts during 2026, so most older tutorials no longer run. The market data examples here were run against Kalshi's API on 15 November 2026, and the signed steps follow Kalshi's current reference.
Most guides to building a Kalshi trading bot were written before 2026, and Kalshi's API has changed under them. If you copy one today, the first price you read comes back empty and the first order you send is refused. This guide uses the API as it is now. It's written for someone who knows some Python and wants to understand each step, not for someone who wants a finished product. If that's you, there's a shortcut at the end.
One rule before any code: build and test on Kalshi's demo exchange, with mock money, until every step behaves. Nothing here is financial advice, and a bot can lose real money faster than you can.
What Changed in Kalshi's API in 2026
These are the changes that break older code. The dates come from Kalshi's changelog.
- Prices are dollar strings. A price now looks like
"0.4200"in a field ending in_dollars, and contract counts are strings in fields ending in_fp. The old whole-cent fields such asyes_bidwere removed in March 2026. - Orders have a new address. You create an order with
POST /portfolio/events/orders. The old/portfolio/orderspath now answers with HTTP 410 and the codedeprecated_v1_order_endpoint. We got exactly that answer when we tried it on 15 November 2026. - Market orders are gone. Every order carries a price and a
time_in_force. To trade at once, you send a priced order that must fill immediately or be cancelled. - New hosts. Production is
external-api.kalshi.comand demo isexternal-api.demo.kalshi.co. The older hosts still answer, but the documentation now lists the new ones first. - New key type. Keys created on the website have been Ed25519 by default since October 2026. Older RSA keys still work, and the code below handles both.
- New Python packages. The official ones are
kalshi_python_syncandkalshi_python_async. The olderkalshi-pythonpackage is deprecated.
What You Need
- Python 3.10 or newer, and three packages:
pip install requests cryptography websockets. - A demo account. Kalshi's demo exchange at demo.kalshi.co is separate from the real one: you sign up there, you get mock funds, and you create an API key there. Keys from one environment don't work in the other. Kalshi's demo environment page has the details.
- An API key from that demo account. You get a key ID and a private key file. The private key is shown once, so save it somewhere safe and never put it in your code or in a repository.
We use plain requests instead of Kalshi's package so that nothing is hidden. Once you understand the steps, switching to the official package is easy.
Step 1: Read Markets Without Logging In
Market data is public. You can list markets, prices and order books with no key at all, which makes this the right place to start. Kalshi groups markets into series. The series KXNBAREB, for example, holds NBA player rebound markets.
import requests
HOST = "https://external-api.kalshi.com" # demo: https://external-api.demo.kalshi.co
API = "/trade-api/v2"
def open_markets(series_ticker, limit=200):
"""Every open market in one series, following the cursor until it runs out."""
markets, cursor = [], None
while True:
params = {"series_ticker": series_ticker, "status": "open", "limit": limit}
if cursor:
params["cursor"] = cursor
data = requests.get(HOST + API + "/markets", params=params, timeout=10).json()
markets += data["markets"]
cursor = data.get("cursor")
if not cursor or not data["markets"]:
return markets
for m in open_markets("KXNBAREB")[:3]:
print(m["ticker"], "|", m["title"], "| bid", m["yes_bid_dollars"], "| ask", m["yes_ask_dollars"])
When we ran this on 15 November 2026 there were 455 open markets in that series. One line of the output looked like this:
KXNBAREB-26NOV15NYKTOR-NYKKTOWNS32-18 | Karl-Anthony Towns: 18+ rebounds | bid 0.1300 | ask 0.1700
Three things to notice. The results come in pages, and you keep sending the cursor back until it comes back empty. You ask for status=open, but each market's own status field reads active. And every price is a string in dollars, so convert with float() before doing arithmetic. A Yes price of 0.13 means the market gives that outcome about a 13% chance.
Step 2: Read the Order Book
The best bid and ask tell you the price. The order book tells you how much you can trade at that price, which matters more than beginners expect.
def best_prices(ticker):
"""Best Yes bid and best Yes ask in dollars. Kalshi's book holds bids only."""
url = f"{HOST}{API}/markets/{ticker}/orderbook"
book = requests.get(url, timeout=10).json()["orderbook_fp"]
yes_bids = [float(price) for price, _size in book.get("yes_dollars") or []]
no_bids = [float(price) for price, _size in book.get("no_dollars") or []]
best_yes_bid = max(yes_bids) if yes_bids else None
best_yes_ask = round(1 - max(no_bids), 4) if no_bids else None
return best_yes_bid, best_yes_ask
This is the part that confuses people. Kalshi's book lists bids only, one list for Yes and one for No, each as pairs of price and size. There is no list of asks, because an offer to sell Yes at 10 cents is the same thing as a bid to buy No at 90 cents. So the best Yes ask is 1 minus the best No bid. A real book we pulled had Yes bids up to 0.09 and No bids up to 0.90, which makes the market 9 cents bid, 10 cents offered. We checked the function above against the market's own bid and ask fields on five busy markets, and it matched on all five.
Step 3: Sign Your Requests
Anything about your own account needs three headers: your key ID, the current time in milliseconds, and a signature. The signature covers the time, the HTTP method and the path, joined with nothing between them. The path starts at /trade-api/v2 and leaves out the query string. Kalshi's API keys page describes the scheme.
import base64
import time
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import ed25519, padding
HOST = "https://external-api.demo.kalshi.co" # stay on demo until everything works
KEY_ID = "your-demo-key-id"
with open("kalshi-demo.key", "rb") as f:
PRIVATE_KEY = serialization.load_pem_private_key(f.read(), password=None)
def auth_headers(method, path):
"""Headers for one request. path starts at /trade-api/v2; the query string is not signed."""
timestamp = str(int(time.time() * 1000))
message = f"{timestamp}{method}{path.split('?')[0]}".encode()
if isinstance(PRIVATE_KEY, ed25519.Ed25519PrivateKey):
signature = PRIVATE_KEY.sign(message)
else: # an RSA key
signature = PRIVATE_KEY.sign(
message,
padding.PSS(mgf=padding.MGF1(hashes.SHA256()), salt_length=padding.PSS.DIGEST_LENGTH),
hashes.SHA256(),
)
return {
"KALSHI-ACCESS-KEY": KEY_ID,
"KALSHI-ACCESS-TIMESTAMP": timestamp,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(signature).decode(),
}
path = API + "/portfolio/balance"
reply = requests.get(HOST + path, headers=auth_headers("GET", path), timeout=10)
print(reply.status_code, reply.json())
If it works you get HTTP 200 and your mock balance in balance_dollars. If you get HTTP 401, check three things in this order: the key ID, that the key belongs to the demo environment, and your computer's clock. Kalshi rejects a timestamp that is more than about 30 seconds off.
Step 4: Place Your First Order on Demo
An order names a market, a side, a price, a size and how long it may wait. Two details trip people up. The price is always the Yes price. And the side is bid or ask: a bid buys Yes at that price, while an ask sells Yes, which is the same as buying No at 1 minus that price.
import uuid
def place_order(ticker, side, price, count, time_in_force="fill_or_kill"):
"""side "bid" buys Yes at price. side "ask" sells Yes (the same as buying No at 1 - price)."""
path = API + "/portfolio/events/orders"
body = {
"ticker": ticker,
"side": side,
"price": f"{price:.4f}", # always the Yes price, as a dollar string
"count": str(count),
"time_in_force": time_in_force, # fill_or_kill, immediate_or_cancel or good_till_canceled
"self_trade_prevention_type": "taker_at_cross",
"client_order_id": str(uuid.uuid4()),
}
reply = requests.post(HOST + path, json=body, headers=auth_headers("POST", path), timeout=10)
reply.raise_for_status()
return reply.json()
def cancel_order(order_id):
path = f"{API}/portfolio/events/orders/{order_id}"
requests.delete(HOST + path, headers=auth_headers("DELETE", path), timeout=10).raise_for_status()
order = place_order("A-DEMO-MARKET-TICKER", "bid", 0.01, 1, "good_till_canceled") # 1 Yes contract at 1 cent
print(order["order_id"], order["fill_count"], order["remaining_count"])
cancel_order(order["order_id"])
A bid of 1 cent that is allowed to wait will almost never fill, which makes it a safe first test: you should see it accepted, resting, and then cancelled. Success is HTTP 201. Don't assume that means you own anything. Read fill_count and remaining_count in the reply to see what happened.
The three values of time_in_force decide how your bot behaves. fill_or_kill fills the whole order at once or cancels all of it. immediate_or_cancel fills what it can and cancels the rest. good_till_canceled leaves the order resting in the book. For a bot that takes a price it has just seen, fill or kill is the safest choice, because you never end up with a part of a position you didn't plan for. That matters most when the order is one half of a cross-venue arbitrage.
A note on honesty: we don't publish code we haven't tried. The public steps above ran against Kalshi's live API. For this step we sent the same request with a throwaway key, and Kalshi answered with an authentication error on the new path and the 410 on the old one, so the path, the method and the headers are right. Filling a real order is the part you'll confirm yourself on demo.
Step 5: Stream Prices Instead of Polling
Asking for the order book over and over is slow and uses up your request allowance. The WebSocket stream sends you one full snapshot of the book and then only the changes.
import asyncio
import json
import websockets
WS_URL = "wss://external-api-ws.demo.kalshi.co/trade-api/ws/v2" # production: external-api-ws.kalshi.com
async def stream(tickers):
headers = auth_headers("GET", "/trade-api/ws/v2")
async with websockets.connect(WS_URL, additional_headers=headers) as ws:
await ws.send(json.dumps({
"id": 1,
"cmd": "subscribe",
"params": {"channels": ["orderbook_delta"], "market_tickers": tickers},
}))
last_seq = None
async for raw in ws:
event = json.loads(raw)
if event["type"] == "orderbook_snapshot":
book = event["msg"] # yes_dollars_fp and no_dollars_fp: the whole book
last_seq = event["seq"]
elif event["type"] == "orderbook_delta":
if last_seq is not None and event["seq"] != last_seq + 1:
raise RuntimeError("missed a message: reconnect and take a fresh snapshot")
last_seq = event["seq"]
change = event["msg"] # market_ticker, side, price_dollars, delta_fp
asyncio.run(stream(["A-DEMO-MARKET-TICKER"]))
The connection is signed the same way as a request, with the method GET and the path /trade-api/ws/v2. Each message carries a sequence number. If one is missing, your copy of the book is wrong and every decision after that is wrong too, so the code stops instead of guessing. (In versions of the websockets package older than 14, the argument is called extra_headers.) Other channels include ticker, trade and fill, the last of which tells you when your own orders trade. Kalshi's WebSocket quick start lists them all.
Step 6: Put Fees Into Every Decision
A price that looks good before fees is often bad after them. On most Kalshi markets the taker fee is:
fee = 0.07 × contracts × price × (1 − price), rounded up
def taker_fee(price, contracts, multiplier=1.0):
"""Kalshi's standard taker fee in dollars, before Kalshi rounds it up."""
return 0.07 * multiplier * contracts * price * (1 - price)
print(round(taker_fee(0.50, 100), 2)) # 1.75
print(round(taker_fee(0.30, 100), 2)) # 1.47
The fee is largest at 50 cents and shrinks toward both ends. Kalshi's fee schedule shows the same range, 7 cents to $1.75 per 100 contracts, for most markets. Orders that rest in the book and get filled later pay nothing on most markets, but some series charge a maker fee, and combos charge makers half the taker fee. Each series reports its own fee settings in the fee_type and fee_multiplier fields of GET /series/{series_ticker}, so a careful bot reads them instead of assuming. To try numbers by hand, use our prediction market calculator.
Step 7: Add the Limits Before the Strategy
Here's a deliberately small decision loop. It buys one Yes contract when the ask, plus the fee, is cheaper than your fair price by a margin you choose. The fair price is the hard part, and it's yours to supply: a model, or a no-vig price from a sharp bookmaker, as described in our guides to Kalshi value betting and removing the vig.
import os
MAX_CONTRACTS = 10 # per market
MIN_EDGE = 0.03 # 3 cents per contract after fees
def position(ticker):
path = API + "/portfolio/positions"
reply = requests.get(HOST + path, params={"ticker": ticker}, headers=auth_headers("GET", path), timeout=10)
rows = reply.json().get("market_positions", [])
return float(rows[0]["position_fp"]) if rows else 0.0
def run_once(ticker, fair):
if os.path.exists("STOP"): # your kill switch: create a file named STOP
return "stopped"
_bid, ask = best_prices(ticker)
if ask is None:
return "no offer"
cost = ask + taker_fee(ask, 1)
if fair - cost < MIN_EDGE:
return "no edge"
if position(ticker) >= MAX_CONTRACTS:
return "position limit"
return place_order(ticker, "bid", ask, 1, "fill_or_kill")
Notice the order of the checks. The kill switch comes first, the limits come before the order, and the order is fill or kill. Write these before you write anything clever. A strategy with a small mistake loses a little. A bot with no limits and a small mistake loses until someone notices.
Keep the request limits in mind as well. On the Basic tier, Kalshi's rate limits work out to about 20 reads and 10 new orders per second. That's plenty for one strategy, and one more reason to use the stream for prices.
Is This Allowed?
Kalshi publishes the API so that members can trade their own accounts by program, and its own quick start lists automated strategies as a next step. Its developer agreement limits API use to your own trading, so read it before you build anything that trades for other people. Whether you may use Kalshi at all depends on where you live. Our guide on whether Kalshi is legal covers that.
From a Script to a Bot
What you have now is a working script. A bot you can leave running needs more, and this is where most of the work is:
- Staying connected. Reconnect when the stream drops, take a new snapshot, and never trade on a book you're unsure of.
- Knowing your own state. Partial fills, orders still resting, and positions that changed while you were offline.
- A second venue. The biggest edges on Kalshi come from comparing it with something else: Polymarket, or a sharp sportsbook. That means matching the same event across venues, placing two orders, and handling the case where only one fills. We cover that failure in naked leg arbitrage.
- A paper mode. A way to run the whole thing with pretend orders against live prices, as in our guide to prediction market paper trading.
- Records. Every decision logged, so you can tell a bad strategy from a bug.
If you'd rather not build and maintain all of that, Claw Arbs is a finished desktop app that does it. It trades Kalshi alongside Polymarket, Polymarket US and sportsbooks, with agents for arbitrage, value betting and market making, player props included, and with paper trading and risk limits built in. The Kalshi bot page shows how it's set up, and our comparison of Kalshi bots covers the other options. New members can start with a 7-day free trial.
Frequently Asked Questions
Does Kalshi have an API for trading bots?
Yes. Kalshi has a REST API and a WebSocket stream. Market data such as markets, prices and order books can be read without logging in. Anything on your account, such as your balance, orders and positions, needs requests signed with your API key. Kalshi also runs a separate demo exchange with mock funds for testing.
Why does older Kalshi bot code fail in 2026?
Kalshi changed several things during 2026. Prices are now dollar strings in fields ending in _dollars and the old whole-cent fields were removed, market orders no longer exist, and orders go to a new endpoint while the old one answers with an error. Code written for the older API has to be updated in all three places.
Can I test a Kalshi bot without risking money?
Yes. Kalshi's demo environment is a separate exchange with its own signup, its own API keys and mock funds. Point your bot at the demo host, create a key there and place test orders. Keys from the demo do not work on the real exchange, and the other way round.
Do I need Kalshi's official Python package?
No. The official packages are kalshi_python_sync and kalshi_python_async, and they save typing, but the API is plain HTTPS with three signed headers. This guide uses the requests, cryptography and websockets packages so that every step is visible. The older kalshi-python package is deprecated.
How many orders per second can a Kalshi bot send?
On the Basic tier, Kalshi's rate limit works out to about 20 read requests and 10 new orders per second. Higher tiers allow more. For a bot that reacts to price changes, the price stream matters more than the request limit, because it removes the need to poll.
Can a Kalshi bot trade player props?
Yes. Player props are ordinary Kalshi markets, so the same calls work. For example, the series KXNBAREB holds NBA player rebound markets, such as a player to get 12 or more rebounds. Props are usually thinner than game winner markets, so check the order book depth before sizing an order.
Bottom Line
Building a Kalshi trading bot in Python is a weekend of work if you use the API as it is today: dollar-string prices, signed requests, the new order endpoint and the price stream. Start on demo, put the limits in before the strategy, and count the fee in every decision. The script is the easy half. Running it unattended across more than one venue is the hard half, and that's the part Claw Arbs is built for.