Trade API — trade.slyderlabs.com

The Kalshi bridge behind Shawn's desktop trading system (2026-11-02). Admin-only. Raw passthrough: the bridge signs and forwards; it does not size, cap, simulate or reorder anything. Every order call is journaled (trade.db order_log and the log hub under app trade).

Auth

Sign in once at https://auth.slyderlabs.com; the estate cookie sid (domain .slyderlabs.com) is your session. Every call carries that value as token: GET → query ?token=…; POST → JSON body field "token"; WebSocket → query ?token=…. Responses: 401 {"error":"no session"} (cookie missing or expired → send the user to https://auth.slyderlabs.com/login?return=…), 403 {"error":"admin only"}, 409 {"error":"no signing key configured …"} until a key is picked.

Response envelope for everything that reaches Kalshi

{ "ok": true|false, "kalshi_status": 201, "kalshi": { …Kalshi's body, verbatim… }, "error": "…only when !ok (Kalshi's message)" }

Our HTTP status mirrors Kalshi's: 2xx → 200, anything else → the same code (400, 401, 403, 404, 409, 429, 500…). 502 = the bridge could not reach Kalshi at all ({"error": "…"}, no kalshi field).

Settings

GET /api/me

{ "username", "is_admin", "app":"trade", "signing_key", "key_ready", "books_connected", "account": { connected, key_name, subscribed[], last_msg_ms, error, since_ms } }

GET /api/keys

Registry keys (console → API Keys): { "keys": [{ "name", "key_id_masked", "notes" }] }. No secrets ever leave the server.

GET /api/settings · POST /api/settings/set {key, value}

keyvalueseffect
signing_keya registry key name, or "" signs every read and order; changing it rebuilds the client, re-spawns the order-book feed and reconnects the account stream
account_stream"1" | "0"keep the fill / user_orders WebSocket on the signing key and fan it out on /api/stream

POST /api/test_connection

GET /portfolio/balance with the signing key. Adds key_kind (rsa | ed25519) and key_name on success.

Portfolio reads (query forwarded verbatim)

Each maps 1:1 to Kalshi's endpoint; every query parameter except token is passed through (e.g. limit, cursor, ticker, event_ticker, status, settlement_status, min_ts, max_ts).

oursKalshi
GET /api/balance/trade-api/v2/portfolio/balance
GET /api/positions/portfolio/positions
GET /api/orders/portfolio/orders (use status=resting for the open book)
GET /api/order/{order_id}/portfolio/orders/{id}
GET /api/fills/portfolio/fills
GET /api/settlements/portfolio/settlements

Orders — raw passthrough

No net. No demo wall, no caps, no dedupe. What you send is what Kalshi gets (plus a generated client_order_id when you omit one, and exchange_index: 3 on baseball tickers when you omit it — the shard rule every estate app follows). A repeated client_order_id is idempotent on Kalshi's side; use your own ids if you retry.

POST /api/order

Forwarded to POST /trade-api/v2/portfolio/events/orders — Kalshi's single-YES-book order body. Fields other than token are forwarded as-is.

{ "token": "…",
  "ticker": "KXNBAGAME-26NOV02LALGSW-LAL",
  "side": "bid" | "ask",            // on the YES book: bid = buy YES (or sell NO), ask = sell YES (or buy NO)
  "count": "10.00",                 // contracts, fixed-point string
  "price": "0.5500",                // YES price in dollars, fixed-point string. Buying NO at 45¢ = ask @ 0.5500
  "time_in_force": "good_till_canceled" | "immediate_or_cancel" | "fill_or_kill",
  "client_order_id": "optional — generated as trade-<uuid> when omitted",
  "self_trade_prevention_type": "taker_at_cross",   // optional
  "post_only": false, "reduce_only": false, "expiration_ts": 0   // optional, Kalshi semantics
}

Mapping from a human ticket (action × side × price of that side, cents): buy yes @p → bid, price p · buy no @p → ask, price 100−p · sell yes @p → ask, price p · sell no @p → bid, price 100−p.

Reply: Kalshi's { "order": { order_id, client_order_id, status: resting|executed|canceled, remaining_count_fp, fill_count_fp, yes_price_dollars, … } } inside the envelope.

POST /api/order/cancel {order_id}

→ DELETE /portfolio/orders/{id}. Reply carries the order with its final state (409 from Kalshi if already filled / canceled).

POST /api/order/decrease {order_id, reduce_by: "3.00"} · POST /api/order/amend {order_id, …}

→ POST /portfolio/orders/{id}/decrease / /amend; body minus token and order_id forwarded.

POST /api/orders/batch {orders: [ {…order body…}, … ]}

→ POST /portfolio/orders/batched. Each order gets a client_order_id and baseball exchange_index if missing.

GET /api/log?limit=50 · GET /api/events?limit=100

log = this bridge's order journal (every create/cancel/decrease/amend/batch with request, Kalshi's response and status, by user and key). events = account-stream fills / order updates the bridge saw (last 20,000).

Market data proxy (GET only)

GET /kalshi/{path}?…

Signed server-side with the signing key and forwarded to /trade-api/v2/{path} with the query. Allowed paths: markets, markets/{ticker}, markets/{ticker}/orderbook, markets/trades, events, events/{event_ticker}, series, series/{s}, series/{s}/markets/{t}/candlesticks, exchange/status, exchange/schedule, exchange/announcements. Anything under portfolio is refused (use the endpoints above, which journal).

GET /kalshi/markets?series_ticker=KXNBAGAME&status=open&limit=100&token=…
GET /kalshi/markets/KXNBAGAME-26NOV02LALGSW-LAL/orderbook?depth=10&token=…

WebSocket /api/stream

wss://trade.slyderlabs.com/api/stream?token=… — one socket per client. Text frames, JSON both ways.

client → servermeaning
{"op":"sub","tickers":["T1","T2"]}start streaming those order books (the bridge holds one Kalshi WS and shares books across clients); a full book snapshot arrives at once
{"op":"unsub","tickers":[…]}stop pushing them to this socket
{"op":"ping"} · {"op":"status"}→ pong / a fresh status
server → clientshape
hello{type, ts_ms, user, text} on connect
status{type, books_connected, account_connected, account_enabled, account_error, key_name} on connect and every link change
book{type, ticker, yes: [[price_cents, contracts]…], no: [[…]], age_ms} — the FULL current book (bids resting on each side, ascending price), coalesced to ≥ 50 ms per ticker. Best YES bid = last yes row; the price to BUY YES = 100 − best NO bid (last no row)
fill{type, ts_ms, sending_ts_ms, ticker, msg} — Kalshi's fill channel message for the signing account
order{type, ts_ms, ticker, msg} — Kalshi's user_orders message (status / last_update_reason / remaining)
error{type, error} e.g. no signing key

The account channel is the signing account's — if that key is a shared account, you see every app's fills on it. Orders never travel over the socket; place them with POST so each one is journaled.

Operational notes