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:
- Use production credentials - Risky, even with paper trading mode
- 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 productionCopytrait: 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:
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¶
- Visit Kalshi Demo Environment
- Create a demo account (separate from production)
- Generate API credentials in demo dashboard
- 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.