Skip to content

Search the documentation by title, section, or page text.

Traffic Control

Every AI Pricing Model is already running a traffic distribution. From the moment you launch one, it holds an AI Model row set to the weight you chose at launch and a Baseline row holding the remainder, and every Paywall request that reaches the model is split between them. Traffic Control is where you see that dial and change it.

Changing it means more than moving the two starting rows. You can hand a share of the model's traffic to a Placement that is running an A/B test, which gives you a user-level test running alongside the model. You can also schedule a different split to take over for a fixed window and hand traffic back on its own.

Open it from AI Pricing Models in the sidebar: open a model, then select the Traffic Control tab. There is no distribution to create first. The tab opens on the split the model launched with. Traffic Control is available on Pro, Pro Plus, and Enterprise plans. Owner and Admin seats can change a distribution; Viewer seats can read it.

What you can do here#

  • Cap how much traffic the model decides. Lower the AI Model row and give the remainder to the model's Baseline Paywall.
  • Run a real A/B test on part of your traffic. Point a row at a Placement running a live A/B test. Users in that share are picked consistently, each keeps the same variant, and the answer carries the test's ID. See A/B Tests in Traffic Control.
  • Hold a slice back for creatives you are not ready to hand to the model. Put the candidates behind their own Placement, run them there as an A/B test, and give that Placement the share you want to withhold.
  • Schedule a split that reverts on its own. See Scheduled Distributions.

The three allocation targets#

A distribution is a list of rows. Each row has a target and a weight, and the weights must total exactly 100 before it can be saved.

TargetWhat it serves
AI ModelHands the request to the AI decision service, which picks one of the Paywall variants attached to this model. Within this model it is the only target that sets isExperiment: true.
BaselineThe Paywall currently set as the model's Baseline Paywall. Baseline is not a separate object. It is that one Paywall, so the row carries that Paywall's own name and a Paywall chip rather than the word Baseline. Changing the model's Baseline Paywall changes what this row serves.
PlacementThe A/B test running on another Placement. The target's audience is resolved in place, the variant is bucketed on the same profile ID the model was called with, and the answer carries abTestId. The row resolves once and stops, so it can never hand the request on to another distribution.

The Placement row is the flexible one. Because it points at a Placement rather than at a Paywall, you keep editing the test on the Placement, the model keeps the rest of the traffic, and neither side changes which Paywalls it runs.

A Placement row needs a live A/B test on the target

The target Placement must have exactly one audience, that audience must be the all-users audience rather than a segment, and it must be running a live A/B test.

The picker does not filter the list for you, so choose a Placement whose chip reads A/B Test and whose test is running. Anything else is refused on save and nothing is written. On the always-on distribution the on-screen error reads only Failed to save distribution, so if saving stops working right after you add a Placement row, that Placement is almost certainly why.

You always point an allocation at a Placement, never at a Paywall ID. Traffic Control has no Paywall target. To put a specific creative in the mix, put it behind a Placement first. See Placements.

For example, a distribution using all three targets might give the model 50%, the model's Baseline Paywall 20%, and a Placement currently routing to an A/B test 30%. Total: 100% confirms it can be saved, and the Active badge means it is the split serving right now. Selecting a row fills the right-hand panel, where Scoped to names the Placement the allocation resolves through.

The always-on distribution and scheduled distributions#

Every model has exactly one always-on distribution. It is created with the model, has no end date, and is the split the model always returns to. Editing it changes the model's steady state.

Scheduled Distributions are additional splits, each with its own start and end. While a schedule's window contains the current moment, that schedule fully replaces the always-on distribution. Nothing is blended, and the always-on weights are ignored for the duration.

  • The start time is inclusive and the end time is exclusive.
  • At the end of the window, traffic reverts automatically on the very next request.
  • No background job runs. The active window is worked out per request against the server clock, so a schedule that has just started or ended takes effect immediately.
  • Only one scheduled window may cover any given moment. A save that would overlap is rejected, and a schedule with no end date counts as covering everything after its start, so it blocks every later schedule.
  • Dates and times are entered and displayed in your browser's local timezone.

"Current Distribution" is a position, not a name

While nothing is scheduled, the always-on distribution is titled Current Distribution. While a schedule is running, that section is retitled Default Distribution and marked Paused, and a second Current Distribution section appears for the schedule actually serving.

Read Current Distribution as a label for whatever is serving right now.

How the split is applied#

The draw runs in two stages, and they behave differently on purpose.

Stage one: who is in a Placement row#

When a request carries a profile ID and the distribution has at least one Placement row, Botsi hashes that profile ID against a seed taken from the distribution and turns it into a number between 0 and 1. The Placement rows occupy the first part of that range, in row order, each as wide as its share of the total. If the number falls inside a Placement row's span, the request goes to that row.

The hash is arithmetic, not a lookup. The same profile gets the same answer on every request, and nothing is stored: no assignment table to keep in step with your edits, no cohort to fill up, but still a reproducible set of users in the test.

Stage two: the traffic that is left#

A profile whose number falls outside every Placement span never reaches one. Its requests draw between AI Model and Baseline, weighted by those rows, on every request. A user in that leftover traffic can be handed to the model on one fetch and the baseline on the next. That is intended: both arms belong to the same model.

The numbers on the rows are still the shares served. Take 30 to a Placement, 50 to AI Model, 20 to Baseline. 30% of profiles sit in the Placement span. The remaining 70% draw between 50 and 20, which normalizes to five sevenths and two sevenths, so they serve 50% and 20% of all traffic.

No profile ID means no stable subject

Stage one needs someone to hash. A request arriving without a profile ID has nobody, so there is no stage one: the whole draw becomes a single per-request weighted pick across every row, Placement rows included.

Such a request can land on a Placement row, and the same caller can land elsewhere on the next request. Inside that row the A/B test has no subject to bucket either, so it picks a variant per request. Send a profile ID on every Paywall request. It is what makes the split user-level.

What stays stable when you edit#

  • Re-saving does not reshuffle anyone. Saving rewrites the rows, but the seed comes from the distribution rather than the rows, so each profile's number is unchanged.
  • Changing a weight moves the boundary, not the users. Widening a Placement row from 30 to 60 keeps every profile already in it and adds more from the leftover traffic. Narrowing releases the profiles nearest the boundary and keeps the rest.
  • A scheduled distribution draws its own membership. The seed is per distribution, so a schedule has a different set of users in its Placement rows. Deleting and recreating it draws again.
  • The variant a user sees is stable too. Inside the A/B test the variant is bucketed on the test's own salt with the same profile ID, so a user keeps their variant for the life of the test. Relaunching the test mints a new salt, which reshuffles everyone deliberately.

There is no warm-up and no drain in either stage. A saved change applies from the next request.

Good to know#

  • Weights must total exactly 100. Save Changes stays disabled until they do.
  • The AI Model and Baseline rows cannot be removed, only re-weighted. To take one out, set its weight to 0, which is a valid saved value.
  • A baseline at 0 can still serve. The Baseline Paywall is what the AI Model target falls back to whenever it cannot get a decision.
  • A request with no profile ID never reaches the AI Model target. It serves the Baseline Paywall instead, whatever the weights say. The same fallback applies if the decision service errors, times out, or returns nothing. A model with no Paywall flagged as best has nothing to fall back to, and the request fails.
  • The answer says which arm served it. The model's arms return aiPricingModelId, with isExperiment: true on an AI decision and false on the baseline. A Placement row that served an A/B test returns abTestId instead and suppresses aiPricingModelId.
  • Stopping the test does not stop the row. A live test is required when you save, but it is not re-checked at serve time. A stopped test keeps handing out variants through the row. Re-weight to 0 or remove the row to take the traffic back.
  • A Placement row resolves once and stops. If the target drifted since you saved, serving stays forgiving: a plain Paywall is served as-is, and a Placement now owned by another AI Pricing Model serves that model's Baseline Paywall rather than entering its distribution.

The model's own charts do not break the arms out yet. There is no per-arm column in its reporting, so read a Placement arm in the A/B test's own results.

Choosing between this and placement-level testing#

Botsi also has placement-level A/B testing, configured on a Placement rather than inside a model.

UseWhen
Placement-level A/B testingThe test is the whole story for that Placement. Every request matching the audience goes into it. See A/B Tests Overview.
Traffic ControlAn AI Pricing Model owns the Placement and you want to decide what share of its traffic goes where.

They also work together. A Traffic Control row pointed at a Placement running an A/B test gives you both: the model on most of your traffic, and a user-level test on the share you set aside.