Kalshi Portfolio and Position Endpoints for Bot State Tracking
Four independent endpoints track bot state; reading any two will cost you capital or payouts.

A Kalshi trading bot's correctness depends on how faithfully it reads its own state. Balance, open positions, order status, and settlement outcomes each arrive from a different endpoint with its own semantics, and none of them can stand in for another. A bot that reads only one or two of these sources operates on a partial world model. It will miscount the capital it can deploy, double-count exposure, or miss payouts that have already resolved. The article that follows maps each endpoint's actual behavior, the partition that splits live from historical data, and the sequence a bot must run to stay coherent over a full trading lifecycle.
What a Kalshi trading bot must know about its own state
A Kalshi bot tracks four state domains at once, and each one answers a question the others cannot. Cash balance tells the bot what capital it can deploy on the next order. Settlement outcomes tell it what has resolved and paid out. These are not redundant views of the same underlying data, so a faithful mental model of bot state means accepting that no single endpoint, and no pair of endpoints, provides a complete answer. A bot that treats its available balance as derivable from positions, or its positions as derivable from orders, has already made the error that leads to double-counted exposure and missed returns.
The cost of getting this wrong compounds with every polling cycle. What matters for a developer is not that these four domains exist, but that they are sourced independently and must be reconciled deliberately. The sections that follow establish the shape of each source.
What GET /portfolio/balance and GET /portfolio/positions actually return
The positions endpoint does not return a single list. It returns two distinct data structures, market_positions and event_positions, that aggregate exposure at different levels, and a bot that parses only one will build an incomplete picture of its book. market_positions entries carry ticker, total_traded_dollars, position_fp, market_exposure_dollars, realized_pnl_dollars, fees_paid_dollars, and last_updated_ts. Those fields describe exactly one market's worth of state: what the bot traded, what it holds, what the position is worth, what it has already realized in profit or loss, and what it has paid in fees. event_positions entries operate one level up. They carry event_ticker, total_cost_dollars, total_cost_shares_fp, event_exposure_dollars, realized_pnl_dollars, and fees_paid_dollars. A bot that netted a position across both sides of a market, or across several markets in the same event, needs the event-level share count to understand what it actually paid and what it actually holds.
Two defaults in this endpoint deserve equal attention. A bot that reads the first page and stops will silently miss positions beyond it, and no error will announce that omission. The default alone is not a complete picture, and the gap between what the endpoint returns by default and what a bot needs for accurate state is exactly the kind of silent failure that undermines trading logic.
How the live-to-historical partition splits position data across two endpoints
Kalshi partitions exchange data into live and historical tiers to keep live endpoints fast, and that partition creates a mandatory two-query pattern for any bot that needs complete position history. The partition does not use one shared cutoff window. For positions specifically, the cutoff is not the moment a position becomes visible in the historical API. A settled position appears in GET /historical/positions only after its entire event completes a buffered, whole-event handoff. The cutoff timestamp for positions, market_positions_last_updated_ts, is the backfill horizon, not a precise handoff timestamp. A position whose last update predates that cutoff can still be absent from the historical endpoint if its event's handoff has not completed.
The two reads are not an atomic snapshot. A bot should page through GET /portfolio/positions?settlement_status=all first, then page through GET /historical/positions. Positions move in that direction during archival, so this order reduces the chance of missing a row that moves between reads. Resting orders do not follow this pattern at all. They always appear in GET /portfolio/orders regardless of the cutoff, an asymmetry that matters for any bot tracking positions and orders together. And both position endpoints default to subaccount 0, so a bot managing multiple subaccounts must repeat the live query and the historical query for each subaccount explicitly.
What GET /portfolio/orders and GET /portfolio/settlements add to the state picture
The orders endpoint supplies state that neither positions endpoint covers, and it follows the same tiered partition with one important exception. Cursor-based pagination applies here as it does for positions, and the same discipline of following the cursor to exhaustion is required. The state picture would be incomplete without them, because fills record the actual executions that changed positions and cash.
Settlements introduce a gap that no other endpoint closes. GET /portfolio/settlements reads only settled positions still in the live data set. Once archival happens, GET /historical/positions provides position, realized P&L, and fee data, but there is no historical endpoint with the same settlement-record fields. A bot that needs settlement records as its own state domain, rather than deriving equivalents from position data, must capture them before archival removes them from the live tier. That is a hard constraint on bot design, and it explains why settlement tracking belongs in the state model from the start rather than as an afterthought that can be backfilled later.
What queue position means and why a bot must query it correctly
Queue position is not a measure of market depth. It is the count of contracts that must be matched before a given order receives a partial or full match, determined by price-time priority, and a bot that ignores it cannot know how much of a resting order is likely to fill or how soon. The endpoint is GET /portfolio/orders/{order_id}/queue_position, and the response is a single field: queue_position_fp as a fixed-point string. The format matters, because a value like "10.00" is a string, not a number, and a parser that converts it incorrectly will silently corrupt the state it feeds into execution logic. A queue position of zero means the order is first in line, with no contracts ahead of it at its price level under price-time priority. The developer who reads the response and treats zero as a null value, or as an error code, has inverted the meaning of the field.
Price-time priority means queue position is not stable between order placement and order fill. It changes whenever another order at the same price is placed ahead of this one, which can happen if that order was placed earlier. It changes when a fill occurs for any order ahead in the queue. And it resets when the order itself is amended, because amendment resets time priority. For a bot polling many resting orders, Kalshi's batch queue position endpoint at GET /portfolio/orders/queue_positions returns positions for multiple orders in one request, and it is the appropriate tool rather than issuing one request per order. Queue position is live-only by nature. It applies to resting orders, which always appear in the live orders endpoint and are unaffected by the historical cutoff, and there is no historical queue position to query.
How the October 2026 MCP grant changes what a bot can read without a full write key
As of October 15, 2026, registered MCP clients can read portfolio balances, positions, orders, and fills using an explicitly granted read::mcp_portfolio OAuth access. The practical consequence for bot architecture is direct: a state-tracking component that only reads portfolio state can now be credentialed separately from the component that executes orders. The blast radius of a compromised key shrinks accordingly, because a read-only credential cannot turn into unauthorized trades or withdrawals even if it leaks. The grants are per-client and explicitly granted, which means the access boundary is a design choice rather than a default.
Market data follows a lighter path. A bot reading market data for pricing or order placement decisions does not need a read key for those endpoints at all. The architectural point is that credentials can now be assigned at the granularity of what a component actually does, and a bot that separates read-only state tracking from execution can reflect that separation in how it holds and rotates its keys.
Session token expiry as a state-tracking hazard in long-running bots
A long-running bot that does not implement token refresh logic will lose authenticated access mid-operation, and the resulting failures are easy to misread. Authentication uses RSA-PSS signatures over a message combining timestamp, HTTP method, and path, and the timestamp component is what ties the request to a valid session window. A bot must generate a fresh signature for every request. When that does not happen, the failure mode is subtle. A 200 response with an empty market_positions array is a legitimate data condition. An auth failure surfaces differently, but a bot that treats both as "no open positions" will act on an incorrect belief about its own book.
The hazard is not theoretical for bots that run for hours or days. Every other part of the state model depends on the bot being able to read it, and a session that expires silently turns the entire query sequence into a no-op that still produces what looks like data. A bot that verifies auth before its reads, and that distinguishes auth failures from empty payloads, can at least tell the difference between a missing position and a missing ability to see positions.
Why testing this state machine against the live API is the wrong foundation
The full state model a bot must track, balance across endpoints, two position tiers, orders, settlements, queue position, session validity, and the partition itself, is too complex to verify against the live API in a development or CI workflow. Testing against live couples the bot's development loop to rate limits, real credentials, and real capital. The error that a simulator would catch harmlessly becomes an actual trading loss or a phantom position when it happens against production.
The partition's two-query requirement is especially hard to test live because triggering the handoff cannot be controlled. A developer would need to wait for real events to resolve and archive, on the exchange's schedule, before observing how the bot handles the transition from live to historical positions. What the bot's test environment actually needs is a simulator that remembers state across calls, enforces the live-to-historical partition logic, respects settlement status filters, returns queue position responses that update as the simulated book changes, and allows the developer to inject specific states on demand, such as a partially archived event, a stale session, or an empty positions page. Running position and balance queries against a simulator with pre-seeded multi-page, multi-subaccount test data lets developers verify their parsing and filtering logic end-to-end before integration, and this is the capability that Rystic's Kalshi simulator ships with, verified against the live API before each release. Rystic sits alongside other verified simulator providers as one option for getting this foundation right without touching live credentials or rate limits, and for a bot whose bugs become real trades, that foundation is not a convenience but a precondition.
The correct query sequence for full bot state on every polling cycle
The order of queries in a polling cycle is not arbitrary. Earlier reads establish the context that later reads depend on, and a coherent sequence is what keeps the state model from drifting into self-contradiction. A bot with a correct model runs its reads in this order.
First, refresh the session and verify auth. Then page through GET /portfolio/positions?settlement_status=all with cursor pagination, repeated per subaccount. This captures live and recently settled positions, and the paging must run to exhaustion before proceeding. Then page through GET /historical/positions with cursor pagination, also repeated per subaccount, and deduplicate against the previous step's results by subaccount and ticker. Then query GET /portfolio/orders with cursor pagination, which returns resting and recent orders, with resting orders always present regardless of the historical cutoff. Then query GET /portfolio/orders/queue_positions with the batch endpoint for all resting orders identified in the previous step. Then query GET /portfolio/settlements for settlement records on resolved markets still in the live data set, bearing in mind that after archival this data has no historical equivalent and must have been captured earlier. Finally, read GET /historical/cutoff for the current cutoff timestamps, specifically market_positions_last_updated_ts, to track the backfill horizon for the two-query position pattern. A bot built before the partition rollout and left unchanged now undercounts its own history.
Two structural facts deserve restatement because they are the hardest edge cases in the whole model. And the cutoff timestamp is a horizon, not a promise. It anchors the boundary region where a bot must reconcile repeatedly, because it is specifically near that boundary, during the buffered whole-event handoff, that a position can be visible in neither tier or in both. The bot that treats that boundary as a static line will periodically misread its own state. The bot that treats it as a region to re-reconcile will catch the transition and hold the full picture through the archival move.


