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/sdkReact 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:
| Prop | Required | Description |
|---|---|---|
apiUrl | Yes | The Levered API endpoint. Use https://api.levered.dev for production. |
anonymousId | Yes | A stable user identifier. Must match the anonymous_id in your metric queries. |
onExposure | No | Callback 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:
| Field | Description |
|---|---|
optimization_id | Which optimization the variant came from |
anonymous_id | The user identifier (must match your metric queries) |
variant | The full variant assignment (one value per design factor) |
timestamp | When the exposure occurred |
is_holdout | true 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, andpaused.dry_run: truebypasses 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) return400 "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_attimestamp on the optimization — noserve_eventsrow is written.
Other checks
- Check the dashboard -- The optimization detail page shows exposure counts once events appear in your warehouse and the model trains.
- Preview your metric -- Run
levered metrics preview --id <metric-uuid>to see if reward rows are being returned. - Check warehouse tables -- Query your exposure and reward tables directly to confirm events are landing.
Next steps
- SDK Reference -- Full API documentation for the JavaScript SDK.
- Concepts: Optimizations -- Deep dive into how optimizations, bandits, and training work.
- CLI Reference -- Manage everything from the terminal.
- API Reference -- REST API endpoints for server-side integration.