Skip to main content
Two reads a bot should make before it quotes anything: what this credential is actually allowed to do, and what inventory it actually has free. Both are designed so you never have to infer state from an error code — a rejected order is an expensive way to discover a missing scope, and a silently-degraded read is a worse way to discover you had no inventory view at all.

Base URL

Authentication

Same API-key headers as Orders:

Credential introspection

Reports the authenticated credential’s effective authority. Enforces no scope by design — a credential must be able to discover what it holds without already holding something — and only ever describes the caller’s own account.
user_id is the stable Kairos user id. It survives API-key rotation — key your own records on it, never on client_id. auth_method is api_key or session. A session JWT carries no scope set, so scopes reports the full set for that caller rather than a granted list. scope_model is explicit when the credential carries that exact grant (every API key), and unrestricted for a session — a session is not scope-limited, so its scopes list names what this service and the RPC layer gate on and is not a platform-wide inventory. Other services define their own scopes. A self-service key carries one of two presets — readonly is read, trade:read, position:read; trading adds trade:execute. Note read: it gates the RPC query procedures (fees.getUserTier, rewards.getMyPoints, lpRewards.getMyRewards, the balances.* reads), and it is separate from trade:read. Scope matching is exact string equality — read:positions does not satisfy read, and no scope implies another. Venue linkage is cached for up to 10 seconds, so trading_linked can lag a just-completed enable-trading call by that much. Nothing else here is cached. rate_limits carries only the overrides on this key. An absent field means the key uses the service default, not that no limit applies. No secret material is returned. client_id is the value you already sent; nothing derived from the API secret is echoed.

The three gates

An order passes three independent checks, and they fail for unrelated reasons. The endpoint reports them separately so you can tell which one is in your way: Predict.fun stores no venue credentials: it signs with your execution wallet, so trading_linked there means that wallet resolves. Whether its on-chain approvals are in place is enabled on the RPC query exchange.getAllowances. api_access_enabled: null means the flag read failed. Treat it as unknown, never as disabled.
Gotcha: these gates are independent. api_access_enabled: true with trading_linked: false is the common case behind a 404 from the RPC query exchange.getKalshiBalance — the platform allows API-key access to Kalshi, but this account has never run POST /exchanges/kalshi/enable-trading, so there are no credentials to read a balance with. The 404 means “no credentials”, not “no endpoint”.

Position exposure

Requires the position:read scope. Returns the authenticated user’s live positions, bucketed so that “absent” is never ambiguous.

Position identity

A position is keyed by all four of (exchange_id, market_id, token_id, holding_wallet). The same market held in two wallets is two positions; collapsing on token_id alone will merge them and overstate what you can sell from either.

The four buckets

Reservation provenance

available_to_sell is net_size minus what in-flight sells have already reserved. That subtraction only exists on the cell-local store. Two things have to hold before available_to_sell is real, and they fail independently, so they are reported separately. A position gets reservations_verified: false when it was seeded from central storage — after a cell restart or state loss — and the query that rebuilds its resting sells did not answer. We deliberately keep recording fills through that window rather than wedging your order path, which means the position is briefly committed with an empty reservation set while real sells may rest at the venue. That is exactly the state this flag exists to expose. It is self-clearing: the next operation that touches the position reconciles it against the open-order ledger before acting, so an unverified row is a transient condition rather than a standing one.
Gotcha: never size a sell off a row with reservations_verified: false, or off any response with reservations_authoritative: false. The numbers are gross size, not free inventory, and treating them as free inventory is how you oversell a position that already has resting sells against it. The condition is transient — re-read until the row verifies.Even on a verified row, bounding every sell by your own outstanding-order book as well costs nothing and removes your dependence on our freshness entirely. Our own web client does that rather than trusting available_to_sell alone.

A sell can be refused while reservations are unverified

Because the same reservation set the exposure read reports is what the oversell gate subtracts, we refuse rather than guess. If you submit a sell against a position whose reservations cannot be confirmed, the order is rejected with a retryable error — “Couldn’t validate your sell against your position right now. Please retry.” — not with an insufficient-size error. That distinction is deliberate. An insufficient-size error carries an available quantity, and shrinking to it and retrying would walk straight into the oversell we just refused. A retryable rejection carries no quantity, so retry is the only correct response. It clears as soon as the reservation set confirms. Buys and fill processing are unaffected — this gate is on the sell-reservation path only. as_of is the server clock when the snapshot was assembled. Use it to order two reads and to age a cached one, rather than stamping a receive-time yourself.

Balances

Cash lives on the RPC API, not here. balances.getAllBalances returns one spendable row per venue, each naming its custody domain (chain or kalshi_usd); balances.getWalletBalances returns the per-chain detail and buyingPower. Both accept the same API-key headers and require position:read. There is no grand total. Chain USDC and Kalshi USD sit in different custody domains, so a figure spanning both is not spendable by any order. Add the rows of one domain to size an order in that domain. Every venue row carries available. A row with available: false could not be read this fetch — its usdValue is the 0.00 sentinel. The response also carries a top-level degraded boolean, and getWalletBalances carries degradedReads keyed by leg.
Gotcha: degraded: true means any figure you add up under-reports — a chain RPC failed, not that the wallet is empty. Sizing off a degraded read will under-quote; treating a degraded venue as empty and skipping it will strand inventory. Back off and re-read instead.