Skip to main content
Kairos issues API keys (the kairos_ck_... triples) for users who need to call the platform from scripts, bots, or other non-browser clients. This page covers what a credential looks like, what each scope unlocks, which procedures are reachable, and how the auth errors are distinguished — read it before your first authenticated call. One key authenticates to both:
  • The order execution service at https://execution.kairos.trade/ (submit / cancel / query orders).
  • The RPC server at https://rpc.kairos.trade/api/rpc/ for a curated subset of read and trading procedures.
API keys use a curated allowlist. Wallet creation, copy-trading subscription management, and browser withdrawal flows remain available only through the web app. Owner-signed Polymarket batches retain their existing programmatic access through the wallet RPC procedures below. Access is opt-in per procedure and defaults to closed, so a newly added procedure is never programmatically reachable until someone explicitly allowlists it.

Credential format

A credential is a three-tuple handed out once at creation time. Lose it and you can’t recover it — create a new one. All three must be included on every authenticated request as HTTP headers:
The server stores SHA-256 hashes of the key and secret and compares them in constant time — both comparisons run even when the first fails, so response timing doesn’t reveal which field was wrong. Plaintext values are never persisted. Credentials do not expire. There is no TTL or expiry date on a credential; it stays valid until it is revoked, or until the owning account is deleted or suspended (either of which makes every one of that user’s keys fail authentication immediately).
Gotcha: send only the API-key headers. Authenticate with X-Client-Id / X-Api-Key / X-Api-Secret alone. When any of the three X-Api-* headers is present the server authenticates on them exclusively and will not fall back to a cookie or Authorization JWT, so a malformed key header fails authentication outright. If your HTTP client injects an Authorization header automatically, disable it for these requests.
CSRF is not required. CSRF protects against browsers auto-attaching cookies to cross-site requests. API keys live in explicit X-Api-* headers that browsers never add automatically, so the RPC server skips the CSRF check when a request authenticates via API key.

Quick start — Python httpx

The same three headers work against the order execution service:
That call requires the trade:execute scope.
Gotcha: the two services use different field casing. Order execution request bodies are snake_case, not the camelCase used by RPC payloads — exchange_id, market_id, token_id, kind (not orderType), quantity (not size). A field mismatch (e.g. sending marketId instead of market_id) fails with a plain deserialization 400, not a helpful validation error — double check the payload shape if you get an unexpected 400.

Scopes

Every credential carries a scopes list. RPC matching accepts exact scopes and *; additionally, position:read and trade:read satisfy an RPC read requirement, while trade:execute satisfies RPC trade. These relationships are one-way. Order execution requires the exact scope and does not treat * as a wildcard. Only three scopes are actually issuable: position:read, trade:read, and trade:execute. The bare read / trade names below are the requirements procedures declare, not values you can be granted; * is a legacy wildcard the server still honours but the issuing flow does not hand out. Because the two services grew their scope checks independently, the naming isn’t uniform — the RPC server uses bare read/trade for most of its own procedures but issues only the colon-namespaced position:read / trade:read / trade:execute. The inheritance above is what bridges them.
Gotcha: inheritance is strictly one-way. position:read grants read, but read never grants position:read. A key carrying only trade:execute gets 403 FORBIDDEN calling positions.getPositions. Ask whoever issued your credential which scopes it carries and make sure it holds every scope your integration needs.

Platform access

Passing a scope check does not guarantee access to every provider. Kairos can disable API-key access to a provider independently of its browser/JWT availability. Selected RPC procedures explicitly enforce this gate, as do provider-scoped order execution requests; kalshi_offchain is checked as kalshi. For RPC calls, a disabled provider returns 403 FORBIDDEN with API access is disabled for <provider>. A settings lookup failure returns 500 INTERNAL_SERVER_ERROR with Unable to verify platform API access.
Gotcha: an aggregating procedure fails as a whole if any provider it touches is disabled. An unfiltered positions.getPositions or balances.getAllBalances request may check several providers at once. Add the narrowest available provider or chain filter when you only need one venue.
Order execution uses a separate error envelope: a disabled provider returns 403 with error_details.code: "AUTH_INSUFFICIENT_SCOPE", while an unavailable access check returns 503 with code "INTERNAL_ERROR". Some status-only endpoints omit the structured body. This provider gate is distinct from the execution killswitch: platform access affects API-key clients only, while an execution killswitch blocks new order submission for all callers. See REST authentication for the cross-API behavior.

What’s allowed

Exchange balances and wallet operations

The public executor balance and wallet REST routes have moved to RPC. These read-only procedures continue to accept API keys: Kalshi balances remain in USD and include portfolio_value and the original by_shard exchange indexes. Allowance amounts remain decimal strings. polymarket.getDepositWalletNonce and polymarket.syncBalances require a browser session. The generic polymarket.submitSignedBatch and polymarket.getSignedBatchStatus procedures have been removed. Browser withdrawals use the session-bound signing and status procedures. Granting allowances still uses POST /exchanges/{exchange_id}/allowances on the executor.

read — general queries (RPC server)

  • markets.getMarkets, getAllMarkets, getMarketsPage, getTokenOutcome, getTrendingMarkets, getTrendingItems
  • polymarket.getMarkets, hasCredentials, getWalletInfo
  • exchange.getActiveExchanges, hasCredentials
  • combo.getComboMarkets
  • perpetuals.venues
  • txodds.listFixtures, getFixture, getFixtureTimeseries
  • lpRewards.getMyRewards
  • infra.executionManifest
  • wallet.list, wallet.getForChain
  • walletTransactions.list, getRecent, getStats, getById
  • deposit.polymarketBridge, polymarketBridgeSupportedAssets, polymarketBridgeQuote, polymarketBridgeStatus, evmWalletBalances
  • auth.me, user.getMe, user.getKalshiExecutionMode
  • trading.getStatus, trading.getKairosPublicKey
  • fees.getUserTier, getUserVolumeMetrics, getFeeTiers
  • rewards.getMyPoints, getMyHistory, getMyBreakdown
  • referrals.getMyStats, getLeaderboard, getShareLink, getMyReferrer, getMyInvitees
  • tournaments.getActive, competitions.getActive, getLeaderboard, getRankEvents
  • invites.listMine, getAllocation, getVolumeProgress
  • geo.getMyCountry, checkAccess, checkAccessBulk
  • featureFlags.get, getMany

position:read — position/portfolio/balance queries (RPC server)

  • positions.getPositions — authoritative current holdings
  • positions.getPosition
  • wallet.getPortfolio — unified personal + trading-account portfolio view
  • wallet.discoverTokens
  • portfolio.getSummary, getActivity, getChartData, getDailyPnLBreakdown, getSettlements
  • balances.getWalletBalances, getAllBalances, getGasEstimate, getBalanceForAddress
  • polymarket.getEoaBalances, getSafeBalances
  • combo.getComboHistory
  • pnl.getPnL
  • copytrading.listSubscriptions, allOpenPositions, subscriptionOpenPositions

trade:read — order/fill history

  • orders.getChartFills (RPC server)
  • copytrading.traderFeed (RPC server) — grouped with the queries above by feature, but gated on trade:read, not position:read
  • GET /orders, GET /orders/{order_id}, GET /orders/fee-quote (order execution service, execution.kairos.trade)

trade — the one RPC trade mutation

  • positions.closePosition — derives close-order parameters for a specific position. See Positions.

trade:execute — execution mutations

Order submission and cancellation are not RPC procedures. They are REST endpoints on the order execution service, authenticated with the same three headers:
  • POST /orders — submit a market or limit order
  • POST /orders/{order_id}/cancel — cancel one order
  • POST /orders/cancel-all — cancel every open order on an exchange (optionally scoped to one market_id)
  • POST /orders/cancel-batch — cancel a specific set of orders in one call
Other order-execution handlers explicitly accept trade:execute, including supported redeem and CTF split/merge flows. Treat the endpoint’s documented scope as authoritative; API-key mutation access is allowlisted, not inferred from the HTTP method.

What’s NOT allowed

A number of high-risk or account-lifecycle operations are only reachable via the web app. Calling any of these with an API key returns 403 Forbidden:
  • Session / credential lifecycle — issuing, revoking, or listing session tokens; enabling or revoking exchange credentials.
  • Wallet lifecycle — creating, renaming, or managing the underlying custodial wallets.
  • Profile changes — updating username, display name, execution-mode preferences, or any other profile fields.
  • Position maintenance — positions.recalculatePosition, updatePosition, backfillPositions, syncFromPolymarket, syncFromPredictfun, and the order-reconciliation mutations (orders.reconcilePolymarketFills, orders.backfillTrades).
  • Copy-trade subscription management — copytrading.createSubscription, updateSubscription, pauseSubscription, resumeSubscription, cancelSubscription, followTrader, and the bulk* variants. (Reading subscription state is allowed.)
  • Fund movement — wallet.transfer and the Hyperliquid deposit / transfer / withdraw prepare+submit pairs.
  • Invites, referrals, notifications, and integrations — managing invites, linking third-party accounts (Telegram, Discord), creating support tickets, saved layouts, or banners.
If you have a use case that needs programmatic access to something in this list, reach out through Kairos support — the right answer is usually a new purpose-built endpoint with its own narrow scope, not a broader API key.

IP whitelisting

A credential can optionally be pinned to a specific source IP (or list of IPs). When a whitelist is set, requests from any other IP return 403 Forbidden (Source IP is not in the credential's whitelist). An empty whitelist means no IP restriction. Matching is an exact string comparison against the last X-Forwarded-For hop — the entry the load balancer appends, which a caller cannot forge because spoofed entries can only be prepended. CIDR ranges are not supported: list each address individually. The check runs on every request and is never cached. If your bot runs on a floating IP (residential cloud, CI/CD pipeline, etc.), ask the issuer to leave the whitelist empty. Empty = no restriction.

Rate limits

Rate-limit buckets are keyed on the credential ID, not on the owning user. Two credentials belonging to the same user get independent budgets, and a per-key override applies to exactly one window. Server defaults (per minute), all overridable per deployment and per credential: A credential can carry absolute per-bucket overrides (rpc.public / rpc.auth / rpc.mutations / rpc.queries / rpc.expensive), plus separate budgets for order submission, the data API, websockets, and the market-data API. Ask the issuer what your key is set to.
Gotcha: 429 responses carry no Retry-After header. The delay is in the error message — Rate limit exceeded. Please retry in <N> seconds.

Key rotation

To rotate:
  1. Request a new credential from Kairos.
  2. Update your client to use the new X-Api-Key / X-Api-Secret.
  3. Verify requests succeed.
  4. Have Kairos revoke the old credential (sets status='revoked').
Revoked credentials fail with 401 Unauthorized — there’s no grace period. Revocation publishes a cache invalidation so every replica of every service drops the credential in near-real-time; if that pub/sub path is unavailable, passive expiry still bounds the window at the credential cache’s TTLs (15 s in-process, 60 s in Redis). Plan rotations accordingly. There is also no “rotate in place” operation — a rotation is always create new, then revoke old, and the plaintext of the new credential is shown exactly once at creation.

Errors

Every error is the standard tRPC envelope (error.message, error.code, error.data.code, error.data.httpStatus, error.data.path) — see Overview. Auth failures short-circuit with their own status rather than collapsing to a generic 401 — a whitelist block stays a 403, a backend outage stays a 500. 401 and 403 are intentionally distinguished: 401 means “your credentials are wrong or revoked”, 403 means “your credentials are fine but you cannot call this thing from here.” Handle them differently in your client.