You don't need a computer science degree to build a Kalshi trading bot. You need Python, an API key, and about an hour. This tutorial takes you from zero to a working bot that monitors markets and places trades automatically — with real code you can run today.
By the end of this guide, you'll have a Python bot that authenticates with Kalshi, fetches market data, evaluates a simple strategy, and places orders. We'll build it step by step, explaining every line. If you'd rather not write code, compare ready-made options in our roundup of the best Kalshi trading bots.
Prerequisites
- Python 3.9+ installed on your machine
- A Kalshi account with API access enabled
- Your Kalshi API credentials (key ID + downloaded RSA private-key file) from account settings
- Basic Python knowledge (variables, functions, loops)
If you've never used Python, this tutorial will still work — but consider running through a quick Python basics course first. If you'd rather skip coding entirely, our no-code bot builder lets you create bots visually.
Project Setup
Create a new project directory and install the required packages:
mkdir kalshi-bot && cd kalshi-bot
python3 -m venv venv
source venv/bin/activate
pip install requests python-dotenv cryptography
Create a .env file for your credentials (never hardcode API keys):
KALSHI_API_KEY_ID=your-key-id-here
KALSHI_PRIVATE_KEY_PATH=/absolute/path/to/your-kalshi-private-key.key
KALSHI_BASE_URL=https://external-api.demo.kalshi.co/trade-api/v2
Start with Kalshi's demo environment and demo credentials. Production uses https://external-api.kalshi.com/trade-api/v2; credentials do not carry between demo and production. See Kalshi's current environment and endpoint reference.
Step 1: Authentication
Kalshi does not use a Bearer secret for trading requests. Each authenticated call carries a key ID, millisecond timestamp, and RSA-PSS signature of timestamp + HTTP method + request path. Query parameters are not part of the signed path. Here's a minimal client:
import base64
import os
import time
import requests
from urllib.parse import urlparse
from dotenv import load_dotenv
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
load_dotenv()
class KalshiClient:
def __init__(self):
self.base_url = os.getenv("KALSHI_BASE_URL")
self.key_id = os.getenv("KALSHI_API_KEY_ID")
self.session = requests.Session()
with open(os.environ["KALSHI_PRIVATE_KEY_PATH"], "rb") as key_file:
self.private_key = serialization.load_pem_private_key(
key_file.read(), password=None
)
def _headers(self, method, path):
timestamp = str(int(time.time() * 1000))
sign_path = urlparse(self.base_url + path).path.split("?")[0]
message = f"{timestamp}{method.upper()}{sign_path}".encode("utf-8")
signature = self.private_key.sign(
message,
padding.PSS(
mgf=padding.MGF1(hashes.SHA256()),
salt_length=padding.PSS.DIGEST_LENGTH,
),
hashes.SHA256(),
)
return {
"Content-Type": "application/json",
"KALSHI-ACCESS-KEY": self.key_id,
"KALSHI-ACCESS-TIMESTAMP": timestamp,
"KALSHI-ACCESS-SIGNATURE": base64.b64encode(signature).decode("utf-8"),
}
def get(self, path, params=None):
url = f"{self.base_url}{path}"
resp = self.session.get(
url, headers=self._headers("GET", path), params=params
)
resp.raise_for_status()
return resp.json()
def post(self, path, data=None):
url = f"{self.base_url}{path}"
resp = self.session.post(
url, headers=self._headers("POST", path), json=data
)
resp.raise_for_status()
return resp.json()
Test your connection:
client = KalshiClient()
balance = client.get("/portfolio/balance")
print(f"Account balance: ${balance['balance'] / 100:.2f}")
If you see your balance printed, you're connected. For a 401, verify that the key and private-key file belong to the same environment, the timestamp is a Unix epoch value with millisecond precision, and the signed path includes /trade-api/v2 but no query string. Kalshi's authenticated-request quickstart is the source of truth.
Step 2: Fetching Market Data
Let's fetch available markets and find one to trade:
def get_markets(client, status="open", limit=20):
"""Fetch open markets from Kalshi."""
params = {"status": status, "limit": limit}
data = client.get("/markets", params=params)
return data.get("markets", [])
def find_market(client, ticker):
"""Fetch a specific market by ticker."""
data = client.get(f"/markets/{ticker}")
return data.get("market", {})
# Example: list some markets
markets = get_markets(client)
for m in markets[:5]:
yes_ask = m.get("yes_ask_dollars")
shown_ask = f"${yes_ask}" if yes_ask is not None else "N/A"
print(f"{m['ticker']}: {m['title']} — YES ask: {shown_ask}")
Current market responses use fixed-point strings such as yes_bid_dollars, yes_ask_dollars, and volume_fp. Parse them deliberately rather than depending on the legacy integer-cent fields removed during Kalshi's fixed-point migration.
Step 3: Building a Simple Strategy
Let's build a simple mean-reversion strategy: when a market's YES price drops sharply (more than 10 cents below its recent average), we buy — betting that the drop is an overreaction.
class SimpleStrategy:
def __init__(self, ticker, lookback=10, threshold=10, max_position=5):
self.ticker = ticker
self.threshold = threshold # cents below average to trigger buy
self.max_position = max_position # max contracts to hold
self.price_history = []
self.lookback = lookback
def update(self, yes_price):
"""Add a new price observation."""
self.price_history.append(yes_price)
if len(self.price_history) > self.lookback:
self.price_history = self.price_history[-self.lookback:]
def should_buy(self, current_price, current_position):
"""Return True if we should buy."""
if len(self.price_history) < self.lookback:
return False # not enough data yet
if current_position >= self.max_position:
return False # position limit reached
avg = sum(self.price_history) / len(self.price_history)
return current_price < (avg - self.threshold)
def should_sell(self, current_price, entry_price):
"""Return True if we should take profit."""
return current_price >= entry_price + 15 # take 15¢ profit
This is intentionally simple. The point isn't to run this strategy in production — it's to show the pattern. Your real strategy should be based on your own research and edge.
Step 4: Placing Orders
import uuid
def place_order(client, ticker, book_side, quantity, price_cents):
"""Place an order on Kalshi.
book_side: 'bid' buys YES; 'ask' sells YES
price_cents: price in cents (1-99)
"""
order = {
"ticker": ticker,
"client_order_id": str(uuid.uuid4()),
"side": book_side,
"count": str(quantity),
"price": f"{price_cents / 100:.4f}",
"time_in_force": "immediate_or_cancel",
"self_trade_prevention_type": "taker_at_cross",
"cancel_order_on_pause": True,
"reduce_only": book_side == "ask",
}
result = client.post("/portfolio/events/orders", data=order)
filled = float(result["fill_count"])
print(
f"Order processed: {book_side.upper()} x{quantity} @ {price_cents}¢; "
f"filled {filled}"
)
return filled
This uses Kalshi's current V2 event-order shape. The older /portfolio/orders payload uses action/yes_price, but Kalshi recommends new integrations use the single-book V2 endpoint with bid/ask and fixed-point dollar prices. Immediate-or-cancel keeps this teaching loop from pretending a resting order is already a position; a production bot should persist the order ID and reconcile every fill.
Step 5: The Bot Loop
Now let's tie it all together into a bot that runs continuously:
import time
def run_bot(ticker, check_interval=30):
"""Main bot loop."""
client = KalshiClient()
strategy = SimpleStrategy(ticker, lookback=10, threshold=10, max_position=5)
current_position = 0
entry_price = None
print(f"Bot started — watching {ticker}")
print(f"Checking every {check_interval} seconds")
while True:
try:
market = find_market(client, ticker)
yes_ask_dollars = market.get("yes_ask_dollars")
yes_bid_dollars = market.get("yes_bid_dollars")
if yes_ask_dollars is None:
print("Market closed or no price available")
time.sleep(check_interval)
continue
yes_price = round(float(yes_ask_dollars) * 100, 2)
exit_price = (
round(float(yes_bid_dollars) * 100, 2)
if yes_bid_dollars is not None else None
)
strategy.update(yes_price)
print(f"[{ticker}] YES: {yes_price}¢ | Position: {current_position}")
# Check buy signal
if strategy.should_buy(yes_price, current_position):
print(f"BUY SIGNAL at {yes_price}¢")
filled = place_order(client, ticker, "bid", 1, yes_price)
if filled:
current_position += filled
entry_price = yes_price
# Check sell signal
elif (
entry_price and exit_price is not None
and strategy.should_sell(exit_price, entry_price)
):
print(f"SELL SIGNAL at {exit_price}¢ (entry: {entry_price}¢)")
filled = place_order(client, ticker, "ask", 1, exit_price)
current_position -= filled
if current_position <= 0:
current_position = 0
entry_price = None
except Exception as e:
print(f"Error: {e}")
time.sleep(check_interval)
# Run the bot
if __name__ == "__main__":
run_bot("YOUR-MARKET-TICKER-HERE")
Step 6: Adding Risk Controls
Never run a bot without risk controls. Here's a simple risk manager:
class RiskManager:
def __init__(self, max_daily_loss=500, max_position_size=10):
self.max_daily_loss = max_daily_loss # cents
self.max_position_size = max_position_size
self.daily_pnl = 0
def can_trade(self, position_size):
if self.daily_pnl <= -self.max_daily_loss:
print("RISK: Daily loss limit reached. Stopping.")
return False
if position_size >= self.max_position_size:
print("RISK: Position limit reached.")
return False
return True
def record_trade(self, pnl):
self.daily_pnl += pnl
Integrate this into your bot loop by checking risk_manager.can_trade() before every order.
Running Your Bot
# Activate your virtual environment
source venv/bin/activate
# Run the bot
python bot.py
For production deployment tips (running 24/7), see our complete bot guide.
Next Steps
You now have a working Kalshi bot. Here's where to go from here:
- Deep dive into the Kalshi API →
- Deploy your bot to production (run it 24/7) →
- Explore testable strategy ideas →
- Explore Kelly position sizing →
Or skip the coding entirely:
Frequently Asked Questions
Quick answers to common questions about How to Build a Kalshi Bot with Python (Step-by-Step Guide).
What do I need to build a Kalshi bot in Python?
Python 3.9+, a Kalshi account with API access enabled, and an API key pair (key ID + private key) generated from your account settings. The official requests library plus the cryptography package for request signing cover everything in this tutorial — no paid dependencies.
How does Kalshi API authentication work?
Kalshi uses RSA request signing. You sign a string made of the timestamp, HTTP method, and request path with your private key, then send the signature, your key ID, and the timestamp as headers. The server verifies it against your public key. The walkthrough above shows the exact signing code.
Can I test a Kalshi bot without risking real money?
Yes. Kalshi offers a demo environment with a separate base URL and demo credentials, so you can place and cancel orders against simulated balances before pointing the same code at production. Always validate order logic and risk limits in demo first.
How often can my bot poll Kalshi for market data?
Kalshi uses token-based read and write buckets whose per-second budgets depend on your API tier. Inspect GET /account/limits, account for each endpoint's token cost, and back off on HTTP 429 responses. For fast-moving markets, prefer WebSockets over tight REST polling.
Should I build from scratch or use a platform?
Building in Python gives you full control and is great for learning, but you own hosting, monitoring, reconnection, and risk plumbing. If you'd rather skip the infrastructure, a hosted builder handles execution and guardrails for you — many traders prototype in code, then move to a platform for reliability.
Spin to win up to 30% off your first month
Every spin wins 10–30% off Complete ($99/month) — the wheel decides how big. Enter your email in the game to spin.
First month only. No account is created until you purchase. See our Privacy Policy.