Methodology
How we score, in plain math.
Every check has a severity. Every surface has a weight. Every audit stamps the spec versions it ran against. Below is the exact formula — the same one our scoring engine uses, generated from the live engine registry.
1 · What every check produces
All 82 checks follow the same shape. Each one targets a single, named signal and produces:
- a permanent
slug(e.g.product-jsonld-present) that never changes once assigned — it's the identifier stored on every result; - a severity — CRITICAL, HIGH, MEDIUM, LOW, or INFO;
- a check status (`pass`, `partial`, `fail`, `na`, `error`) plus a discrete raw score (`100`, `50`, `0`) for scored states;
- a per-surface weight — how much the signal matters to each AI surface;
- rich evidence — what we looked at, what method we used, the raw HTTP / JSON-LD / sitemap artifacts, and per-issue findings with offending and expected snippets.
Each check resolves to one of five states:
pass/partial/failScored states. Raw scores are `100` (pass), `50` (partial), `0` (fail); included in aggregation.naCheck does not apply to this audit; excluded from aggregation.errorRuntime error during this check; excluded from aggregation.
How a check lands on pass, partial, or fail comes down to one of two measurement shapes:
- Binary — the signal is either present and valid or it isn't. Example: a non-empty
robots.txtat the site root is a pass (`100`); a missing or blank one is a fail (`0`). No partial. - Coverage-banded — the check samples a set of product pages and measures the share that satisfy the rule, then bands the result. A typical check passes at ≥ 95% coverage (`100`), is partial in a middle band such as 50–95% (`50`), and fails below it (`0`). Exact thresholds are tuned per check and fixed by the engine version; if nothing applicable is found, the check is
naand drops out of aggregation rather than scoring zero.
2 · Severity weights
Severity weights drive the impact each check has on its surface sub-score.
| Severity | Weight | What it means |
|---|---|---|
| CRITICAL | 10 | Blocks one or more agent surfaces from transacting or discovering the store |
| HIGH | 5 | Significantly degrades discoverability or trust on at least one surface |
| MEDIUM | 3 | Suboptimal but won't block; agents fall back to inference |
| LOW | 1 | Minor polish; affects relative ranking, not inclusion |
| INFO | 0 | Informational; doesn't affect the score |
3 · Surface weights
The five AI shopping surfaces don't contribute equally to the overall score. Weights reflect how much of each surface's behavior is public-crawl-verifiable today — Google UCP carries the heaviest weight because most of its conformance is observable from the public web.
| Surface | Weight | Spec pinned to |
|---|---|---|
| ChatGPT (via ACP) | 25 | ACP 2026-04-17 |
| Google AI Mode (via UCP) | 30 | UCP 2026-04-08 |
| Microsoft Copilot | 15 | UCP-compatible (inferred) |
| Perplexity | 17 | Google merchant listing + GTIN-mandatory |
| Meta AI | 13 | Meta catalog feed spec |
| Total | 100 | — |
Weights are configuration, not constants — they're versioned in the scoring engine and change only through a reviewed release, never edited ad hoc.
4 · Per-surface sub-score formula
For each surface S, the sub-score is the weighted average of all applicable checks:
subscore(S) = sum( rawScore_i × severity_i × surfaceAffected_i[S] )
─────────────────────────────────────────────────────
sum( 100 × severity_i × surfaceAffected_i[S] )
for all checks i where:
- result is not `na`
- result is not `error`
- surfaceAffected_i[S] > 0Worked example
Perplexity sub-score on a Shopify store with 3 relevant checks:
| Check | Severity | surfaceAffected[perplexity] | rawScore |
|---|---|---|---|
| product-gtin-populated | CRITICAL (10) | 100 | 0 |
| merchant-return-policy-present | HIGH (5) | 80 | 50 |
| offer-shipping-details-present | MEDIUM (3) | 100 | 100 |
numerator = (0 × 10 × 100) + (50 × 5 × 80) + (100 × 3 × 100)
= 0 + 20000 + 30000
= 50000
denominator = (100 × 10 × 100) + (100 × 5 × 80) + (100 × 3 × 100)
= 100000 + 40000 + 30000
= 170000
subscore(perplexity) = round(50000 / 170000 × 100) = 29Perplexity sub-score for this store: 29.
5 · Overall score formula
The overall score is a surface-weight-blended average of the five sub-scores:
overall = sum( subscore(S) × surfaceWeight(S) )
──────────────────────────────────────
sum( surfaceWeight(S) )
= ( subscore(chatgpt_acp) × 25
+ subscore(google_ucp) × 30
+ subscore(microsoft) × 15
+ subscore(perplexity) × 17
+ subscore(meta) × 13 )
/ 100The denominator is 100 by construction (the surface weights sum to 100). The division is shown explicitly for clarity.
Issue-level impact in the report uses this same blend. A check that costs 3 points on ChatGPT contributes 0.75 points to the headline score because ChatGPT carries 25% of the overall weight. In the issue list, hover the impact value (for example `−5`) to see the unweighted per-surface score subtraction for each surface.
6 · Grade bands
The 0–100 overall maps to a letter grade for at-a-glance reading:
90–100
75–89
60–74
40–59
0–39
Grade colors are semantic and always paired with the letter. The color is never the sole signal.
7 · Check inventory
The full list of 82 active checks, grouped by category. Each row shows the canonical slug, what the check measures, its severity, the one-line fix it recommends, and the specs it normatively cites.
Discovery
25 checks · prefix DISC| Slug | Name | Description | Severity |
|---|---|---|---|
| googlebot-allowed-on-products | Googlebot allowed on product paths | robots.txt permits Googlebot at a representative product-detail path. Fix: Allow Googlebot on product paths | CRITICAL |
| wildcard-root-disallow | No global wildcard root disallow | robots.txt has no wildcard `User-agent: *` group with `Disallow: /` that locks every crawler out. Fix: Remove the wildcard `Disallow: /` from robots.txt | CRITICAL |
| openai-search-bot-allowed | OAI-SearchBot allowed | robots.txt permits OAI-SearchBot (OpenAI's ChatGPT discovery crawler) at the site root. Fix: Allow OAI-SearchBot in robots.txt Specs: OpenAI bots — OAI-SearchBot | CRITICAL |
| bingbot-allowed | Bingbot allowed | robots.txt permits Bingbot (the crawler behind Microsoft Copilot Shopping's index) at the site root. Fix: Allow Bingbot in robots.txt | HIGH |
| pdp-single-product-page | Each PDP carries at most one Product JSON-LD node | Each sampled product page exposes a single Product JSON-LD node. Fix: Emit a single Product JSON-LD node per PDP | HIGH |
| pdp-not-noindex | No sampled PDP returns a noindex directive | Every sampled product page is indexable — no `noindex` in HTML meta or X-Robots-Tag. Fix: Remove the noindex directive from every PDP | HIGH |
| perplexity-bot-allowed | PerplexityBot allowed | robots.txt permits PerplexityBot (Perplexity's shopping index crawler) at the site root. Fix: Allow PerplexityBot in robots.txt | HIGH |
| products-discoverable-no-js | Product pages discoverable without JavaScript | At least one product page is discoverable from a non-JavaScript crawl of the sitemap or initial HTML. Fix: Make product pages discoverable without JavaScript | HIGH |
| products-machine-discoverable | Products are machine-discoverable | Agents can reliably discover the store's products via a declared feed, platform catalog, typed sitemap, or content-verified crawl. Fix: Publish a product feed or a crawlable product sitemap Specs: Google merchant listing — product feed delivery, sitemaps.org — XML sitemap discovery | HIGH |
| robots-txt-present | robots.txt present at root | A non-empty /robots.txt is reachable at the site root, giving the merchant a control surface for every crawler. Fix: Publish a non-empty robots.txt at the site root | HIGH |
| pdp-not-behind-login | Sampled PDPs are not gated behind a login wall (401 / 403) | No sampled PDP returns 401 or 403 to an anonymous request. Fix: Open PDPs to anonymous fetches | HIGH |
| sitemap-entries-escaped | Sitemap <loc> entries are entity-escaped | Every sampled <loc> value escapes `&`, `<`, and `>` as XML entities. Fix: Entity-escape `&`, `<`, `>` in every <loc> | MEDIUM |
| sitemap-declared-in-robots | Sitemap declared in robots.txt | robots.txt includes at least one `Sitemap:` line pointing at the XML sitemap. Fix: Add a `Sitemap:` line to robots.txt | MEDIUM |
| sitemap-same-host | Sitemap entries share the host of the containing sitemap | Every <loc> URL shares the host of its containing sitemap document. Fix: Keep every sitemap entry on the sitemap's own host | MEDIUM |
| sitemap-resolvable-with-products | Sitemap resolvable and includes at least one product URL | An XML sitemap is reachable and includes at least one product-shaped URL. Fix: Publish a sitemap containing product URLs | MEDIUM |
| sitemap-urlset-namespace | Sitemap root declares the sitemaps.org 0.9 namespace | Every fetched sitemap document declares the sitemaps.org 0.9 xmlns. Fix: Add the sitemaps.org 0.9 xmlns to the root element | MEDIUM |
| robots-content-type-plain | /robots.txt is served as text/plain | The /robots.txt response carries `Content-Type: text/plain`. Fix: Send Content-Type: text/plain on /robots.txt | LOW |
| robots-utf8 | /robots.txt is served as UTF-8 | The /robots.txt body decodes cleanly as UTF-8 per RFC 9309 §2.3. Fix: Serve /robots.txt as UTF-8 | LOW |
| robots-under-500kib | /robots.txt is under 500 KiB (RFC 9309 §2.5 parser cap) | The /robots.txt body is under 512,000 bytes (500 KiB). Fix: Trim /robots.txt below 500 KiB Specs: RFC 9309 §2.5 — Limits | LOW |
| chatgpt-user-allowed | ChatGPT-User allowed | robots.txt permits ChatGPT-User (OpenAI's user-initiated live fetcher) at the site root. Fix: Allow ChatGPT-User in robots.txt (advisory) Specs: OpenAI bots — ChatGPT-User | LOW |
| sitemap-loc-under-2048 | Every sitemap <loc> URL is under 2048 characters | Every <loc> URL across all resolved sitemap documents is shorter than 2,048 characters. Fix: Keep every <loc> URL under 2,048 characters | LOW |
| perplexity-user-allowed | Perplexity-User allowed | robots.txt permits Perplexity-User (Perplexity's live user-initiated fetcher) at the site root. Fix: Allow Perplexity-User in robots.txt (advisory) | LOW |
| sitemap-utf8 | Sitemap is served as UTF-8 | Every fetched sitemap document decodes cleanly as UTF-8. Fix: Serve every sitemap document as UTF-8 | LOW |
| sitemap-size-limits | Sitemap respects 50 MiB / 50,000-URL caps per document | No fetched sitemap document exceeds 50 MiB or 50,000 URLs. Fix: Split over-cap sitemaps into a sitemap index | LOW |
| llms-txt-present | llms.txt present (informational) | An /llms.txt manifest is reachable at the site root (informational; never fails). Fix: Publish an /llms.txt manifest (optional) | INFO |
Structured data
12 checks · prefix STRUCT| Slug | Name | Description | Severity |
|---|---|---|---|
| offer-price-currency-valid | Offer price + priceCurrency valid | Each Offer carries a parseable numeric `price` (≥ 0) and a 3-letter ISO 4217 `priceCurrency`. Fix: Set price as a number and priceCurrency as an ISO 4217 code Specs: Google merchant listing — price + priceCurrency required, schema.org/Offer.price + priceCurrency | CRITICAL |
| offer-availability-schema-url | Offer `availability` is a Schema.org URL | Each Offer's `availability` is a canonical Schema.org IRI (InStock / OutOfStock / PreOrder / BackOrder). Fix: Use a canonical Schema.org availability IRI on every Offer Specs: schema.org/ItemAvailability, Google merchant listing — Offer availability | HIGH |
| product-image-populated | Product `image` populated | Each Product JSON-LD node resolves at least one image URL. Fix: Add a resolvable image URL to every Product node Specs: schema.org/Product.image, Google merchant listing — image required | HIGH |
| product-name-populated | Product `name` populated | Each Product JSON-LD node carries a non-empty `name` string. Fix: Populate `name` on every Product JSON-LD node Specs: schema.org/Product.name, Google merchant listing — title required | HIGH |
| product-offers-present | Product JSON-LD includes `offers` | Each Product JSON-LD node has a resolvable Offer (or AggregateOffer) block. Fix: Add an `offers` object to every Product node Specs: schema.org/Offer, Google merchant listing — offers required | HIGH |
| product-jsonld-present | Product JSON-LD present on PDPs | Each sampled PDP exposes a Product JSON-LD node in its initial HTML. Fix: Publish a Product JSON-LD block on every PDP Specs: schema.org/Product, Google merchant listing — Product structured data | HIGH |
| product-brand-string-or-object | Product `brand` is a string or Brand/Organization object | Each Product JSON-LD node carries `brand` as either a non-empty string or an object with a non-empty `name`. Fix: Emit `brand` as either a string or a typed Brand object on every Product Specs: schema.org/brand — Text | Brand | Organization, Google merchant listing — brand recommended | MEDIUM |
| product-description-present | Product `description` present | Each Product JSON-LD node carries a non-empty `description` after HTML stripping. Fix: Populate `description` on every Product JSON-LD node Specs: schema.org/Product.description, Google merchant listing — description recommended | MEDIUM |
| product-sku-populated | Product `sku` populated | Each Product JSON-LD node carries a non-empty `sku` string or numeric value. Fix: Populate `sku` on every Product JSON-LD node Specs: schema.org/Product.sku, Google merchant listing — sku optional | MEDIUM |
| breadcrumb-list-present | BreadcrumbList present on PDPs | Each sampled PDP exposes a BreadcrumbList JSON-LD block with a populated itemListElement. Fix: Add a BreadcrumbList JSON-LD block to every PDP Specs: schema.org/BreadcrumbList | LOW |
| offer-item-condition-when-not-new | Offer `itemCondition` is canonical when present | Each Offer either omits `itemCondition` (defaults to NewCondition) or sets it to a canonical Schema.org IRI. Fix: Either omit `itemCondition` (defaults to NewCondition) or set it to a canonical IRI Specs: schema.org/OfferItemCondition, Google merchant listing — condition required only for non-new | LOW |
| product-aggregate-rating-present | Product `aggregateRating` present | Each Product JSON-LD node carries an AggregateRating with ratingValue in [0,5] and reviewCount/ratingCount ≥ 1. Fix: Add an AggregateRating to Product nodes when you have real reviews Specs: schema.org/AggregateRating | LOW |
Product data
4 checks · prefix PROD| Slug | Name | Description | Severity |
|---|---|---|---|
| product-brand-attribution | Brand attribution on PDPs | Each PDP carries brand attribution — either via Product JSON-LD `brand` or via visible HTML (Open Graph or Microdata). Fix: Surface brand attribution on every PDP Specs: schema.org/Product.brand, Google merchant listing — brand recommended | HIGH |
| product-gtin-populated | GTIN coverage on PDPs | Each PDP carries a valid GTIN (8, 12, 13, or 14 digits) on its Product JSON-LD. Fix: Populate `gtin` on every branded Product node Specs: schema.org/Product.gtin, Google merchant listing — GTIN required for branded goods | HIGH |
| product-title-no-placeholders | Product title not a placeholder | Each Product title is not a known placeholder (Default Title, Untitled, Product 1, …) and not a slug-shape token (e.g. `red-hat-001`). Fix: Replace placeholder and slug-shape titles with real product names | MEDIUM |
| product-title-quality | Product title quality (present, not all-caps) | Each Product title is a non-empty string and contains at least one lowercase letter (rejects all-caps promotional titles). Fix: Use sentence-case product titles Specs: Google merchant listing — title quality, schema.org/Product.name | LOW |
Policy
15 checks · prefix POL| Slug | Name | Description | Severity |
|---|---|---|---|
| merchant-return-policy-finite-days | MerchantReturnPolicy finite-window has positive merchantReturnDays | Finite-window return policies carry `merchantReturnDays` as a positive number. Fix: Add a positive `merchantReturnDays` to finite-window return policies Specs: Google return-policy SD — merchantReturnDays required for finite windows | HIGH |
| merchant-return-policy-present | MerchantReturnPolicy node present on Product or Offer | Each PDP's Product JSON-LD (or its Offer) carries a `hasMerchantReturnPolicy` node. Fix: Emit `hasMerchantReturnPolicy` on Product or Offer JSON-LD Specs: Google return-policy structured data — entry-point field, schema.org/MerchantReturnPolicy | HIGH |
| merchant-return-policy-option-a-or-b | MerchantReturnPolicy satisfies Option A (country+category) or B (returnLink) | Every MerchantReturnPolicy node satisfies Option A (country + category) or Option B (returnLink URL). Fix: Make every MerchantReturnPolicy node satisfy Option A or Option B Specs: Google return-policy SD — required-shape MUSTs, schema.org/MerchantReturnPolicy | HIGH |
| offer-shipping-details-present | Offer JSON-LD carries shippingDetails (OfferShippingDetails) | Each PDP's Offer JSON-LD carries `shippingDetails: OfferShippingDetails`. Fix: Emit shippingDetails (OfferShippingDetails) on Offer JSON-LD Specs: schema.org/OfferShippingDetails, Google shipping-policy SD — entry-point field | HIGH |
| privacy-policy-page-reachable | Privacy policy page reachable | A privacy policy page is reachable from the homepage nav or at a platform-conventional URL with non-stub body content. Fix: Publish a privacy policy page and link it from your site nav/footer | HIGH |
| terms-of-service-page-reachable | Terms of service page reachable | A terms-of-service page is reachable from the homepage nav or at a platform-conventional URL with non-stub body content. Fix: Publish a terms of service page and link it from your site nav/footer | HIGH |
| merchant-return-policy-applicable-country-iso | MerchantReturnPolicy applicableCountry uses ISO 3166-1 alpha-2 codes | When present, `applicableCountry` is a 2-letter ISO 3166-1 alpha-2 code (or an array of codes). Fix: Use ISO 3166-1 alpha-2 country codes in applicableCountry Specs: Google return-policy SD — applicableCountry MUST be ISO alpha-2 | MEDIUM |
| merchant-return-link-reachable | MerchantReturnPolicy merchantReturnLink URL is reachable | Every `merchantReturnLink` URL on a MerchantReturnPolicy node returns a 2xx response. Fix: Repair every merchantReturnLink URL Specs: Google return-policy SD — merchantReturnLink reachability | MEDIUM |
| merchant-return-policy-category-enum | MerchantReturnPolicy returnPolicyCategory uses valid Schema.org enum | When present, `returnPolicyCategory` is one of MerchantReturnFiniteReturnWindow / MerchantReturnUnlimitedWindow / MerchantReturnNotPermitted (bare or schema.org URL form). Fix: Use a valid Schema.org returnPolicyCategory enum value | MEDIUM |
| offer-shipping-destination-valid | OfferShippingDetails shippingDestination is a valid DefinedRegion | When present, `shippingDestination` is a DefinedRegion (or array) with ISO alpha-2 `addressCountry`. Fix: Emit shippingDestination as a DefinedRegion with ISO addressCountry Specs: Google shipping-policy SD — shippingDestination as DefinedRegion | MEDIUM |
| offer-shipping-rate-valid | OfferShippingDetails shippingRate is a valid MonetaryAmount | When present, `shippingRate` is a MonetaryAmount with numeric value/maxValue and ISO 4217 currency. Fix: Emit shippingRate as a valid MonetaryAmount Specs: Google shipping-policy SD — shippingRate as MonetaryAmount | MEDIUM |
| returns-policy-page-reachable | Returns/refund policy page reachable | A returns/refund policy page is reachable from the homepage nav or at a platform-conventional URL with non-stub body content. Fix: Publish a returns policy page and link it from your site nav/footer | MEDIUM |
| shipping-policy-page-reachable | Shipping policy page reachable | A shipping policy page is reachable from the homepage nav or at a platform-conventional URL with non-stub body content. Fix: Publish a shipping policy page and link it from your site nav/footer | MEDIUM |
| merchant-return-policy-enums-valid | MerchantReturnPolicy enrichment enums use valid Schema.org values | When present, `returnFees`, `returnMethod`, and `refundType` are members of their Schema.org enums. Fix: Use Schema.org enum values for returnFees / returnMethod / refundType | LOW |
| offer-shipping-delivery-time-valid | OfferShippingDetails deliveryTime is a valid ShippingDeliveryTime | When present, `deliveryTime` is a ShippingDeliveryTime with handlingTime and/or transitTime populated. Fix: Emit a ShippingDeliveryTime with handlingTime and/or transitTime populated Specs: Google shipping-policy SD — deliveryTime as ShippingDeliveryTime | LOW |
Trust
9 checks · prefix TRUST| Slug | Name | Description | Severity |
|---|---|---|---|
| https-and-hsts-enforced | HTTPS enforced sitewide + HSTS (≥ 6-month max-age) | Homepage is served over HTTPS, http:// redirects to https://, and the HSTS max-age is ≥ 6 months. Fix: Enforce HTTPS sitewide and ship a Strict-Transport-Security header with max-age ≥ 6 months | CRITICAL |
| contact-with-email-or-phone | Contact page exposes email or phone | Contact page is reachable AND exposes at least one of: mailto link, tel link, plain email, or plausible phone number. Fix: Add a `mailto:` email link or `tel:` phone link to your contact page Specs: Google merchant listing — customer-support contact info | HIGH |
| hsts-include-subdomains | HSTS policy carries the includeSubDomains directive | The Strict-Transport-Security header on the homepage includes the `includeSubDomains` directive. Fix: Add `includeSubDomains` to your Strict-Transport-Security header | MEDIUM |
| organization-jsonld-with-contact | Organization/OnlineStore JSON-LD with contactPoint on homepage | Homepage JSON-LD includes an Organization or OnlineStore node with a contactPoint exposing email or telephone. Fix: Add an Organization (or OnlineStore) JSON-LD block to your homepage with a contactPoint Specs: schema.org/Organization (and OnlineStore subclass) with contactPoint | MEDIUM |
| review-app-detected | Third-party review-platform integration detected | Homepage or sampled PDPs reference a known third-party review platform. Fix: Install a third-party review platform so agents see syndicated reviews on your storefront Specs: schema.org/Review vocabulary | MEDIUM |
| about-page-reachable | About page reachable with substantive copy | An About page is reachable at a standard URL with at least 200 characters of text content. Fix: Publish a substantive About page at a standard URL Specs: Google merchant listing — customer-support / about page | LOW |
| hsts-preload-directive | HSTS policy carries the preload directive | The Strict-Transport-Security header on the homepage includes the `preload` directive. Fix: Add `preload` to your Strict-Transport-Security header and submit to hstspreload.org Specs: RFC 6797 — HSTS `preload` directive (vendor extension) | LOW |
| apple-pay-detected | Apple Pay markers detected (informational) | Homepage or sampled PDPs reference the Apple Pay JS SDK, web-component, or domain-association file. Fix: Enable Apple Pay through your payment processor (informational only) | INFO |
| google-pay-detected | Google Pay markers detected (informational) | Homepage or sampled PDPs reference the Google Pay JS SDK, Payments API namespace, or web-component. Fix: Enable Google Pay through your payment processor (informational only) | INFO |
Protocol (UCP)
15 checks · prefix PROT| Slug | Name | Description | Severity |
|---|---|---|---|
| ucp-profile-no-auth-required | /.well-known/ucp is publicly fetchable with no auth | The `/.well-known/ucp` URL returns a 200 to unauthenticated requests. Fix: Allow unauthenticated access to /.well-known/ucp Specs: Google UCP merchant profile — public fetchability, UCP overview — discoverable profiles | HIGH |
| ucp-profile-present | /.well-known/ucp profile is present with a `version` field | `/.well-known/ucp` is reachable and carries a top-level `version` string. Fix: Publish `/.well-known/ucp` with at minimum a `version` field Specs: UCP overview — profile location and shape, Google UCP merchant profile guide | HIGH |
| ucp-profile-content-type-json | /.well-known/ucp response Content-Type is application/json | The `/.well-known/ucp` response declares `Content-Type: application/json`. Fix: Serve /.well-known/ucp with `Content-Type: application/json` | HIGH |
| ucp-profile-no-redirects | /.well-known/ucp returns 200 directly with no redirects | Fetching `/.well-known/ucp` returns 200 with no redirect chain. Fix: Serve /.well-known/ucp directly with a 200 response | HIGH |
| ucp-service-transport-enum | Each service `transport` is rest, mcp, a2a, or embedded | Each `services[].transport` is one of `rest`, `mcp`, `a2a`, `embedded`. Fix: Set transport to one of rest, mcp, a2a, or embedded | HIGH |
| ucp-service-transport-conditional-fields | Each service satisfies the transport-conditional field requirements | rest/mcp services declare `endpoint` + `schema`; a2a declares `endpoint`; embedded declares `schema`. Fix: Populate the conditional fields required by each service's transport | HIGH |
| ucp-signing-keys-valid | Every signing_keys[] entry is a valid JWK | Every `signing_keys[]` entry parses as a valid JWK with `kty` and the kty-specific required fields. Fix: Make every signing_keys[] entry a JWK with kty + kty-specific params | HIGH |
| ucp-cache-headers-valid | UCP profile Cache-Control is shared-cacheable with max-age ≥ 60s | `/.well-known/ucp` Cache-Control is `public, max-age=N` with N ≥ 60 and no `no-store`/`no-cache`/`private`. Fix: Serve `/.well-known/ucp` with `Cache-Control: public, max-age=…` | HIGH |
| ucp-profile-required-keys | UCP profile carries all four required top-level keys | Profile carries top-level `version`, `services`, `capabilities`, and `signing_keys`. Fix: Add every required top-level key to the UCP profile | HIGH |
| ucp-shopping-service-valid | UCP profile declares a valid shopping service entry | UCP profile declares at least one shopping service with a recognised transport and an HTTPS endpoint. Fix: Declare a shopping service entry with a recognised transport and an HTTPS endpoint Specs: UCP overview — services declaration, Google UCP profile — services structure | HIGH |
| ucp-capability-required-fields | Each capability has version + spec + schema | Every `capabilities[]` entry declares `version`, `spec`, and `schema`. Fix: Populate version, spec, and schema on every capabilities[] entry Specs: UCP overview — capabilities | MEDIUM |
| ucp-service-spec-url-origin-matches | Each service's `spec` URL origin matches its namespace authority | Every `services[].spec` URL points at the canonical UCP authority or includes the service's namespace. Fix: Point each service `spec` URL at the canonical UCP authority Specs: UCP overview — service specs | MEDIUM |
| ucp-service-version-date-format | Every service `version` matches YYYY-MM-DD | Each `services[].version` matches the ISO-date pattern `YYYY-MM-DD`. Fix: Use ISO-date `version` strings on every service | MEDIUM |
| webmcp-declarative-tools-valid | Declarative WebMCP forms are valid | Any declarative WebMCP <form> carries a valid toolname, tooldescription, and well-formed parameters. Fix: Give every declarative WebMCP form a toolname, tooldescription, and named, described parameters | LOW |
| ucp-mcp-transport-valid | UCP MCP-transport entries have valid HTTPS endpoints | Every UCP service entry declaring `transport: "mcp"` carries an absolute HTTPS endpoint. Fix: Make every declared MCP transport endpoint an absolute HTTPS URL | LOW |
Images
2 checks · prefix IMG| Slug | Name | Description | Severity |
|---|---|---|---|
| image-alt-text-coverage | Alt text on at least 80% of PDP images | At least 80% of <img> tags on each sampled PDP carry non-empty alt text. Fix: Add descriptive alt text to product images (WCAG 2.x SC 1.1.1) | LOW |
| image-area-50k-pixels | Product images meet Google’s 50,000-pixel area threshold | At least one product image on each PDP has width × height ≥ 50,000 pixels. Fix: Upload higher-resolution product images (area ≥ 50,000 pixels) | LOW |
Inventory rendered from the live check registry at build time. The retired check ledger and parked checks live on /spec-status.
8 · Platform normalization
A few signals can't be scored identically on every platform because the out-of-the-box default differs — penalizing a store for something its platform already handles wouldn't be fair. Where that's the case, the check accounts for the platform when it scores, and the normalization is versioned with each engine release.
Every fix ships in two forms: platform-specific steps for Shopify, BigCommerce, and WooCommerce, plus a generic version for custom and other platforms. So even when we can't determine the platform, the merchant still gets actionable guidance.
9 · Reproducibility
Every audit persists:
- Engine version — current semver of
packages/engineis 2026-06-08.v1. Stamped on every audit row. - Spec versions — exact pinned versions of ACP, UCP, Stripe SPT (Shared Payment Token). Perplexity, Meta, and Copilot tracks are modeled from public docs but are not first-class pinned spec-version fields in the audit record today.
- Per-check evidence — every check records the artifacts it observed (robots.txt, sitemap, JSON-LD nodes, response headers, and the offending vs. expected snippets) onto the audit record, so the report renders from stored evidence rather than a re-crawl.
Spec-drift changes are surfaced in /spec-status.
10 · Confidence and inference
Some signals are not in canonical spec documents. Microsoft Copilot in particular has thin merchant documentation — anything past Merchant Center, UCP feed, and Bingbot is inference.
When a check is based on inference rather than canonical spec, the report tags it:
- canonicalDerived from the published ACP / UCP / Stripe / Meta / Perplexity spec.
- inferredBased on observed behavior of the surface, vendor blog posts, or partner statements.
- speculativeKnown unknowns; included for transparency but does not affect the score.
11 · What we deliberately don't score
- Brand awareness or perceived authority. We score data hygiene, not reputation. Domain Rating, social signals, and editorial coverage don't enter the score.
- Conversion rate, CTR, or revenue. Those are CRO and analytics problems, not agent-readiness problems.
- Aesthetic quality of the storefront. Beautiful stores can score 30; ugly stores can score 90. Agents read structured data, not screenshots.
- Whether you should enable agentic commerce. That's a business decision. We tell you whether your store is ready — opting in or out is yours.
12 · Changelog of methodology changes
Every scoring change (new severity weights, new surface weights, new platform floors, new checks) is published with the engine version and ISO date. The retired check ledger on /spec-status tracks the why for every check that has ever been removed.
See it on your store.
Free 82-check audit. Async run with live status updates.