Skip to content

Kalshi Demo Environment Support

This post covers the implementation of ADR-015 (Kalshi Demo Environment Support), enabling safe testing with Kalshi's demo API without risking production credentials or capital.

The Problem

Testing Kalshi integration presents a challenge: the API requires valid credentials, and production means real money. Before this change, developers had two options:

  1. Use production credentials - Risky, even with paper trading mode
  2. Mock everything - Fast but doesn't validate real API behavior

Neither is ideal. We need real API behavior without production risk.

Kalshi's Demo Environment

Kalshi provides a demo environment at demo-api.kalshi.co that mirrors production:

Feature Production Demo
API behavior Real Identical
Market data Real Real (mirrored)
Funds Real USD Mock funds
Credentials Separate Separate

This gives us the best of both worlds: real API validation with zero financial risk.

The Solution: KalshiEnvironment Enum

A type-safe enum centralizes all environment-specific configuration:

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum KalshiEnvironment {
    #[default]
    Production,
    Demo,
}

impl KalshiEnvironment {
    pub fn api_base_url(&self) -> &'static str {
        match self {
            KalshiEnvironment::Production => "https://trading-api.kalshi.com",
            KalshiEnvironment::Demo => "https://demo-api.kalshi.co",
        }
    }

    pub fn websocket_url(&self) -> &'static str {
        match self {
            KalshiEnvironment::Production => "wss://trading-api.kalshi.com/trade-api/v2/ws",
            KalshiEnvironment::Demo => "wss://demo-api.kalshi.co/trade-api/v2/ws",
        }
    }
}

Key design decisions:

  • #[default] on Production: Safe default prevents accidental demo usage in production
  • Copy trait: Cheap to pass by value, no allocation
  • Static strings: No runtime allocation for URLs
  • Single source of truth: All Kalshi URLs in one place

Credential Namespacing

Separate environment variables prevent credential mixups:

Environment Key ID Variable Private Key Variable
Production KALSHI_KEY_ID KALSHI_PRIVATE_KEY
Demo KALSHI_DEMO_KEY_ID KALSHI_DEMO_PRIVATE_KEY

This pattern ensures you can't accidentally use production credentials in demo mode or vice versa:

let (kalshi_key_var, kalshi_priv_var) = if args.kalshi_demo {
    ("KALSHI_DEMO_KEY_ID", "KALSHI_DEMO_PRIVATE_KEY")
} else {
    ("KALSHI_KEY_ID", "KALSHI_PRIVATE_KEY")
};

Client Integration

Both KalshiClient and KalshiMonitor accept the environment via with_environment() constructors:

impl KalshiClient {
    pub fn new(key_id: String, private_key_pem: &str, dry_run: bool) -> Result<Self, String> {
        // Default to production
        Self::with_environment(key_id, private_key_pem, dry_run, KalshiEnvironment::Production)
    }

    pub fn with_environment(
        key_id: String,
        private_key_pem: &str,
        dry_run: bool,
        environment: KalshiEnvironment,
    ) -> Result<Self, String> {
        // ... initialization with environment-specific URLs
    }
}

The existing new() constructor delegates to with_environment() with production default, maintaining backward compatibility.

CLI Integration

A simple --kalshi-demo flag switches environments:

#[derive(Parser, Debug)]
struct Args {
    /// Use Kalshi demo environment instead of production
    #[arg(long, default_value_t = false)]
    kalshi_demo: bool,
    // ...
}

Usage:

# Production (default)
cargo run -- --paper-trade

# Demo environment
cargo run -- --paper-trade --kalshi-demo

# Check demo connectivity
cargo run -- --check-connectivity --kalshi-demo

Combining with Paper Trading

The most powerful combination is paper trading with Kalshi demo:

cargo run -- --paper-trade --kalshi-demo --fidelity realistic

This provides:

Layer Source Risk
Market data Real (from Kalshi demo) None
Order execution Simulated (paper trading) None
Credentials Demo (mock funds) None

You get realistic market conditions without any financial exposure.

Testing

Four unit tests validate URL generation:

#[test]
fn test_production_urls() {
    let env = KalshiEnvironment::Production;
    assert_eq!(env.api_base_url(), "https://trading-api.kalshi.com");
    assert_eq!(env.websocket_url(), "wss://trading-api.kalshi.com/trade-api/v2/ws");
}

#[test]
fn test_demo_urls() {
    let env = KalshiEnvironment::Demo;
    assert_eq!(env.api_base_url(), "https://demo-api.kalshi.co");
    assert_eq!(env.websocket_url(), "wss://demo-api.kalshi.co/trade-api/v2/ws");
}

#[test]
fn test_default_is_production() {
    assert_eq!(KalshiEnvironment::default(), KalshiEnvironment::Production);
}

Module Structure

arbiter-engine/src/market/
├── mod.rs              # Exports KalshiEnvironment
├── kalshi_env.rs       # Environment enum and URL config
├── kalshi.rs           # KalshiMonitor with environment support
└── client/
    └── kalshi.rs       # KalshiClient with environment support

Getting Demo Credentials

  1. Visit Kalshi Demo Environment
  2. Create a demo account (separate from production)
  3. Generate API credentials in demo dashboard
  4. Set environment variables:
export KALSHI_DEMO_KEY_ID=your_demo_key_id
export KALSHI_DEMO_PRIVATE_KEY="-----BEGIN RSA PRIVATE KEY-----
...your demo key...
-----END RSA PRIVATE KEY-----"

Extensibility

The pattern is ready for future exchange demo environments:

// Future: Polymarket demo (if they add one)
pub enum PolymarketEnvironment {
    #[default]
    Production,
    Demo,  // Hypothetical
}

Council Review

The implementation passed council review with strong scores:

Dimension Score
Accuracy 8.5/10
Completeness 8.0/10
Clarity 9.0/10
Conciseness 8.5/10
Relevance 9.5/10
Weighted 8.52/10

No blocking issues were identified.

References