These checks read the Shopify store itself, so they apply to every merchant regardless of which analytics or email platforms are connected.
| Outcome | Meaning |
|---|---|
| Error | Proven problem, costing money or damaging something now. |
| Warning | Worth fixing. |
| Info | FYI. |
| Success | Rule passed. |
| Unknown | Missing data, missing scope, or nothing to compare. |
What we look for: a script creating checkouts with invented shoppers. Each one produces a real Shopify customer record and a real abandonment event, so any connected email platform will try to message an address that does not exist — inflating bounce rates and burning sending reputation. This is the cause; the Klaviyo Spam profiles created check sees the effect.
Because it reads the store rather than an ESP, it fires whatever the merchant sends email with, and it catches floods that never reach a marketing list at all.
What we read: one REST call to checkouts.json?limit=250, never paginated, needing only read_orders (satisfied by write_orders). Flat cost across store sizes.
Store-wide averages do not work, and this was measured rather than assumed. Across ten stores, the signals that look obvious all failed to separate a known flood from healthy stores:
| Signal | Known flood | A healthy store beat it |
|---|---|---|
| Landing-page concentration | 0.55 | cornish-bed-company 0.60 |
| No referrer and no cart attributes | 0.58 | kinvara-skin-care 0.86 |
| Single-line-item share | 0.74 | tenpin-2 0.93 |
| Low gmail share | 0.28 | cornish-bed-company 0.14 |
A small store with one hero product looks identical to a bot flood on every one of them.
What works is a within-store contrast. Bot checkouts carry no referrer and no theme-written cart attributes, because no storefront JS ran — so we split the store’s own checkouts on that (“dark” half), cluster the dark half by landing page, and score the largest cluster against the rest of the same store. The dark half alone proves nothing and is used only to find the candidate cluster, never as evidence.
Headless and custom storefronts are all-dark by construction — no theme JS means no referrer and no cart attributes for any shopper — so darkness can never convict. Every cohort is selected and scored by identity (generated-address structure); an all-dark store full of real customers has nothing for the identity signals to find, and the fleet’s darkest clean store (86% dark) correctly scores SUCCESS.
Rotation cover: floods that spread across many entry pages (the measured campaign already uses up to 7) can fragment every per-landing cluster below the 20-checkout floor. A second cohort — dark checkouts with generated addresses outside the main cluster (the “residue”) — is scored on its own: ≥20 of them whose addresses read as drawn from a provider pool is a primary signal, whatever the landing pages look like.
| Signal | Tier | Clean-fleet baseline (306 clusters) |
|---|---|---|
| Cluster addresses following a generated name template | Primary ≥40%, corroborating ≥25% | p50 2.6%, p99 19% |
| Addresses spread evenly over a few free providers | Primary | 0 clean clusters trip it |
| Addresses that spell out the billing name submitted with them | Corroborating ≥80% | p50 33%, p99 75% — real emails contain real names, so this can never convict alone |
| Entered through one cart-permalink URL with no browsing | Corroborating ≥60% | p99 0% |
| Every checkout holds exactly one item | Corroborating ≥95% | p90 97% — weak, corroborates only |
| Effectively none accepted marketing | Corroborating ≤5% opted in | weak, corroborates only; the bullet names the exact count |
Cart permalinks are legitimate — ad “buy now” links use them — so that signal only ever corroborates.
Two signals were retired after review (#644), and neither loss costs detection because both were already listed as measurement-rejected:
priceVariety is still stored in the check details as diagnostics. Because Error needs two corroborators, a store whose second corroborator was few-totals now grades Warning — the intended direction.The message states the scale; furtherInfo carries the forensics. The message says how many of the store’s recent abandoned checkouts look suspicious (the landing cluster plus the rotated residue, since both cohorts are the same flood) and offers Bot Protection — nothing more. It used to open with the primary signal clauses, which meant a merchant met “generated-looking addresses and 97% have an address that spells out the billing name” before being told what was happening, and there can be six such clauses. furtherInfo lists every signal as a bullet — red for the primary ones that carry the verdict, orange for the corroborating ones — plus the cluster context the message leaves out (the shared landing URL, the residue’s spread across entry pages).
Error and Warning are correct severities here because Littledata can fix it: Bot Protection filters these fakes out of ad and email targeting, and the check links to its help article.
Rules:
| Outcome | Rule |
|---|---|
| Error | ≥2 primary signals and ≥2 corroborating. |
| Warning | Any single primary signal. |
| Info | A firing verdict (either row above) on a store where every connected destination has bot filtering (Connection.settings.filterUnidentifiedEvents) enabled — the fakes exist, but Littledata is already filtering them from all the merchant’s marketing channels, so the message reports what was filtered instead of asking the merchant to act. One unprotected destination keeps full severity, and the message names it as what Bot Protection would filter these events from — “turn on Bot Protection to filter these fake events from Google Analytics 4 and Segment so you don’t waste budget chasing ghosts”. That used to be a second sentence naming the same channels again, which restated the offer it followed. |
| Success | No cluster of ≥20 scriptable checkouts, or no primary signal. |
| Unknown | No Shopify token, missing read_orders, API unreachable, or fewer than 40 recent checkouts carrying an email. |
Corroborators alone never flag: the sweep tried “three weak signals agreeing” and every store it added was a false positive — single-product stores trip one-item, no-opt-in and (before it was retired) few-totals together as a matter of course.
Thresholds were calibrated by a read-only sweep of the whole fleet (3,424 stores, 1,262 scored, 306 with a scoreable cluster), then verified against hand labels: 36/36 confirmed floods detected (13 Error, 23 Warning), 0/8 confirmed false positives flagged, 0 unexplained new flags. The sweep also showed the August 2026 campaign is running on ~36 stores, not the 3 originally known.
Shopify only returns retained abandoned checkouts — far fewer than the checkouts actually started in a period (266 retained against ~3,200 real Checkout Started events in one six-day window we measured). That is enough to detect a cohort but the count is never a flood size. It also means the check reflects the current state: a flood that ended a week ago has aged out of the retained set and reads Success, which is correct but worth knowing when a merchant asks about a past incident.
The endpoint is the legacy REST one; GraphQL’s AbandonedCheckout exposes no landingSite, referringSite or noteAttributes, and abandonmentByAbandonedCheckoutId returns null for ordinary abandoned checkouts. If REST is withdrawn, the fallback is GraphQL minus the landing-page and cart-attribute signals — which the measured evidence does not depend on.
See the check’s own message; it reports whether Littledata’s Markets connection is live and which markets a store runs.
GA4 storefront traffic from Amazon and/or orders through the Amazon sales channel, compared against a cross-store benchmark.
What we look for: an alternative tracking app on the storefront, and — the part no other check can reach — which ad, analytics and email destinations it feeds.
Every other alternative-tracking check holds one destination’s API token, so it can only ever report that one destination: the Google Ads check sees conversion actions in the ad account, the Meta check sees events on the dataset, the Klaviyo check sees flows in Klaviyo. A store running Elevar therefore reads as “an alternative is in Google Ads” when the truth is “an alternative is in Google Ads, Meta, TikTok, Pinterest, Floodlight and two affiliate networks” — and the ones we cannot see are precisely the ones we hold no token for and never will.
The storefront answers all of it, because a client-side tag has to be in the page to fire. No token and no merchant permission — the only input is the store’s domain. It runs on the customer audit path today because that is where a domain is on record; a Klaviyo prospect has none stored, so that path needs a domain before it can run there.
What we read — public URLs only, in this order, each step only taken because the one before it found something:
| Source | Gives |
|---|---|
| The homepage HTML | Theme app embeds (the app handle), the Shopify web-pixel manifest (every app pixel by apiClientId, every custom pixel by name), vendor signatures, and Littledata’s own inline config |
| The Elevar config the page points at | Whether GTM is allowed, which containers, Elevar’s own non-GTM destinations, and whether server-side ingestion is on |
| Each published GTM container | Every destination the container fires at, with the account ids, plus the GA4 event names |
Blotout EdgeTag’s first-party /init |
Every destination with its flow type (browser / api / hybrid) — when the host answers, which from a server it does not; see below |
Account ids are normalised to the spelling each destination’s own config uses (GTM stores a Google Ads conversion as the bare number, Google writes AW-…). Without that the shared account on the measured example read as two different accounts and the overlap was missed entirely.
A Shopify custom web pixel is merchant-authored code behind a merchant-typed label, so its name is all we can read. Those names are matched first against the storefront signatures (which recognise the vendor product — Elevar - Checkout Tracking → Elevar) and otherwise against the competitor list in src/lib/competitors.ts, the one place that records which brands Littledata would replace.
That fallback is what catches the spaced brands. The signatures match compact spellings (blackcrow, triplewhale) because that is how a brand appears in a script host; a person typing a pixel name writes “Black Crow AI” or “Triple Whale Pixel”. Brands with no signature at all (DataWiz, Fueled, Upstack, Venon) were previously invisible this way.
Matching here is whole-word, unlike the Klaviyo metric matcher. A metric name is written by the vendor’s own integration, so a loose substring is safe there; a pixel name is free text, and several brands on the list are ordinary words — a merchant’s “Polarised lens promo” must not read as a Polar install.
Retention.com counts as a competitor on both its products (decision, 2026-09-15). The brand used to be matched only through Reclaim, on the grounds that a name carrying just “Retention.com” might be their identity-resolution product, which is additive the way Opensend is. That reading was overruled, so “Retention.com Script” is now an install. It is the commonest custom-pixel competitor in the fleet sample.
Measured on 44 live paying storefronts: 30 carry custom pixels, 132 names between them, one store (biohm) gains a competitor it had nothing else to show for, and nothing in the sample was flagged wrongly. A detection resting only on a label is recorded as custom_pixel evidence on the row, distinct from an app pixel’s web_pixel.
/init never answers a serverBlotout’s collector is a Cloudflare custom hostname on the merchant’s domain (nmhoj.tubbytodd.com) over Blotout’s own fallback origin (*.customers.edgetag.io). Measured on two unrelated Blotout stores, the rule in front of it is path-scoped, not IP- or fingerprint-scoped:
| Path | From a plain server request |
|---|---|
/tag, /tags (event ingest) |
200-class app JSON — open, because server-side integrations have to post events |
/init, /load, /consent, / |
403 Cloudflare challenge, whatever headers are sent |
So browser-like headers do not help, and going to the edge directly does not either: customers.edgetag.io has no certificate of its own and fails the TLS handshake, because the custom hostname is the only route in.
Reading /init for real needs the page’s own context — a browser — where /load also serves the config-derived script. That is an on-demand scan, not something this Lambda can do. The check keeps one cheap /init attempt per Blotout store so we notice if the policy changes, and otherwise reports Blotout as installed and says nothing about its destinations.
Blotout is the vendor worth that effort: its config is the only one that enumerates server-side destinations, which Elevar’s does not.
Stape’s whole job is to move tagging off the page, so the page is the wrong place to look for it. The check therefore also asks BuiltWith, through the brand’s marketing-automations company record (builtWithTechnologies, matching Stape and Stape Server GTM — 103 companies carry one, 2 of them Littledata customers).
The live page decides; BuiltWith only names the vendor. A server-side GTM vendor’s product is proxying the container — serving gtm.js from the merchant’s own domain, or routing events to a server container — so the page answers “is anyone carrying this container” directly:
| Live signal | Meaning |
|---|---|
gtm.js loaded from a non-Google host |
Someone is proxying the container |
The container declares a server_container_url on a non-Google host |
Events are routed server-side |
| A Stape string in the markup | Stape is on the page, whatever else is true |
A container the page loads from www.googletagmanager.com with no server container is not being carried by anybody, and BuiltWith saying otherwise does not change that. Only once a live signal is present does BuiltWith get to name the vendor; then that container’s destinations are attributed to Stape, the same relationship Elevar has with the container it publishes. A container named in an Elevar config stays Elevar’s, and details.gtmContainers keeps the container id beside the destinations.
This rule exists because the first version got it wrong. Attribution originally shipped as “BuiltWith says Stape + a container exists”, and on all five BuiltWith-Stape stores that had a container — firstmfg.com, goodsleepco.health, murisbrand.com, pairofthieves.com, nakedsundays.com — the page loaded the container straight from Google with no server container. BuiltWith was months stale on every one, and Stape was being credited with the merchant’s own tag manager. Under the current rule four of those five correctly report Google Tag Manager, and the fifth keeps Stape because its markup says so.
BuiltWith alone has never been enough on its own, and ellasie.com is why that caution was right from the start: BuiltWith records Stape, and the storefront carries neither a Stape string nor a container id.
Both the message and furtherInfo state findings: the vendors, their client-side destinations and account ids, and whether a server-side connector is on. A vendor whose destination list is not public — Triple Whale’s obfuscated SDK, an EdgeTag host that refused us, a container we could not fetch — is reported as installed, full stop.
No copy disclaims what the scan could not read, because a note about our own reach tells a merchant nothing about their store. It used to: on tubbytodd.com, where none of Black Crow, Blotout or Triple Whale publish a list, half the message was an account of our blind spots.
That does not mean the fact is lost. destinationsEnumerable is stored per vendor on the row, and it — not an empty destination list — is what anything reading the data should test to tell “publishes nothing” from “feeds nothing”.
Server-side destinations are the same rule the other way: that a connector is on is a finding and is reported; what it forwards to is not published by Elevar or Triple Whale, so nothing is said about it.
| Outcome | Rule |
|---|---|
| Warning | One destination account id is fed by both Littledata and something else — a duplicate we can prove and Littledata can fix (turn one of the two tags off). |
| Info | An alternative is installed with no shared account. Nothing is broken; it is a commercial fact, and Info is the ceiling for a finding Littledata cannot fix today. |
| Success | No alternative tracking app on the storefront. |
| Unknown | No domain on record, bot protection, or the storefront could not be reached. Never Success — “we could not see” is not “there is nothing there”. |
Two GA4 properties are parallel setups, not an overlap, and are never flagged. A destination both sides feed where neither config exposes an account id (Klaviyo is the usual case) is reported in the message for someone to check, and is deliberately not graded on: likely is not evidence.
Destination apps (a Klaviyo or TikTok app pixel), attribution collectors (Northbeam) and session tools (Clarity) route nothing, so they are listed on one line rather than given a destination map, and are never reported as competitors. That line is the “already paying for N tools” context for a CS conversation.
The check writes the competitor brands onto its own row, so a storefront-only detection appears on GET /audit/competitors beside the Klaviyo, Google Ads and Meta rows for the same store. It contributes no performance percentage — the storefront says which destinations an alternative feeds, never how well it feeds them.