DEV Community

Cover image for I pre-registered a study on AI visibility signals. The main result was null.
Ievgenii Gryshkun
Ievgenii Gryshkun

Posted on Originally published at angeo.dev

I pre-registered a study on AI visibility signals. The main result was null.

Originally published on angeo.dev. Full tables, p-values and the sealed plan are there.

Most claims about AI visibility are untestable by design: publish the signals, wait, attribute anything good that happens to the signals.

I wanted a version I could not fudge, so I wrote the analysis plan first, hashed it, and sent the hash to the other party before I had any data.

The question

Do businesses AI assistants name repeatedly differ, on observable technical signals, from businesses the same assistants name once?

Every business in the corpus was named at least once, so this says nothing about how to enter an answer. It compares repeat against one-off mentions inside a named-business corpus.

Four signals, all externally observable:

Signal Check
Crawler access Does robots.txt block any of 8 AI crawlers
Content map Does the site serve /llms.txt
Structured data Does a product page emit JSON-LD Product
Buyability Does that node carry offers.availability

Study setup

The answers came from a partner (connexion.me), who ran 44 product-level home-decor buying questions across ChatGPT, Gemini and Perplexity, twice, in two arms — 264 answers per arm.

Blinding was deliberate. I did not write the questions and did not see their store list until my plan was sealed; they never saw my frame, my scan results or my thresholds.

Roster rows                                    669
  no resolvable domain                        -186
  resolved to a different company               -3
  marketplaces and listing surfaces            -12
  duplicate rows collapsed                     -10
Unique domains analysed                        458
  scanned successfully                         455
Enter fullscreen mode Exit fullscreen mode

Cases: 3+ mentions across both runs and present in both. Controls: exactly one mention across both runs. Head excluded first — anything in 53+ of 264 answers (Amazon, Etsy, Wayfair, Target, Home Depot).

The pre-registration

Sealed 10 August, SHA-256 9b4ccf12629e…:

  • Under 15% of named businesses would be Magento
  • No signal would separate the groups by more than 15 points
  • Refutation condition: any signal differing by 20+ points with the named group higher

Result — generic arm

Two-sided Fisher exact. Percentages are of stores where the signal was observable.

Signal Cases Controls Diff p
Blocks an AI crawler 1/55 (2%) 7/181 (4%) −2.0 .685
Serves llms.txt 10/55 (18%) 61/181 (34%) −15.5 .030
JSON-LD Product 3/10 (30%) 15/46 (33%) −2.6 1.000
offers.availability 3/10 (30%) 13/46 (28%) +1.7 1.000

The refutation condition was not met — in this arm, in the context arm, or pooled. No signal separates the repeatedly named group from the once-named group in the direction the field assumes.

Magento share of the 455 scanned domains: 13 stores, 2.9%.

The one that went backwards

llms.txt is more common among businesses named once than among those named repeatedly. Same direction in all five cuts, diffs of −15.3 to −17.4, nominal p between .012 and .030. Four signals across five cuts, no multiplicity correction — report them, do not treat them as confirmatory.

Before reading that as "llms.txt hurts you", I fetched the flagged files and read the first line of each:

113 of 159 llms.txt files — 71% — matched the same generated heading, differing only in the brand name. Not 113 independent decisions to publish. One generator.

Narrowed to the analysis universe: 146 of 455 serve the file (32%); 104 of those 146 are the template. Strip it and adoption is 9%, against 11% on my separately measured frame of 762 Magento stores.

I was about to publish 32% as adoption. Most of the apparent difference between the two populations was boilerplate.

The engineering lesson, if you want one

Three things I would do again on any measurement like this:

  1. Write the failure condition before the data exists, and publish it when it fires. Mine fired in a secondary cut on a case group of four stores. Uninterpretable — and reported anyway, because a refutation condition you drop when it is inconvenient was never one.
  2. Record presence, then inspect content separately. The scanner keeps no page content by policy. That is correct for a crawler, and it is also why the template finding needed a second, explicit pass.
  3. Watch what your sampling heuristic does. In a related panel, 12% of stores had a different product page sampled on the second visit. That alone manufactures schema "changes" that never happened.

Caveats that matter

  • Two of four signals need a product page. Effective samples: 10 of 55 cases, 46 of 181 controls. Too small to conclude anything, and the missingness is probably not random — discovery works worst on large sites, which dominate the case group.
  • Losses are not random: shops behind bot mitigation returned 403/429, and those shops may also be more likely to block AI crawlers.
  • Crawler access is one route into an answer, not the only one — indexes, caches, feeds and listing sites all bypass it.
  • One category, three engines, two days.

Reproducing it

Sealed plan and both amendments with hashes, the marketplace exclusion list, the dropped-domain list, the per-business analysis rows and the scanner are all published. The scanner identifies itself, obeys robots.txt, keeps no page content and can be blocked in two lines.

Full write-up with every table: AI assistants named 458 shops

Earlier instalments: 462 stores scanned in July, 762 in August · 376 stores measured twice, 94% unchanged

I maintain a set of MIT-licensed Magento 2 modules for this layer — llms.txt generation, robots.txt AI-bot management, JSON-LD, MCP and UCP endpoints — on Packagist under angeo/. If you have run a similar measurement and got a different direction on llms.txt, I would genuinely like to see it.

Top comments (7)

Collapse
 
mike_viewfy profile image
Mike Viewfy

respect the discipline here, sealing the hash before seeing any data is what makes the 71% duplicate-heading catch on llms.txt actually mean something instead of just another vibes-based SEO claim. i use viewfy for checking whehter crawlers can reach a page at all and this for deciding if the fix even matters. worth rerunning the JSON-LD arm on a bigger sample before trusting that null.

Collapse
 
angeo profile image
Ievgenii Gryshkun

Thanks — though I should correct one thing myself: the 71% template finding sits outside the seal. The sealed plan covered the four tests and the refutation condition; the duplicate-heading pass was post-hoc content inspection after llms.txt came out backwards. Pre-registration is why the null stands, not why that catch counts.

On JSON-LD, agreed, but more domains alone will not help. Effective n was 10 cases and 46 controls, because both schema signals need a product page discovered first, and discovery fails worst on large sites — most of the case group. Scale the roster and the missingness scales with it. That arm needs better product-page discovery and its own pre-registration. Right now it is not a null, it is no result.

Collapse
 
citedy profile image
Dmitry Sergeev

tbh a preregistered null result in SEO is way more valuable than most of the fluff out there

Collapse
 
citedy profile image
Dmitry Sergeev

{"comment":"null results are honestly more useful than the usual hypes... did you look at the breakdown for Perplexity specifically?"}

Collapse
 
angeo profile image
Ievgenii Gryshkun

Thanks. Per-engine breakdown was not part of the pre-registered analysis, so anything I say about Perplexity alone is exploratory rather than tested — I'd rather flag that than dress it up. The raw per-engine answers are in the published data if you want to cut it yourself, and it's a fair thing to pre-register properly next round.

Collapse
 
citedy profile image
Dmitry Sergeev

wild that a pre-registered study actually came back null, honestly feels way more realistic than the usual "hack" videos lol

Collapse
 
angeo profile image
Ievgenii Gryshkun

Thanks. Setting the bar before the data came in meant there was nowhere to move it afterwards — so the null got published. Protocol and data are linked in the post.