Skip to main content
Execution: venue.trade() in chat code, strategy.execute() in strategy agents. Data reference: Kalshi Data.

Overview

Kalshi is a CFTC-regulated exchange for binary event contracts. Winning contracts settle at $1; prices are probabilities in roughly [0.01, 0.99]. Scalar Field connects to your own Kalshi account — we never create a trader account on your behalf. Cash stays on Kalshi; deposits and withdrawals happen at Kalshi, never through us.

Connecting

  1. Sign in at kalshi.com/account/profile and click Create API Key. Kalshi shows the private key exactly once — copy both the API Key ID and the PEM private key.
  2. From Portfolio → Kalshi (or the venue card), open Connect and paste the API Key ID and private key.
  3. Scalar Field validates the key against Kalshi, then stores it Fernet-encrypted for trading and portfolio reads.
  4. Disconnect removes our stored copy only; your key stays active on Kalshi.
The stored key is never granted write::transfer. Perpetual futures (Kalshi margin) are not supported in this integration.

Execution and accounting model

This is the contract for audit-grade use of venue.trade, venue.history, venue.account, and venue.cancel on Kalshi. Field meanings come from Kalshi’s Trade API: V2 orders are a single YES book, and fill outcome_side / book_side describe directional exposure, not a second price scale.

Symbol model

Discover markets with screenKalshiMarkets(). Do not invent tickers. Pass symbol to trade YES and symbol_no to trade NO. Bare tickers are treated as the YES leg. Positions, fills, and history use the same handles. Kalshi stores a signed position_fp per market: positive = YES, negative = NO. Scalar Field splits that into one position row per held outcome, matching Polymarket’s one-row-per-outcome shape.

Book identities

Kalshi’s public order book returns bids only on each leg. Asks are implied by complementarity (Kalshi orderbook docs):
The V2 create path quotes that same book from the YES leg only:
  • bid = buy YES (economically sell NO)
  • ask = sell YES (economically buy NO)
venue.trade converts your (action, outcome, limit_price) into that YES-leg (side, price) before submit. avg_price on the trade response is converted back to the outcome you passed in.

Quantity rules

Kalshi contract counts are fixed-point strings with minimum granularity 0.01 (Fixed-Point Representation). Requests accept 0–2 decimal places; responses emit 2 decimals. This integration truncates toward zero (ROUND_DOWN) to 0.01 before submit. If the result is 0, the order is rejected locally — Kalshi never sees it.
More than two decimal places are not rounded to nearest; they are truncated. Size from a dollar budget with qty = int(budget / price * 100) / 100, then confirm the resting/filled qty — do not assume the exchange kept extra precision.

Order support

Kalshi’s V2 create path (POST /portfolio/events/orders) requires price and time_in_force. There is no native market order type. Off-grid limit prices are snapped, not rejected at our layer. Kalshi may still reject a price that falls outside the market’s price_ranges after snapping. During the Thursday 03:00–05:00 ET pause, placement fails with TRADING_PAUSED; cancels still work. Resting GTC orders are submitted with cancel_order_on_pause=false, so they survive the pause. Settlement is automatic. Winning contracts pay $1; there is no redeem call (REDEEM_NOT_SUPPORTED). Settled positions appear in venue.history(category="settlement").

Cash vs buying power

Kalshi’s GET /portfolio/balance returns balance_dollars (documented as available USD) and portfolio_value (open-position valuation in cents). It does not return a buying-power field. venue.account(account="KALSHI") therefore looks like:
When buying_power is missing or None, use cash as the available USD balance. Clients that do buying_power or 0 will conclude the account cannot trade. cash is USD on the Kalshi account. It does not include position mark-to-market; that lives in positions[].market_value / portfolio_value.

Resting-order cash reservation

Kalshi does not publish a reserved_cash (or equivalent) on the direct-member balance payload. GET /portfolio/total_resting_order_value is documented as FCM-only and is not used here. Observed behavior, consistent with that API:
  • A resting GTC bid appears in open_orders immediately.
  • cash (balance_dollars) often does not decrease while the order rests.
  • The matching engine still enforces availability; a bid that cannot be collateralized is rejected at place or fill time.
Do not treat cash as “unreserved buying power” and stack resting bids that sum to more than cash without checking open_orders. A workable available-cash estimate when buying_power is null:
Treat that as an estimate. Kalshi’s availability check is authoritative.

History semantics and cash formulas

This is the accounting trap. Kalshi fills carry both yes_price_dollars and no_price_dollars (they sum to 1) plus canonical direction fields (Order direction): Unified venue.history(category="trade", account="KALSHI") maps those as: The general venue invariant amount == qty * price * multiplier holds. amount is not cash. Closing a held YES contract at a YES price of $0.43 is a sell-YES, which Kalshi encodes as outcome_side=no. History then looks like:
Gross cash credited by the YES book is the YES price:
qty * price (0.57) is the wrong proceeds figure.

Quoted notional vs cash

Reconstruct cash from a history row. V2 cash follows the YES book: bid pays yes_price, ask receives yes_price.

Worked examples (1 contract, ignore fees first)

Buy-NO opening a short YES credits the YES price. That is the V2 single-book identity: buying NO is selling YES. Terminal PnL still matches “paid 0.57forNO"oncethe0.57 for NO" once the 1 complementary payoff is included at settlement. Net cash is always gross − fees. Do not use:
Settlement rows (category: "settlement") are different: amount is revenue (payout), closed_pnl is already revenue − cost_basis − fees, and price is payout per contract. Portfolio venue.history merges Kalshi live fills/settlements with /historical/* so completed activity older than the rolling ~3-month live cutoff is not dropped. Public market-data helpers do not do that merge — see Kalshi Data.

Fees

Kalshi charges exchange fees plus a rounding fee so balances stay on the member’s precision grid (0.0001fordirectmembers).Netfee=tradefee+roundingrebate,always0.0001 for direct members). Net fee = trade fee + rounding − rebate, always ≥ 0 (Fee rounding). Kalshi order responses may omit finalized fees. Use venue.history(category="trade", account="KALSHI") for authoritative fee and cash accounting. Scalar Field does not add a platform fee on Kalshi; fees on history is Kalshi’s fee_cost. There is a short delay before fills, balances, and positions show up on REST (Get User Data Timestamp). After a fill, prefer the order response for filled_qty / avg_price, then re-read venue.history / venue.account rather than treating the synchronous fees field as final.

Cancel

venue.cancel(broker_order_id, account="KALSHI") is supported. The primary path is Kalshi V2: DELETE /portfolio/events/orders/{order_id}. Success: Pass symbol when you have it (market_ticker is required for V2 auto-routing). Cancels still work during the weekly trading pause. Failure is status="FAILED" with reason set to the exception string. Typical cases: Do not parse 410 / V1 deprecation text as “cancel is unsupported”. Resting-order cancel on V2 works. Treat any FAILED after a cancel as “confirm via open_orders / get_order” rather than retrying blindly.

Errors

Normalized reasons this integration emits itself: Everything else (insufficient balance, invalid price after snap, unknown order, already cancelled) is forwarded from Kalshi as FAILED + the HTTP/error body. Those strings are not a stable enum.

Usage

End-to-end: find a market, buy YES, verify, close the held qty, account for complement-encoded history, confirm flat.
Limit on the NO leg:
In strategy agents, use strategy.execute(symbol, target_qty) with brokerage_account="KALSHI".

Data Functions

Market data is public (no Kalshi connection required). Full reference, including sparse candles and live-vs-historical coverage: Kalshi Data.

Notes

  • Weekly maintenance pause: Thursday 03:00–05:00 ET — order placement fails with TRADING_PAUSED; cancels still work.
  • The stored API key cannot withdraw or transfer funds (write::transfer is never granted).
  • Perpetual futures (margin) are not supported in this integration.
  • Orders, fills, and settlements older than Kalshi’s ~3-month live cutoff are merged from /historical/* on portfolio reads (venue.history, positions, resting/completed orders). Public getKalshiTrades / getKalshiCandles also fail over to GET /historical/trades and GET /historical/markets/{ticker}/candlesticks.

Related