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}
| key | values | effect |
|---|---|---|
signing_key | a 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).
| ours | Kalshi |
|---|---|
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
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 → server | meaning |
|---|---|
{"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 → client | shape |
|---|---|
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
- Service
kalshi-tradeon 127.0.0.1:8087; nginx routes/api/,/api/stream(upgrade),/kalshi/,/healthz; static root/var/www/trade. - Journal:
~/kalshi-trade/trade.db(order_log,events,settings); admin console Activity under apptrade(kindstrade-order,trade-order-rejected,trade-cancel,trade-decrease,trade-amend,trade-batch,trade-fill,trade-setting). - Kalshi rate limits are the key's (10 req/s shared across every app on that key). The bridge adds no pacing.
- Shards: a key sees only its own shard's balance and orders. Baseball lives on shard 3 (
exchange_indexadded automatically).