Quick Start

A complete predict→reward cycle for a sleep improvement campaign.

Sleep Improvement

One-size-fits-all sleep advice ignores individual physiology. A 25-year-old male athlete and a 60-year-old sedentary woman respond differently to the same environmental change. BanditDB learns those differences automatically — routing each participant to the intervention most likely to work for their profile, improving with every reported outcome.

from banditdb import Client

db = Client("http://localhost:8080", api_key="your-secret-key")

# 1. Create the campaign once at startup
db.create_campaign(
    "sleep",
    arms=["decrease_temperature", "decrease_light", "decrease_noise"],
    feature_dim=5,
    metadata={
        "owner": "wellness-team",
        "features": ["sex", "age_norm", "weight_norm", "activity", "bedtime_norm"],
    },
)

# 2. A participant is ready for tonight's intervention.
# Context: [sex, age/100, weight_kg/150, activity_0–1, bedtime_hour/24]
context = [
    1.0,   # female
    0.35,  # age 35
    0.50,  # 75 kg
    0.60,  # moderately active
    0.96,  # bedtime 23:00
]

# 3. Ask BanditDB which intervention to apply
arm, interaction_id = db.predict("sleep", context)
print(f"Tonight's intervention: {arm}")  # e.g., "decrease_temperature"

# 4. Apply the intervention, then reward the next morning
score_before = 62
score_after  = 79

# A raw improvement ratio is unbounded — a score that more than doubles
# exceeds 1.0 and the server rejects it. Clamp before sending.
reward = min(max((score_after - score_before) / score_before, 0.0), 1.0)  # → 0.27

db.reward(interaction_id, reward)

Rewards must be in [0, 1]. As of 2.0.0 this is enforced: anything outside the range returns 400. Earlier versions accepted any finite number and silently corrupted the arm's confidence bounds — an unscaled metric like revenue-in-dollars would make one arm look unbeatable and collapse exploration. Divide by the maximum possible value, or clamp a ratio as above.

Reward within the feedback window. A prediction is held in memory until its reward arrives, then expires — 24 hours by default (BANDITDB_REWARD_TTL_SECS). Rewarding the next morning fits comfortably; a campaign whose outcome takes days needs a longer TTL, or the reward comes back 404 and the observation is lost. Raise the window and the pending cache holds proportionally more, so size BANDITDB_MAX_PENDING_INTERACTIONS to match.

The call to reward() returns only once the record is fsynced to disk, so a crash immediately afterwards cannot lose it. That costs a few milliseconds per call and the cost is per-call, not per-record — submit rewards in parallel if you are backfilling.