If you publish a JSON version of a product’s facts, a “twin” of the page, it has one job: to say exactly what the page says, in a form a program can read without guessing. Generate it from the same source as the visible page. Before every release, test that the two agree on each value, unit, condition, scope, source, unknown and date. Put a version on the file so anyone reading it can tell which page it matches.

Be clear about what it isn’t. No platform we checked requires a twin, and none says it helps you rank. Google says you don’t need new machine-readable files for its AI features. On Shopify, the Catalog is the authoritative feed to agentic channels. A twin is a convenience for readers that want structured facts. The format and the checks below are our proposal.

What the platforms say about extra files

Before building anything, read what each route already takes.

Route What the documentation says What it means for a twin
Google AI Overviews and AI Mode No new machine-readable files, AI text files or markup needed; no special schema.org structured data. Make sure structured data matches the visible text A twin isn’t required. If you publish one, the same matching rule is the sensible bar
Shopify agentic storefronts Stores serve /agents.md, /llms.txt and /llms-full.txt automatically. Catalog is the authoritative product feed; those files don’t replace it Fix product data in Shopify first. A twin sits beside Catalog, not in place of it
ChatGPT You don’t need a feed if ChatGPT crawls your site, but feeds give you more control. Shopify and Etsy catalogs are already integrated Control comes from the feed or Catalog route, not a file on your site
OpenAI feed format Omit unknown optional values; no “unknown” placeholders; empty isn’t zero or false; conflicting values aren’t reconciled for you Good rules for any structured copy, including a twin

Whether a product needs a public data endpoint at all is a separate decision. Our access-route guide works through it. This page assumes you’ve decided to publish one and want it to stay honest.

A versioned product-evidence twin

Here is a complete twin for one hypothetical product. Square brackets mark placeholders. The structure is what matters: every fact carries the exact text the page shows, so a test can compare them.

{
  "twin_format": "product-evidence",
  "twin_version": "1.2",
  "record_version": 7,
  "page_url": "https://example.com/products/humidifier-4l",
  "built_from": "[content hash of the source record]",
  "built_at": "2026-10-27T14:05:00-04:00",
  "product": {
    "group_id": "humidifier-4l",
    "title": "4 L top-fill ultrasonic humidifier",
    "variants": [
      { "variant_id": "humidifier-4l-white", "options": { "Colour": "White" } }
    ]
  },
  "facts": [
    {
      "fact_id": "runtime-max",
      "applies_to": "humidifier-4l-white",
      "value": 32,
      "unit": "h",
      "qualifier": "up_to",
      "condition": "low mist setting, full 4 L tank",
      "scope": "whole unit",
      "state": "stated",
      "visible_text": "Up to 32 hours on the low setting with a full 4 L tank",
      "source": { "type": "manufacturer_test", "document": "[test report]", "issued": "[date]" },
      "last_verified_at": "[date]"
    },
    {
      "fact_id": "noise",
      "applies_to": "humidifier-4l-white",
      "value": null,
      "unit": "dB(A)",
      "state": "unknown",
      "unknown_reason": "not measured",
      "visible_text": null
    },
    {
      "fact_id": "room-limit",
      "applies_to": "humidifier-4l-white",
      "polarity": "excludes",
      "value": 400,
      "unit": "sq_ft",
      "qualifier": "not_above",
      "state": "stated",
      "visible_text": "Not for rooms over 400 sq ft",
      "source": { "type": "manufacturer_spec", "document": "[manual]", "issued": "[date]" },
      "last_verified_at": "[date]"
    }
  ],
  "offer": {
    "variant_id": "humidifier-4l-white",
    "price": "69.99 USD",
    "availability": "in_stock",
    "observed_at": "2026-10-27T14:05:00-04:00"
  }
}

What each part is for:

Part What it holds Why it’s there
twin_format, twin_version The shape of the file A reader can tell when the structure changed
record_version, built_from Which edit of the product record this is, and a hash of its source Ties the twin to one specific page build, so a cached copy can be spotted as old
built_at When the file was made, with a time zone Says when it was built, nothing more. Verification dates live on each fact
facts[].visible_text The exact string the page shows, or null The anchor every parity test compares against
facts[].state, unknown_reason stated, unknown, conflicting or not_applicable Keeps “not measured” apart from “zero” and from “nobody looked”
facts[].polarity affirms or excludes “Not for rooms over 400 sq ft” is a fact too
facts[].source, last_verified_at The document and when someone checked it Lets a reader trace the claim
offer Price, stock, and when they were read Offer facts change on their own schedule, so they carry their own timestamp

The internal claim record behind each fact, with every copy and whether it still matches, is covered in our claim-record guide. The twin is the public, page-shaped slice of it.

Notice the unknown. The noise fact is in the twin with "state": "unknown" and "visible_text": null, and the page says nothing about noise. That’s parity too. What breaks parity is a twin that says 0, or a page that says “whisper quiet” while the twin says unknown.

The parity checks

Run these on every change, before release. Each compares the twin with the rendered page, not with your memory of it. The last column shows the same kind of break in real records, all from copies inside Shopify catalog records in our snapshot of October 26, 2026.

Check Passes when Fails when Seen in a real record
Value The twin’s value and unit appear in visible_text, and visible_text appears on the page The numbers differ Description “up to 32 hours”; generated spec “Up to 36 hours”
Unit The unit in the twin is the unit on the page A unit or attribute is swapped Description “Depth: 2.4lbs”; generated spec “Depth 2.4 in”
Scope The twin’s scope matches the page’s wording A part-level claim becomes a whole-product claim Description: the hood’s insulation rated to -30°C. Generated spec: “Insulation” rated to -30°C
Unknown Unknown in the twin means silence or “not rated” on the page, and never 0 A blank becomes a zero Our own first script turned 440 absent ratings into zeros, and 28 of 162 searches showed a median of 0 reviews
Presence Every fact in the twin is visible on the page, and every factual claim on the page is in the twin One copy says more than the other Google’s rule: structured data should match the visible text
Variant Each fact is bound to the same variant in both A value from one size shows up on another See the variants guide: a returned variant whose size wasn’t in the option list
Source and date The twin’s source and last_verified_at match what the page cites The twin says “verified” where the page cites nothing A build date presented as a check date. See the freshness guide
Offer Price and stock in the twin equal the page at the same moment They differ See the price-mismatch guide

Two rules make these checks reliable.

Compare against the rendered page. A test that compares the twin with the database it was built from only proves the build ran. Parse the HTML a shopper gets, find each visible_text string, and fail if it’s missing.

Fail closed on conflicts. OpenAI’s feed reference says neither its top-level fields nor its variant map will reconcile conflicting values. Don’t expect any other reader to either. If your two copies disagree, don’t publish either until the source is settled.

Worked example: how this site does it

We run the same idea on this site, for articles rather than products. Every article has a data twin at the same address plus .json. For this page, that’s /guides/machine-readable-product-data.json. The page links to it with a <link rel="alternate" type="application/json"> tag.

What we do How What it doesn’t cover
One source The page, the twin and the Article JSON-LD are built from the same frontmatter The article body isn’t in the twin, so body tables are checked by editors, not tests
Field tests For every article, an automated test checks that the twin’s id, title, description, answer, claims, sources, limitations, dates and kind equal the page’s source fields, and that the JSON-LD headline, description and dates equal the twin’s Agreement at build time, not after someone edits a deployed file by hand
Rendered checks A browser script loads each page and compares its title, meta description, H1, answer, deck, checked date and limitations with the twin It counts claim blocks on the page against the twin’s claims; it doesn’t compare their text
Dates per claim Each claim carries its own source date; the article carries published, updated and checked Whether the sources still say the same thing today. That needs a human re-read

Building from one source makes drift much less likely. It doesn’t make it impossible. A hand edit to a deployed file, a template bug or a body table that disagrees with a claim can all get past it, which is why the rendered checks exist and why editors still read the body against the claims.

One more lesson from our own files. Our homepage’s data version carries a generated date. It records when the file was built, in UTC, so a build on the evening of October 27 in Toronto reads October 28. In the product twin above, that kind of date is built_at, with its time zone, and it never stands in for last_verified_at.

Generate it, don’t maintain it by hand

A hand-maintained twin will drift. The safe shape is one source record that feeds every copy:

Copy Built from Checked by
Product page text The source record, through your theme or template Rendered-page checks
Twin JSON The same source record Value, unit, scope, unknown and presence checks
Product JSON-LD The same source record Google’s rule: match the visible text
Feed or Catalog fields Your product data in Shopify, or your feed The received record, read back after each change

On Shopify, the last row is the one agents on the Catalog route actually receive. Shopify marks some Catalog fields, including the description and generated spec lists, as generated or enriched by Shopify. A perfect twin doesn’t fix a wrong Catalog field. Fix the product data first, then make the twin agree with it.

What a twin can’t do

  • It can’t make you rank. Nothing we read says any assistant prefers sites with JSON twins, and we haven’t measured it.
  • It can’t replace the route that matters. On Shopify, ChatGPT and Copilot get products through Catalog. Google AI Mode and Gemini get them through the Google & YouTube channel, which Shopify says is still rolling out to eligible stores.
  • It can’t make a fact true. Parity proves two copies agree. If both say 36 hours and the manual says 32, both are wrong.
  • It can’t carry hidden instructions. A twin holds facts the page shows, nothing aimed at the agent instead of the shopper.

How we checked

On October 27, 2026 we re-read Google’s AI features page, Shopify’s agentic storefronts help page, OpenAI’s merchant page and its products feed reference (Stable), and quoted or paraphrased only what they say now.

The real failure examples are copies inside Shopify agent-catalog records saved on October 26, 2026 (162 searches, top 10 each, no buyer context), already published in our receipts and missing review data audit. They compare a record’s description with Shopify’s generated lists, not a merchant’s page with a twin.

For this site’s twins, we read the twin builder, the article tests and the browser verification script in our repository on October 27, 2026, and opened a locally built twin and its page to confirm the alternate link and fields. We describe what those checks compare; we didn’t change them for this article.

Your next step

Pick one product and write down five facts its page states, each with the exact sentence the page uses. If you already publish structured data or a JSON file for that product, check each fact against it: same value, same unit, same condition, same variant. Any fact that appears in only one of the two, and any blank that became a zero, is your first fix.