@vncsleal/bandito
v0.1.0
Published
Server-side contextual bandit router
Maintainers
Readme
@vncsleal/bandito
Public SDK + types for Bandito (assign + recordOutcome).
Install
pnpm add @vncsleal/banditoQuick Start
import { createBanditoClient } from "@vncsleal/bandito";
const bandito = createBanditoClient({
baseUrl: "https://bandito-server.fly.dev",
apiKey: process.env.BANDITO_API_KEY,
timeoutMs: 2000,
});
const decision = await bandito.assign("pricing.hero", {
context: { device: "desktop", traffic: "paid", region: "na" },
sessionId: "session-123",
});
await bandito.recordOutcome({
assignmentId: decision.assignmentId,
metrics: { conversion: 1, revenue: 120 },
});Next.js Route Example (Production)
// app/api/experiments/assign/route.ts
import { NextResponse } from "next/server";
import { createBanditoClient, isBanditoHttpError } from "@vncsleal/bandito";
const bandito = createBanditoClient({
baseUrl: process.env.BANDITO_API_URL!,
apiKey: process.env.BANDITO_API_KEY!,
timeoutMs: 2000,
retries: 1,
});
export async function POST(request: Request) {
const body = (await request.json()) as {
experimentId: string;
sessionId?: string;
projectId?: string;
context: { device?: "desktop" | "mobile" | "unknown"; traffic?: "organic" | "paid" | "referral" | "unknown"; region?: "na" | "eu" | "apac" | "unknown" };
};
try {
const decision = await bandito.assign(body.experimentId, {
context: body.context,
sessionId: body.sessionId,
projectId: body.projectId,
});
return NextResponse.json(decision);
} catch (error) {
if (isBanditoHttpError(error)) {
return NextResponse.json(
{ error: error.responseText, status: error.status },
{ status: error.status },
);
}
return NextResponse.json({ error: "Unexpected error" }, { status: 500 });
}
}Usage
import {
BanditoHttpError,
createBanditoClient,
isBanditoHttpError,
} from "@vncsleal/bandito";
const client = createBanditoClient({
baseUrl: "https://bandito-server.fly.dev",
apiKey: process.env.BANDITO_API_KEY,
timeoutMs: 2000,
retries: 1,
});
const decision = await client.assign({
experimentId: "pricing.hero",
projectId: "project_123",
context: { device: "desktop", traffic: "paid", region: "na" },
sessionId: "session-123"
});
await client.recordOutcome({
assignmentId: decision.assignmentId,
metrics: { conversion: 1, revenue: 120 }
});
try {
await client.getResults({ experimentId: "pricing.hero", projectId: "project_123" });
} catch (error) {
if (isBanditoHttpError(error)) {
console.error(error.status, error.responseText);
}
}Notes:
decision.priorSourceis"baseline"or"informed".projectIdis required only if you are using a master key instead of a project-scoped key.- Requires Node.js 18+.
- ESM-only package (
"type": "module").
Auth Modes
Project API key(recommended): configure API key mapped to a single project inbandito-server;projectIdis optional in SDK calls.Master/Admin flow: if using a broad key, passprojectIdin calls so requests are scoped correctly.
API Surface
assign(input)orassign(experimentId, input)recordOutcome(outcome)getResults(input)orgetResults(experimentId, projectId?)
Error Handling
BanditoHttpErrorprovides:statusresponseTextmethodpath
Use isBanditoHttpError(error) for safe narrowing across runtimes/bundlers.
