Live trading and risk
Every real order goes through one in-process guard on your machine, with hard limits, a kill switch, reconciliation and an audit log. Kalshi from the command line; Polymarket US from the library.
On this page
Every order the runtime sends to an exchange goes through one in-process gate, the live guard (langbot.live.LiveGuard). A strategy cannot reach the exchange around it. The guard is not tied to one venue: Kalshi plugs in as KalshiVenue, Polymarket US as PolymarketUSVenue.
langbot live run my_strategy.py # Kalshi demo (the default)
langbot live run my_strategy.py --dry-run # check and log every order, send nothing
langbot live run my_strategy.py --env prod --yes-real-money
langbot live halt [--cancel-orders] [--reason "..."] # stop new orders now
langbot live resume # clear the halt
langbot live status # halt state + latest audit lines
langbot live limits [--strategy Name] [--env prod] # the limits in forcelangbot run --mode live still works and goes through the same guard; langbot live run is the recommended entry.
Environments
| Kalshi demo | Kalshi prod | |
|---|---|---|
| selected by | default, --env demo | --env prod --yes-real-money |
| REST | external-api.demo.kalshi.co | external-api.kalshi.com |
| money | play money | real money |
empty series_allow | every series | nothing |
A Kalshi client built for prod refuses to send or amend an order unless it was created with allow_prod_orders=True (which only --yes-real-money does). The kalshi.env value in config.toml never switches langbot live run to prod. Polymarket US has no demo, so its adapter always counts as prod.
Hard limits (~/.config/langbot/live.toml)
Checked before every send, in this order. If the file is missing, the defaults below apply: the smallest limits, never "no limit". An unknown key is an error, so a typo cannot drop a limit. [strategy.<Name>] (the strategy's class name) can tighten [account] but never loosen it.
| Key | Default | Rejects an order when |
|---|---|---|
series_allow / series_deny | demo ["*"], prod [] / [] | the series (ticker before the first -) is not allowed, or is denied |
min_price / max_price | 0.02 / 0.98 | the price is outside the band |
max_contracts_per_order | 5 | the order is larger |
max_order_usd | 5.00 | contracts × risk per contract is larger (risk is price for a buy of YES, 1 − price for a sell) |
| (fresh book) | [halt] stale_book_s = 10 | there is no book for the market from the last 10 s |
max_price_through_cents | 3 | a bid is more than 3¢ above the best ask, or an ask more than 3¢ below the best bid |
max_daily_loss_usd | 10.00 | today's realised + marked loss has reached the limit (this also halts) |
max_position_contracts | 10 | position + resting orders on that side + this order would exceed it (per market) |
max_position_usd | 10.00 | the same in dollars at risk |
max_total_exposure_usd | 25.00 | dollars at risk over all markets (positions + resting orders + this order) would exceed it |
max_open_orders | 5 | that many orders are already resting |
max_orders_per_minute | 10 | that many orders were sent in the last 60 s |
An order that only shrinks a position toward zero skips the position and exposure checks. Fills an ack reports before the fill message arrives count as exposure at $1 a contract until they are confirmed. The daily loss is the change in equity (cash + positions at the book mid) since 00:00 UTC or the session start. Fees are included. A position held from earlier takes its first mark as the baseline, so old losses do not count as today's.
[account]
max_contracts_per_order = 5
max_order_usd = 5.0
max_position_contracts = 10
max_position_usd = 10.0
max_total_exposure_usd = 25.0
max_open_orders = 5
max_orders_per_minute = 10
max_daily_loss_usd = 10.0
max_price_through_cents = 3
series_allow = ["KXFEDMENTION"]
[strategy.MentionSniper]
max_order_usd = 2.0
[halt]
stale_book_s = 10
reconcile_interval_s = 15
max_consecutive_api_errors = 3
max_clock_skew_s = 3.0
cash_tolerance_usd = 0.05
cancel_on_halt = trueKill switch and automatic halts
A halt is a file, ~/.config/langbot/live/HALT. While it exists, no live process on the machine sends an order. Every order checks it, and running processes poll it every 0.25 s. langbot live halt writes it, and --cancel-orders also makes running processes cancel their resting orders. The guard writes the same file when it halts itself, so a halt survives a restart. Only langbot live resume clears it. A running process that sees the file go re-reads positions, orders and cash from the exchange before it trades again.
The guard halts on its own, and by default cancels resting orders, when:
- the daily loss limit is reached;
- reconciliation finds a mismatch with the exchange that is still there one second later;
- 3 API errors in a row happen;
- a book goes stale: a market where we have resting orders has had no update for 10 s and a REST refresh fails;
- the clock is skewed: the local clock is more than 3 s off the exchange's
Dateheader (Kalshi); - an order's state is unknown: a send timed out and the order could not be looked up.
Reconciliation
At start, and on every resume, the guard takes Kalshi's GET /portfolio/positions (position_fp, market_exposure_dollars), GET /portfolio/orders?status=resting and GET /portfolio/balance (balance_dollars) as the truth. Every 15 s it compares what it believes (positions, resting order ids and remaining counts, cash) with the same three calls. A mismatch is logged with both sides, re-checked once after 1 s to absorb a fill message still in flight, and then halts. Trading the same account by hand, or from a second process, will halt the guard. Use a separate account or subaccount for the bot.
Idempotent sends
Each order gets a client order id before its first send, and every retry reuses it unchanged. On a timeout, a dropped connection or a 5xx, the guard waits 1 s and looks the order up by that id. It resends only when the exchange confirms the order is not there. If the lookup itself fails, it halts instead of guessing. A 409 (duplicate id) is looked up and never resent. A 4xx means the order was not placed. Polymarket US has no server-side client id, so an ambiguous submit there always halts.
Audit log
~/.config/langbot/live/audit-YYYY-MM-DD.jsonl: append-only, one fsynced JSON line per intent, limit rejection, send, ack, uncertain send, lookup, venue rejection, fill, cancel, reconciliation, halt and resume, each with a millisecond timestamp and a session id. Keys that look like credentials are masked before they are written.
Recommended ladder
langbot live run strategy.py --dry-runon demo, then on demo for real, for at least one full event. Read the audit log.- Backtest over at least 10 archived events. Look at max drawdown and no-trade rate, not just P&L.
--env prod --dry-runfor one event: the same checks against real books, nothing sent.--env prod --yes-real-moneywithmax_order_usd = 1,max_daily_loss_usd = 5and an allowlist of one series.- Raise limits only after a week of live fills behaves like the backtest.
Polymarket US
langbot live run trades on Kalshi. Polymarket US is available from the library: PolymarketUSClient for orders and PolymarketUSVenue, which plugs into the same live guard.
US residents trade Polymarket on Polymarket US: a CFTC-regulated exchange (QCX LLC, doing business as Polymarket US) with USD accounts and identity checks. It is a separate venue from the international Polymarket, which does not accept new orders from the US. The SDK's PolymarketUSClient connects to Polymarket US only, and you trade with your own account and API key.
Account and key
- Install the Polymarket US app, create an account and finish identity verification (legal name, address and SSN, and sometimes an ID and a selfie). Eligibility, including whether your state is supported, is checked during this step.
- Sign in at
polymarket.us/developerwith the same sign-in method you used in the app, and create an API key. You get a Key ID and a Secret Key. The secret is shown only once. - If you plan to send more than about 20 requests a second, email Polymarket US support with your use case to ask for higher limits.
Configure
| Variable | Meaning |
|---|---|
POLYMARKET_KEY_ID | your Key ID |
POLYMARKET_SECRET_KEY | your Secret Key (base64) |
POLYMARKET_US_ALLOW_ORDERS=1 | turns on order entry; leave it unset to read only |
Keep these values in your own environment or secret store, and never commit them. You don't need a key for order books and market data.
from langbot.polymarket_trading import PolymarketUSClient
async with PolymarketUSClient(ticker_map={"pm_516710": "your-us-market-slug"}) as pm:
book = await pm.get_orderbook("pm_516710") # public, no key needed
balance = await pm.get_balance() # {"balance": cents, "buying_power_dollars": ...}
order = await pm.create_order(
"pm_516710", "bid", 10, 0.42, post_only=True, client_order_id="my-run-001"
)
await pm.cancel_all("pm_516710")Name the market
Polymarket US markets have their own slugs, for example paccc-usho-midterms-2026-11-03-dem. Langbot's pm_<id> tickers refer to markets on the international venue, and Polymarket doesn't publish a mapping between the two. You can pass a US slug directly, or map a pm_ ticker to the US market you mean with ticker_map. Before you map them, check that the two markets have the same rules. An unmapped pm_ ticker raises an error instead of being guessed.
How it maps to the Kalshi client
PolymarketUSClient uses the same method names as KalshiClient, so the strategy runtime and risk rails can drive either one.
KalshiClient | PolymarketUSClient | Polymarket US endpoint |
|---|---|---|
get_markets, get_market | same | GET /v1/markets, GET /v1/market/slug/{slug} (public) |
get_orderbook → Book | same | GET /v1/markets/{slug}/book (public) |
get_balance (cents) | same, plus balance_dollars, buying_power_dollars | GET /v1/account/balances |
get_positions | same keys (ticker, position_fp, market_exposure_dollars, realized_pnl_dollars) | GET /v1/portfolio/positions |
get_orders(status="resting") | open orders only | GET /v1/orders/open |
create_order(ticker, side, count, price, tif, client_order_id, post_only) | same, plus optional intent= | POST /v1/orders |
amend_order | same, then re-reads the order | POST /v1/order/{id}/modify |
cancel_order | same | POST /v1/order/{id}/cancel |
cancel_all | same | POST /v1/orders/open/cancel |
There are some differences to know about:
- Sides. Each market trades a single YES instrument, and every price is a YES price. A
bidbuys YES. Anasksells YES that you hold. If you hold none, anaskbuys NO at1 − price, which is the same position. If you hold less YES than the size of yourask, the order is refused, so split it or passintent=explicitly. client_order_id. Polymarket US has no idempotency key. The client tracks ids locally: if you reuse an id that already placed an order, you get that order back and nothing new is sent. An order whose outcome is unknown, such as after a timeout or a 5xx error, raisesPolymarketUSAmbiguousOrderand is not retried. Checkget_orders()first, then callpm.registry.resolve(...)orpm.registry.forget(...). PassClientOrderIdRegistry(path)to keep the ids across restarts.- Time in force. Supported values are
good_till_canceled,fill_or_killandimmediate_or_cancel.post_only=Truebecomes the exchange's maker-only flag. - Fees. Polymarket US publishes its own fee schedule; it is the authority.
- Order flag. Orders are flagged as placed by an automated system. Pass
manual=Trueonly for orders a person enters by hand.
Things that will hurt you
- Venue order delays. Kalshi may delay orders on live-event markets (Rule 13.1(i)). Polymarket runs taker delays of 50 ms to 3 s. A strategy that only works with sub-second fills is fragile. Prefer resting orders and breadth.
- Settlement during a session. A settled market leaves the positions list and moves cash, so reconciliation halts. Resume after you check it.
- Rules nuance. "Any company representative" includes the operator, plurals usually count, and a phrase said by an analyst in Q&A usually does not.
get_market_rulessummarises this, but read the actual rules text before trading. - ASR confidence. Homophones happen ("Blackwell" vs "black well"). Gate on
confidence ≥ 0.9and, where it matters, onspeaker.name. - Clock drift. Keep NTP on. The guard halts past 3 s of skew, and Kalshi rejects badly skewed signatures.
Nothing here is financial advice. You can lose money, and in live-event markets you can lose it in under a second.