Repository navigation
Kalshi
Typed client for Kalshi's CFTC-regulated event-contract
(prediction market) REST API, /trade-api/v2, for Deno, Bun, Node.js and
Cloudflare Workers. Public market data needs no key. RSA-PSS-signed trading
covers balance, positions, fills and orders, and placing, amending and
cancelling orders.
One Kalshi client covers both halves of the API:
-
Market data (public, no auth) —
getMarkets()/getMarket(),getOrderbook(),getTrades(),getEvents()/getEvent(),getSeriesList()/getSeries(),getExchangeStatus(). -
Portfolio/orders (RSA-PSS-signed) —
getBalance(),getPositions(),getFills(),getOrders()/getOrder(),submitOrder()/submitOrders()(single/bulk — no fixed per-request cap, unlike Polymarket's 15),amendOrder(),cancelOrder()/cancelOrders(), andcancelAllOrders()(a client-side sweep — Kalshi has no cancel-all endpoint).
Construct with auth.accessKey + auth.privateKeyPem to trade; omit
auth entirely for market-data-only, credential-free usage.
Kalshi and Polymarket are both event-contract exchanges this repo covers
with a deliberately similar shape — the same price/OrderSide/
OrderResult conventions, and a shared method vocabulary across market
data, portfolio reads, and order management:
| Capability | Kalshi | Polymarket |
|---|---|---|
| Market data |
getMarkets(), getOrderbook()
|
getMarkets(), getOrderbook()
|
| Balance | getBalance() |
getBalance({ tokenId? }) |
| Positions | getPositions() |
getPositions() (Data API) |
| Orders on the venue |
getOrders(), getOrder()
|
getOpenOrders() |
| Executions | getFills() |
getFills() |
| Place |
submitOrder(), submitOrders()
|
submitOrder(), submitOrders()
|
| Cancel |
cancelOrder(), cancelOrders()
|
cancelOrder(), cancelOrders()
|
| Cancel everything |
cancelAllOrders() (client sweep) |
cancelAllOrders() (DELETE /cancel-all) |
| Cancel by market | — (no vendor endpoint) | cancelMarketOrders() |
| Amend | amendOrder() |
— (CLOB has no amend; cancel + resubmit) |
Beyond that the vendors differ enough that full parity isn't honest to force:
-
Auth: Polymarket signs with a wallet's secp256k1 key (needs
@noble/curves, since Web Crypto doesn't implement that curve). Kalshi signs with an RSA-PSS-SHA256 keypair issued from its own dashboard — a standard Web Crypto algorithm, so this connect stays@tundralibs-only with no extra dependency. There's also no Polymarket-style one-time L1→L2 credential-derivation step:accessKey+privateKeyPemARE the ready-to-use credential. -
No on-chain component: Kalshi settles positions directly on its own
ledger — there is no CTF/outcome-token model, so
split()/merge()/redeem()(and the Relayer they depend on) have no Kalshi equivalent. -
No
order()dispatcher: Polymarket'sorder()unifies five distinct actions (BUY/SELL/SPLIT/MERGE/REDEEM) behind one entry point. Kalshi only ever has one order-placing action (side: 'BUY'|'SELL'onsubmitOrder()), so a dispatcher would just besubmitOrder()under a different name — not implemented. -
OrderResult.filledCount/remainingCountreplace Polymarket'smakingAmount/takingAmount— those describe a maker/taker ASSET-LEG split (USDC vs. shares) that doesn't exist here; a Kalshi order only ever moves one thing, contracts.
submitOrder()/submitOrders()/amendOrder()/cancelOrder()/
cancelOrders(), and every /portfolio/* read, all sign for real: this client
holds the account's RSA private key in memory for the lifetime of the
instance and produces genuine RSA-PSS-SHA256 signatures — the same
credential Kalshi's own dashboard issues, not a delegated call to one. The
key is consumed once at construction (as DER bytes), imported into a
non-extractable Web Crypto CryptoKey on first use, and never logged,
returned, or included in a thrown error. Review KalshiSigner.ts/
KalshiAuth.ts yourself before trusting this client with real money.
-
The authenticated WebSocket feed (
/trade-api/ws/v2— fills, order updates, position changes) is a different integration shape than this repo's REST-only connects; whether/how to add one is an open decision, not yet built. - Order Groups, Multivariate Event Collections, Structured Targets, and the FIX protocol are advanced/institutional surfaces outside this connect's REST-trading scope.
-
decreaseOrder()(a legacy size-only-decrease endpoint) isn't wired up —amendOrder()already covers a size decrease (and a price change), which is this connect's one order-modification method. -
A single cancel-all endpoint doesn't exist on Kalshi's side;
cancelAllOrders()is a client-side sweep overGET /portfolio/orders?status=resting— a snapshot, not a lock. See API →cancelAllOrders(). - Historical candlesticks (
GET .../candlesticks) and the separate historical-markets surface aren't covered.
Your tests don't need to fake HTTP. Have your code take a Kalshi instance
and pass in a stand-in that returns the shapes from
@tundraconnect/kalshi/schemas or throws a real KalshiError:
import { KalshiError } from '@tundraconnect/kalshi/errors';
const outage = new KalshiError('SERVICE_UNAVAILABLE', { status: 503 });
console.log(outage.code);To exercise the client itself against a fake transport, subclass it and
reassign the protected _fetch.
| Topic | Description |
|---|---|
| API | Client configuration and endpoint methods |
| Errors | Error codes and diagnostic metadata |
| Schemas | Public Guardian schemas and inferred types |
Deno:
deno add jsr:@tundraconnect/kalshiBun:
bunx jsr add @tundraconnect/kalshiNode.js:
npx jsr add @tundraconnect/kalshiimport { Kalshi } from '@tundraconnect/kalshi';
// Market data only — no credentials needed.
const markets = new Kalshi({});
const { markets: page } = await markets.getMarkets({
status: 'open',
limit: 20,
});
const market = page[0];
console.log(market?.ticker, market?.yesBid, market?.yesAsk);
// Trading — auth.privateKeyPem is the PKCS#8 PEM from Kalshi's API-key settings.
const client = new Kalshi({
auth: {
type: 'CUSTOM',
accessKey: Deno.env.get('CONNECTOR_KALSHI_ACCESS_KEY')!,
privateKeyPem: Deno.env.get('CONNECTOR_KALSHI_PRIVATE_KEY_PEM')!,
},
});
const balance = await client.getBalance();
console.log('available cents:', balance.balance);
const result = await client.submitOrder({
ticker: market!.ticker,
side: 'BUY',
price: 0.42,
count: 3,
orderType: 'GTC',
});
if (result.filled) {
console.log(`filled ${result.filledCount} at ${result.actualPrice}`);
}MIT. Part of Tundra Connect, typed vendor API clients for Deno, Bun, Node.js and Cloudflare Workers.