// PREDICTION-MARKET APIS
Rate limits, order types, and websockets on Kalshi and Polymarket
2026-07-16 · Mithril
Once your first order fills, the interesting problems stop being "how do I call the API" and become operational: which order type expresses what you actually want, how you keep a live view of the market without burning your request budget, and what your code does when the venue says slow down. This post covers that layer for both Kalshi and Polymarket — order types, WebSocket channels, rate limits, and the backoff patterns that separate bots that run for months from bots that get disconnected on day two.
Order types: what each venue gives you
Both venues are limit-order-book markets at heart, so the limit order is the native primitive everywhere. Around it:
| Concept | Kalshi | Polymarket |
|---|---|---|
| Limit order | Yes — rests at your price | Yes — rests at your price |
| Market order | Yes — crosses immediately | Marketable/fill-type orders that take liquidity |
| Time-in-force | Expiration options on orders (immediate-or-rest behavior; check current docs) | GTC plus time-bound and fill-or-kill-style types (check current docs) |
| Post-only | Check current docs — not guaranteed on either venue | Check current docs |
Venue order-type menus change more often than any other part of these APIs, so verify the current list against the docs rather than trusting any blog post — this one included. The durable advice is about usage:
- Default to limit orders. In thin books a market order is a blank check: the book on a quiet contract can be a few hundred dollars deep, and a market order walks straight through it. If you want immediacy, send a marketable limit — a limit priced at (or slightly through) the touch — which caps your worst fill. The gap between the two is quantified in slippage in thin prediction markets.
- Know your fee role. On Kalshi, takers pay the fee formula and resting makers generally pay no trading fee on most markets, so whether your limit order rests or crosses changes your costs, not just your fill probability.
- Express urgency in price, not type. A limit at the ask is "fill now, bounded price". A limit inside the spread is "I'll wait for a better price". That vocabulary works identically on both venues.
WebSockets: the only sane way to watch a market
Both venues expose WebSocket feeds, and for anything beyond casual polling you should use them — both because REST polling is stale by construction and because it eats your rate-limit budget.
Kalshi runs a WebSocket endpoint with subscription-based channels covering, among others: order book updates for chosen tickers, ticker/price updates, trade prints, and — authenticated — your own fills. You subscribe with a JSON command listing channels and market tickers, and receive deltas.
Polymarket's CLOB exposes WebSocket channels along a market/user split: a market channel for order book and price events on the token IDs you subscribe to, and a user channel (authenticated with your L2 credentials) for events about your own orders and trades — placement, matches, cancels.
Exact channel names and message schemas evolve; treat the docs as the source of truth. The architecture advice does not change:
- Snapshot, then stream. On connect, fetch a REST snapshot of the book, then apply WebSocket deltas. On any reconnect or sequence gap, throw the local book away and re-snapshot. Never patch a book you are not certain is contiguous.
- Heartbeat and staleness-check. Track the last message time per subscription. Silence beyond a threshold means your data is dead even if the TCP connection looks alive — stop trading on it.
- Reconnect with backoff and resubscribe idempotently. Reconnect storms are how bots hit connection-level rate limits.
Rate limits: budget like they're real, because they are
Both venues rate-limit REST requests, and the published tiers, windows, and per-endpoint costs change over time — Kalshi documents per-tier limits with higher tiers available on request, and Polymarket's CLOB enforces its own limits per endpoint class. Look the current numbers up; hard-coding them is a bug with a delay on it. What stays true:
- Reads are cheap to avoid; writes are not. Book and price data should come from WebSockets, reserving your REST budget for what only REST can do: placing, canceling, and reconciling orders.
- Cancels are sacred. Design your budget so that cancels always go through — being rate-limited out of canceling is strictly worse than being limited out of placing. A simple approach: separate token buckets for order placement and cancels/reads, sized so a placement burst can never exhaust the cancel bucket.
- Per-venue budgets. If one process trades both venues, never share a limiter across them; each venue meters you independently.
Backoff: the pattern, once, correctly
Every retry in your system — REST 429s, 5xxs, WebSocket reconnects — should go through one implementation of capped exponential backoff with jitter:
import random, asyncio
async def with_backoff(fn, *, base=0.5, cap=30.0, retries=6):
for attempt in range(retries):
try:
return await fn()
except RateLimited as e:
# Honor an explicit server hint if present
delay = e.retry_after or min(cap, base * 2 ** attempt)
await asyncio.sleep(delay * (0.5 + random.random()))
except TransientError:
delay = min(cap, base * 2 ** attempt)
await asyncio.sleep(delay * (0.5 + random.random()))
raise GaveUp()
The details that matter:
- Jitter is not optional. A fleet of workers (or one worker with many markets) that backs off deterministically retries in lockstep and re-triggers the limit forever.
- Honor server hints (a
Retry-Afterheader or equivalent) when the venue provides one. - Distinguish retryable from non-retryable. A 429 or timeout is retryable. A validation error or a signature error is not — retrying it is pure waste and, for order placement, dangerous without an idempotency check first. The full retry-with-idempotency pattern is in the Python bot guide.
- Degrade, don't die. When a market-data request keeps failing, mark that market stale and keep the rest of the bot alive. When order endpoints keep failing, stop quoting entirely — flying blind is worse than being flat. Note the asymmetry: data failures should narrow your trading; execution failures should stop it.
A worked failure: the cancel that couldn't
The classic incident shape, worth internalizing: a bot quoting several markets gets a burst of fills, tries to re-quote everything at once, hits the rate limit, and — because placements and cancels share one budget — its cancels start failing too. Now it has stale quotes resting in a moving market, which is precisely when you least want them, and each failed cancel is retried immediately, keeping it pinned at the limit. Every piece of advice above exists because of this loop: separate cancel budget, jittered backoff, and a kill switch that pulls quotes and halts placement when the system detects it is rate-limited on the order path.
The two-venue version
All of the above, times two: different channels, different limits, different order-type menus, different error shapes (the differences that bite has the catalog). Running that operational layer is undifferentiated work, which is why Mithril packages it — one API that fronts both venues with worked orders for thin books, server-side risk limits, and an execution receipt on every fill, so your code holds a strategy instead of two WebSocket stacks.
APIs change. Details above reflect public documentation as of July 2026 — always confirm against the venues' own docs before trading real money.
One API + MCP for Kalshi and Polymarket
Fee-aware routing, unified market IDs, and hard server-side risk limits — live today, free during beta.