Agent fetchers typically request a URL once and read whatever comes back — no JavaScript engine, no DOM, no hydration step. This checklist gets a storefront readable by that class of client: server-rendered Product/Offer markup, enumerated price and availability fields, ProductGroup variants, a feed matched to the platform you’re targeting, and a robots.txt that tells shopping agents apart from training crawlers. It targets schema.org 30.0 (released 2026-03-19), the UCP specification dated 2026-04-08, the Agentic Commerce Protocol as of 2026-04-17, and Cloudflare’s AI Crawl Control default change effective 2026-09-15.
Serve product markup an agent can read without running JavaScript
Agent fetchers request a URL once and read the response — they don’t run a JavaScript engine, so anything injected into the DOM after page load is invisible to them. A product page can look perfect in a browser and still return no name, price or availability to an agent hitting the exact same URL. Product (opens in a new tab) and Offer (opens in a new tab) JSON-LD has to sit in the raw HTTP response, not get hydrated in client-side.
The usual cause is a JS framework that renders the page shell server-side but injects the structured-data script tag during hydration, after the initial paint. That pattern is common enough that Google’s structured data guidance (opens in a new tab) assumes crawlers read markup from the response a server returns, not from a rendered DOM. The fix is narrow: server-render or statically pre-render the JSON-LD block specifically, even if the rest of the page still hydrates client-side.
Verify with a plain HTTP client, not a browser tab:
curl -s https://example.com/products/example-item \
| grep -o '<script type="application/ld+json">[^<]*</script>' \
| sed -e 's#<script type="application/ld+json">##' -e 's#</script>##' \
| jq .Expected output:
You should see a JSON object with "@type": "Product", a name, and an offers block — no HTML wrapped around it. An empty result means the block only exists after JavaScript runs, and that specific block needs a server-rendered or statically built version.
sequenceDiagram
participant Agent as Agent fetcher (no JS)
participant Server
participant Browser as Browser (JS engine)
Agent->>Server: GET /products/example-item
Server-->>Agent: HTML response
Note over Agent: No script execution — sees whatever HTML arrived
Browser->>Server: GET /products/example-item
Server-->>Browser: Same HTML response
Browser->>Browser: Execute client-side JS
Browser->>Browser: Inject Product/Offer JSON-LD into DOM
Note over Browser: Page "looks right" only after this step
You should now see: the Product/Offer JSON-LD block in the raw response from every product template, confirmed without opening a browser.
Fix the price and availability fields agents actually parse
Google’s merchant listing structured data (opens in a new tab) requires surprisingly little at the baseline: a name, an image and an offers object carrying price and priceCurrency. Most validation failures come from getting those two fields into the wrong shape, not from leaving them out.
A formatted price string like "$19.99", or a price field with no separate priceCurrency, trips the price/currency check in the Rich Results Test or Search Console. price has to be a bare numeric string — "19.99", not "$19.99" or "19.99 USD" — with priceCurrency set separately to an ISO 4217 code such as "USD".
Offer.availability fails the same way in a different shape: it takes a full-URL value from schema.org’s ItemAvailability enumeration (opens in a new tab) — schema.org/InStock, OutOfStock, PreOrder, SoldOut, LimitedAvailability — never free text like "available" or "yes". An agent parsing free text has no enumerated value to branch on, so it either skips the offer or guesses.
itemCondition, priceValidUntil, shippingDetails and hasMerchantReturnPolicy are recommended rather than required, but each gives an agent a concrete signal it would otherwise have to infer:
{
"@context": "https://schema.org",
"@type": "Product",
"name": "Trail Running Shoe - Men's",
"image": "https://example.com/images/trail-shoe.jpg",
"sku": "TRS-M-42",
"offers": {
"@type": "Offer",
"url": "https://example.com/products/trail-shoe-mens",
"price": "119.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock",
"itemCondition": "https://schema.org/NewCondition",
"priceValidUntil": "2026-12-31"
}
}You should now see: this block pass Google’s price and currency checks, and an agent reading a full-URL availability value instead of guessing at free text.
Model variants with ProductGroup so an agent buys the right SKU
Fold every colour and size into one Product node with a single offers array, and an agent has no way to say which variant it means — it can only point at the page, not a SKU. Even when the page renders a working variant picker, the markup underneath doesn’t expose the choice the picker makes.
Google’s product-variant structured data (opens in a new tab) guidance solves this by giving each purchasable variant its own Product entry, with its own sku and url, grouped under a parent ProductGroup. The parent carries a variesBy list naming only the properties that actually differ between variants — color and size, say, not every attribute on the page.
{
"@context": "https://schema.org",
"@type": "ProductGroup",
"name": "Trail Running Shoe",
"productGroupID": "TRS-GROUP",
"variesBy": ["color", "size"],
"hasVariant": [
{
"@type": "Product",
"name": "Trail Running Shoe - Blue, Men's 9",
"sku": "TRS-BLU-M9",
"url": "https://example.com/products/trail-shoe-blue-m9",
"color": "Blue",
"size": "M9",
"offers": {
"@type": "Offer",
"price": "119.00",
"priceCurrency": "USD",
"availability": "https://schema.org/InStock"
}
},
{
"@type": "Product",
"name": "Trail Running Shoe - Red, Men's 10",
"sku": "TRS-RED-M10",
"url": "https://example.com/products/trail-shoe-red-m10",
"color": "Red",
"size": "M10",
"offers": {
"@type": "Offer",
"price": "119.00",
"priceCurrency": "USD",
"availability": "https://schema.org/OutOfStock"
}
}
]
}graph TD
PG["ProductGroup: Trail Running Shoe<br/>variesBy: color, size"] --> P1["Product: Blue, M9<br/>SKU: TRS-BLU-M9"]
PG --> P2["Product: Red, M10<br/>SKU: TRS-RED-M10"]
P1 --> O1["Offer: 119.00 USD, InStock"]
P2 --> O2["Offer: 119.00 USD, OutOfStock"]
You should now see: every purchasable variant resolving to its own SKU and URL, so an agent can add the exact item to a cart without guessing.
Validate the markup against both Google’s and schema.org’s own checkers
Two validators check two different things, and passing one doesn’t mean the other passes. The Rich Results Test (opens in a new tab) checks eligibility for Google’s own rich-result and merchant-listing features specifically — it’s a Google product, checking against Google’s rules.
The community-run Schema Markup Validator (opens in a new tab) checks conformance to the schema.org vocabulary itself, independent of any search engine or agent platform. A non-Google agent — a UCP client, an ACP checkout flow, a research agent that parses application/ld+json directly — has no reason to respect a rule that exists only because Google enforces it.
Run both on every product template change, not once at launch. This checklist assumes schema.org 30.0 (opens in a new tab), released 2026-03-19 — confirm the validator you’re using checks against a current vocabulary release before trusting a clean result, since a stale cached validator can pass markup that a newer spec would flag.
You should now see: a live product URL pass both validators with zero errors, not just the one your team happens to check by habit.
Publish the feed the agent platform you’re targeting actually reads
schema.org markup, a Google Merchant Center feed, and a checkout-capable protocol integration are three separate integrations, not one feed that covers every surface. Treating them as interchangeable is the most common reason a storefront looks agent-ready on one platform and invisible on another.
OpenAI’s commerce product feed (opens in a new tab) accepts parquet (preferred, ideally zstd-compressed), jsonl.gz, csv.gz or tsv.gz, published as a full snapshot at least once a day under a stable file name that gets overwritten on every update — not a new file per upload. A feed that skips a day, uses an unsupported format, or changes file name between uploads fails validation, or simply never surfaces in ChatGPT’s shopping results.
The Universal Commerce Protocol (opens in a new tab) (UCP), an Apache-2.0 standard whose shopping-category co-developers include Google, Shopify, Etsy, Wayfair, Target, Walmart, Amazon, Microsoft, Meta, Salesforce and Stripe, defines a Catalog capability for search and lookup over REST or MCP transport. The Agentic Commerce Protocol (opens in a new tab) (ACP), from Stripe, OpenAI and Meta, defines five building blocks — agentic checkout, cart and feed, delegated payment, delegated OAuth 2.0 authentication and orders/webhooks — and powers Instant Checkout inside ChatGPT (opens in a new tab). UCP and ACP are distinct, competing protocols, not two names for the same standard, and Stripe and Meta back both.
| Agent surface | What it reads | What to publish |
|---|---|---|
| Google AI Mode, Merchant Center | schema.org Product/Offer markup, plus a Merchant Center feed | Structured data on every product page, plus a feed submitted to Merchant Center |
| ChatGPT shopping results, Instant Checkout | OpenAI’s commerce product feed (opens in a new tab) (parquet/jsonl.gz/csv.gz/tsv.gz), plus ACP for checkout | Daily full snapshot under a stable file name, plus an ACP integration for in-chat checkout |
| UCP-based agents | UCP Catalog capability | A UCP-compliant catalogue endpoint over REST or MCP |
flowchart TD
A{"Which agent surface<br/>are you targeting?"} -->|Google AI Mode,<br/>Merchant Center| B["schema.org markup<br/>+ Merchant Center feed"]
A -->|ChatGPT shopping,<br/>Instant Checkout| C["OpenAI commerce feed<br/>+ ACP for checkout"]
A -->|UCP-based agents| D["UCP Catalog capability<br/>REST or MCP"]
A -->|Not sure yet| E["Ship schema.org markup first —<br/>every surface above reads it"]
You should now see: a clear name for the exact feed or protocol your target agent platform requires, instead of one feed you assume covers all three.
Separate training crawlers from shopping agents in robots.txt
OpenAI runs three separately controllable crawlers, each with its own robots.txt user-agent token (opens in a new tab): GPTBot for model-training collection, OAI-SearchBot for the real-time retrieval that powers ChatGPT’s search features and ChatGPT-User for the live fetch that happens when a user or agent session requests a page inside ChatGPT. Anthropic runs the same split: ClaudeBot for training, Claude-User for the fetch that happens when a Claude user or agent asks a question and Claude-SearchBot for indexing content for Claude’s search results (opens in a new tab).
A blanket “block all AI bots” rule catches all six tokens at once. The storefront disappears from ChatGPT and Claude’s answers even though it was never blocked from Google Search — nothing else on the site changed, so the cause is easy to miss.
Write a separate Disallow/Allow rule per token instead of one rule for “AI”:
# Opt out of AI model training
User-agent: GPTBot
Disallow: /
User-agent: ClaudeBot
Disallow: /
# Allow OpenAI's real-time shopping and search agents
User-agent: OAI-SearchBot
Allow: /
User-agent: ChatGPT-User
Allow: /
# Allow Anthropic's on-demand agents
User-agent: Claude-User
Allow: /
User-agent: Claude-SearchBot
Allow: /Anthropic states that blocking by IP address is unreliable, because it can prevent the bot from reading robots.txt in the first place — the token-based rule above is the supported mechanism, not an IP allowlist.
flowchart LR
subgraph OpenAI
GPTBot["GPTBot (training)"] -->|Disallow| Blocked1(("Blocked"))
OAISearch["OAI-SearchBot (search)"] -->|Allow| Allowed1(("Allowed"))
ChatGPTUser["ChatGPT-User (live fetch)"] -->|Allow| Allowed2(("Allowed"))
end
subgraph Anthropic
ClaudeBot["ClaudeBot (training)"] -->|Disallow| Blocked2(("Blocked"))
ClaudeUser["Claude-User (live fetch)"] -->|Allow| Allowed3(("Allowed"))
ClaudeSearchBot["Claude-SearchBot (search)"] -->|Allow| Allowed4(("Allowed"))
end
You should now see: training crawlers opted out while every on-demand shopping and search agent from OpenAI and Anthropic keeps full access.
Check Cloudflare’s AI Crawl Control before the 15 September 2026 default change
Cloudflare’s AI Crawl Control (opens in a new tab) classifies bots into three categories — Search, Agent and Training — and manages access to each independently of robots.txt. From 15 September 2026, Cloudflare blocks Training- and Agent-category bots by default (opens in a new tab) on ad-monetized pages, for newly onboarded and free-tier sites.
The catch is “mixed-use” crawlers — bots that combine Search with Training under one identity. Cloudflare names Googlebot, Applebot and Bingbot as mixed-use, so the same default that opts a site out of Training bots can also drop organic Google Search traffic, with nothing else on the site having changed.
Before 15 September 2026, open AI Crawl Control settings and explicitly confirm that the Search category stays allowed on monetized product pages, independent of whatever the Training and Agent defaults do. This applies even to sites that never intended to block AI bots — the change is a platform default, not something a site owner opts into, so silence is not the same as being unaffected.
You should now see: the Cloudflare dashboard confirming Search-category crawlers remain allowed on monetized product pages ahead of the 15 September 2026 default change.
Know what this checklist doesn’t verify yet
This checklist gets a product page and its feed into a shape agents can read; it does not walk through the checkout side. Implementing UCP or ACP checkout means an authentication and payment-delegation integration — OAuth 2.0 delegated auth, delegated payment tokens, order webhooks — well beyond schema.org markup and a feed, and that deserves its own article.
A few items still need confirming against primary sources before you treat a page as fully agent-ready. Check the exact required-versus-optional field list in OpenAI’s own product feed schema (opens in a new tab) directly, since this checklist covers format and publishing cadence, not every field the schema defines. Treat llms.txt (opens in a new tab) as what it currently is — a community proposal — not a confirmed input to OpenAI’s, Google’s or Anthropic’s shopping or retrieval agents, until a vendor documents it as one.
One more failure mode worth knowing about, even though fixing it is outside this checklist’s scope: a request against a pay-per-crawl-protected (opens in a new tab) page returns HTTP 402 if the requesting crawler hasn’t implemented Web Bot Auth (opens in a new tab) — an Ed25519 key pair with a published key directory and HTTP Message Signatures. A 402 there doesn’t necessarily mean your site is misconfigured; it can mean the crawler is.
Re-run both validators and the robots.txt check after any template or bot-management change. Cloudflare’s default alone changes on 15 September 2026, independent of anything you do to the site.