Skip to content

Optimize for what a visit is worth

Without a value, every conversion counts the same, and routing learns to get more of them. A value says what each conversion is worth, so that routing learns to get the most value per visit (VPV): the conversion rate times the value per conversion, in your value’s own unit, as you define it. It then favors the annual plan over the monthly one, the bundle over the single item. This guide chooses what the value counts, declares it with its forms, window and cap, says which amounts count, and reads the results.

Give a value to the pages whose variants can change what is bought: products, prices, plans and offers. Where variants can only change how many visitors convert, as most headlines do, a value only adds the spread between orders for routing to see through, so keep the objective as it is.

Choose what the value counts

A value counts what a conversion leaves your business. For orders, take the first of these you can:

  1. Real margins: what each product sells for, less what it costs you. You or your agent look for the costs where they live, in your product catalog, your code or a file you keep, or you give them. A table of each product’s margin values it, as { items: './margins.csv' }, and on a Shopify store’s purchases, 'margin' reads each product’s cost from Shopify.
  2. An average margin for the products without a cost: one rate on their revenue, less shipping. otherwise and plus give it, as { margin: true, otherwise: 0.4, plus: -6 } on a Shopify store’s purchases, or { items: './margins.csv', otherwise: 0.4, plus: -6 } beside your table. It comes before revenue less shipping, which counts every dollar of sales as profit, so that shipping weighs too little: at a 40% margin, a $50 order earns $20, and $6 of shipping takes a third of it, while revenue less shipping scores the order $44 and takes off about a tenth.
  3. Revenue less shipping, { amount: true, plus: -6 }, where no margin can be estimated: a partial correction, in the right direction.
  4. Revenue, conversions or another value you choose: revenue is 'amount', conversions keep the objective without a value, and a value of your own, such as points, takes a number or a property.

Shipping, in each, is what shipping an order costs you, net of what the customer pays for it.

Leave out costs already paid for the visit, such as ad spend. It is paid before routing acts, whichever variant the visitor sees, so taken off each order it would only make the conversions routing adds look worth less, and tell no variants apart. A cost per order, in plus, is what fulfilling the order costs, such as its shipping.

Plans and trials are worth their lifetime value, which you or your agent fill in from your own numbers:

  • a monthly plan’s lifetime value ≈ its price × your gross margin ÷ its monthly churn;
  • an annual plan’s ≈ its price × the gross margin × (1 + its expected renewals), in the same unit as the monthly one;
  • a trial’s ≈ the share of its trials that pay × its plan’s lifetime value.

A $19 monthly plan at an 80% gross margin, whose customers leave at 5% a month, is worth about $19 × 0.8 ÷ 0.05 = $304, and where 40% of its trials pay, a trial of it is worth about $122.

Declare a value

An objective with a value says what each of its events is worth, in routingcat.config.ts, or in a page’s page() for that page alone. Its events are the value’s keys, so it takes no events: objective: { value: { trial_started: 15, purchase: 'amount' }, unit: 'USD', name: 'gross profit', cap: 1000 }.

  • unit is a currency’s code in three capital letters, as USD, or a word of your own in lowercase letters, at most 32, as points. A word of three letters, such as pts, reads as a currency’s code in lowercase, so plan refuses it. Every event counts in that one unit, and RoutingCat converts no currency.
  • name is what results call the value, value by default: with gross profit, they read VPV (gross profit).
  • window is the objective’s window, as for any objective: 24 hours by default.
  • cap is the most one visit counts for, below.

npx routingcat plan checks the value and prints it once, before the page’s limits, here for a value named gross profit, an event a line:

value of demo_booked+lead+order_packed+order_paid+order_placed+purchase+renewal+trial_started: gross profit in USD, at most 1,000 a visit
demo_booked: its property expected_value, −2 each
lead: its property expected_value
order_packed: its items' margins, others of unknown value
order_paid: items by a table of 1,240, others at 35% of their amount, −6.50 each
order_placed: its items' margins, others at 35% of their amount
purchase: its amount
renewal: its amount, −6.50 each
trial_started: 15 each

Config and declarations gives every option.

Choose what each event is worth

Each event takes one form:

  • A number, the same each time: what you estimate a trial or a lead is worth, such as a trial’s value, above.
  • 'amount', the event’s own amount, its value: a purchase whose price is what it is worth to you.
  • { property: 'expected_value' }, a number your own code computes and sends in the event’s properties. Any rule of yours fits here, since it runs where your code runs.
  • { items: … }, its items, each by what its id is worth in a table: plans by their lifetime value, as { items: { 'stripe:price_m': 120, 'stripe:price_y': 400 } }, or products by their margin. A long table goes in a JSON file, or a CSV file of id,value lines, from your config’s folder. otherwise values the items it leaves out, a share of their amount or a constant each; without it, an event with an item the table leaves out is of unknown value.
  • 'margin', its items’ amounts less their costs, on a Shopify store’s purchases, whose costs RoutingCat’s Shopify app records, an item without a cost counting by otherwise. Elsewhere, the margin is your code’s to compute and send as a property.

Every form but a number takes plus, a number added once per event whose value is known, negative for a cost per order such as shipping: { amount: true, plus: -6.5 }. A value can be negative, as an order sold below its shipping cost is, and the values of a visit’s events add up within the window, so two orders count twice.

The item ids are your connection’s, stripe:price_…, whop:plan_… or shopify:variant:<id>, or your own, as your code passes them to track in its items. Stripe and Whop say which of their outcomes carry an amount and items. A trial’s start carries no amount: on Stripe it names its plan in its items, so a table values each trial by what a trial of its plan is worth to you, and otherwise values a plan the table leaves out at a share of its price; on Whop it names no price, so a trial is worth a number.

Choose the window and the cap

  • The window is the trade-off it is for any objective: a 24-hour window misses a purchase on day three, and a longer one, up to the visit’s length, delays learning by as long. Conversions and visits explains it.
  • The cap keeps one visit from steering routing: a visit worth more counts the cap, in learning and in the measured gain alike. It expresses a choice, such as keeping wholesale orders or enterprise deals from steering a landing page, and catches a value sent in cents as dollars. Set it near what your largest ordinary visit is worth.
  • A proposed cap comes once enough visits have converted with a known value under the objective’s current values, on every page that uses it. It is one figure for the objective, which npx routingcat status --json gives on each of those pages as results.progress.value.proposed_cap: as value, the percentile its quantile names of what those visits were worth at the current values, before any cap, and the visits it rests on. Declaring it, or any new cap, is a change of the values, below.

What counts, and what doesn’t

  • An amount counts only from your server or a connection. A browser can report that a conversion happened, never what it was worth, since anyone can send its events. So an amount, a property, items or a margin from a browser, or forwarded by PostHog, leaves the conversion’s value unknown, and only a number counts from anywhere.
  • A conversion of unknown value is still a conversion, never a zero: an amount that is missing, or in another currency than the unit, counts so too. Routing learns from it as a conversion, and the measured gain counts it at the mean value per conversion of its visits that day, routed or control, as status says.
  • plan tells you when nothing can carry a value yet: when no event of the objective has come from your server or a connection, and when no connection sends costs for a margin.
  • A value is gross: a refund never lowers it, while revenue, reported beside it, is net of refunds.

When values go missing

npx routingcat status and doctor, and the dashboard on the page, warn when more than a quarter of a page’s conversions of the last 28 days have no known value, or when that share differs between routed and control visits. Routing learns nothing from such a conversion of what a conversion is worth: at a quarter, it needs a third more conversions to know as much. The warning names the cause behind most of them, with its fix:

  • Products without a cost, under 'margin': fill in what is missing, taking the first of these you can, as the value’s choice does:
    1. Real costs, where they live: in Shopify, each variant’s Cost per item. On a store with RoutingCat’s Shopify app, its Settings lists the products without one that sold most, each linked to it in Shopify.
    2. An average margin for the products without a cost: otherwise, as { margin: true, otherwise: 0.4, plus: -6 }, or the app’s margin for products without a cost.
    3. Revenue less shipping, { amount: true, plus: -6 }, where no margin can be estimated.
  • Events without an amount: send value with each from your server.
  • Events from browsers: send them from your server or a connection, since an amount a browser sends never counts.
  • Another currency than the value’s unit: send amounts in the unit, or value the event by a table of its items, since RoutingCat converts no currency.
  • A property without a number, or items a table doesn’t value: send the property with each event from your server; add the items to the table, or value them with otherwise.

Costs you fill in count from your next orders on, so the warning clears as older conversions leave its 28 days. An average margin, or any other new value, is a change of the values, which values the earlier visits too.

Read the results

npx routingcat status leads a value page with its measured value gain, here a pricing page whose value is a plan’s lifetime value:

pricing · objective pricing/trial_started, 24 h · control: the defaults, 10% · live since 08-14 · publication pb_01m4xettc0f7mv04w90b68m1yp
gain +$5,084 lifetime value [−8,421, +18,589] · +37% [−43, +192] VPV vs control · measured · 2,648 matured routed visits since 09-15 · maturity 0.98
+25 conversions (pricing/trial_started) vs defaults [−59, +110] · measured · 5,647 routed / 654 control visits · maturity 0.98
routed VPV (lifetime value) $7.23, control $5.45 · conversion rate 4.0%, control 3.6% · $179 per conversion, control $152 · counted
unknown 5.9% of conversions had no known value, each imputed at the mean per conversion of its visits that day, routed or control
restated at the current definition, the visits of pricing/trial_started@2 valued again from their events
plans default monthly-first · 2 live · fixed share 100%
monthly-first default VPV (lifetime value) $6.40 [5.30, 7.60] · counted VPV $6.33, $152 per conversion · signal 68 Strong · served 49% · 2,763 visits
annual-first favored VPV (lifetime value) $7.79 [6.25, 9.41] · +25% [−4, +59] vs default · counted VPV $7.88, $208 per conversion · signal 62 Strong · served 51% · 2,884 visits
+25% VPV (lifetime value): −9% conversions, +37% per conversion
  • gain is the measured value gain: the value routed visits added over showing everyone the defaults, measured against the control, a total in the value’s own name with its range, then its change in VPV against the control’s, in percent. Beside it, the measured conversion gain counts the conversions routing added. On a small site it often leaves zero first, since a value varies from one conversion to the next.
  • routed is VPV, routed against the control, counted, then what it is made of: the conversion rate and value per conversion.
  • unknown is the share of conversions whose value is unknown, each counted at the mean value per conversion of its visits that day, routed or control. A ! follows where that share differs between routed and control visits: then a variant may be losing values, as through a checkout that drops its amount, and the gain may be off.
  • restated says that a change to the values alone valued the earlier visits again, from their stored events, so the whole gain reads at today’s definition.
  • Each variant shows its predicted VPV with its range, its difference from the default, and its counted VPV and value per conversion. The line under it breaks the difference down, exactly, since the parts multiply: here the model predicts 9% fewer conversions for the annual-first table, each worth 37% more, for 25% more VPV, as 0.91 × 1.37 ≈ 1.25.

npx routingcat show pricing/plans:annual-first gives the variant’s predicted rate and value per conversion with their own ranges.

Explain them to a person as any figure, with the kind and the range:

The output What to say
+$5,084 lifetime value [−8,421, +18,589] “Too early to tell: what routing adds to the pricing page’s lifetime value is somewhere between $8,421 less and $18,589 more than showing everyone the defaults.”
+25% VPV (lifetime value): −9% conversions, +37% per conversion, with show’s ranges “The model predicts the annual-first table’s visits are worth about 25% more, between 4% less and 59% more. Its conversions are each worth more, between 13% and 64% more, while whether it brings fewer of them isn’t known yet: bigger baskets, not more buyers.”
unknown 5.9% of conversions had no known value “About one conversion in seventeen came without a value, so it counts at the average, and nothing shows that share differing between routed and control visits.”

Change the value

Giving an objective a value, or changing its values, makes a new version of it, as signup@2, and the page keeps what routing learned: RoutingCat values the visits routing learns from at the new values, from their stored events, and routing goes on from there.

  • A change to the values alone, such as new margins or a new lifetime value, keeps the page’s scorecards and its measured conversion gain going. The measured value gain values the earlier visits again too, so it reads at the current values, and status says it was restated.
  • Giving a value to an objective keeps its scorecards and its measured conversion gain going too, and routing learns what a visit is worth from the page’s earlier visits at once. The measured value gain starts from that deploy, so give the value before the page is routed where you can.
  • Changing its events or its window starts everything afresh, routing’s learning, figures and scorecards included, as for any objective.

Next

RoutingCat routes each visitor of your site to the variants that convert.