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) plus a category display ID derived from its position (e.g.STRUCT-9); - 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.
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 and Schema.org carry the heaviest weight because most of their 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 display ID (derived from the check's position in its category), the canonical slug, severity, and which specs the check normatively cites.
Discovery
25 checks · prefix DISC| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| DISC-1 | bingbot-allowed | Bingbot allowed Fix: Allow Bingbot in robots.txt | HIGH | |
| DISC-2 | chatgpt-user-allowed | ChatGPT-User allowed Fix: Allow ChatGPT-User in robots.txt (advisory) | LOW | |
| DISC-3 | googlebot-allowed-on-products | Googlebot allowed on product paths Fix: Allow Googlebot on product paths | CRITICAL | |
| DISC-4 | llms-txt-present | llms.txt present (informational) Fix: Publish an /llms.txt manifest (optional) | INFO | |
| DISC-5 | openai-search-bot-allowed | OAI-SearchBot allowed Fix: Allow OAI-SearchBot in robots.txt | CRITICAL | |
| DISC-6 | pdp-not-behind-login | Sampled PDPs are not gated behind a login wall (401 / 403) Fix: Open PDPs to anonymous fetches | HIGH | |
| DISC-7 | pdp-not-noindex | No sampled PDP returns a noindex directive Fix: Remove the noindex directive from every PDP | HIGH | |
| DISC-8 | pdp-single-product-page | Each PDP carries at most one Product JSON-LD node Fix: Emit a single Product JSON-LD node per PDP | HIGH | |
| DISC-9 | perplexity-bot-allowed | PerplexityBot allowed Fix: Allow PerplexityBot in robots.txt | HIGH | |
| DISC-10 | perplexity-user-allowed | Perplexity-User allowed Fix: Allow Perplexity-User in robots.txt (advisory) | LOW | |
| DISC-11 | products-discoverable-no-js | Product pages discoverable without JavaScript Fix: Make product pages discoverable without JavaScript | HIGH | |
| DISC-12 | products-machine-discoverable | Products are machine-discoverable Fix: Publish a product feed or a crawlable product sitemap | HIGH | |
| DISC-13 | robots-content-type-plain | /robots.txt is served as text/plain Fix: Send Content-Type: text/plain on /robots.txt | LOW | |
| DISC-14 | robots-txt-present | robots.txt present at root Fix: Publish a non-empty robots.txt at the site root | HIGH | |
| DISC-15 | robots-under-500kib | /robots.txt is under 500 KiB (RFC 9309 §2.5 parser cap) Fix: Trim /robots.txt below 500 KiB | LOW | |
| DISC-16 | robots-utf8 | /robots.txt is served as UTF-8 Fix: Serve /robots.txt as UTF-8 | LOW | |
| DISC-17 | sitemap-declared-in-robots | Sitemap declared in robots.txt Fix: Add a `Sitemap:` line to robots.txt | MEDIUM | |
| DISC-18 | sitemap-entries-escaped | Sitemap <loc> entries are entity-escaped Fix: Entity-escape `&`, `<`, `>` in every <loc> | MEDIUM | |
| DISC-19 | sitemap-loc-under-2048 | Every sitemap <loc> URL is under 2048 characters Fix: Keep every <loc> URL under 2,048 characters | LOW | |
| DISC-20 | sitemap-resolvable-with-products | Sitemap resolvable and includes at least one product URL Fix: Publish a sitemap containing product URLs | MEDIUM | |
| DISC-21 | sitemap-same-host | Sitemap entries share the host of the containing sitemap Fix: Keep every sitemap entry on the sitemap's own host | MEDIUM | |
| DISC-22 | sitemap-size-limits | Sitemap respects 50 MiB / 50,000-URL caps per document Fix: Split over-cap sitemaps into a sitemap index | LOW | |
| DISC-23 | sitemap-urlset-namespace | Sitemap root declares the sitemaps.org 0.9 namespace Fix: Add the sitemaps.org 0.9 xmlns to the root element | MEDIUM | |
| DISC-24 | sitemap-utf8 | Sitemap is served as UTF-8 Fix: Serve every sitemap document as UTF-8 | LOW | |
| DISC-25 | wildcard-root-disallow | No global wildcard root disallow Fix: Remove the wildcard `Disallow: /` from robots.txt | CRITICAL |
Structured data
12 checks · prefix STRUCT| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| STRUCT-1 | breadcrumb-list-present | BreadcrumbList present on PDPs Fix: Add a BreadcrumbList JSON-LD block to every PDP | LOW | |
| STRUCT-2 | offer-availability-schema-url | Offer `availability` is a Schema.org URL Fix: Use a canonical Schema.org availability IRI on every Offer | HIGH | |
| STRUCT-3 | offer-item-condition-when-not-new | Offer `itemCondition` is canonical when present Fix: Either omit `itemCondition` (defaults to NewCondition) or set it to a canonical IRI | LOW | |
| STRUCT-4 | offer-price-currency-valid | Offer price + priceCurrency valid Fix: Set price as a number and priceCurrency as an ISO 4217 code | CRITICAL | |
| STRUCT-5 | product-aggregate-rating-present | Product `aggregateRating` present Fix: Add an AggregateRating to Product nodes when you have real reviews | LOW | |
| STRUCT-6 | product-brand-string-or-object | Product `brand` is a string or Brand/Organization object Fix: Emit `brand` as either a string or a typed Brand object on every Product | MEDIUM | |
| STRUCT-7 | product-description-present | Product `description` present Fix: Populate `description` on every Product JSON-LD node | MEDIUM | |
| STRUCT-8 | product-image-populated | Product `image` populated Fix: Add a resolvable image URL to every Product node | HIGH | |
| STRUCT-9 | product-jsonld-present | Product JSON-LD present on PDPs Fix: Publish a Product JSON-LD block on every PDP | HIGH | |
| STRUCT-10 | product-name-populated | Product `name` populated Fix: Populate `name` on every Product JSON-LD node | HIGH | |
| STRUCT-11 | product-offers-present | Product JSON-LD includes `offers` Fix: Add an `offers` object to every Product node | HIGH | |
| STRUCT-12 | product-sku-populated | Product `sku` populated Fix: Populate `sku` on every Product JSON-LD node | MEDIUM |
Product data
4 checks · prefix PROD| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| PROD-1 | product-brand-attribution | Brand attribution on PDPs Fix: Surface brand attribution on every PDP | HIGH | |
| PROD-2 | product-gtin-populated | GTIN coverage on PDPs Fix: Populate `gtin` on every branded Product node | HIGH | |
| PROD-3 | product-title-no-placeholders | Product title not a placeholder Fix: Replace placeholder and slug-shape titles with real product names | MEDIUM | |
| PROD-4 | product-title-quality | Product title quality (present, not all-caps) Fix: Use sentence-case product titles | LOW |
Policy
15 checks · prefix POL| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| POL-1 | merchant-return-link-reachable | MerchantReturnPolicy merchantReturnLink URL is reachable Fix: Repair every merchantReturnLink URL | MEDIUM | |
| POL-2 | merchant-return-policy-applicable-country-iso | MerchantReturnPolicy applicableCountry uses ISO 3166-1 alpha-2 codes Fix: Use ISO 3166-1 alpha-2 country codes in applicableCountry | MEDIUM | |
| POL-3 | merchant-return-policy-category-enum | MerchantReturnPolicy returnPolicyCategory uses valid Schema.org enum Fix: Use a valid Schema.org returnPolicyCategory enum value | MEDIUM | |
| POL-4 | merchant-return-policy-enums-valid | MerchantReturnPolicy enrichment enums use valid Schema.org values Fix: Use Schema.org enum values for returnFees / returnMethod / refundType | LOW | |
| POL-5 | merchant-return-policy-finite-days | MerchantReturnPolicy finite-window has positive merchantReturnDays Fix: Add a positive `merchantReturnDays` to finite-window return policies | HIGH | |
| POL-6 | merchant-return-policy-option-a-or-b | MerchantReturnPolicy satisfies Option A (country+category) or B (returnLink) Fix: Make every MerchantReturnPolicy node satisfy Option A or Option B | HIGH | |
| POL-7 | merchant-return-policy-present | MerchantReturnPolicy node present on Product or Offer Fix: Emit `hasMerchantReturnPolicy` on Product or Offer JSON-LD | HIGH | |
| POL-8 | offer-shipping-delivery-time-valid | OfferShippingDetails deliveryTime is a valid ShippingDeliveryTime Fix: Emit a ShippingDeliveryTime with handlingTime and/or transitTime populated | LOW | |
| POL-9 | offer-shipping-destination-valid | OfferShippingDetails shippingDestination is a valid DefinedRegion Fix: Emit shippingDestination as a DefinedRegion with ISO addressCountry | MEDIUM | |
| POL-10 | offer-shipping-details-present | Offer JSON-LD carries shippingDetails (OfferShippingDetails) Fix: Emit shippingDetails (OfferShippingDetails) on Offer JSON-LD | HIGH | |
| POL-11 | offer-shipping-rate-valid | OfferShippingDetails shippingRate is a valid MonetaryAmount Fix: Emit shippingRate as a valid MonetaryAmount | MEDIUM | |
| POL-12 | privacy-policy-page-reachable | Privacy policy page reachable Fix: Publish a privacy policy page and link it from your site nav/footer | HIGH | |
| POL-13 | returns-policy-page-reachable | Returns/refund policy page reachable Fix: Publish a returns policy page and link it from your site nav/footer | MEDIUM | |
| POL-14 | shipping-policy-page-reachable | Shipping policy page reachable Fix: Publish a shipping policy page and link it from your site nav/footer | MEDIUM | |
| POL-15 | terms-of-service-page-reachable | Terms of service page reachable Fix: Publish a terms of service page and link it from your site nav/footer | HIGH |
Trust
9 checks · prefix TRUST| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| TRUST-1 | about-page-reachable | About page reachable with substantive copy Fix: Publish a substantive About page at a standard URL | LOW | |
| TRUST-2 | contact-with-email-or-phone | Contact page exposes email or phone Fix: Add a `mailto:` email link or `tel:` phone link to your contact page | HIGH | |
| TRUST-3 | review-app-detected | Third-party review-platform integration detected Fix: Install a third-party review platform so agents see syndicated reviews on your storefront | MEDIUM | |
| TRUST-4 | https-and-hsts-enforced | HTTPS enforced sitewide + HSTS (≥ 6-month max-age) Fix: Enforce HTTPS sitewide and ship a Strict-Transport-Security header with max-age ≥ 6 months | CRITICAL | |
| TRUST-5 | hsts-include-subdomains | HSTS policy carries the includeSubDomains directive Fix: Add `includeSubDomains` to your Strict-Transport-Security header | MEDIUM | |
| TRUST-6 | hsts-preload-directive | HSTS policy carries the preload directive Fix: Add `preload` to your Strict-Transport-Security header and submit to hstspreload.org | LOW | |
| TRUST-7 | apple-pay-detected | Apple Pay markers detected (informational) Fix: Enable Apple Pay through your payment processor (informational only) | INFO | |
| TRUST-8 | google-pay-detected | Google Pay markers detected (informational) Fix: Enable Google Pay through your payment processor (informational only) | INFO | |
| TRUST-9 | organization-jsonld-with-contact | Organization/OnlineStore JSON-LD with contactPoint on homepage Fix: Add an Organization (or OnlineStore) JSON-LD block to your homepage with a contactPoint | MEDIUM |
Protocol (UCP)
15 checks · prefix PROT| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| PROT-1 | ucp-cache-headers-valid | UCP profile Cache-Control is shared-cacheable with max-age ≥ 60s Fix: Serve `/.well-known/ucp` with `Cache-Control: public, max-age=…` | HIGH | |
| PROT-2 | ucp-capability-required-fields | Each capability has version + spec + schema Fix: Populate version, spec, and schema on every capabilities[] entry | MEDIUM | |
| PROT-3 | ucp-mcp-transport-valid | UCP MCP-transport entries have valid HTTPS endpoints Fix: Make every declared MCP transport endpoint an absolute HTTPS URL | LOW | |
| PROT-4 | ucp-profile-content-type-json | /.well-known/ucp response Content-Type is application/json Fix: Serve /.well-known/ucp with `Content-Type: application/json` | HIGH | |
| PROT-5 | ucp-profile-no-auth-required | /.well-known/ucp is publicly fetchable with no auth Fix: Allow unauthenticated access to /.well-known/ucp | HIGH | |
| PROT-6 | ucp-profile-no-redirects | /.well-known/ucp returns 200 directly with no redirects Fix: Serve /.well-known/ucp directly with a 200 response | HIGH | |
| PROT-7 | ucp-profile-present | /.well-known/ucp profile is present with a `version` field Fix: Publish `/.well-known/ucp` with at minimum a `version` field | HIGH | |
| PROT-8 | ucp-profile-required-keys | UCP profile carries all four required top-level keys Fix: Add every required top-level key to the UCP profile | HIGH | |
| PROT-9 | ucp-service-spec-url-origin-matches | Each service's `spec` URL origin matches its namespace authority Fix: Point each service `spec` URL at the canonical UCP authority | MEDIUM | |
| PROT-10 | ucp-service-transport-conditional-fields | Each service satisfies the transport-conditional field requirements Fix: Populate the conditional fields required by each service's transport | HIGH | |
| PROT-11 | ucp-service-transport-enum | Each service `transport` is rest, mcp, a2a, or embedded Fix: Set transport to one of rest, mcp, a2a, or embedded | HIGH | |
| PROT-12 | ucp-service-version-date-format | Every service `version` matches YYYY-MM-DD Fix: Use ISO-date `version` strings on every service | MEDIUM | |
| PROT-13 | ucp-shopping-service-valid | UCP profile declares a valid shopping service entry Fix: Declare a shopping service entry with a recognised transport and an HTTPS endpoint | HIGH | |
| PROT-14 | ucp-signing-keys-valid | Every signing_keys[] entry is a valid JWK Fix: Make every signing_keys[] entry a JWK with kty + kty-specific params | HIGH | |
| PROT-15 | webmcp-declarative-tools-valid | Declarative WebMCP forms are valid Fix: Give every declarative WebMCP form a toolname, tooldescription, and named, described parameters | LOW |
Images
2 checks · prefix IMG| ID | Slug | Name | Severity | Specs |
|---|---|---|---|---|
| IMG-1 | image-area-50k-pixels | Product images meet Google’s 50,000-pixel area threshold Fix: Upload higher-resolution product images (area ≥ 50,000 pixels) | LOW | |
| IMG-2 | image-alt-text-coverage | Alt text on at least 80% of PDP images Fix: Add descriptive alt text to product images (WCAG 2.x SC 1.1.1) | 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
Some checks are unfair to score uniformly across platforms because the default differs. Platform normalization currently lives in check logic (per-check) and is versioned through engine releases.
Every check has a fix.byPlatform recipe for shopify, bigcommerce, woocommerce, and custom — so a platform-detection miss never leaves the merchant without actionable guidance.
9 · Reproducibility
Every audit persists:
- Engine version — current semver of
packages/engineis 2.0.0. 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.
- Raw snapshots — every v2 audit captures the robots.txt / sitemap / JSON-LD it observed into a per-audit tarball so the report can be re-rendered without a recrawl.
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.