Base URL
Authentication
Same API-key headers as Orders:Credential introspection
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
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.
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.

