Levered Docs
Getting Started

Set Up Your Data Pipeline

Create the warehouse tables that Levered reads during training -- exposures for what users saw, rewards for what they did.

Levered is designed around a simple principle: your data stays in your warehouse. Levered never stores raw event data. Instead, it connects to your warehouse, reads exposure and reward events via SQL, and uses them to train bandit models.

This means you get:

  • Full data ownership -- no vendor lock-in, no data leaving your infrastructure.
  • Your existing pipelines -- log events through Segment, Rudderstack, direct inserts, or whatever you already use.
  • No extra SDK calls for rewards -- Levered reads conversions from the tables your analytics pipeline already writes to.

For this to work, Levered needs two things in your warehouse:

  1. An exposures table -- records of which variant each user saw.
  2. A rewards table -- records of successful outcomes (conversions, purchases, etc.). This often already exists.

Exposures table

Every time a user sees a variant, you log an exposure event to your warehouse. It does not matter how you got the variant -- the JavaScript SDK, the REST API, or a server-side call. If a user saw it, log it.

The resulting table is how Levered knows which users saw which variants.

Required columns

ColumnTypeDescription
anonymous_idSTRINGA stable user or session identifier. Must match the anonymous_id in your reward/metric queries.
timestampTIMESTAMPWhen the user was exposed to the variant.
optimization_idSTRINGWhich optimization this exposure belongs to.
variantJSON / STRINGThe design factor values as a JSON object, e.g., {"headline": "Ship faster", "cta_text": "Start free"}.

You can optionally store context attributes (device type, country, etc.) if you use CMAB:

ColumnTypeDescription
contextJSON / STRINGContext attributes as a JSON object, e.g., {"device_type": "mobile", "country": "US"}.

Create the table

CREATE TABLE IF NOT EXISTS `your_project.your_dataset.exposures` (
  anonymous_id STRING NOT NULL,
  timestamp TIMESTAMP NOT NULL,
  optimization_id STRING NOT NULL,
  variant JSON,
  context JSON
);
CREATE TABLE IF NOT EXISTS exposures (
  anonymous_id VARCHAR NOT NULL,
  timestamp TIMESTAMP_NTZ NOT NULL,
  optimization_id VARCHAR NOT NULL,
  variant VARIANT,
  context VARIANT
);
CREATE TABLE IF NOT EXISTS exposures (
  anonymous_id TEXT NOT NULL,
  timestamp TIMESTAMPTZ NOT NULL,
  optimization_id TEXT NOT NULL,
  variant JSONB,
  context JSONB
);

Alternative: individual columns per design factor

Instead of a single variant JSON column, you can store each design factor as its own column. This works well if you prefer flat tables or your analytics pipeline does not emit JSON.

CREATE TABLE IF NOT EXISTS `your_project.your_dataset.exposures` (
  anonymous_id STRING NOT NULL,
  timestamp TIMESTAMP NOT NULL,
  optimization_id STRING NOT NULL,
  headline STRING,
  cta_text STRING
);
CREATE TABLE IF NOT EXISTS exposures (
  anonymous_id VARCHAR NOT NULL,
  timestamp TIMESTAMP_NTZ NOT NULL,
  optimization_id VARCHAR NOT NULL,
  headline VARCHAR,
  cta_text VARCHAR
);
CREATE TABLE IF NOT EXISTS exposures (
  anonymous_id TEXT NOT NULL,
  timestamp TIMESTAMPTZ NOT NULL,
  optimization_id TEXT NOT NULL,
  headline TEXT,
  cta_text TEXT
);

Both approaches work. Levered handles column mapping when you configure the assignment source (see below).

Logging exposures

However you request variants -- the JavaScript SDK, the REST API, or a server-side call -- you are responsible for logging the exposure to your warehouse whenever the user actually sees the variant.

From the SDK -- the onExposure callback fires automatically when a variant is served:

onExposure={(exposure) => {
  analytics.track('levered_exposure', {
    anonymous_id: exposure.anonymousId,
    timestamp: exposure.timestamp,
    optimization_id: exposure.optimizationId,
    variant: exposure.variant,       // e.g. {"headline": "Ship faster", "cta_text": "Start free"}
    context: exposure.context,       // e.g. {"device_type": "mobile"}
  });
}}

From the REST API -- after calling the /serve endpoint, log the response yourself:

const res = await fetch(`https://api.levered.dev/api/v2/optimizations/${optimizationId}/serve`, {
  method: 'POST',
  headers: { 'Authorization': `Bearer ${apiKey}`, 'Content-Type': 'application/json' },
  body: JSON.stringify({ anonymous_id: userId, context: { device_type: 'mobile' } }),
});
const data = await res.json();

// Log the exposure to your warehouse
analytics.track('levered_exposure', {
  anonymous_id: userId,
  timestamp: new Date().toISOString(),
  optimization_id: optimizationId,
  variant: data.variants[0].props,
});

Use whatever pipeline you already have -- Segment, Rudderstack, a direct warehouse insert, or a custom ETL.

Rewards table

The rewards table records when users do something valuable -- sign up, purchase, click a CTA. You likely already have this data in your warehouse through your existing analytics pipeline.

Levered does not require a special SDK call for rewards. It reads them from the SQL query you define as a metric.

Required columns

ColumnTypeDescription
anonymous_idSTRINGThe same user identifier used in exposures. This is how Levered joins rewards back to the variant a user saw.
timestampTIMESTAMPWhen the reward event occurred.
valueNUMBERThe reward value. For boolean rewards (converted/didn't), use 1. For numeric rewards (revenue), use the actual amount.

Example tables

A signup conversion table (boolean reward):

-- Your existing signups table probably already looks like this
SELECT
  user_id AS anonymous_id,
  created_at AS timestamp
FROM signups

A purchase table (numeric reward):

SELECT
  user_id AS anonymous_id,
  purchased_at AS timestamp,
  amount AS value
FROM purchases

You do not need to create new tables for rewards. In the next step (Define Metrics), you will write a SQL query that reads from whatever tables you already have.

How Levered joins exposures and rewards

During training, Levered:

  1. Queries your exposures table to see what each user was shown.
  2. Queries your rewards table (via the metric SQL) to see who converted.
  3. Joins them on anonymous_id using last-touch attribution within a conversion window (default: 24 hours).
  4. Trains the bandit model on the result -- learning which variants lead to more rewards.
Exposures table                 Rewards table
┌──────────────┬──────────┐    ┌──────────────┬───────┐
│ anonymous_id │ variant  │    │ anonymous_id │ value │
├──────────────┼──────────┤    ├──────────────┼───────┤
│ user_abc     │ {"h":"A"}│───>│ user_abc     │ 1     │  ✓ Matched
│ user_def     │ {"h":"B"}│    │ user_ghi     │ 1     │  ✗ No exposure
│ user_ghi     │ {"h":"A"}│    └──────────────┴───────┘
└──────────────┴──────────┘

            Observations (training data)
            ┌──────────────┬──────────┬────────┐
            │ anonymous_id │ variant  │ reward │
            ├──────────────┼──────────┼────────┤
            │ user_abc     │ {"h":"A"}│ 1      │
            │ user_def     │ {"h":"B"}│ 0      │
            │ user_ghi     │ {"h":"A"}│ 1      │
            └──────────────┴──────────┴────────┘

Next step

Your data pipeline is set up. Next, define the metrics that tell Levered what SQL to run against your rewards data.