JavaScript / TypeScript SDK

A tiny, isomorphic client for multi-armed-bandit selection and feedback. Works on Node 18+, edge runtimes (Vercel, Cloudflare Workers, Deno Deploy), Bun, and the browser. Ships dual ESM + CJS with bundled TypeScript types and zero runtime dependencies.

Info

The JS SDK covers select and feedback only. Pool, experiment, and gate management is available via the Python SDK or the REST API.

Installation

npm install qbrix

Works with any package manager:

pnpm add qbrix
yarn add qbrix

Requires Node 18+. TypeScript types are included — no @types/ package needed.

Quickstart

import { QbrixClient } from "qbrix";
 
// reads QBRIX_API_KEY and QBRIX_BASE_URL from the environment automatically
const qbrix = new QbrixClient();
 
// select an arm for this user/context
const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
 
// render the chosen arm
console.log(`showing: ${arm.name}`);
 
// report the outcome — pass back the requestId from select
await qbrix.feedback(requestId, 1.0); // 1 = converted, 0 = no action

Client Initialization

import { QbrixClient } from "qbrix";
 
const qbrix = new QbrixClient({
  apiKey: "optiq_...",
  baseUrl: "https://cloud.qbrix.io",
  timeout: 5000,
  maxRetries: 2,
});

All options are optional. When omitted, the client reads from environment variables then falls back to built-in defaults.

Configuration

Resolution order per option: explicit argument → environment variable → default.

OptionTypeDefaultEnv Var
apiKeystringQBRIX_API_KEY
baseUrlstringhttp://localhost:8080QBRIX_BASE_URL
timeoutnumber (ms)30000
maxRetriesnumber2
retryOnnumber[][429, 502, 503, 504]
fetchtypeof fetchruntime global
headersRecord<string, string>{}
loggerQbrixLoggersilent

Select

select(experimentId: string, context: Context): Promise<SelectResult>
interface Context {
  id: string;                          // required — a stable user/session identifier
  vector?: number[];                   // optional feature vector for contextual policies
  metadata?: Record<string, unknown>;  // optional arbitrary attributes
}
 
interface SelectResult {
  arm: { id: string; name: string; index: number };
  requestId: string;   // pass this back into feedback()
  isDefault: boolean;  // true when the platform returned the fallback arm
}

Example:

const { arm, requestId, isDefault } = await qbrix.select("checkout-banner", {
  id: "usr_8a1k2",
  metadata: { country: "US", plan: "pro" },
});
 
showBanner(arm.name);

Feedback

feedback(requestId: string, reward: number): Promise<void>

Report the outcome for a prior select. requestId is the value returned by that call; reward is the observed signal — for example 1.0 for a conversion and 0.0 for no action, or any numeric reward your experiment defines.

await qbrix.feedback(requestId, 1.0);

Your API key (optiq_…) is a secret. Anywhere the client runs, the key goes too — bundling it into browser code exposes it to every visitor. The recommended pattern is a thin server-side or edge handler that keeps the key in an environment variable and returns only what the browser needs:

// edge / route handler — runs on the server
import { QbrixClient, QbrixAPIError } from "qbrix";
 
const qbrix = new QbrixClient({ apiKey: process.env.QBRIX_API_KEY });
 
export default async function handler(req: Request): Promise<Response> {
  const { userId } = await req.json();
  try {
    const { arm, requestId } = await qbrix.select("homepage-cta", { id: userId });
    // return only what the browser needs — never the api key
    return Response.json({ arm, requestId });
  } catch (err) {
    if (err instanceof QbrixAPIError) {
      return Response.json({ error: err.code ?? "qbrix_error" }, { status: err.status });
    }
    return Response.json({ error: "internal_error" }, { status: 500 });
  }
}

Browser usage is supported for internal tools and prototypes where the key exposure is an accepted trade-off.

Error Handling

Every failure throws a typed error from the QbrixError hierarchy. Catch the ones you care about with instanceof:

import {
  QbrixAPIError,
  RateLimitedError,
  AuthenticationError,
  QbrixTimeoutError,
} from "qbrix";
 
try {
  const { arm, requestId } = await qbrix.select("homepage-cta", { id: "user-42" });
} catch (err) {
  if (err instanceof RateLimitedError) {
    console.warn(`rate limited; retry after ${err.retryAfter}s`);
  } else if (err instanceof AuthenticationError) {
    throw new Error("check your QBRIX_API_KEY");
  } else if (err instanceof QbrixTimeoutError) {
    // request exceeded the configured timeout
  } else if (err instanceof QbrixAPIError) {
    console.error(`qbrix ${err.status} ${err.code}: ${err.detail}`);
  }
  throw err;
}

Error hierarchy

ClassWhen thrown
QbrixAPIErrorNon-2xx response from the proxy. Subclasses: BadRequestError (400), AuthenticationError (401), ForbiddenError (403), NotFoundError (404), ConflictError (409), RateLimitedError (429), InternalServerError (500)
RateLimitedErrorHTTP 429. Adds retryAfter (seconds)
QbrixConnectionErrorNetwork failure — request never completed
QbrixTimeoutErrorRequest exceeded timeout

All classes extend QbrixError.