169M offer decisions a month on 0.77 CPU cores

SamraatBansal1 pts0 comments

How Our Pricing Engine Evolved from an If-Ladder

Solutions

Resources

EnterprisePricingSign up

Sign up

How Our Pricing Engine Evolved from an If-Ladder

Samraat

August 14, 2026

min read

Last month Flux: our Engine, answered 169 million versions of one question: does this person, buying this service, right now, get a discount on it?<br>Each answer is computed from current inputs. Flux evaluates stack of facts available when the page loads: the request country, the accountʼs plan, warehouse-derived lifetime spend, and a propensity score for the likelihood of a top-up. It repeats the evaluation for every item on the page. One itemʼs result cannot determine anotherʼs.

A campaign request can fit in one sentence: 20% off the 500-credit tile for repeat top-up users in the US, paying through one gateway, during the nine days around Black Friday. In code, those clauses cross user, billing, data, and time boundaries. Put them in checkout, and checkout still knows about Black Friday in March.<br>We wrote that code multiple times before we admitted it was a product.<br>THE SHAPE OF THE PROBLEM<br>Every tile on the screen is a separate question.<br>Every pricing surface has this shape. A catalog of things you can sell, a person looking at it, and a handful of campaigns that apply to some pairings of the two. Ours happens to be credit bundles and subscription plans. Yours might be seats, add-ons, shipping tiers, or storage. The count does not matter. What matters is that the page cannot show a price until something has decided, one item at a time, whether this particular person gets a discount on that particular thing.

The obvious implementation lives in the payments service. Load the active campaigns, loop over the tiles, and for each pair check the conditions in an if-ladder: right country, big enough amount, campaign still running, user has topped up before.<br>That version works. We shipped it, and it was the correct call at the time.<br>Here is what it grows into. The following is a compressed facsimile of our own checkout path, renamed but structurally honest, after four campaigns had landed in it:

None of that is a payments concern. Every branch in it is a marketing decision that happens to be written in Python and shipped by the team that owns money movement. The comment declaring the priority order is load bearing, because the order is not expressed anywhere a person could read it except that comment.<br>The cost arrives later, and not where you expect. Every new campaign becomes a code change riding the billing deploy train. Then the growth team needs the same answer on a different surface, writes the ladder a second time, and gets it subtly wrong, because nothing forces the second copy to agree with the first. Now two services disagree about who qualifies, and the bug arrives as a sentence no engineer wants to read: the app showed me 20% off and checkout charged me full price.<br>THE MODEL<br>Separate the item, the audience, and the exposure.<br>Flux evaluates eligibility through three independently authored predicates.

The rule lives on<br>The question it answers<br>Changes when

The offer<br>Does this item qualify? Is it a big enough purchase, in the right window?<br>a campaign is designed

The segment<br>Is this person the kind of customer we mean? High spenders in the US, accounts on a trial, anyone who churned last month.<br>growth defines an audience

The rollout<br>May this audience see it yet?<br>as launch exposure expands

The last two look similar on a whiteboard, but the schema gives them different vocabularies. A segment may use any of the 33 attributes. A rollout is limited to only a few exposure attributes like: internal user, tier, user id, timestamp etc.<br>That is what makes a staged launch possible. A discount goes live to internal users on the real production path with real money, sits there while someone watches whether anyone takes it, then opens to a tier, then to everyone the segment already described. The audience never changes. Only the door moves.<br>Two of the three are append-only. Segments have no update endpoint at all, and an offer’s benefit cannot be edited after it is created; you supersede either one by writing a new row and archiving the old. Dates, priority and status still move, because those are operational. The discount itself never does. A redemption from March still points at the exact benefit that was granted, and an audience defined in May still means in November what it meant then, which is how we avoid answering “what did this customer actually get” by reading a changelog.<br>An offer is visible when its own rule passes and at least one rollout passes. A rollout passes only when both its segment and its exposure condition pass: offer rule AND ((segment AND exposure) OR ...) . Another audience can therefore be added without changing the first.

That formula also sets the execution order. Because the whole predicate is an AND, the cheap half can go first. Flux loads the active offers once and filters them in memory by...

audience offer person item still exposure

Related Articles