The ZAG Standard — definitions of every score in the ZAG audit report

Version v0.1 (draft) · Date 2026-09-21 · Owner CTO · Applies to the public ZAG audit report

There is no universal score for how readable a product catalogue is to an AI agent. This page says what ours mean: what each number measures, what data it is computed from, the exact arithmetic, and what a reader can check without us. Every formula below is quoted from the code that produced it, with file:line. Where the code contains a judgement rather than a formula, this page says so instead of inventing one.

Paths are relative to the report repository (report_renderer/, api/).

zag-standard_v0.1.json, next to this file, is the machine-readable form of this page: one entry per score, carrying its stable id, the anchor of the section that defines it, its range, its English and Japanese titles, and the file:line of its formula. Each score section below opens with an anchor whose id is exactly the anchor value in that index. A score with an entry in the index is defined here; a number with no entry has no definition and must not be published as a score.


Shared inputs

Two inputs feed nearly every score.

The pages we measure. Up to five pages per company, chosen by the same algorithm for the audited company and for each comparison company, so the numbers are like-for-like (api/lib/dataforseo.ts:361-465). We ask DataForSEO for every page on the domain that has organic ranking data, up to 1,000, sorted by estimated traffic value descending (dataforseo.ts:328-345). Tier 1 keeps pages whose path contains a product word (product, products, item, items, goods, catalog, service, services, lineup, series, spec, shop, store, plus category, p in EC mode and solution, application in B2B mode) and drops corporate boilerplate paths (news, ir, recruit, saiyo, csr, esg, sustainability, about, company, contact, sitemap, privacy, policy, campaign, blog, faq, support, careers, investor) (dataforseo.ts:293-315). Pages are then clustered by parent directory and only the highest-traffic page of each cluster is kept (dataforseo.ts:323-326, 416-423). If fewer than five survive, the include filter is dropped (tier relaxed), then all filters are dropped (tier fallback_top_pages). The company home page is appended to the list and the list is cut to five (api/scripts/run_company_audit.ts:140-147). The tier used is recorded in the audit JSON.

Search and AI data. All DataForSEO calls use location code 2392 (Japan) and language ja (run_company_audit.ts:57-59). Endpoints and windows are named under each score.


Cover

1. Product data AI readability — 商品データAI可読性

Range 0–100, integer. Shown as the lead cover tile with /100, plus a three-row bar comparing the audited company against two comparison companies (templates/report_body.j2:50-71; generate_all_payloads.py:448-457, 588-594).

Meaning. The share of machine-readability checks a representative sample of the company's product pages passes.

Inputs. The five pages described above. Each page is fetched as an AI agent would: platform product JSON, then static HTML and JSON-LD, then the Shopify catalogue, then a Playwright render, in that order (api/lib/agentic-readiness.ts:396-517). A page a plain crawler cannot reach is retried through an unlocking proxy and the original refusal status is kept alongside the score (agentic-readiness.ts:175-197).

Formula. Each of twelve checks returns pass = 1.0, warn = 0.5, fail = 0 (agentic-readiness.ts:307-311). Checks that do not apply to the company's mode return na and are excluded. The page score is the unweighted mean of the applicable checks, times 100, rounded to an integer (agentic-readiness.ts:802-803). The company score is the unweighted mean of the page scores of every page that could be scored, rounded to an integer (agentic-readiness.ts:848-856). All checks carry equal weight; there are no weights in the code.

The twelve checks (agentic-readiness.ts:554-800). B2B mode marks six of them na, so a B2B company is scored on six.

CheckPassWarnLine
------------
Product structured data (JSON-LD or public API)a Product node existsany JSON-LD block, or platform JSON554-579
Part number / GTIN / SKUany identifier key, sku or gtin present—581-593
Filter-attribute depthattribute count ≥ 8 (B2B) or ≥ 6 (EC)≥ half the threshold, rounded up595-608
Name and description machine-readabilityname present and description ≥ 120 charactersname present and description ≥ 40610-622
Offer price and currency (EC only)both presentprice only624-643
Availability (EC only)present—645-664
Return and shipping policy (EC only)return or shipping schema presenta policy link on the page666-685
ACP feed field coverage (EC only)ratio ≥ 0.9ratio ≥ 0.6687-739
Ratings and reviews (EC only)aggregate rating or review present—741-756
AI crawler permission in robots.txtat least one AI bot named and not a blanket disallowone of the two758-771
UCP manifest at /.well-known/ucp (EC only)reachablenever warns773-789
llms.txtreachablealways warns when absent, never fails791-800

Attribute count is the number of additionalProperty entries on the Product node, plus every <tr> in every <table>, plus every <dt> in every <dl> on the page (agentic-readiness.ts:251-262). AI bots looked for in robots.txt: gptbot, oai-searchbot, chatgpt-user, google-extended, perplexitybot, claudebot, amazonbot (agentic-readiness.ts:264-271).

Mode. A page counts as EC when it publishes a price or is detected as Shopify or WooCommerce; otherwise B2B (agentic-readiness.ts:518-519). Mode can be forced per audit and in practice is set to b2b by default (run_company_audit.ts:57).

Evidence rows under the number. The comparison bars on the cover; in section 04, the same three scores as bars on a 0–100 axis, the count of pages successfully analysed over pages attempted, and the list of gap items.

What a reader can verify. Open any page listed in the report, view source, and look for a application/ld+json block with "@type": "Product", a sku/gtin/mpn field, and a description of at least 120 characters. Fetch /robots.txt and /llms.txt on the same domain.

Limits. Five pages are a sample, not the catalogue. The attribute count treats any table row on the page as an attribute, so a page with large non-specification tables scores higher than it should. A page behind a bot block is scored on what the unlocker retrieved, which an ordinary agent would not receive.

Source. Our own fetches; page list from DataForSEO dataforseo_labs/google/relevant_pages/live.

2. Change in search keyword footprint — 検索表示キーワード数の変化

Range a signed percentage, one decimal. Shown as the second cover tile with % and an 18-month window note (generate_all_payloads.py:588-604).

Meaning. How the number of keywords the domain appears for has moved over 18 months.

Inputs. DataForSEO dataforseo_labs/google/historical_rank_overview/live, from the first day of the month 18 months ago to the first day of the current month (api/lib/dataforseo.ts:110-148). Items are sorted chronologically; the first and last are taken as past and current.

Formula. round((current - past) / past * 100, 1) (extract_audit_metrics.py:48-53). The percentage is suppressed and printed as "算出せず(初期段階)" when the past value is 10 or below, so a tiny baseline cannot produce a large number (extract_audit_metrics.py:52; generate_all_payloads.py:67-72).

Evidence rows. Section 01 row 2 and section 02 row 3 show the same percentage against the two comparison companies over the identical window, so a market-wide movement is visible as such.

What a reader can verify. Nothing directly; the underlying series is a commercial database. The window endpoints are printed on the report.

Limits. A domain migration inside the window makes the percentage an artifact. When that is known, the report replaces this tile and the matching rows with the latest keyword count and prints the reason (generate_all_payloads.py:849-875).

Source. DataForSEO, 18-month window.

3. Change in AI-search mentions — AI検索での言及数の変化

Range a signed percentage, one decimal, or a count when the percentage is suppressed. Shown as the third cover tile (generate_all_payloads.py:191-211).

Meaning. Whether the domain is being cited more or less often in AI answers.

Inputs. DataForSEO ai_optimization/llm_mentions/historical/live, platform google, from a fixed start of 2025-08-01 to the present, subdomains included (api/lib/dataforseo.ts:215-234). Subdomains are included deliberately: excluding them returned zero for a company whose content sits on a subdomain.

Formula. The calendar month in progress is dropped (dataforseo.ts:171-177). At least four complete months are required. The complete months are split at floor(n / 2): the first half is the baseline, the rest is recent. Each half is summed. Change is round(((recentSum - olderSum) / olderSum) * 1000) / 10 (dataforseo.ts:184-213). The percentage is suppressed when the baseline half has fewer than 10 mentions (dataforseo.ts:158, 197, 209); the tile then shows the period total instead (generate_all_payloads.py:194-200).

Evidence rows. Section 03 draws every complete month as a bar with the two halves shaded differently, labels both windows, prints the excluded partial month, and lists both half-sums and the percentage for the audited company and both comparison companies.

What a reader can verify. Nothing directly; this is a commercial sample of AI answers.

Limits. The series starts 2025-08-01 because the provider has nothing earlier, so the window is not yet a full two years for any company. Mention counts are a sample of answers to a set of questions the provider chooses, not a census.

Source. DataForSEO, from 2025-08-01, monthly.


Section 01 — Summary

Four rows, each repeating a number defined elsewhere on this page, for the audited company and two comparison companies (generate_all_payloads.py:610-650): estimated organic search traffic (score 4), search keyword footprint (score 2, or the latest count in a migration case), AI-search mentions total (score 6), and product data AI readability (score 1). It introduces no new arithmetic.


Section 02 — Traditional search

4. Estimated organic search traffic — 推定オーガニック検索トラフィック

Range a count of visits per month, prefixed ~. Meaning. Estimated monthly visits from conventional search.

Inputs and formula. DataForSEO dataforseo_labs/google/ranked_keywords/live; the response's metrics.organic.etv is taken as traffic and metrics.organic.count as the keyword count (api/lib/dataforseo.ts:79-108), then rounded (extract_audit_metrics.py:148-149). No further arithmetic.

Limits. This is the provider's model of traffic, not measured visits. It comes from a different endpoint than the 18-month series, and the two do not agree (see Gaps, item 13).

5. 18-month change in estimated traffic — 推定流入の18ヶ月変化

Same window, endpoint and formula as cover score 2, applied to etv rather than keyword count (dataforseo.ts:110-148; extract_audit_metrics.py:48-53, 157).


Section 03 — AI search

6. AI-search mentions, period total — AI検索での言及数(期間合計)

Range a count. Meaning. How many times the domain was cited across the observed months.

Formula. The sum of metrics.mentions over the complete months of the window, the same months the change (score 3) uses; the month in progress is excluded from the total and shown separately as "month to date" in the series (score 7). Estimated AI search volume alongside it is the sum of metrics.ai_search_volume over the same complete months. (The old code summed every month returned including the month in progress, dataforseo.ts:246-247, so its total and its trend covered different spans; v0.1 makes them agree.)

Limits. The window is labelled as cut: the first and last complete month are printed beside the number, never fixed copy such as "過去12ヶ月".

7. Monthly mention series

A drawn value rather than a score, defined here because the report shows it. Each bar's height is round(max(1.5, mentions / peak * 100), 1) percent of the plot, where peak is the highest complete month (generate_all_payloads.py:261-276). The 1.5 floor keeps a zero month visible; a bar is not proportional below that point.


Section 04 — Product data readability

8. ACP conformance, 20 items — AIコマース標準規格(20項目)への適合状況

Range three integers that sum to 20: pass, gap, not-observable. Shown as a segmented bar and three tiles reading /20項目 (report_body.j2:266-290; i18n_ui.py:111-114, 317-320).

Meaning. Of the 20 fields the Agentic Commerce Protocol product feed marks REQUIRED or CONDITIONAL, how many a public product page evidences, how many are missing, and how many no outside crawl can judge.

Inputs. One representative page — the first entry of the page list, which is the highest-traffic product page (extract_audit_metrics.py:208-209). The 20 fields come from the ZAG ledger (api/data/zag_profiles.json, profile acp, version 2026-04-17, pulled from project-zag at a pinned commit; spec https://developers.openai.com/commerce/product-feeds/spec).

Formula (extract_audit_metrics.py:34-102). Nine of the 20 fields are judgeable from a public page: item_id, title, description, url, brand, image_url, price, availability, gtin. In B2B mode price and availability join the not-observable bucket rather than counting as failures, leaving seven judged. Each judged field is decided from the page's own signals:

  • item_id: any identifier, sku or gtin present
  • title: a product name present
  • description: description length ≥ 40 characters
  • url: always true, the analysed page URL
  • brand, image_url: true only when the Product node itself carries the field, in any of the forms schema.org allows: brand as a plain string or as an object with a name; image as a string, an array of strings, or an ImageObject (or array of them) with a url or contentUrl. A Product node with neither passes neither. The parser accepts every one of these forms, so a reader who views the source and finds the field in any form sees what the report records. (The old renderer passed both on the mere presence of a Product node, extract_audit_metrics.py:80-81; v0.1 judges the field, since the item is about the field being there for an agent to read.)
  • gtin: a gtin present
  • price, availability (EC only): present in the Offer

pass = judged fields that are true; gap = judged fields that are false; not-observable = 20 minus the number judged, which is 13 in B2B. The renderer refuses to publish a report whose three numbers do not sum to 20 (validate_report.py:602-606).

Evidence rows. Three tiles, each with a sentence naming what falls in that bucket, and the segment widths drawn as each count over 20 (report_body.j2:269-290).

What a reader can verify. Open the representative page, view source, and check for each of the nine fields inside a "@type": "Product" block.

Limits. One page, not the catalogue. Brand and image count only when they sit in structured data, so a page that shows both to a human but not to a parser is marked as a gap, which is the intended reading but is stricter than a person would judge. The eleven or thirteen not-observable fields are seller and feed level and are never claimed either way.

9. Pages analysed — 解析成功ページ数

pagesAnalyzed / pagesAttempted: pages that returned a score, over pages tried (agentic-readiness.ts:848-873). A count, not a score. It is the honest denominator for score 1.


10. AI answer citability — AI回答における自社サイト引用率

Range and unit. 0–100 percent, per assistant; no cross-assistant average.

Meaning. Of the answers an AI assistant gave to the report's product questions with web search on, the share that cited a page on the company's own domain (the entered domain with its www/apex pair).

Inputs. The report's question set (A3; the instant tier's fixed questions for the domain); one answer per question per assistant through the vendor's LLM Responses endpoint with web search on; each answer's citation annotations (URL and quoted text), model name, token counts and cost, which the answer must carry or it is refused (the per-call evidence rule).

Formula. For each assistant: asked = answers in which a web search actually happened; cited = those of them whose annotations contain at least one URL whose host is the company's own domain, defined as the apex and its www form only, subdomains excluded; the score is shown as the fraction cited of asked (for example 3 of 20), with the percentage only in the index's range. Answers in which no search happened are excluded from asked and counted separately on the face of the score, never as a failure to cite. An assistant with asked = 0 has no number and reads "not measured" with the reason (not requested, refused, or no search happened).

Evidence rows under the number. Per assistant: asked, cited, the list of cited own-domain URLs, the questions whose answers cited a third party instead (host only), model name, date.

What a reader can verify. Ask the same question in the same assistant with web search and look at the citations.

Limits. Assistants change answers day to day; the number is a sample on its date. Only assistants reached through the vendor's official-API path are asked (the assistant list at the guard). No question is about a person.

Source. New in v0.1; no prior code. Data source: DataForSEO LLM Responses (live), per assistant.

Section 05 — Improvement opportunity

No score. The left pane is a screenshot of the product page; the right pane lists the fields an agent does or does not receive. In extraction mode those rows are what a language model extracted from the page, not a measurement (see Gaps, item 7).


Score index — for the engineering team

idScoreComputed inFunction
------------
acrProduct data AI readability (page)api/lib/agentic-readiness.ts:802-803analyzeAgenticReadiness
acrProduct data AI readability (company)api/lib/agentic-readiness.ts:848-856summarizeAgenticReadiness
acrSignal verdict scaleapi/lib/agentic-readiness.ts:307-311verdict
acrPage sampleapi/lib/dataforseo.ts:361-465getTopProductPages
keyword_footprint_changeKeyword footprint changereport_renderer/extract_audit_metrics.py:48-53, 158pct_change
keyword_footprint_change18-month seriesapi/lib/dataforseo.ts:110-148getDomainTrafficHistory
ai_mention_changeAI-mention changeapi/lib/dataforseo.ts:184-213buildMentionsTrend
ai_mention_changePartial-month exclusionapi/lib/dataforseo.ts:171-177completeMentionMonths
organic_traffic_estimateEstimated organic trafficapi/lib/dataforseo.ts:79-108getDomainTraffic
organic_traffic_change18-month traffic changereport_renderer/extract_audit_metrics.py:157company_metrics
ai_mention_totalMention and volume totalsapi/lib/dataforseo.ts:246-247getLlmMentionsHistory
ai_mention_monthly_seriesMonthly bar heightsreport_renderer/generate_all_payloads.py:261-276build_mentions_chart
acp_conformance_20ACP 20-item splitreport_renderer/extract_audit_metrics.py:63-102acp_block
acp_conformance_20ACP ledgerapi/lib/zag-profiles.ts:116-129acpCoverage
acp_conformance_20Sum-to-20 gatereport_renderer/validate_report.py:602-606validate_payload
pages_analysedPages analysedapi/lib/agentic-readiness.ts:869-873summarizeAgenticReadiness
—Bar widths (5% floor)report_renderer/generate_all_payloads.py:87-90calc_bar_width
—Payload vs metrics cross-checkreport_renderer/validate_report.py:232-310validate_against_metrics

Gaps

Each item is something the current code does by hand, by judgement, without a stated rubric, or with a constant that has no recorded source. None of these is filled in with an invented number here.

  1. No AEO score exists. Section 00 defines "AI回答引用性スコア / AI answer citability score" as one

of the report's four metrics (report_renderer/i18n_ui.py:45-47, 253-254), but nothing computes it. The registry field is written as an empty string every time (report_renderer/extract_audit_metrics.py:237). 2. The cover evidence line never renders. report_body.j2:73-75 prints a sentence built from cover.evidence.queried and cover.evidence.reachable. The field is absent from the schema and from all 200 client payloads, and no code writes it. 3. Hardcoded ACP fallback. When a client's metrics file has no acp block, the payload generator substitutes pass 2 / gap 5 / not-observable 13 (generate_all_payloads.py:517-520). These constants describe no company. 4. Hardcoded readability fallback. When the readability average is null, the campaign registry receives 15 (extract_audit_metrics.py:238). No source is recorded for 15. 5. Thresholds have no stated derivation. 8 and 6 attributes (agentic-readiness.ts:596), 120 and 40 description characters (:611), 0.9 and 0.6 ACP ratio (:714), warn = 0.5 (:308-310), and the equal weighting of all twelve checks (:803) are chosen constants with no recorded basis. 6. Two asymmetric file checks. A missing llms.txt always scores 0.5 and can never fail (agentic-readiness.ts:794-795); a missing UCP manifest always scores 0 and can never warn (:778-779). The asymmetry is not explained anywhere. 7. Section 05 content is a model judgement. The field rows come from a Gemini call with a prose prompt and no rubric (report_renderer/extract_ai_panel.py:74-109). The only automatic gate is density: at least 6 groups and 25 rows (extract_ai_panel.py:131-138). Rows matching a placeholder pattern are dropped (:112-114, 117-128). Whether a value is real or illustrative is the model's own example flag. 8. Attribute depth is a proxy, not a measurement. Every table row and every definition term on the page counts as an attribute, including navigation and unrelated tables (agentic-readiness.ts:251-262). 9. Comparison companies are chosen by hand. They are a command-line argument (api/scripts/run_company_audit.ts:55), with no recorded selection rule. 10. The representative page for the ACP panel is not chosen deliberately. It is the first entry of the page list (extract_audit_metrics.py:208-209), which is whichever product page has the most estimated traffic. 11. All narrative sentences are hand-written templates. Section headlines, insights and callouts are f-strings in generate_all_payloads.py (for example :610-615, 796-802); the gap table copy is fixed in i18n_ui.py and the per-client line is written by hand. 12. Zero values can be overridden. An all-zero company is blocked unless a separate guard has confirmed the zeros, but --allow-suspect-zero publishes them with a console warning only (extract_audit_metrics.py:105-132, 194-203). 13. Two traffic numbers, two endpoints. traffic_latest and keywords_latest come from the ranked keywords endpoint; traffic_current and keywords_current come from the historical endpoint. For one published client they are 59,268 against 58,982 and 229 against 256 (report_renderer/clients/advantest/metrics.json). The report prints the first pair as "latest" and the second pair's deltas as the change, without saying they are different measurements. 14. The catalogue-scale figure is carried over, not re-measured. It is copied from the previous metrics file on every re-extraction and originates from a separate crawl outside the audit (extract_audit_metrics.py:299-304).

Where two sources disagree

  • Who owns the ACP standard. The report body and footer attribute it to "Agentic Product Protocol /

Klarna" (generate_all_payloads.py:796, 843; generate_en_payload.py:673, 794). The code and ledger attribute it to OpenAI and Stripe, citing developers.openai.com/commerce/product-feeds/spec (agentic-readiness.ts:730; api/data/zag_profiles.json). The report text is wrong and must be corrected before publication. - How many of the 20 fields are observable. The ledger module's comment says "we can judge 8 of 20" (api/lib/zag-profiles.ts:20-21) while its own observable set holds 9 entries (:88-98) and B2B mode judges 7 (extract_audit_metrics.py:84-94). - How brand and image are judged. The renderer's ACP block requires them to come from a Product node (extract_audit_metrics.py:80-81), in practice on the node's presence alone; the TypeScript ACP check accepts any brand or image value (agentic-readiness.ts:703-704). Two implementations of the same item can produce different counts. v0.1 rules: the field inside the Product node, see score 8. - The mention window. The label says 12 months (generate_all_payloads.py:633); the data runs from 2025-08-01 to the present and includes the partial month (dataforseo.ts:231, 246).


Instant run: endpoints called (ruling 2026-09-21)

With the mention total taken over the complete months of the historical series, no score reads the vendor's target-metrics endpoint; the instant run does not call it. The client keeps it for the requested tier's grouping. This is what the measurement prices. Nothing in the report may imply a figure that endpoint would have supplied.

Versioning

A score's definition changes only with a version bump of this document. A change to any input, window, threshold, weight or rounding rule is a definition change and requires a new version here before it ships. The version of this document in force is printed on every report that quotes these scores, so a report from any date can be read against the definitions that produced it. Superseded versions stay published. The JSON index ships with the same version string and is updated in the same change: an id, anchor or range may not move without a version bump.

Rulings for v0.1 on the gaps above (CTO, 2026-09-21)

  • A missing input never becomes a number: the hardcoded ACP fallback (2 / 5 / 13) and the readability default (15) are removed; the report shows "not measured" with the reason, and the score index marks the score absent.
  • Attribution: the report body and footer name the specification the ledger cites (OpenAI and Stripe, the OpenAI commerce specification), never "Klarna".
  • One series per number: latest value and change come from the same endpoint; the report says which one.
  • Every window is labelled as the data is cut: the mention trend excludes the partial month and the label says so.
  • Chosen constants stay chosen in v0.1 and are stated as such here; changing any of them is a version bump.
  • The AI answer citability score named in section 00 is defined as score 10 (Rahul, 2026-09-21).

Addendum v0.1 (2026-09-21): market and platform beside every mention number

The vendor's mention data is held per platform, location and language; its ChatGPT mention data exists for the United States and English only, and its endpoints default to that market when a caller names none. So every mention score (ai_mention_change, ai_mention_total, ai_mention_monthly_series) carries beside the number the platform asked, the location and the language, as evidence rows; a report for a Japanese manufacturer computes them for Japan and Japanese on the platforms that hold that market, and a platform that does not hold it reads "not measured for this market", never a number. The index gains the same three fields per mention score. Shapes for the implementation: target metrics returns counts under aggregated_metrics by grouping with no scalar total; historical returns one item per calendar month with year and month as integers, which is the partial-month cut's input.

Absence and a missing field are different things: a field we looked at the page for and did not find is a measurement about that page (the item reads gap, and the twenty items still sum to twenty); a page or a check we could not run is not measured. Absence is not zero (secprod, same day): when a platform does not hold the market, the number is absent, not zero, and any score computed from it reads "not measured for this market" rather than being computed with a zero in it. No mention score adds or averages across platforms whose markets differ, even when both have numbers.