# Build a Multi-Armed Bandit for Dynamic Traffic Optimization

## Overview:

Multi-armed bandit (MAB) testing moves beyond static A/B splits by dynamically optimizing your traffic in real-time. Instead of waiting weeks for a test to conclude, a MAB approach identifies high-performing variants early and automatically shifts traffic toward them.

This creates a high-efficiency feedback loop that:

* **Maximizes Conversions**: Scales winning variants as soon as they show a statistical lead.
* **Reduces "Regret"**: Minimizes the "opportunity cost" of sending traffic to underperforming variants during the data-collection phase.
* **Automates Agility**: Removes the need for manual intervention, allowing your experiments to be self-optimizing and revenue-focused from day one.

<figure><img src="/files/Imd3uykCe16L1XGofICj" alt=""><figcaption></figcaption></figure>

### Decision Logic:

We'll use a **4-hour polling interval** with a **minimum observation window of 72 hours**.

The logic we'll use has a three layers, each acting as a gate before a shift is made:

1. **Layer 1 — Is there enough data?** Each variant needs a minimum sample size before its metrics are trustworthy. We'll set a floor, something like 300+ orders per variant, before any reallocation is considered. Below that threshold, the workflow does nothing.
2. **Layer 2 — Is the signal meaningful?** We compare variants on revenue per visitor. Rather than reacting to any difference at all, we'll require a meaningful gap — a variant must be outperforming the control by at least 5% (or control must be beating all variants by at least 5% each) to qualify for a traffic boost. This prevents micro-fluctuations from triggering constant reshuffling.
3. **Layer 3 — How much do you shift, and when do you stop?** We'll use a stepped reallocation approach rather than slamming all traffic to the winner. A reasonable ladder looks like: start at 50/50, then shift to 60/40, then 70/30, then 80/20. Each step requires the winning variant to maintain its lead across **two consecutive check cycles** before advancing to the next step. This protects against a variant that looks good in one window but regresses in the next. We'll also set a hard cap — never go above 80/20, so the control always retains enough traffic to remain statistically valid and to detect any reversal.

Plus we'll add in **guardrails**. If a previously winning variant starts underperforming (drops below the control), the logic should revert the split back toward 50/50 rather than just stalling. This handles novelty effects or external factors like a sale event skewing results temporarily.

### Softwares Used: <a href="#softwares-used" id="softwares-used"></a>

To build this automation, I'll be using [n8n](https://n8n.io/), but you could chose to build this with GitHub Actions, Make, or other softwares.

## How to Build a Multi-Bandit Automation:

### Step 1: Get Your API Keys <a href="#step-1-get-your-api-keys" id="step-1-get-your-api-keys"></a>

\[[How to get Intelligems API Key](https://docs.intelligems.io/developer-resources/external-api#getting-started)]

### Step 2: Create the Workflow in n8n <a href="#step-2-create-the-workflow-in-n8n" id="step-2-create-the-workflow-in-n8n"></a>

#### **Node 1: Schedule Trigger**

* Type: `Schedule Trigger`
* Mode: `Every Hours`
* Hours Between Triggers: `4`

<figure><img src="/files/YHl8RKvGByPzHhEYyIXG" alt=""><figcaption></figcaption></figure>

#### **Node 2: Account Config**

* Add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Take the below code, add in your API Keys & paste into n8n:

```
const accounts = [
  {
    client: "Store A",
    apiKey: "ig_live_abc123..." // paste actual key here
  },
  {
    name: "Store B",
    apiKey: "ig_live_xyz789..."
  }
  // Add more accounts here as needed
];

return accounts.map(account => ({ json: account }));
```

<figure><img src="/files/EnLqvi6kYZ1h9YXM7MXG" alt=""><figcaption></figcaption></figure>

#### **Node 3: Get All Running Tests**

1. Click **"+"** after the Schedule node
2. Search for **"HTTP Request"**
3. Configure:
   * **Method**: GET
   * **URL**: `https://api.intelligems.io/v25-10-beta/experiences-list?category=experiment&status=started`
   * **Authentication**: None
   * Enable **Send Headers**
     * **Name**: `intelligems-access-token`
     * **Value**: `{{ $json.apiKey }}`
4. Click "Execute step" to verify it works

<figure><img src="/files/yLY6JTSUfOcvs5Zb97Lk" alt=""><figcaption></figcaption></figure>

#### **Node 4: Flatten the Results**

* Add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Update the below code so that `InsertOrgId` maps to match the Intelligems organization IDs of your clients (you can find these in Intelligems under Settings > General > Organization Settings), the `name` value is display name for your clients, and the `apiKey` value is the Intelligems API Key for those clients. Then paste this code into the Code section in n8n.

```
const items = $input.all();
const results = [];

const clientMapping = {
  "orgId1": {
    name: "Client 1",
    apiKey: "apiKey1"
  },
  "orgId2": {
    name: "Client 2",
    apiKey: "apiKey2"
  }
};

for (const item of items) {
  const orgId = item.json.organizationId || item.json.organization?.id || (item.json.experiencesList && item.json.experiencesList[0]?.organizationId);
  const clientData = clientMapping[orgId] || { name: "Unknown Client", apiKey: "" };

  const experiencesList = item.json.experiencesList;

  if (experiencesList) {
    for (const test of experiencesList) {
      if (test.status !== "started") {
        continue;
      }

      results.push({
        json: {
          clientName: clientData.name,
          apiKey: clientData.apiKey,
          experienceId: test.id,
          experienceName: test.name
        }
      });
    }
  }
}

return results;
```

<figure><img src="/files/KpIXHf0JElG0IcWizGTJ" alt=""><figcaption></figcaption></figure>

#### **Node 5: Get Test Analytics Data**

1. Add **"HTTP Request"** node
2. Configure:
   * **Method**: GET
   * **URL**: `https://api.intelligems.io/v25-10-beta/analytics/resource/{{ $json.experienceId }}`
   * **Authentication**: None
   * Enable **Send Headers**
     * **Name**: `intelligems-access-token`
     * **Value**: `{{ $json.apiKey }}`
3. Click "Execute step" to verify it works

<figure><img src="/files/vdkzmCHVkw5X2DWmIfGx" alt=""><figcaption></figcaption></figure>

#### **Node 6: Check if enough data is gathered**

* Add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Update the below code so that the `MIN_ORDERS_PER_VARIATION` & `MIN_DAYS_RUNNING` values match your threshold requirements. Then paste this code into the Code section in n8n.

```
const item = $input.item.json;

const experienceId = item.experienceId;
const experienceName = item.experienceName;
const clientName = item.clientName;
const apiKey = item.apiKey;
const startedAtTs = item.startedAtTs;

// The analytics response
const metrics = item.metrics || [];
const variations = item.variations || [];

// Minimum thresholds before we allow any shift
const MIN_ORDERS_PER_VARIATION = 300;
const MIN_DAYS_RUNNING = 3; // 72 hour lock-in period

// Calculate how long the test has been running
const daysRunning = startedAtTs
  ? Math.floor((Date.now() - new Date(startedAtTs).getTime()) / (1000 * 60 * 60 * 24))
  : 999;

// Build a lookup of metric values keyed by variation_id
const metricsByVariation = {};
for (const m of metrics) {
  const vid = m.variation_id;
  if (!vid) continue;
  if (!metricsByVariation[vid]) metricsByVariation[vid] = {};
  if (m.net_revenue_per_visitor !== undefined) {
    metricsByVariation[vid].netRevenuePerVisitor = m.net_revenue_per_visitor;
  }
  if (m.n_orders !== undefined) {
    metricsByVariation[vid].nOrders = m.n_orders;
  }
  if (m.n_visitors !== undefined) {
    metricsByVariation[vid].nVisitors = m.n_visitors;
  }
  if (m.conversion_rate !== undefined) {
    metricsByVariation[vid].conversionRate = typeof m.conversion_rate === 'object'
      ? m.conversion_rate.value
      : m.conversion_rate;
  }
}

// Build variation summary
const variationSummary = variations.map(v => ({
  id: v.id,
  name: v.name,
  isControl: v.isControl,
  currentPercentage: v.percentage,
  nOrders: metricsByVariation[v.id]?.nOrders ?? 0,
  nVisitors: metricsByVariation[v.id]?.nVisitors ?? 0,
  netRevenuePerVisitor: metricsByVariation[v.id]?.netRevenuePerVisitor ?? 0,
  conversionRate: metricsByVariation[v.id]?.conversionRate ?? 0,
}));

// Gates: both must pass before bandit logic is allowed to run
const tooEarly = daysRunning < MIN_DAYS_RUNNING;
const belowThreshold = variationSummary.some(v => v.nOrders < MIN_ORDERS_PER_VARIATION);

return [{
  json: {
    clientName,
    apiKey,
    experienceId,
    experienceName,
    daysRunning,
    variations: variationSummary,
    readyForBandit: !tooEarly && !belowThreshold,
    skipReason: tooEarly
      ? `Test only ${daysRunning} day(s) old — minimum is ${MIN_DAYS_RUNNING}`
      : belowThreshold
      ? `One or more variations below ${MIN_ORDERS_PER_VARIATION} order minimum`
      : null
  }
}];
```

<figure><img src="/files/7OeQXbtyCLQm3dOqbK3y" alt=""><figcaption></figcaption></figure>

#### **Node 7: If readyForBandit = True**

1. Add an **"If"** node
2. Set the condition as `{{ $json.readyForBandit }}` is equal to `true`

<figure><img src="/files/1LeGd7pXl4JodaxvBao1" alt=""><figcaption></figcaption></figure>

#### **Node 8: Is Signal Meaningful (Round 1)**

* For the **"true"** response only, add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Copy the below code into n8n:

```
const item = $input.item.json;

const {
  clientName,
  apiKey,
  experienceId,
  experienceName,
  variations,
} = item;

// ─── Config ───────────────────────────────────────────────────────────────────
const MIN_LIFT_TO_SHIFT = 0.05;
const TRAFFIC_LADDER = [
  [50, 50],
  [40, 60],
  [30, 70],
  [20, 80],
];
const MAX_LADDER_INDEX = TRAFFIC_LADDER.length - 1;

// ─── Helper: extract RPV regardless of data shape ─────────────────────────────
const getRPV = (v) => {
  const raw = v.netRevenuePerVisitor ?? v.allMetrics?.netRevenuePerVisitor ?? 0;
  return typeof raw === 'object' && raw !== null ? (raw.value ?? 0) : Number(raw) || 0;
};

// ─── Find control and challengers ─────────────────────────────────────────────
const control = variations.find(v => v.isControl);
const challengers = variations.filter(v => !v.isControl);

if (!control || challengers.length === 0) {
  return [{ json: { ...item, action: "skip", reason: "Could not identify control or challenger" } }];
}

const controlRPV = getRPV(control);

const challengers_with_lift = challengers.map(v => {
  const rpv = getRPV(v);
  const lift = controlRPV > 0 ? (rpv - controlRPV) / controlRPV : 0;
  return { ...v, rpv, lift };
});

// Best challenger = highest RPV
const bestChallenger = challengers_with_lift.reduce((best, v) =>
  v.rpv > best.rpv ? v : best
, challengers_with_lift[0]);

const lift = bestChallenger.lift;

// ─── Find current ladder position ─────────────────────────────────────────────
const currentControlPct = control.currentPercentage;
const currentLadderIndex = TRAFFIC_LADDER.findIndex(([cp]) => cp === currentControlPct);
const resolvedIndex = currentLadderIndex === -1 ? 0 : currentLadderIndex;

// ─── Decision logic ───────────────────────────────────────────────────────────
let newLadderIndex;
let action;
let reason;

const controlDominatesAll = challengers_with_lift.every(v => v.lift < -MIN_LIFT_TO_SHIFT);
const challengerIsWinning = lift > MIN_LIFT_TO_SHIFT;

if (controlDominatesAll) {
  newLadderIndex = 0;
  action = "revert";
  reason = `Control RPV ($${controlRPV.toFixed(4)}) is outperforming all challengers by 5%+ — reverting to 50/50`;

} else if (challengerIsWinning) {

  if (resolvedIndex >= MAX_LADDER_INDEX) {
    newLadderIndex = MAX_LADDER_INDEX;
    action = "hold";
    reason = `Already at max split (${TRAFFIC_LADDER[MAX_LADDER_INDEX][1]}/${TRAFFIC_LADDER[MAX_LADDER_INDEX][0]}). RPV lift: ${(lift * 100).toFixed(2)}%`;
  } else {
    // Signal winning — the flow will wait 4hrs and re-run before acting
    newLadderIndex = resolvedIndex + 1;
    action = "winning";
    reason = `RPV lift is ${(lift * 100).toFixed(2)}% — challenger is ahead`;
  }

} else {
  newLadderIndex = resolvedIndex;
  action = "hold";
  reason = `RPV lift is ${(lift * 100).toFixed(2)}% — below 5% threshold, holding`;
}

// ─── Build output ─────────────────────────────────────────────────────────────
const [newControlPct, newChallengerPct] = TRAFFIC_LADDER[newLadderIndex];

const updatedVariations = variations.map(v => {
  if (v.id === control.id)        return { ...v, newPercentage: newControlPct };
  if (v.id === bestChallenger.id) return { ...v, newPercentage: newChallengerPct };
  return { ...v, newPercentage: v.currentPercentage };
});

return [{
  json: {
    clientName,
    apiKey,
    experienceId,
    experienceName,
    controlName:    control.name,
    controlRPV,
    challengerName: bestChallenger.name,
    challengerRPV:  bestChallenger.rpv,
    liftPct:        parseFloat((lift * 100).toFixed(2)),
    action,
    reason,
    currentSplit:   `${bestChallenger.currentPercentage}/${currentControlPct}`,
    newSplit:       `${newChallengerPct}/${newControlPct}`,
    updatedVariations,
  }
}];
```

<figure><img src="/files/EF4KfbjaFfWiRgYPZRBr" alt=""><figcaption></figcaption></figure>

#### **Note 9: If action = winning or revert**

* Add an **"If"** node
* Set the condition as `{{ $json.action }}` is equal to `winning` OR `{{ $json.action }}` is equal to `revert`

<figure><img src="/files/Rh4afDqJYtti4cAPLHzG" alt=""><figcaption></figcaption></figure>

#### **Node 9: Wait 4 hours**

* For the **"true"** response only, add **"Wait"** node
* Set Wait Amount to `4`
* Set Wait Unit to `Hours`

<figure><img src="/files/833HMDR8IqqjSwdTEIdH" alt=""><figcaption></figcaption></figure>

#### **Node 5: Get Fresh Test Analytics Data**

1. Add **"HTTP Request"** node
2. Configure:
   * **Method**: GET
   * **URL**: `https://api.intelligems.io/v25-10-beta/analytics/resource/{{ $json.experienceId }}`
   * **Authentication**: None
   * Enable **Send Headers**
     * **Name**: `intelligems-access-token`
     * **Value**: `{{ $('Code in JavaScript1').item.json.apiKey }}`
3. Click "Execute step" to verify it works

<figure><img src="/files/ryRJ9kylzFIET4LP5xNr" alt=""><figcaption></figcaption></figure>

#### **Note 10: Is Signal Meaningful (Round 2)**

* Add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Copy the below code into n8n:

```
const analyticsData = $input.item.json;

// Identity fields from the first bandit check (still in memory from Code in JavaScript3)
const prev = $('Code in JavaScript3').first().json;
const clientName     = prev.clientName;
const apiKey         = prev.apiKey;
const experienceId   = prev.experienceId;
const experienceName = prev.experienceName;

// ─── Config ───────────────────────────────────────────────────────────────────
const MIN_LIFT_TO_SHIFT = 0.05;
const TRAFFIC_LADDER = [
  [50, 50],
  [40, 60],
  [30, 70],
  [20, 80],
];
const MAX_LADDER_INDEX = TRAFFIC_LADDER.length - 1;

// ─── Helper: extract RPV regardless of data shape ─────────────────────────────
const getRPV = (v) => {
  const raw = v.netRevenuePerVisitor ?? v.allMetrics?.netRevenuePerVisitor ?? 0;
  return typeof raw === 'object' && raw !== null ? (raw.value ?? 0) : Number(raw) || 0;
};

// ─── Reshape fresh analytics into variation summary ───────────────────────────
const metrics    = analyticsData.metrics    || [];
const variations = analyticsData.variations || [];

const metricsByVariation = {};
for (const m of metrics) {
  const vid = m.variation_id;
  if (!vid) continue;
  if (!metricsByVariation[vid]) metricsByVariation[vid] = {};
  if (m.net_revenue_per_visitor !== undefined) metricsByVariation[vid].netRevenuePerVisitor = m.net_revenue_per_visitor;
  if (m.n_orders    !== undefined) metricsByVariation[vid].nOrders    = m.n_orders;
  if (m.n_visitors  !== undefined) metricsByVariation[vid].nVisitors  = m.n_visitors;
  if (m.conversion_rate !== undefined) {
    metricsByVariation[vid].conversionRate = typeof m.conversion_rate === 'object'
      ? m.conversion_rate.value
      : m.conversion_rate;
  }
}

const variationSummary = variations.map(v => ({
  id:                   v.id,
  name:                 v.name,
  isControl:            v.isControl,
  currentPercentage:    v.percentage,
  nOrders:              metricsByVariation[v.id]?.nOrders    ?? 0,
  nVisitors:            metricsByVariation[v.id]?.nVisitors  ?? 0,
  netRevenuePerVisitor: metricsByVariation[v.id]?.netRevenuePerVisitor ?? 0,
  conversionRate:       metricsByVariation[v.id]?.conversionRate ?? 0,
}));

// ─── Find control and challengers ─────────────────────────────────────────────
const control     = variationSummary.find(v => v.isControl);
const challengers = variationSummary.filter(v => !v.isControl);

if (!control || challengers.length === 0) {
  return [{ json: { experienceId, experienceName, action: "skip", reason: "Could not identify control or challenger" } }];
}

const controlRPV = getRPV(control);

const challengers_with_lift = challengers.map(v => {
  const rpv  = getRPV(v);
  const lift = controlRPV > 0 ? (rpv - controlRPV) / controlRPV : 0;
  return { ...v, rpv, lift };
});

const bestChallenger = challengers_with_lift.reduce((best, v) =>
  v.rpv > best.rpv ? v : best
, challengers_with_lift[0]);

const lift = bestChallenger.lift;

// ─── Find current ladder position ─────────────────────────────────────────────
const currentControlPct  = control.currentPercentage;
const currentLadderIndex = TRAFFIC_LADDER.findIndex(([cp]) => cp === currentControlPct);
const resolvedIndex      = currentLadderIndex === -1 ? 0 : currentLadderIndex;

// ─── Decision logic ───────────────────────────────────────────────────────────
let newLadderIndex, action, reason;

const controlDominatesAll  = challengers_with_lift.every(v => v.lift < -MIN_LIFT_TO_SHIFT);
const challengerIsWinning  = lift > MIN_LIFT_TO_SHIFT;

if (controlDominatesAll) {
  newLadderIndex = 0;
  action = "revert";
  reason = `Control RPV ($${controlRPV.toFixed(4)}) is outperforming all challengers by 5%+ — reverting to 50/50`;

} else if (challengerIsWinning) {
  if (resolvedIndex >= MAX_LADDER_INDEX) {
    newLadderIndex = MAX_LADDER_INDEX;
    action = "hold";
    reason = `Already at max split (${TRAFFIC_LADDER[MAX_LADDER_INDEX][1]}/${TRAFFIC_LADDER[MAX_LADDER_INDEX][0]}). RPV lift: ${(lift * 100).toFixed(2)}%`;
  } else {
    newLadderIndex = resolvedIndex + 1;
    action = "winning"; // confirmed second cycle — IF node will advance
    reason = `RPV lift is ${(lift * 100).toFixed(2)}% for 2 consecutive cycles — ready to advance`;
  }

} else {
  newLadderIndex = resolvedIndex;
  action = "hold";
  reason = `RPV lift is ${(lift * 100).toFixed(2)}% — lead not maintained after 4 hours, holding`;
}

const [newControlPct, newChallengerPct] = TRAFFIC_LADDER[newLadderIndex];
const splitChanged = action === "winning" || action === "revert";

const updatedVariations = variationSummary.map(v => {
  if (v.id === control.id)        return { ...v, newPercentage: newControlPct };
  if (v.id === bestChallenger.id) return { ...v, newPercentage: newChallengerPct };
  return { ...v, newPercentage: v.currentPercentage };
});

return [{
  json: {
    clientName,
    apiKey,
    experienceId,
    experienceName,
    controlName:    control.name,
    controlRPV,
    challengerName: bestChallenger.name,
    challengerRPV:  bestChallenger.rpv,
    liftPct:        parseFloat((lift * 100).toFixed(2)),
    action,
    reason,
    splitChanged,
    currentSplit:   `${bestChallenger.currentPercentage}/${currentControlPct}`,
    newSplit:       `${newChallengerPct}/${newControlPct}`,
    updatedVariations,
  }
}];
```

<figure><img src="/files/FE34O5suKG7tpDm5ACbz" alt=""><figcaption></figcaption></figure>

#### **Node 11: If action = winning or revert**

1. Add an **"If"** node
2. Set the condition as `{{ $json.action }}` is equal to `winning` OR `{{ $json.action }}` is equal to `revert`

<figure><img src="/files/Rh4afDqJYtti4cAPLHzG" alt=""><figcaption></figcaption></figure>

#### Node 12: Get Full Experience Config <a href="#bonus-enhancement-ideas" id="bonus-enhancement-ideas"></a>

* For the **"true"** response only, add a **"HTTP Request"** node
* Configure:
  * **Method**: GET
  * **URL**: `https://api.intelligems.io/v25-10-beta/analytics/resource/{{ $('Code in JavaScript1').item.json.experienceId }}`
  * **Authentication**: None
  * Enable **Send Headers**
    * **Name**: `intelligems-access-token`
    * **Value**: `{{ $('Code in JavaScript1').item.json.apiKey }}`
* Click "Execute step" to verify it works

<figure><img src="/files/naxxsqsiS6ELGKm44VwW" alt=""><figcaption></figcaption></figure>

#### Node 13: Build the Payload <a href="#bonus-enhancement-ideas" id="bonus-enhancement-ideas"></a>

* Add **"Code"** node
* Select **"Code in JavaScript"**
* Select **"Run Once for All Items"**
* Copy the below code into n8n:

```
const fullExperience = $input.item.json.experience;
const prev = $('Code in JavaScript4').first().json;

const newPctById = {};
for (const v of prev.updatedVariations) {
  newPctById[v.id] = v.newPercentage;
}

const updatedVariations = fullExperience.variations.map(v => ({
  ...v,
  percentage: newPctById[v.id] ?? v.percentage
}));

return [{
  json: {
    apiKey: prev.apiKey,
    experienceId: prev.experienceId,
    bodyString: JSON.stringify({   // <-- stringify here
      experience: {
        ...fullExperience,
        variations: updatedVariations
      }
    })
  }
}];
```

<figure><img src="/files/SoVidRy1QyMkjqvc3PoW" alt=""><figcaption></figcaption></figure>

#### Node 12: Update the Traffic Allocation <a href="#bonus-enhancement-ideas" id="bonus-enhancement-ideas"></a>

* Add **"HTTP Request"** node
* Configure:
  * **Method**: PUT
  * **URL**: `https://api.intelligems.io/v25-10-beta/experiences/{{ $json.experienceId }}`
  * **Authentication**: None
  * Enable **Send Headers**
    * **Name**: `intelligems-access-token`
    * **Value**: `{{ $('Code in JavaScript1').item.json.apiKey }}`
    * **Name:** `Content-Type`
    * **Value:** `application/json`
  * Enable **Send Body**
    * Content Type: `Raw`
    * Body: `{{ $json.bodyString }}`
      * Be sure to use "Expression" & not "fixed"
    * Content Type: `application/json`
* Click "Execute step" to verify it works

<figure><img src="/files/th6UP44K6oclOvk5WYQT" alt=""><figcaption></figcaption></figure>

### You did it! <a href="#bonus-enhancement-ideas" id="bonus-enhancement-ideas"></a>

Now your tests traffic allocations will automatically allocate more traffic to a winning variant.

<figure><img src="/files/Imd3uykCe16L1XGofICj" alt=""><figcaption></figcaption></figure>

### Bonus: Enhancement ideas <a href="#bonus-enhancement-ideas" id="bonus-enhancement-ideas"></a>

* **Dynamically use Primary Metrics** - Instead of always using Revenue per Visitor, dynamically use whatever is set as the primary metric.
* **Use Multiple Metrics** - Use a combination of Revenue per Visitor, Conversion Rate & others before changing traffic allocation.
* **Set this up to only run on certain tests instead of all** - Don't run the Bandit on every single test. Add a filter in your n8n workflow to only target specific experiments.
* **Time-Weighted Decay** - Using a moving window (e.g., the last 7 days of data) ensures your traffic allocation stays "fresh"


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.intelligems.io/developer-resources/external-api/automations-and-guides/build-a-multi-armed-bandit-for-dynamic-traffic-optimization.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
