BanditDB Python SDK
The official Python client and Model Context Protocol (MCP) server for BanditDB — the ultra-fast, lock-free Contextual Bandit database written in Rust.
BanditDB abstracts away the complex linear algebra of Reinforcement Learning (LinUCB, Thompson Sampling) behind a dead-simple API. Build real-time personalizers, dynamic A/B tests, and give LLM agents mathematically rigorous persistent memory.
mcp-name: com.banditdb/mcp
Installation
pip install banditdb-python
Requires the BanditDB Rust server running (default: http://localhost:8080).
1. Standard SDK Usage
The client features automatic connection pooling, exponential backoff retries, and strict timeouts.
from banditdb import Client, BanditDBError
# Connect to the BanditDB server.
# Pass api_key if BANDITDB_API_KEY is set on the server.
db = Client(
url="http://localhost:8080",
timeout=2.0,
api_key="your-secret-key", # omit if server runs without auth
)
try:
# 1. Create a campaign (run once at startup)
# algorithm defaults to "linucb"; use "thompson_sampling" for Bayesian exploration
db.create_campaign(
campaign_id="checkout_upsell",
arms=["offer_discount", "offer_free_shipping"],
features=["is_mobile", "cart_value", "is_returning_user"],
)
# or: db.create_campaign(..., algorithm="thompson_sampling")
# 2. A user arrives — ask the database what to show them.
# Scale each value to 0–1; the SDK puts them in order, adds a constant 1
# and normalises the vector.
arm_id, interaction_id = db.predict("checkout_upsell", {
"is_mobile": 1.0,
"cart_value": min(cart_total / 200, 1.0),
"is_returning_user": 0.0,
})
print(f"Showing: {arm_id}") # e.g., "offer_free_shipping"
# 3. The user clicked — send the reward
db.reward(interaction_id, reward=1.0)
except BanditDBError as e:
print(f"Database error: {e}")
Named features or a vector. With features=[...] the campaign stores the
feature order in its metadata, and predict, batch_predict and interact accept
a dict keyed by name. A missing or misspelt name raises ValueError before any
request is sent. Campaigns created in the BanditDB console accept dicts too. The
order is read once per campaign with campaign_info() and cached on the client.
To manage the vector yourself, pass feature_dim instead and send lists —
normalize_context() scales one to unit length.
All Client methods
Health
Campaigns
Predict & Reward
Data & Export
Budget constraints
Server 2.3.0+. Give a campaign one or more budgeted resources and per-arm costs; the server learns a shadow price per resource and steers spend to last the horizon, choosing costly arms only where their uplift is worth it.
from banditdb import Client, PacingConfig, ResourceConstraint
db.create_campaign(
"promos", ["none", "discount_10", "discount_20"], feature_dim=4,
pacing=PacingConfig(resources=[
ResourceConstraint(
name="promo_budget",
budget=5_000.0, # units available over the horizon
horizon=100_000, # decisions the budget should last
arm_costs={"discount_10": 1.0, "discount_20": 2.0}, # "none" costs 0
),
]),
)
# Arms added later declare their own costs
db.add_arm("promos", "discount_30", costs={"promo_budget": 3.0})
for r in db.pacing("promos")["resources"]:
print(f"{r['name']}: {r['utilization']:.0%} spent, lambda={r['lambda']:.3f}")
Worth knowing:
- Budget is spent at
predict, notreward. A recommendation you never act on still consumes budget. - Budget never resets and pacing is immutable. Once spent, costly arms stay unselectable until the campaign is recreated.
arm_costskeys must be campaign arms. The server would silently treat a misspelt arm as free; the SDK raisesValueErrorinstead.- Predicts on a pacing campaign can return 503 if the server's WAL is saturated.
- Downgrading the server below 2.3.0 silently drops pacing.
2. The AI "Hive Mind" (Model Context Protocol)
Standard LLM agents are stateless — if they route a task to the wrong model and fail, they repeat the same mistake tomorrow. BanditDB's built-in MCP server gives the entire agent swarm shared persistent memory.
Starting the MCP server
# Set environment variables before starting
export BANDITDB_URL=http://localhost:8080
export BANDITDB_API_KEY=your-secret-key # omit if server runs without auth
banditdb-mcp
Connecting to Claude Desktop
Add to your Claude configuration file:
- Mac:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
{
"mcpServers": {
"banditdb": {
"command": "banditdb-mcp",
"args": [],
"env": {
"BANDITDB_URL": "http://localhost:8080",
"BANDITDB_API_KEY": "your-secret-key"
}
}
}
}
The agent swarm now has twelve tools:
Every decision made by any agent in the network improves the routing for all future agents.
3. Data Science & Offline Evaluation
BanditDB event-sources every prediction and reward to a Write-Ahead Log (WAL). Calling checkpoint() compiles completed prediction→reward pairs into Snappy-compressed Parquet files — one per campaign — for offline analysis with Polars or Pandas.
Every prediction is guaranteed to appear in the Parquet file even if its reward arrives hours later: BanditDB re-emits in-flight interactions at each checkpoint so delayed rewards are always captured in a future cycle.
# Checkpoint: snapshot models, write Parquet, rotate the WAL.
# Call this on a schedule or after significant traffic.
summary = db.checkpoint()
print(summary)
# "Checkpoint written and WAL rotated: 2 campaigns, offset 4821 bytes,
# 150 interactions exported, 3 in-flight re-emitted"
# List which Parquet files are available
print(db.export())
# 'Parquet files in /data/exports: ["llm_routing.parquet"]'
# Load directly from the mounted volume into Polars.
# Flat schema: interaction_id | arm_id | reward | predicted_at | rewarded_at | propensity | feature_0 | ...
import polars as pl
df = pl.read_parquet("/data/exports/llm_routing.parquet")
print(df.head())
print(df.columns)
Offline Policy Evaluation (OPE)
The SDK ships three OPE estimators in banditdb.eval. They answer the question: "what would my average reward have been under a different policy — without running a live experiment?"
Install the eval dependencies:
pip install "banditdb-python[eval]"
All three estimators:
- Accept a Polars or pandas DataFrame loaded from a BanditDB Parquet export
- Evaluate the uniform random policy as the target (the unbiased baseline to beat)
- Raise
ValueErrorfor Thompson Sampling campaigns (propensity column is null — TS does not log propensities) - Return an
OPEResultwithestimate,std_error,n_used,n_total, andmethod
import polars as pl
from banditdb.eval import replay, ips, doubly_robust
df = pl.read_parquet("/data/exports/llm_routing.parquet")
# How much reward would a uniform random policy have earned?
print(replay(df))
# OPEResult(method='replay', estimate=0.4821, std_error=0.0312, coverage=22.1% [33/149])
print(ips(df))
# OPEResult(method='ips', estimate=0.5103, std_error=0.0187, coverage=100.0% [149/149])
print(doubly_robust(df))
# OPEResult(method='doubly_robust', estimate=0.5219, std_error=0.0141, coverage=100.0% [149/149])
# Compare against the observed reward of the logging policy:
print("Observed (logging policy):", df["reward"].mean())
# If observed >> estimate, the campaign has learned something real — it outperforms random.
Practical use: sweep alpha offline before deploying. Train a campaign on real traffic, checkpoint to Parquet, then replay different alpha values through doubly_robust() to find the best exploration level — no live experiment needed.
Note: OPE requires the
propensitycolumn, which is only written for LinUCB campaigns. Thompson Sampling campaigns lognullpropensities because TS arm selection is stochastic and propensity scoring requires a deterministic logging policy.
Choosing an Algorithm
BanditDB supports five algorithms, selected at campaign creation time.
from banditdb import Client, NeuralLinUCBConfig, NeuralTSConfig, ProgressiveConfig
db = Client("http://localhost:8080")
# LinUCB (default)
db.create_campaign("routing", ["fast", "cheap"], feature_dim=4, alpha=1.5)
# Thompson Sampling — natural Bayesian exploration, alpha=1.0 is ideal
db.create_campaign("routing_ts", ["fast", "cheap"], feature_dim=4,
algorithm="thompson_sampling")
# NeuralLinUCB — learns a deep embedding of the context, then applies LinUCB
cfg = NeuralLinUCBConfig(
context_dim=4, # must match feature_dim
embed_dim=32, # arm matrix dimension (default 32)
hidden_dim=128, # MLP hidden layer width (default 128)
retrain_every=200, # retrain the MLP every N cumulative rewards
)
db.create_campaign("routing_neural", ["fast", "cheap"], feature_dim=4, algorithm=cfg)
# NeuralTS — same parameters as NeuralLinUCB, posterior-sampling exploration
cfg = NeuralTSConfig(context_dim=4, embed_dim=32)
db.create_campaign("routing_nts", ["fast", "cheap"], feature_dim=4, algorithm=cfg)
# Progressive — runs LinUCB vs NeuralLinUCB, shifts traffic to whoever wins SNIPS checkpoints
cfg = ProgressiveConfig(
base="linucb",
challenger=NeuralLinUCBConfig(context_dim=4, embed_dim=32),
min_obs=100, # minimum buffer entries per arm before any traffic shift
required_wins=3, # consecutive checkpoint wins to earn one traffic step
step_bps=1000, # traffic delta per win run, in basis points (1000 = 10%)
)
db.create_campaign("routing_prog", ["fast", "cheap"], feature_dim=4, algorithm=cfg)
All five algorithms share the same predict → reward loop. Either neural config also works as a Progressive base or challenger.
NeuralLinUCB vs NeuralTS
Both neural algorithms need a server built with the neural feature: check "neural" in db.server_info()["features"].
Error Handling
License
Apache-2.0 — Copyright (C) 2026 Simeon Lukov and Dynamic Pricing Ltd. See the main repository for details.