> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kairos.trade/llms.txt
> Use this file to discover all available pages before exploring further.

# Account Access & Exposure

> Read a credential's scopes and per-venue readiness, and read position exposure with its reservation provenance

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

```
https://execution.kairos.trade
```

## Authentication

Same API-key headers as [Orders](/rest/orders):

```
X-Client-Id: kairos_ck_...
X-Api-Key: <64 hex chars>
X-Api-Secret: <64 hex chars>
```

## Credential introspection

```
GET /account/scopes
```

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.

```bash theme={null}
curl -s https://execution.kairos.trade/account/scopes \
  -H "X-Client-Id: $KAIROS_CLIENT_ID" \
  -H "X-Api-Key: $KAIROS_API_KEY" \
  -H "X-Api-Secret: $KAIROS_API_SECRET"
```

```json theme={null}
{
  "user_id": "8f14e45f-ceea-467a-9f0a-1c2d3e4f5a6b",
  "auth_method": "api_key",
  "client_id": "kairos_ck_9f2a...",
  "scopes": ["read", "trade:read", "position:read", "trade:execute"],
  "scope_model": "explicit",
  "rate_limits": { "orders_per_minute": 600 },
  "venues": [
    { "exchange_id": "kalshi", "api_access_enabled": true, "trading_linked": false,
      "link_with": "POST /exchanges/kalshi/enable-trading" },
    { "exchange_id": "polymarket", "api_access_enabled": true, "trading_linked": true },
    { "exchange_id": "predictfun", "api_access_enabled": true, "trading_linked": true }
  ],
  "as_of": "2026-09-11T18:22:04.118Z"
}
```

`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:

| Field | What it means when false |
| - | - |
| `scopes` | The key was never granted the scope. Mint a new key with it. |
| `api_access_enabled` | The venue's platform gate for API-key callers is off. Ask us — you cannot fix this from your side. |
| `trading_linked` | *You* have no active credentials stored for the venue. Call the endpoint in `link_with`. |

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.

<Note>
  **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".
</Note>

## Position exposure

```
GET /positions/exposure
```

Requires the `position:read` scope. Returns the authenticated user's live positions, bucketed so that "absent" is never ambiguous.

```json theme={null}
{
  "positions": [
    {
      "token_id": "8610...36152",
      "market_id": "249495",
      "outcome": "Yes",
      "holding_wallet": "0xAbC...",
      "net_size": "1200",
      "available_to_sell": "900",
      "reserved": "300",
      "reservations_verified": true,
      "avg_entry_price_bps": 4200,
      "realized_pnl": "0",
      "last_trade_at": "2026-09-11T18:20:55Z"
    }
  ],
  "closed_token_ids": [],
  "closed": [],
  "resolved": [],
  "as_of": "2026-09-11T18:22:04.118Z",
  "reservation_source": "cell",
  "reservations_authoritative": true
}
```

### 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

| Bucket | Meaning |
| - | - |
| `positions` | Open, `net_size != 0`. |
| `closed` / `closed_token_ids` | Positively folded to flat. Authoritatively zero. |
| `resolved` | Market resolved. `redeemable: true` = won and claimable. |
| *absent from all four* | No current opinion — **not** zero. Leave your prior value alone. |

### 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.

| Field | Level | False means |
| - | - | - |
| `reservation_source` | response | `central` — this node has **no reservation view at all**. `reserved` is `0` and `available_to_sell` equals `net_size` because nothing was subtracted, not because nothing is reserved. |
| `reservations_verified` | per position | This row's reservation set was never confirmed against the open-order ledger. `reserved` is a **floor**, not a total. |
| `reservations_authoritative` | response | The one-field summary: source is `cell` **and** every returned row is verified. |

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.

<Warning>
  **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.
</Warning>

### 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.

<Warning>
  **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.
</Warning>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.