Real-Time Prediction Market Monitoring

Lesson 1 of 2

Setting Up Kalshi Market Streaming

Estimated time: 10 minutes

Setting Up Kalshi Market Streaming

Kalshi API Rate Limits

Kalshi allows 10 requests per second. For monitoring multiple markets, polling alone won't scale. You need smart strategies.

Strategy 1: Batch Polling

Fetch multiple markets in one request:

const fetch = require('node-fetch');

async function batchFetchMarkets(tickers) {
  const promises = tickers.map(ticker => 
    fetch(`https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${ticker}`)
      .then(r => r.json())
  );
  
  return Promise.all(promises);
}

const markets = await batchFetchMarkets(['KXSB', 'KXEL', 'KXMA']);

Strategy 2: Polling with Exponential Backoff

Handle rate limiting gracefully:

async function fetchWithBackoff(url, maxRetries = 3) {
  let delay = 100;  // Start at 100ms
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      const res = await fetch(url);
      
      if (res.status === 429) {  // Rate limited
        console.log(`Rate limited. Waiting ${delay}ms...`);
        await new Promise(resolve => setTimeout(resolve, delay));
        delay *= 2;  // Double the wait time
        continue;
      }
      
      if (!res.ok) throw new Error(`HTTP ${res.status}`);
      return await res.json();
    } catch (e) {
      if (attempt < maxRetries - 1) {
        await new Promise(resolve => setTimeout(resolve, delay));
        delay *= 2;
      } else {
        throw e;
      }
    }
  }
}

Strategy 3: In-Memory Cache with TTL

Cache market data to reduce API calls:

class MarketCache {
  constructor(ttlMs = 5000) {
    this.cache = new Map();
    this.ttlMs = ttlMs;
  }
  
  get(key) {
    const entry = this.cache.get(key);
    if (!entry) return null;
    
    if (Date.now() - entry.timestamp > this.ttlMs) {
      this.cache.delete(key);
      return null;
    }
    
    return entry.data;
  }
  
  set(key, data) {
    this.cache.set(key, { data, timestamp: Date.now() });
  }
}

const cache = new MarketCache(5000);  // 5 second TTL

async function getMarketData(ticker) {
  const cached = cache.get(ticker);
  if (cached) return cached;
  
  const data = await fetchWithBackoff(
    `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${ticker}`
  );
  
  cache.set(ticker, data);
  return data;
}

Strategy 4: Interval-Based Polling

Poll at controlled intervals:

class MarketMonitor {
  constructor(pollIntervalMs = 30000) {
    this.pollIntervalMs = pollIntervalMs;
    this.watchers = new Map();
    this.pollingActive = false;
  }
  
  watch(ticker) {
    this.watchers.set(ticker, { lastFetch: 0 });
    if (!this.pollingActive) this.startPolling();
  }
  
  startPolling() {
    this.pollingActive = true;
    
    setInterval(async () => {
      for (const [ticker, state] of this.watchers) {
        try {
          const data = await getMarketData(ticker);
          this.onMarketUpdate(ticker, data);
          state.lastFetch = Date.now();
        } catch (e) {
          console.error(`Failed to fetch ${ticker}:`, e);
        }
      }
    }, this.pollIntervalMs);
  }
  
  onMarketUpdate(ticker, data) {
    // Override this to handle updates
    console.log(`${ticker}: ${data.markets.length} markets`);
  }
}

const monitor = new MarketMonitor(30000);  // Poll every 30 seconds
monitor.watch('KXSB');
monitor.watch('KXEL');

Handling Market Status

Kalshi markets have statuses. Only "active" markets have live odds:

function filterActiveMarkets(markets) {
  return markets.filter(m => m.status === 'active');
}

Next Steps

Combine this polling strategy with a database (SQLite, PostgreSQL) to log price changes over time. Track volatility and detect sentiment shifts.