Zack Design has published bezant — a typed async Rust client for the Interactive Brokers Client Portal Web API, with HTTP, CLI, MCP, and TypeScript surfaces all generated from the same vendored OpenAPI spec. It’s the first Rust project we’ve open-sourced, and it exists because trading against IBKR from modern code shouldn’t mean hand-rolling 155 HTTP endpoints from a PDF.
Why it exists
Interactive Brokers’ Client Portal Web API (CPAPI) is the gateway most retail-adjacent trading tools reach for — it covers accounts, positions, orders, market data, watchlists, scanners, PnL, and more. The surface area is 155 paths, 167 methods, 1030 types. The official docs ship an OpenAPI spec, but the spec has real-world quirks: missing or duplicate operationIds, malformed security[] blocks, integer fields with floating-point example values, and a few other gremlins that break naive code generators.
Bezant vendors that spec, normalises it through a 13-step pipeline, and regenerates every client surface from a single command. When IBKR revises the spec, one ./scripts/codegen.sh re-runs the whole thing and every language/runtime updates together.
Five surfaces, one spec
| Crate / package | Purpose |
|---|---|
bezant-core |
Ergonomic async Rust facade — Client, session keepalive, health, WebSocket streaming, pagination, symbol cache, typed errors |
bezant-api |
Auto-generated Rust client covering every CPAPI endpoint |
bezant-server |
HTTP sidecar — exposes CPAPI as plain REST+JSON so any language can consume it |
bezant-cli |
bezant CLI — bezant health, bezant positions DU123456, bezant conid AAPL |
bezant-mcp |
Model Context Protocol server — exposes IBKR as MCP tools for Claude Code, Cursor, Continue |
bezant-client |
TypeScript client for Node / Deno / browser |
The MCP surface is the one that surprised us the most. Once it was there, driving IBKR from a conversation — “what are my open positions in my paper account?” — became a single /plugin install away. The same spec drove the typed Rust client, the CLI, and the TypeScript package. Zero duplication.
Rust, because it earns the weight
Rust is new to our open-source lineup. We chose it for bezant specifically because:
- A long-running trading session wants to not crash. Rust’s memory safety and absence of GC pauses feel right for code that holds an authenticated session, streams over a WebSocket, and needs to keepalive a 5-minute-expiring cookie cleanly.
reqwest+tokiois genuinely pleasant. Strong types end-to-end, async streaming viatokio-tungstenite, and error handling viathiserror— the Rust HTTP/WebSocket story in 2026 is excellent.- Codegen is unforgiving. When you regenerate a client from a noisy third-party spec, the compiler catches mismatches the moment you rebuild. Dynamic languages find out at runtime.
The ergonomic facade in bezant-core sits on top of the raw generated client so callers don’t have to think about method-naming quirks or type re-wrapping:
use std::time::Duration;
#[tokio::main]
async fn main() -> bezant::Result<()> {
let client = bezant::Client::new("https://localhost:5000/v1/api")?;
let _keepalive = client.spawn_keepalive(Duration::from_secs(60));
client.health().await?;
let positions = client.all_positions("DU123456").await?;
let aapl = bezant::SymbolCache::new(client).conid_for("AAPL").await?;
println!("{} positions; AAPL = conid {aapl}", positions.len());
Ok(())
}
The spec-normalisation pipeline
The most unexpectedly interesting piece of this project is the 13-step spec-normalisation pipeline. IBKR’s published OpenAPI isn’t wrong — it’s realistic. Real specs have duplicate operation IDs, missing required fields, and security definitions that don’t validate.
Rather than patch-forward into our codegen, bezant normalises the spec before codegen runs. Each step is idempotent and documented:
- Add missing
operationIds deterministically from path + method - De-duplicate the operationIds that IBKR repeats
- Repair malformed
security[]blocks - Coerce integer fields with float example values
- Upgrade OAS 3.0 → 3.1 where it matters for our generator
- …and eight more
The output is a clean, modern OpenAPI 3.1 document that every downstream generator can consume without complaint. The full pipeline is documented at Spec normalisation — if you have your own fights with a gnarly third-party spec, the pattern is worth stealing.
Testing against reality
34 tests across the workspace, all green in CI:
- Unit for the facade and the CLI
- Snapshot tests keyed to real IBKR example payloads — catches upstream spec drift before users feel it
- Integration against
wiremockfor fault-injection (session expiry, 5xx retries) - End-to-end through Docker Compose against a mocked Gateway
The Docker Compose quickstart is one command: docker compose up, log in to the IBKR Gateway once in a browser, and the HTTP sidecar is live on http://localhost:8080.
MCP: IBKR as a tool for Claude
One of the weirder, more fun surfaces is bezant-mcp — a Model Context Protocol server that exposes IBKR endpoints as MCP tools. Drop it into Claude Code or Cursor, and you can ask “show me the PnL on my paper account this week” and the model drives the actual CPAPI to answer. The MCP tools are generated from the same spec, so new IBKR endpoints become new MCP tools automatically.
Status and licensing
- Alpha — v0.1. Works end-to-end against IBKR paper accounts; API surface will evolve until v1.0
- Dual-licensed MIT / Apache-2.0 following Rust ecosystem convention
- Not affiliated with Interactive Brokers — the vendored spec is IBKR’s IP, included under fair-use for interoperability
- Docs: isaacrowntree.github.io/bezant
- Source: github.com/isaacrowntree/bezant
If you’re building a trading bot, an analytics tool, an AI-assistant-with-broker-access, or anything that wants typed access to IBKR without the PDF-reading phase — bezant is waiting. Contributions welcome, especially on the spec-normaliser and on new client languages.