Littledata MonitorAI guide

How value metrics are calculated

This guide explains what the monitoring product means by “value”—the dollar and percentage figures you see on customer views, ROI, and some emails. It is the detailed mechanics version: windows, formulas, suppression rules, edge cases.

Looking for a plain-English summary of each banner label? See Value banner glossary — one short entry per tier with worked examples.

For Klaviyo flow checks (live flows, competitors, exclusions), see Klaviyo flows. For ROI emails in Intercom, see Customer emails (Intercom).


What “value” is for

Monitoring pulls together several signals from recent audits (Klaviyo, Meta, Google Ads, GA4 where connected). Each signal can produce a value tier—for example “extra Klaviyo revenue we can attribute” or “extra Google Ads conversion value from server-side tracking.”

The product then:

  1. Estimates a monthly dollar value (or a percentage, for some Meta metrics) for each tier that has enough data.
  2. Compares that value to the store’s Littledata subscription cost (plan price and, when applicable, a simple overage estimate) to show return on spend and how long it might take for the value to cover a month of subscription—where the data supports it.
  3. Picks one “headline” tier to feature when multiple tiers exist, usually favouring positive dollar value with a clear ROI story when plan cost is known.

You may still see other tiers listed (for example Littledata Klaviyo flow revenue when flow exclusion rules are not yet in a passing state, or predicted incremental Klaviyo revenue from audience modelling). They answer different questions; the headline is the one the system thinks is the best single story for ROI.


Klaviyo contributes up to three different value ideas. They are not double-counting the same thing; they measure different things.

Self-serve validation: the “How is this calculated” explainers for the observed Klaviyo tiers link directly to the exact Klaviyo flow reports behind the figures (https://www.klaviyo.com/flow/<flowId>/reports, built in klaviyoFlowReportsUrl), so a customer can check the revenue against Klaviyo’s own dashboard. The explainers deliberately show conclusions, not step-by-step formulas — the full method lives in the “How we calculated your Littledata revenue boost” modal.

1. Measured incremental Klaviyo revenue (observed)

What it is: Total Klaviyo-attributed Placed Order revenue from live flows triggered by Littledata abandonment metrics (viewed product, added to cart, checkout started), summed across all such flows. When flow exclusion rules pass, the Littledata flows are set up to exclude subscribers who already entered the native Klaviyo abandonment flows, so this revenue is fully incremental—it would not be generated by the native flows for the same shoppers.

When it appears: Only if flow exclusion rules are in a passing state and there is positive attributed revenue in either the 30-day or 90-day window on those Littledata-triggered flows. The window matches Littledata Klaviyo flow revenue logic (typically ~30 days of order dates; 90 days is used when that gives a stronger monthly run-rate, or when the rolling 30-day window has zero attributed orders but the 90-day window does not — so a single quiet month doesn’t drop the tier entirely). Prospect audits always attempt this tier, and the conditions above decide it: an account with no live Littledata-triggered flow earns nothing here, so a true prospect never gets it, while an account whose Littledata flows are plainly earning headlines its observed revenue rather than the more conservative prediction. It is deliberately not gated on a Customer record existing — that record is keyed on customers.klaviyo.accountId and misses accounts whose Klaviyo connection is recorded elsewhere. For the same reason the flow-exclusion check is resolved by Klaviyo entityId rather than by shop: a Klaviyo account that also belongs to a customer has that check written under the customer’s shopName, not under __prospect__<accountId>, and looking only at the prospect shopName withheld the tier from every account that had measured revenue to show.

Plain English: “How much incremental email/SMS-attributed order revenue are your Littledata-triggered abandonment flows driving, over the reporting window, when we trust the setup is not double-counting the same people as native flows?”

2. Littledata Klaviyo flow revenue

What it is: Total Klaviyo-attributed order revenue tied to Littledata abandonment triggers (the same three stages), summed across flows—not net of native. This is a gross “how much revenue is flowing through Littledata-named triggers” figure.

Window: Usually about the last 30 days of order dates in Klaviyo’s reporting; if 90 days of data shows a clearly stronger monthly run-rate, the system may use that longer window so sparse months do not understate the store.

Plain English: “How much revenue do your Littledata abandonment flows claim, before comparing to anything else?”

3. Predicted incremental Klaviyo revenue

What it is: A modelled monthly dollar estimate when we have revenue from native (and/or Littledata) flows but want to translate audience reach into money. It does not replace the observed incremental tier; it is another way to express upside when the comparison is about how many people each side can reach.

When it appears: When the Klaviyo flow revenue snapshot on the customer has enough native and/or Littledata flow revenue to anchor the maths, and the Klaviyo account is connected so we can measure audiences. It is hidden from display once measured incremental Klaviyo revenue (tier 1) matches or beats the prediction on a monthly-equivalent basis — a smaller “predicted” figure beside a larger measured one reads as a downgrade. The tier is still computed and stored (prediction accuracy tracking, backtests, the audience-maturity modal); only the row is dropped.

Alternative-trigger prospects (Triple Whale, Elevar, …): A prospect already running a competitor’s server-side trigger has no native baseline, so instead of the native-uplift model we anchor on the competitor’s own flow revenue and credit only the net head-to-head uplift Littledata gains over the alternative (floored at parity — at break-even the value is consolidation + additional flows, not a stage uplift). The uplift comes from a rolling 12-month head-to-head benchmark of shops that ran both. Because the pitch is Littledata replacing the alternative as the single trigger source, we quote the upstream ratio (how Littledata performs when it’s the sole/first source), not a blend that includes shops where Littledata was only a downstream backstop. The benchmark is deliberately conservative in two ways. Until enough comparable upstream shops exist for a competitor + funnel stage, it shows no uplift (parity) rather than a noisy estimate. And where the only evidence is the cross-competitor pool for that stage — we have measured Littledata against some alternatives, but never against this one — we claim half of that uplift: tools at the same funnel stage differ a lot in what they capture, and an identity-resolution tool that already de-anonymises traffic is a much harder baseline to beat than a client-side tag, so quoting the pool at full strength would state a lead over a tool we have never raced. Once three stores run both, that competitor has its own ratio and goes to full weight. Where we can’t yet quote a revenue uplift, the bullet instead cites an audience-reach proof point — “Littledata’s tracking captured +X% more shoppers than {competitor} at the same stage” — drawn from a larger, cleaner cohort (any shop where both trigger metrics fire, not just both run live flows; reach isn’t distorted by flow attribution). Reach is a coverage figure only and never feeds the predicted dollar value. The ROI headline is unchanged; an “upgrade from {competitor}” bullet explains the replace-and-add story. See Klaviyo flows for how the benchmark is collected.

Two things follow from a prospect’s flows being someone else’s. The abandonment flow revenue we quote back to them (“your Klaviyo abandonment flows earned $X in the last 30 days”) counts every live browse / cart / checkout flow whichever tool fires it — matching only Klaviyo’s own three trigger names told a store running its whole programme on an alternative source that it had earned $116 when it had earned $4,619. And the replacement estimate is available on the first audit: the competitor check runs in the audit’s slow phase, so the value compute derives the revenue rows it needs from the live flows directly rather than waiting for a later run to publish them.


How the Klaviyo prediction is built (step by step)

Think of it in four layers: window, pairs, audience uplift, money.

Comparison window

We compare Littledata and native on a fair calendar window:

So the “audience” side and the “revenue” side are trying to line up with how long Littledata has been in the picture (or 30 days, whichever is shorter in effect).

The three pairs (funnel stages)

For each stage—viewed product, added to cart, checkout started—we compare:

We only use a stage if we have both metrics and enough Littledata-side reach to treat the number as stable (at least about 50 unique people on the Littledata trigger in that window). Stages that fail those checks are skipped for the prediction.

Audience uplift (how the revenue multiplier is calculated)

The revenue scaling is based on all unique people who triggered each metric during the comparison window. This gives a consistent result: if Littledata reaches X% more people overall than the native trigger, total flow revenue can be expected to scale by roughly X%.

As a separate, informational figure we also show net-new Klaviyo profiles—people whose Klaviyo profile was created after the start of the window (brand-new-to-Klaviyo). This tells you how much more Littledata is growing the Klaviyo list versus the native trigger, but it is not used as the revenue multiplier. Applying a “share of new profiles” ratio to total flow revenue (which includes long-standing profiles) would mix incompatible denominators and could overstate the result.

Behind the scenes, audience counts are built from daily snapshots of Klaviyo activity so we do not have to re-scan entire months on every run; if snapshots are not complete yet, we fall back to the “all unique people” count only.

Turning uplift into dollars

If native abandonment flows have meaningful attributed revenue in the audit:

If there is no native abandonment revenue but there is Littledata flow revenue:

Audience-uplift damping and the floor (measured-uplift path): when we have the store’s own audience comparison, the Littledata-flow contribution is scaled by how much extra audience Littledata actually reaches versus the native trigger at each stage. A store with strong measured uplift keeps the full contribution; a store with little or no measurable uplift is held to a 30% floor rather than crediting the whole attributed amount. This stops a high-AOV store from booking a large “incremental” figure off last-touch-attributed flow revenue when there is no evidence Littledata expanded the audience — while still recognising that some revenue is genuinely rescued (via identity stitching) even when the Littledata-tagged audience doesn’t visibly exceed native.

Prospects and disconnected stores (peer-benchmark path): when Littledata’s Klaviyo connection isn’t enabled — so there’s no usable native-vs-Littledata audience comparison at all — we don’t fall back to the 30% floor on native revenue. Instead we multiply the store’s native flow revenue by the incremental-to-native flow-revenue ratio that similar, mature Littledata stores actually see (the cohort median, with a fixed fallback when the cohort is still thin). Only stores with ≥200 orders/month back this ratio — it divides two small, noisy numbers, so low-volume stores produce wild per-store ratios; the higher order floor (vs. the ≥30 used for absolute-value cohorts) trims that tail. Because both sides of that ratio are denominated in native flow revenue, the estimate is units-consistent and needs no proxy or extra conservatism trim. The 30% floor still applies only to the rarer fallback where a store has Littledata-tagged flow revenue but no native flows to anchor on.

The result is expressed as an estimated incremental dollars per month (not a guaranteed outcome—see Limitations below).

New Klaviyo connections (maturity and benchmarks)

For integrations newer than about 30 days, raw audience-to-audience ratios are still ramping as rolling Klaviyo audiences fill. The prediction blends each store’s measured uplift with a peer prior (median fractional reach uplift from mature stores on the predicted tier, with a fixed fallback when the cohort is still thin), weighted by min(1, daysSinceConnect / 30). Per-stage maturity factors (from aggregated uplift research) scale early-window measurements toward longer-run behaviour. A small native-flow revenue lift assumption for rescued events (where research supports it) is added so the headline stays conservative but not blind to server-side rescue.

When building benchmark rows for peers, observed Klaviyo incremental and predicted incremental tier values omit shops whose Klaviyo connectedAt is within the last 30 days, so cohort percentiles are not skewed by immature audiences. Customer-facing copy can include prediction confidence and a short audience maturity caveat in the first month.

These cohort percentiles are computed at most once per week and cached (a single valueBenchmarksCache document); every customer/prospect estimate in that window reads the same snapshot. The cohort is a large, fat-tailed, continuously-re-stamped distribution, so recomputing on every audit otherwise made the median swing run-to-run. The first request after the snapshot ages past 7 days recomputes and re-stamps it; the admin value-benchmarks endpoint can force a refresh.


Two related numbers can appear for Google Ads, depending on what the account looks like.

Google Ads ROAS uplift (googleAdsROAS) compares the server-side Purchase - Littledata conversion action — orders Littledata uploads to Google with a matched GCLID — against the next best conversion goal already feeding Smart Bidding on the same Google Ads account, over the same spend. The dollar headline is the coverage gap (Littledata minus baseline) — revenue that next-best goal isn’t tracking — for the chosen window, scaled to a 30-day equivalent. Framed as “revenue your next best conversion goal doesn’t track” rather than “revenue LD adds”: those orders happen regardless, but without LD’s server-side upload they’re not attributed back to Google Ads, and Smart Bidding can’t bid against them.

Purchase revenue tracked (googleAdsTrackedRevenue, microsoftAdsTrackedRevenue) is the fallback on either channel when no baseline purchase goal can anchor a comparison — no goal at all, or one rejected by the baseline rules below.

These are deliberately not incrementality claims, and the naming reflects that. With no baseline there is no measured lift over one, so calling the figure “incremental” would credit Littledata for revenue no comparison established. Each tier states only what it can defend: the purchase revenue Littledata tracked on that channel’s clicks, and the spend it was tracked across — the number that stops the revenue reading as a return.

Consequently they carry no roi and are barred from every ROI path by isNonIncrementalValueTier — the same bar klaviyoLittledataDimensionRevenue sits behind. They cannot set the ROI multiple, headline a hero card, or join an incremental rollup. They surface as additional value: real, worth stating, not counted as a return. Being the only purchase signal an account can bid on is a meaningful finding on its own — it is just not the same as having proved the revenue incremental.

Why Google changed. This tier previously applied the 10% Smart Bidding lift to Littledata’s whole uploaded revenue and published the result as ROI — with no SMART_BIDDING_SPEND_CAP, which only guarded the baseline branch. Across 79 accounts on the tier, 26 claimed a Smart Bidding contribution above half their ad spend and four above 100% of it (one at 305%): telling a merchant that bidding contributed three times their entire budget. Removing the lift removes the claim rather than capping it, which is the honest fix when there is no baseline to measure against.

The tooltips show the calculation only — revenue tracked, spend, resulting ROAS. Why no comparison exists belongs in the ad-platform audit checks, which name the goal and the rejection reason; repeating it in the tooltip made it an argument rather than a breakdown of the figure.

One exception on Google, because it was a factual error rather than commentary: the tooltip used to say “Purchase - Littledata is the only purchase conversion goal in this account” whenever no baseline was chosen. That is true for 77 of 79 accounts on the tier but false when a goal existed and was rejected — revendo.ch has sale_complete with 1,168 conversions, excluded because every one reports exactly 100.00 CHF. It now names the excluded action instead.

When a baseline is rejected

A purchase goal can exist and still be unusable as the other side of a ROAS comparison. src/lib/roasBaseline.ts holds the rules, shared by both ad channels:

Reason Meaning
too_few_conversions Fires on a small fraction of the orders Littledata uploads — the client-side tag is blocked or half-installed, so the uplift multiple is meaningless
no_value Records the conversion but no revenue, so roasWithoutLd is 0 and all of Littledata’s revenue reads as incremental
implausible_aov Carries a placeholder or partial value (a fixed £1, shipping only), far off Littledata’s AOV
hardcoded_value Reports the same exact value-per-conversion every day — a number typed into the ads UI, not order revenue

Microsoft Ads enforces all four and falls back to microsoftAdsTrackedRevenue. Google Ads enforces hardcoded_value and the AOV rule; valueless actions stay eligible there, and a primaryForGoal action bypasses the AOV check, because Smart Bidding optimises against it regardless.

How we pick the “best comparison” baseline

When multiple purchase-like conversion actions exist on the account, the system picks one baseline rather than summing them (summing would double-count the same orders across competing tags):

  1. Eligible candidates are non-Littledata actions whose name contains a purchase word (purchase, sale, plus localised equivalents: compra, köp, 구매, kauf, achat) or whose action type is GOOGLE_ANALYTICS_4_PURCHASE. Names suggesting non-purchase events (add_to_cart, begin_checkout, page_view, cart, store visit, add_payment, form, call, contact, lead, …) are excluded.
  2. Noise filter: when there are more than three candidates, very long action names (>45 characters) with less than 10% of Littledata’s conversion volume are dropped — these are usually one-off or test actions that would distort the picture.
  3. The remaining candidates are sorted by conversion volume (descending). The intuition: Smart Bidding doesn’t care which tag belongs to whom — whichever primary action carries the most conversions is the closest reflection of what the bidding algorithm is actually optimising against.
  4. Pick:
    • If any candidate is primaryForGoal (Google’s flag for “Smart Bidding optimises against this”) → take the highest-volume primary action. (Primary is trusted regardless of AOV/category — Smart Bidding optimises against it whether we like it or not.)
    • If no primaries exist → narrow to fallback-eligible candidates by dropping any with explicit non-PURCHASE actionCategory (when present on the snapshot) and any whose AOV is outside [1/10, 10] × Littledata AOV (catches non-purchase goals whose name slipped through the include list, e.g. "HubSpot - Contact Sales" with a £1 placeholder value). From those, pick the first with conversions ≤ Littledata’s (so we don’t compare to something far larger and produce a misleadingly negative uplift).
    • If even that fails → the lowest-volume remaining fallback-eligible candidate (last resort).

Why “primary first”: Google Smart Bidding only optimises toward conversion actions marked primary. Comparing Littledata’s ROAS against the action Smart Bidding is actually trying to maximise is the comparison that matters for spend efficiency.

If the chosen baseline is something with a known caveat — GA4 purchase, the Shopify Google Shopping App pixel (which reports product-feed values, not order values), or another UPLOAD_CLICKS server-side feed — a short baselineNote is surfaced explaining how that baseline tends to over- or under-count vs Littledata’s order-level upload. We do not silently re-order candidates around these caveats: Smart Bidding sees what it sees, and the comparison should reflect that.

Comparison window (dynamic 7–90 days)

We try five candidate windows — the last 7, 14, 30, 60 and 90 days — and pick the one that gives the fairest like-for-like ROAS comparison:

The chosen window’s length is the window label shown alongside the figure.

LD-active clamping: the window is also pinned to start no earlier than the first day Littledata had any activity in that account, or the Google Ads connectedAt date, whichever is later. This avoids attributing pre-install spend to Littledata. When connectedAt is unknown, we use a sustained-activity rule — an LD-active day must be followed by ≥7 more LD-active days in the next 14 — so a one-off historical upload isn’t mistaken for the start of live operation.

From snapshots to dollars (the maths)

Inside the chosen window, on the Littledata-active span:

If the baseline conversions audit has flagged the chosen baseline as inflated — duplicate counting, currency mismatch, or an AOV skew that suggests pixel double-fires or modeled conversions — the Smart Bidding lift and ROI line are suppressed entirely. We don’t fabricate a dollar number from an untrusted gap; the baselineNote already surfaces the inflated-baseline caveat for support.

From the coverage gap to monthly Smart Bidding contribution and ROI

For the ROI line and the monthly headline:

This whole section applies only when a baseline exists. When none does (the googleAdsTrackedRevenue fallback), no lift is applied and no ROI is produced: with nothing to subtract there is no coverage gap to size a lift against, so the tier reports tracked revenue as additional value instead. See the two related numbers above.

When the tier is hidden or has caveats


Other channels (short overview)

Idea What you are seeing
Meta additional conversions (ACR) Percentage uplift: Conversions API vs pixel-only, from Meta’s reporting—not a dollar line by itself in the same way.
Google Ads ROAS / tracked revenue Compares server-side Littledata purchase reporting to the best browser-side baseline purchase action over a dynamic 7–90 day window. See Google Ads section above for the full picking logic.
Meta Ads pixel ROAS uplift Compares ROAS on the primary pixel identified as Littledata’s against other pixels on the same ad account.
Additional revenue tracked in GA4 A rough estimate from recent GA4 purchase revenue (when connected), not a full incremental attribution model.
Klaviyo and paid media incremental value When more than one of the dollar tiers above is positive, a combined monthly line may appear that adds Klaviyo’s best incremental story plus Google plus Meta Ads uplift pieces—so you can see total “incremental” across channels.

ROI and “days to value”

When plan cost is known (from billing data we sync, including order limits and overage where applicable):

If plan cost is missing, ROI lines may be hidden or incomplete.

“This would be X× on a cheaper plan”

A store that grows past its order limit keeps paying per-order overage on the smaller plan long after another tier would work out cheaper — e.g. legacy Pro at $449 + 13,905 overage orders = $1,840/month, where the same volume on Plus is $1,257. The ROI reads low purely because the denominator is wrong.

Each audit prices every active tier at the store’s own order volume — base price plus overage on the orders above that tier’s included allowance, never the headline base price alone — and, when one is genuinely cheaper, adds a line under the ROI multiple:

On the Plus plan this would be 1.5× ROI

The plan’s price and the saving behind that multiple sit in the ROI tooltip, so the banner line stays one line wide:

The Plus plan would cost $1,257/month, $582/month less than the current plan

(Plus is $990 for the first 10,000 orders then $0.03/order, so 18,905 orders = 990 + 8,905 × 0.03 = $1,257.)

Multi-store groups on one ad account

A brand often runs several Shopify stores off a single ad account — regional storefronts (a .co.uk and a .com), or a -stage copy of a production store. Across the estate this is common, not exceptional: 18 groups covering 47 shops share a Google Ads customer ID, and two 5-store groups share a Microsoft Ads account.

When such stores are linked as parent/subsidiary, the parent’s rollup must not add the subsidiary’s ad-channel value on top of its own — both describe the same account’s revenue, so the total would double-count.

sharedAdChannels() compares the two customers’ ad-account identifiers per channel (googleAds.customerId, meta.adAccountId, microsoftAds.accountId) and returns the channels where they match. Any tier whose destinationForTierName() resolves to a matching channel is dropped from the rollup.

Note this dedupe applies to the parent/subsidiary rollup. Two unlinked stores sharing an account each still report that account’s value as their own, which is correct per store — but means any aggregate across customers must dedupe by ad-account id before summing.


Limitations (read this with customers)


Technical runbooks and cron debugging for staff live under engineering/ in the repo, not in this folder.