Skip to content
github-actions[bot] edited this page Oct 4, 2026 · 2 revisions

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.

JSR JSR Score

Overview

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(), and cancelAllOrders() (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.

Compared to this repo's Polymarket connect

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 + privateKeyPem ARE 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's order() unifies five distinct actions (BUY/SELL/SPLIT/MERGE/REDEEM) behind one entry point. Kalshi only ever has one order-placing action (side: 'BUY'|'SELL' on submitOrder()), so a dispatcher would just be submitOrder() under a different name — not implemented.
  • OrderResult.filledCount/remainingCount replace Polymarket's makingAmount/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.

Key custody

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.

Out of scope (for now)

  • 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 over GET /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.

Testing your code

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.

Documentation

Topic Description
API Client configuration and endpoint methods
Errors Error codes and diagnostic metadata
Schemas Public Guardian schemas and inferred types

Upstream

Installation

Deno:

deno add jsr:@tundraconnect/kalshi

Bun:

bunx jsr add @tundraconnect/kalshi

Node.js:

npx jsr add @tundraconnect/kalshi

Quick Start

import { 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}`);
}

License

MIT. Part of Tundra Connect, typed vendor API clients for Deno, Bun, Node.js and Cloudflare Workers.

Clone this wiki locally