Installation

BanditDB has two components: the Rust engine and a client SDK (Python or TypeScript/JavaScript). Start the engine first, then install the SDK.

1. Start the Engine

Binary (fastest, no Docker required)

curl -fsSL https://raw.githubusercontent.com/dynamicpricing-ai/banditdb/main/scripts/install.sh | sh
banditdb
curl http://localhost:8080/health   # {"status":"ok"}

Requires glibc >= 2.39 (Ubuntu 24.04+ or equivalent). On older distros — e.g. Ubuntu 22.04 (glibc 2.35) — the binary won't run. Use Docker or Build from Source below instead.

Docker

docker run -d -p 8080:8080 \
  -e BANDITDB_API_KEYS=admin-key=admin \
  -v banditdb_data:/data \
  simeonlukov/banditdb:latest

Docker Compose (recommended for production)

Includes persistence, RBAC, and auto-checkpointing:

curl -fsSL https://raw.githubusercontent.com/dynamicpricing-ai/banditdb/main/docker-compose.yml -o docker-compose.yml
# Set BANDITDB_API_KEYS=admin-key=admin;app-key=writer in .env
docker compose up -d

Key environment variables:

VariableRequiredDefaultDescription
BANDITDB_API_KEYSProductionMulti-key RBAC: key1=admin;key2=writer;key3=reader. Without it the server runs in open mode — every caller is granted admin. Fine for local dev, never for production.
BANDITDB_REQUIRE_AUTHProductionfalseSet true so a missing key set is fatal at startup instead of silently opening the database. A mistyped secret then stops the pod rather than publishing an unauthenticated instance. Enabled by default in the Helm chart.
BANDITDB_CORS_ORIGINSOptionaldeny allComma-separated browser origins allowed to call the API. Empty denies all cross-origin requests; * permits any — avoid that if keys ever reach client-side code. Before 2.0.0 any origin was allowed.
BANDITDB_METRICS_PUBLICOptionalfalse/metrics requires a reader key by default, since its output names campaigns and arms. Set true to restore anonymous access for a scraper that cannot send headers.
BANDITDB_TENANT_MODEOptionalfalseSet true to enable multi-tenancy. Requires tenant-scoped keys in BANDITDB_API_KEYS: key=role:tenant_id, e.g. acme-key=writer:acme;globex-key=writer:globex. Each tenant's campaigns are namespaced and isolated — a key can only see/write its own tenant's data.
BANDITDB_CHECKPOINT_INTERVALOptional5000Auto-checkpoint after N rewarded events
BANDITDB_MAX_WAL_SIZE_MBOptional100Also checkpoint when WAL exceeds N MB
DATA_DIROptional/dataWAL, checkpoint, and Parquet export directory. Set it to a persistent volume — the default in a plain binary run is the current directory.
PORTOptional8080HTTP listen port
LOG_FORMATOptionalunsetLog output format. Unset (default): human-readable text, best for terminals and docker logs. json: one structured JSON object per line — for CloudWatch, Datadog, Splunk, Loki and other log aggregators. Any other value falls back to text.
RUST_LOGOptionalinfoLog verbosity filter. Examples: debug (everything), warn (quiet), banditdb=debug,tower_http=warn (per-module). Standard Rust EnvFilter syntax.
BANDITDB_RATE_LIMIT_PER_SECOptional1000Per-key request rate limit. Raise it for load tests and bulk backfill — the default will quietly fail a tight ingest loop with 429s.
BANDITDB_MAX_PENDING_INTERACTIONSOptional100000Ceiling on predictions awaiting a reward, roughly 1 KB each. TTL alone does not bound this: at 1,000 predictions/second the 24-hour default would hold 86 million records. Eviction permanently breaks reward matching for that prediction, so alert on banditdb_interactions_evicted_total.
BANDITDB_MAX_CONTEXT_MAGNITUDEOptional1e6Largest absolute value accepted in a context vector. Guards against values that overflow the rank-one update and poison the campaign with NaN.
BANDITDB_EXPORT_RETAIN_SHARDSOptional50Parquet shards kept per campaign. 0 keeps everything, which grows until the volume fills and checkpointing starts failing.
BANDITDB_FSYNC_INTERVAL_MSOptional200Group-commit window. Measured to make almost no difference across a 20× range — the writer also syncs whenever it goes idle — so this is not a useful tuning lever. Leave it.

Kubernetes (Helm)

helm install banditdb ./helm/banditdb \
  --set auth.apiKeys="admin-key=admin;app-key=writer" \
  --set persistence.size=10Gi \
  --set ingress.enabled=true \
  --set ingress.host=banditdb.example.com

The Helm chart includes a PersistentVolumeClaim, Secret for API keys, liveness/readiness probes, and a Recreate deployment strategy — required for single-writer WAL safety.

Build from source

Requires Rust 1.97 or newer.

git clone https://github.com/dynamicpricing-ai/banditdb
cd banditdb
cargo build --release                        # standard build
# cargo build --release --features neural   # with NeuralLinUCB
./target/release/banditdb

One process per data directory. BanditDB takes an exclusive lock on DATA_DIR; a second process on the same volume refuses to start. Two writers would interleave WAL appends and corrupt both copies with no error at the time, so this is enforced rather than documented. It is also why the Helm chart pins replicaCount: 1 with the Recreate strategy.

2. Install the SDK

Python

pip install banditdb-python
from banditdb import Client
db = Client("http://localhost:8080", api_key="your-secret-key")

Smallest possible working loop — create a campaign, ask for a decision, report the outcome:

# One campaign: which button color converts best?
db.create_campaign("button-color", arms=["red", "green", "blue"], feature_dim=2)

# Ask for a decision. Context: [is_mobile, is_returning_visitor]
arm, interaction_id = db.predict("button-color", [1.0, 0.0])
print(arm)  # e.g. "green"

# Report what happened: 1.0 = clicked, 0.0 = ignored
db.reward(interaction_id, 1.0)

Normalise your context if the features are on different scales. It matters more than it looks: the exploration term scales with the vector's magnitude, and unnormalised input still converges — just far more slowly, with no error to tell you. On one benchmark this single change cut cumulative regret from 2,026 to 709.

from banditdb import normalize_context

ctx = normalize_context([age, income, sessions])   # unit L2 norm
arm, iid = db.predict("button-color", ctx)

SDK 0.2.0 requires server 2.0.0 or newer. It also validates locally before sending, so a bad context raises ValueError naming the offending index rather than costing a round trip.

That is the whole API surface you need day to day. See Quick Start for a realistic end-to-end example.

TypeScript / JavaScript

npm install banditdb-js
import { BanditDBClient } from "banditdb-js";
const db = new BanditDBClient({ url: "http://localhost:8080", apiKey: "your-secret-key" });

await db.createCampaign("button-color", { arms: ["red", "green", "blue"], feature_dim: 2 });

const { arm_id, interaction_id } = await db.predict("button-color", [1.0, 0.0]);
console.log(arm_id); // e.g. "green"

await db.reward(interaction_id, 1.0);

HTTP (no SDK)

Every SDK call is a plain JSON HTTP request. Use X-Api-Key header when auth is enabled:

curl -s http://localhost:8080/health
# {"status":"ok",...}

curl -s -X POST http://localhost:8080/predict \
  -H "Content-Type: application/json" \
  -H "X-Api-Key: your-secret-key" \
  -d '{"campaign_id":"my-campaign","context":[1.0,0.5,0.3]}'