Levered Docs
Getting Started

Integrate the SDK

Install the JavaScript SDK, serve optimized variants to users, and log exposure events to your warehouse.

The Levered SDK requests a variant assignment from the Levered API and gives you the values to render. You are responsible for logging exposure events to your data warehouse so Levered can join them with reward data at training time.

Install

npm install @levered_dev/sdk

React integration

Set up the provider

Wrap your application (or the relevant subtree) with LeveredProvider. This initializes the SDK client and makes variant hooks available to child components.

import { LeveredProvider } from '@levered_dev/sdk/react';

function App() {
  return (
    <LeveredProvider
      apiUrl="https://api.levered.dev"
      anonymousId={getUserId()}
      onExposure={(exposure) => {
        // Log the exposure to your warehouse
        analytics.track('levered_exposure', {
          optimization_id: exposure.optimizationId,
          anonymous_id: exposure.anonymousId,
          variant: exposure.variant,
          timestamp: exposure.timestamp,
        });
      }}
    >
      <YourApp />
    </LeveredProvider>
  );
}

Key props:

PropRequiredDescription
apiUrlYesThe Levered API endpoint. Use https://api.levered.dev for production.
anonymousIdYesA stable user identifier. Must match the anonymous_id in your metric queries.
onExposureNoCallback fired when a variant is assigned. You should log this event to your warehouse -- without it, the model cannot learn from exposure data.

Use variants in components

The useVariant hook fetches the assigned variant for a given optimization. Provide a fallback so your component renders immediately while the SDK loads.

import { useVariant } from '@levered_dev/sdk/react';

function HeroSection() {
  const { variant, isLoading } = useVariant({
    optimizationId: 'your-optimization-id',
    fallback: { headline: 'Welcome', cta_text: 'Sign Up' },
  });

  return (
    <section>
      <h1>{variant.headline}</h1>
      <button>{variant.cta_text}</button>
    </section>
  );
}

The variant object contains one key per design factor with the assigned level as the value. While loading, it returns the fallback values so there is no layout shift.

Full React example

import { LeveredProvider } from '@levered_dev/sdk/react';
import { useVariant } from '@levered_dev/sdk/react';

function HeroSection() {
  const { variant, isLoading } = useVariant({
    optimizationId: '699ede80-c94b-4f98-9c79-dd3e07748703',
    fallback: { headline: 'Ship faster', cta_text: 'Start free' },
  });

  return (
    <section className="hero">
      <h1>{variant.headline}</h1>
      <p>Optimize your product with contextual bandits.</p>
      <button className="cta">{variant.cta_text}</button>
    </section>
  );
}

export default function App() {
  return (
    <LeveredProvider
      apiUrl="https://api.levered.dev"
      anonymousId={getUserId()}
      onExposure={(e) => analytics.track('levered_exposure', e)}
    >
      <HeroSection />
    </LeveredProvider>
  );
}

Vanilla JavaScript

If you are not using React, use LeveredClient directly.

import { LeveredClient } from '@levered_dev/sdk';

const FALLBACK = { headline: 'Welcome', cta_text: 'Sign Up' };

const client = new LeveredClient({
  apiUrl: 'https://api.levered.dev',
  onExposure: (event) => {
    // Log the exposure to your warehouse
    analytics.track('levered_exposure', {
      anonymous_id: event.anonymousId,
      timestamp: event.timestamp,
      optimization_id: event.optimizationId,
      variant: event.variant,
      context: event.context,
      is_holdout: event.isHoldout,
      ready: event.ready,
    });
  },
});

// Get a variant assignment
const result = await client.getVariant({
  anonymousId: getUserId(),
  optimizationId: 'your-optimization-id',
});

// Use the variant values (fall back if null)
const variant = result?.variant ?? FALLBACK;
document.querySelector('h1').textContent = variant.headline;
document.querySelector('.cta').textContent = variant.cta_text;

Logging exposures

Levered does not receive or store exposure events directly. You must log them to your data warehouse yourself. This is by design -- it keeps your data pipeline under your control and avoids vendor lock-in.

An exposure event should contain at minimum:

FieldDescription
optimization_idWhich optimization the variant came from
anonymous_idThe user identifier (must match your metric queries)
variantThe full variant assignment (one value per design factor)
timestampWhen the exposure occurred
is_holdouttrue when the user was routed to the holdout (control) arm. Required to compute lift vs. holdout

Log exposures in the onExposure callback (React) or manually after calling getVariant (vanilla JS). Send them to your warehouse through whatever pipeline you already use -- Segment, Rudderstack, a direct BigQuery/Snowflake insert, or any other method.

Context factors (CMAB)

If your optimization uses a CMAB model with context factors, pass them when requesting a variant:

const { variant } = useVariant({
  optimizationId: 'your-optimization-id',
  fallback: { headline: 'Welcome', cta_text: 'Sign Up' },
  context: {
    device_type: 'mobile',
    country: 'US',
  },
});

Context values let the CMAB model personalize variant selection per user segment.

Server-side usage

You can also request variants from your backend using the REST API directly:

curl -X POST https://api.levered.dev/api/v2/optimizations/{id}/serve \
  -H "Authorization: Bearer $LEVERED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "anonymous_id": "user_abc123",
    "context": {"device_type": "mobile"}
  }'

This is useful for server-rendered pages, email personalization, or any non-browser environment.

Verifying the integration

Dry-run requests

The fastest way to confirm the SDK is wired up correctly is a dry-run call to /serve. Pass "dry_run": true and the request runs through the full decision path (holdout bucketing, inference) and returns a real variant payload -- but no serve_events row is written, no sticky-variant lookup runs, and auto-training is not triggered. The response shape is identical to a real serve, so you can validate the wire-up end-to-end without polluting exposures or training data.

curl -X POST https://api.levered.dev/api/v2/optimizations/{id}/serve \
  -H "Authorization: Bearer $LEVERED_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "anonymous_id": "test-user-1",
    "dry_run": true
  }'

A 200 OK with a variants array means everything is reaching us correctly. Drop the same call into CI, smoke tests, or a local one-off curl whenever you want to confirm the integration without writing to metrics.

Allowed on draft, live, and paused. dry_run: true bypasses the usual live-only gate for the three states where it's reasonable to verify the SDK wire-up — before launch (draft), while serving (live), and after a temporary stop (paused). Terminal states (completed, archived) return 400 "Dry-run is only allowed on draft, live, or paused optimizations", since there's no useful workflow that exercises inference against a finished optimization.

Either way, the first dry-run we see for a draft flips the dashboard's "Verify SDK integration" checklist green and unlocks Go live. We track that via a one-shot first_serve_call_at timestamp on the optimization — no serve_events row is written.

Other checks

  1. Check the dashboard -- The optimization detail page shows exposure counts once events appear in your warehouse and the model trains.
  2. Preview your metric -- Run levered metrics preview --id <metric-uuid> to see if reward rows are being returned.
  3. Check warehouse tables -- Query your exposure and reward tables directly to confirm events are landing.

Next steps