Use Cases
Any decision that has a context, a finite set of choices, and a measurable outcome is a candidate for BanditDB.
| Domain | Arms (choices) | Context | Reward signal |
|---|---|---|---|
| LLM routing | GPT-4o, Claude Sonnet, Gemini Flash, … | Task type, input length, session turn, user expertise | LLM-as-judge score, user thumbs up, task completion |
| Prompt strategy | Zero-shot, chain-of-thought, few-shot, structured output | Task complexity, domain, input length, session context | LLM-as-judge quality score (0–1) |
| Agent tool selection | Which tool or sub-agent to invoke for a given step | Task type, prior tool results, cost budget | Task success, latency, cost |
| Dynamic pricing | Price tiers or discount levels | Inventory, seasonality, competitor pricing, customer segment | Revenue per unit or sell-through rate |
| Checkout optimisation | Upsell offer, free shipping, no offer | Cart value, customer history, device type | Conversion (binary) or order value lift |
| Content personalisation | Article, offer, layout variant | User demographics, history, session signals | Click, time-on-page, downstream conversion |
| Legal intake routing | Consult, intake form, refer, decline | Case value, matter complexity, conflict risk, capacity | Matter opened, revenue collected |
| Adaptive clinical trials | Treatment arms | Patient demographics, comorbidities, baseline score | Outcome score normalised to [0, 1] |
| Sleep / wellness | Temperature, light, noise reduction | Sex, age, weight, activity level, bedtime | PSQI score improvement ratio |
The reward must be a scalar in [0, 1]. If your natural metric has a different range, divide by its maximum (e.g. revenue / max_possible_revenue) or use a ratio like (after - before) / before, clipped to [0, 1].
Example: LLM Routing
The first row of the table, end to end — route each request to the cheapest model that can handle it:
from banditdb import Client
db = Client("http://localhost:8080", api_key="your-secret-key")
db.create_campaign(
"llm-routing",
arms=["haiku", "sonnet", "opus"],
feature_dim=4,
metadata={"features": ["task_complexity", "input_len_norm", "session_turn_norm", "code_task"]},
)
# Incoming request: medium complexity, short input, first turn, not a code task
context = [0.5, 0.12, 0.0, 0.0]
arm, interaction_id = db.predict("llm-routing", context)
response = call_llm(model=arm, prompt=prompt)
# Reward: LLM-as-judge quality score, already in [0, 1]
quality = judge(prompt, response) # e.g. 0.85
db.reward(interaction_id, quality)
To fold cost into the signal, use a blended reward such as quality - 0.2 * cost_norm, clipped to [0, 1]. The bandit then learns the cheapest model that maintains quality per context — not just the best model overall.
When BanditDB is not the right tool
- Pure exploration / discovery — if you have no feedback signal yet and are building a dataset from scratch, start with random assignment and switch to BanditDB once you have ~100 outcomes per arm.
- Very high-dimensional action spaces (thousands of arms) — LinUCB scales with arms × feature_dim² in memory. For catalogue-scale recommendation, consider embedding-based retrieval first and use BanditDB for the final re-ranking stage.
- Non-stationary rewards with hard concept drift — LinUCB assumes rewards are stationary. Gradual drift is handled by setting
decay_half_life_hours(below). Only a genuinely discontinuous break — a repriced catalogue, a replaced product — is worth deleting and recreating the campaign for, and that discards everything learned, so reach for decay first.
Adapting to drift: decay_half_life_hours
Without decay a campaign weighs a click from six months ago exactly as heavily as one from this morning. The longer it runs, the more inertia it accumulates, and the slower it reacts to a shift in what actually works.
Set a half-life at creation and older evidence loses influence geometrically — after one half-life it counts half as much, after two a quarter:
db.create_campaign(
"homepage-hero",
arms=["a", "b", "c"],
feature_dim=8,
decay_half_life_hours=168, # one week
)
Decay is applied at checkpoint, to every arm including the tournament challenger. Pick the half-life from how fast your environment actually moves: days for fashion or news, weeks-to-months for a stable product surface. Too short and the model forgets faster than it learns, widening confidence intervals and pushing it back toward exploring. Leave it unset for a stationary problem — it costs accuracy you do not need to spend.