Abstract
The Agent-Adoption Specification defines an open methodology for measuring how observable a website's agent-readiness signals are to AI agents and other automated HTTP clients. It specifies a fixed catalog of 25 checks organized into 4 categories, a three-status verdict enum (pass / fail / neutral), a deterministic integer scoring model producing a 0–100 aggregate score, a gate-driven three-level readiness ladder (1, 2, 3), and a normative JSON output schema governing the wire format. The specification is implementation-agnostic: any scanner that emits a JSON document validating against schemas/output-v1.schema.json, applies the scoring formula and cluster math defined herein, and adheres to the conformance rules in §11 is conformant.
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in BCP 14 (RFC 2119 and RFC 8174) when, and only when, they appear in all capitals.
1. Overview & purpose
The Agent-Adoption Specification (the “Specification” or “spec”) defines how a website signals readiness to be consumed by autonomous agents — search bots, AI assistants, retrieval systems, and protocol-aware clients. It defines a fixed catalog of 25 observable HTTP-level checks across four categories, a deterministic scoring model, a three-level readiness ladder, and a JSON output schema. Conforming implementations (“scanners”) accept a domain as input and emit a single self-contained JSON document conforming to the wire schema in schemas/output-v1.schema.json.
The Specification is written for three audiences:
- Consumers of scan output — analytics tools, search-readiness dashboards, agentic platforms reading scan JSON to make routing decisions.
- Implementers of scanners — organizations building tools that emit conforming output.
- Brand owners — operators of websites who want a stable definition of what “agent-ready” means.
The 25 checks each correspond to a specific external standard, vendor convention, or measurable HTTP behavior. The Specification does not invent semantics; it codifies measurement.
1.1 Defensive framing
The Specification measures agent-readiness signals, not predicted AI traffic, conversion, brand mention frequency, or search-result rank. A high score does not guarantee that any particular agent will visit, cite, or correctly interpret the site; it indicates that the standardized signals an agent would expect to find are present and well-formed. Conversely, a low score does not predict invisibility — many agents tolerate missing signals via heuristics. The Specification is a measurement contract, not a marketing promise.
This framing carries through every part of the Specification: status verdicts, scores, levels, and cluster verdicts describe observable site state, not predicted behavior of any agent. Annex A elaborates.
1.2 Relationship to companion research
This Specification is descriptive (defining what a conforming scanner measures and how) rather than predictive. Empirical findings about which signals correlate with downstream LLM visibility outcomes are reported in the companion Agent-Adoption Correlation Study, which uses this Specification as its measurement protocol.
A v2.0 update is planned for approximately Q3 2026, calibrated against findings from quarterly correlation re-runs. Tagged releases are immutable; v1.0 and any future major versions remain accessible at their respective version-suffixed URLs. See §12 for the full versioning policy.
2. Terminology
This section defines the terms used throughout the Specification. Implementations and downstream content SHOULD use these terms with the meanings given here.
- Specification (or “spec”) — This document plus the JSON schemas and profile JSON it references. Normative authority for what a conforming implementation MUST emit.
- Scanner (or “Conforming scanner”) — Any software implementation that accepts a domain as input and emits JSON output validating against
schemas/output-v1.schema.json, applies the scoring formula and cluster math defined in this document, and adheres to the conformance rules in §11. - Implementer — A party building a scanner that conforms to this Specification.
- Brand — The website being measured. The Specification operates on a single root domain (eTLD+1) per scan.
- Profile — A bundled JSON document declaring which checks are active for a given vertical or use case, plus per-check weight overrides if any. v1.0 ships exactly one profile (
b2b-saas). v1.0 does NOT define a custom-profile mechanism; future versions may. See §9. - Check — One of 25 named, atomic measurements. Each check has a stable kebab-case identifier, belongs to exactly one category, carries an integer weight, and emits exactly one status per scan plus a structured
detailspayload. The full list is in §5. - Score — An integer 0–100 derived deterministically from the per-check statuses and weights via the formula in §7. Score is computed both at the top level (across all scored checks) and per category (scoped to that category's scored checks).
- Level — An integer (1, 2, or 3) representing site readiness tier. v1.0 defines exactly three levels; future versions may add more. See §8.
- Level Gate — A check whose
passstatus is required to advance from one level to the next. v1.0 defines two gates:content-signals(L1→L2) andmarkdown-negotiation(L2→L3). Level is gate-driven, NOT score-driven; a site MAY have a low score but reach L2 if the gate passes, or vice versa. - Status — The per-check verdict, exactly one of
pass,fail,neutral. Defined in §6. - Weight — An integer drawn from the closed set
{0, 4, 5, 6, 7, 8, 10}declaring how much a check contributes to scoring. Weight0means informational (not scored); weights4–10are scored. Sum of all scored weights in v1.0 = 80. - Scored vs Informational — A check is “scored” when its weight > 0 (and
scored: trueon the wire); “informational” when weight = 0 (andscored: false). v1.0 has 12 scored checks and 13 informational checks. Informational checks still emit a status anddetailspayload but contribute nothing to the score; some informational checks are level gates. - Cluster — A named cross-check rule that adjusts the final score based on a combination of check results. v1.0 defines exactly three clusters (
htmlPath,spaRenderingCap,noViablePathCap) emitted in fixed order as the top-levelclusters[]array. Clusters are part of the score-computation discipline; the wire shape lives at the top level. See §7.3. - Cluster verdict — A single entry in the top-level
clustersarray describing whether one named cluster's effect was applied during scoring, plus its parameters. - Coefficient cluster (
kind: "coefficient") — A cluster that multiplies one or more per-check weights before scoring. v1.0 defines one (htmlPath). - Cap cluster (
kind: "cap") — A cluster that clamps the final top-level score to a maximum value. v1.0 defines two (spaRenderingCap,noViablePathCap). When multiple cap clusters trigger, the lowest cap wins (caps do not compound). - Cluster math — The composition rule by which clusters affect the final score: coefficient-kind clusters multiply per-check weights first, base score is computed, then cap-kind clusters clamp the final top-level score. Per-category scores are NOT subject to cluster math.
- Wire Format — The on-the-wire JSON encoding rules. v1.0 uses camelCase for all object keys and category enum values, kebab-case for check IDs, ISO 8601 with
Zsuffix for timestamps, and integer types forlevel,score,weight, all duration fields, and all category counts. - Vendor extension surfaces — v1.0 does NOT define a top-level vendor namespace. Implementations that need to emit vendor-specific data have two open surfaces: the per-check
detailsobject (additionalProperties: true, exceptdetails.bodyis forbidden), and themeta.countersobject (open at both levels). nextLevel— An optional top-level object describing the next level the site can attempt and the check IDs that must flip topassto advance. Omitted (not nullified) when the site is at the highest defined level (v1.0: level 3). See §8.details.kind— A string field that, when present inside a check'sdetailspayload, MUST equal that check'sid. Allows consumers to discriminate per-check structured payloads via duck-typing without re-deriving the field from context. The schema does not require it; implementations that emit it MUST set it equal to the check ID.- Categories — The four top-level groupings of checks. Defined in §4:
discoverability,accessControl,contentReadability,agentEndpoints.
3. Scope & non-goals
3.1 v1.0 covers
- A fixed catalog of 25 checks organized into 4 categories, with stable kebab-case IDs and stable integer weights.
- A single bundled profile (
b2b-saas) declaring the active checks and weights. - A normative JSON output schema (
schemas/output-v1.schema.json) validating a single self-contained scan response (13 top-level fields). - A deterministic scoring formula with explicit edge-case behavior.
- A three-level readiness ladder with two named gates.
- Three named cluster verdicts with their trigger conditions and effects on score.
- Defensive framing language clarifying that the spec measures readiness signals, not predicted AI traffic (Annex A).
- Conformance rules (MUST / MAY / MUST NOT) (§11).
3.2 v1.0 does NOT cover
- No
errorstatus. The status enum is exactly three values. Implementations encountering a runtime failure within a single check MUST map it to one of the three defined statuses (see §6). v2.0+ MAY introduce a fourth status. - No
tierabstraction. Weights are integers in a closed set; the spec does not define named tiers (Critical / High / Medium / Low) on the wire. Vendor descriptions MAY group weights into tiers for human consumption, but the wire MUST emit raw integers. - No formal profile schema. v1.0 ships one profile JSON; the structure of profile JSON is illustrative, not normatively schema-validated. v2.0+ MAY introduce
profile-v1.schema.json. - No custom-profile mechanism. v1.0 implementations MUST emit
profile: "b2b-saas". Custom profiles are deferred. - No localization. All check
descriptionandmessagestrings are English. Localization layers are out of scope for v1.0. - No signed responses. Scan output is not signed; consumers wanting attestation must implement their own signing layer.
- No browser fingerprint or render-time JS analysis beyond what
rendering-strategyperforms (a single before/after-render content-length comparison). - No active probes that mutate site state. All checks are read-only.
- No throttling or rate-limit specification. Scanners decide their own polling cadence subject to robots-respecting behavior.
- No conformance test suite. A formal test suite is deferred. Implementations MAY use any conforming output as a validation target for their own schema-compliance tests.
3.3 What v2.0+ may add
Specifics intentionally deferred to avoid pre-committing v2.0 design.
- An
errorstatus value distinct fromfailandneutral. - A formal profile schema and a custom-profile mechanism.
- Additional checks, additional categories, or additional level tiers.
- A normative localization layer.
- Signed/attested scan output.
4. Categories
v1.0 defines exactly four categories, in this canonical order. Conforming scanners MUST emit categoryReports as an array of length 4 in this order.
4.1 discoverability
Description: Can agents find the signals they need to navigate the site? Covers robots.txt, XML sitemap, and Link: response headers — the first three signals any crawler reads before parsing HTML.
Weight subtotal: 11
4.2 accessControl
Description: Is the site telling AI systems what they may and may not do with its content? Covers per-bot robots.txt rules, content-usage signals (Content-Signal / AIPREF), cryptographic bot authentication, and blanket-allow posture detection.
Weight subtotal: 7
4.3 contentReadability
Description: Once an agent is inside, can it actually read what it fetches? Covers markdown availability (per-URL twins and Accept-header negotiation), page size relative to context windows, rendering strategy, HTTP status-code honesty, redirect behavior, llms.txt shape, AGENTS.md presence, and cache header hygiene.
Weight subtotal: 39
4.4 agentEndpoints
Description: Does the site expose the surfaces agents need to call it programmatically? Covers MCP Server Cards, A2A Agent Cards, Agent Skills indexes, OAuth Authorization Server / OpenID Connect discovery, OAuth Protected Resource metadata, and /.well-known/api-catalog.
Weight subtotal: 23
4.5 Totals
25 checks across 4 categories. Sum of all weights = 11 + 7 + 39 + 23 = 80. 12 scored checks (weight > 0), 13 informational checks (weight = 0).
5. Checks (catalog)
The 25 checks in canonical category order, with checks within each category sorted by weight (descending) then ID (alphabetic). Trigger conditions are normative: conforming scanners MUST classify status as defined here. External standards URLs are non-normative pointers — implementations MAY include any subset in the per-check specUrls array on the wire.
5.1 discoverability
robots-txt-exists
- Category:
discoverability - Weight: 7 (scored)
- What it tests: Whether
/robots.txtis reachable and parseable as a robots-exclusion file per RFC 9309. - Returns
passwhenGET /robots.txtreturns a 2xx status, the responseContent-Typebegins withtext/, and the body contains at least oneUser-agent:directive line. - Returns
failwhen the response is non-2xx, the content-type is nottext/*, the body is empty, the body lacks anyUser-agent:directive, or the fetch fails at the network level. - Returns
neutral: This check has no neutral case; fetch failures map tofail. - Notes: Several access-control checks (
ai-bot-rules,content-signals,robots-allow-all) declare a hard dependency on this check. When this check does not pass, those dependents emitneutralwith a dependency-skip message.
sitemap-exists
- Category:
discoverability - Weight: 4 (scored)
- What it tests: Whether at least one valid XML sitemap is reachable, discovered first via
Sitemap:directives inrobots.txt, then falling back to/sitemap.xml. See sitemaps.org protocol. - Returns
passwhen at least one candidate URL returns a 2xx response that parses as XML with a root element of<urlset>or<sitemapindex>. - Returns
failwhen all candidates were probed, at least one was reachable, and none parsed to a valid sitemap structure. - Returns
neutralwhen all candidates failed at the network level, or every reachable candidate's body exceeded an implementation-defined pre-parse size cap (a conservative default of 2 MB is RECOMMENDED).
link-headers
- Category:
discoverability - Weight: 0 (informational)
- What it tests: Whether the homepage emits
Link:response headers exposing agent-relevant resources (API catalogs, alternates, service docs) per RFC 8288. - Returns
passwhen aLink:header is present on the homepage AND at least one rel value is something other than browser-presentation rels (preload,prefetch,stylesheet,icon,dns-prefetch,preconnect,modulepreload,apple-touch-icon, orunknown). - Returns
failwhen aLink:header is present but every rel value is browser-presentation only (or rel-less). - Returns
neutralwhen noLink:header is present on the homepage.
5.2 accessControl
ai-bot-rules
- Category:
accessControl - Weight: 7 (scored)
- What it tests: Whether
robots.txtdeclares per-AI-bot rules or content-usage directives. - Returns
passwhen the robots body contains at least one per-User-agentblock targeting a known AI crawler with at least oneAllow:orDisallow:rule, OR the body contains at least oneContent-Signal:(legacy) orContent-Usage:(AIPREF) directive with a non-empty value. - Returns
failwhen the robots body is present and parseable, but neither per-AI-bot rules nor a Content-Signal / Content-Usage directive is found. - Returns
neutralwhen the dependencyrobots-txt-existsdid not pass (dependency-skip), or the robots body is empty. - Relevant standards: RFC 9309, Cloudflare Content-Signal, IETF AIPREF Vocabulary.
- Notes: Hard
dependsOn: ["robots-txt-exists"].
content-signals
- Category:
accessControl - Weight: 0 (informational)
- Level gate: L1 → L2. The site advances from Level 1 to Level 2 when this check returns
pass. - What it tests: Whether
robots.txtcarries at least one Content-Signal (Cloudflare) or AIPREF Content-Usage directive declaring permitted post-fetch use of the content. - Returns
passwhen the robots body contains at least oneContent-Signal:orContent-Usage:directive line. Token-level recognition is forward-tolerant. - Returns
fail: This check has no fail case. Verdicts other than pass map toneutral. - Returns
neutralwhen the robots body is empty, or no Content-Signal / Content-Usage directives are present, or the dependencyrobots-txt-existsdid not pass. - Relevant standards: Cloudflare Content-Signal, IETF AIPREF Vocabulary, IETF AIPREF Attach.
- Notes: Hard
dependsOn: ["robots-txt-exists"]. Although informational (weight 0), this check is the L1→L2 level gate.
web-bot-auth
- Category:
accessControl - Weight: 0 (informational)
- What it tests: Whether the site publishes a signing-key directory at
/.well-known/http-message-signatures-directoryfor HTTP Message Signatures-based bot authentication. Presence signals that the site operates an AI crawler whose identity targets can verify. - Returns
passwhenGET /.well-known/http-message-signatures-directoryreturns a 2xx response with parseable JSON containing akeysarray of objects, each with a non-emptyktystring. - Returns
failwhen the response is non-2xx, the body is unparseable JSON, thekeysarray is missing or empty, or any key lacks aktyfield. - Returns
neutralwhen the endpoint is unreachable at the network level. - Relevant standards: HTTP Message Signatures Directory draft, Web Bot Auth Architecture draft.
robots-allow-all
- Category:
accessControl - Weight: 0 (informational)
- What it tests: Whether
robots.txtdeclares a blanket-allow posture: a wildcardUser-agent: *group that does not block, AND no other UA group containing a blanketDisallow: /. - Returns
passwhen aUser-agent: *group is present AND that group either contains an explicitAllow: /or has noDisallow:rules, AND no other UA group containsDisallow: /. - Returns
failwhen the robots body is present but does not satisfy the wildcard-permissive + no-cross-bot-blanket-block conditions. - Returns
neutralwhen the robots body is empty (dependency-skip path), or the dependencyrobots-txt-existsdid not pass. - Relevant standards: RFC 9309.
- Notes: Hard
dependsOn: ["robots-txt-exists"].
5.3 contentReadability
rendering-strategy
- Category:
contentReadability - Weight: 10 (scored)
- What it tests: Classifies the homepage's rendering strategy by comparing the plain HTML body to a headlessly-rendered version. Determines whether agents can read meaningful content without executing JavaScript.
- Returns
passwhen the plain (non-rendered) body is NOT detected as an SPA shell, the headless render succeeds, AND the rendered/plain content-length ratio is < 1.05 (full server-side rendering) or between 1.05 and 1.20 inclusive (server-side rendering with partial hydration). - Returns
failwhen the plain body is detected as an SPA shell (immediate short-circuit); OR when plain is empty and rendered is empty; OR when plain is empty and rendered has content; OR when the rendered/plain ratio exceeds 1.20. - Returns
neutralwhen the headless rendering capability is unavailable AND the plain body is not an SPA shell. - Notes: Scoring interaction: when this check fails, the
htmlPathcluster triggers (zeroing outpage-size-html's weight) AND thespaRenderingCapcluster triggers (capping the final top-level score at 39 or 59 depending on severity).
markdown-url-support
- Category:
contentReadability - Weight: 8 (scored)
- What it tests: Whether the site provides
.mdtwins for HTML pages (e.g.,/page.mdalongside/page.html), giving agents a markdown fetch path with no HTML wrappers, ads, or modals. Implementations sample up to 5 URLs (sitemap pool first; homepage fallback). - Returns
passwhen the number of measurable probes (served + missing) is at least 3, AND the ratio of served (2xx + markdown-shaped body) to measurable is at least 0.5. - Returns
failwhen measurable probes ≥ 3 AND served/measurable < 0.5. - Returns
neutralwhen no samples could be collected at all; OR at least half of probe attempts were unmeasurable (blocked, errored); OR fewer than 3 measurable probes were obtained (insufficient sample). - Relevant standards: llms.txt convention, Cloudflare markdown-for-agents.
http-status-codes
- Category:
contentReadability - Weight: 6 (scored)
- What it tests: Whether the site honestly returns HTTP 4xx for missing pages instead of returning 200 with a “page not found” body (a “soft 404” that pollutes agent caches with garbage canonical content).
- Returns
passwhen a synthetic probe to a URL designed to not exist on the target site (e.g., a randomized path under a sentinel prefix) returns an HTTP 4xx status. - Returns
failwhen the probe returns a 2xx status AND the body is not detectably an SPA shell (i.e., a soft-404 has been confirmed). - Returns
neutralwhen the probe fails at the network level; OR returns 3xx (indeterminate); OR returns 5xx; OR returns 1xx; OR returns 2xx with an SPA-shell body (the SPA may legitimately handle 404 client-side).
page-size-html
- Category:
contentReadability - Weight: 6 (scored)
- What it tests: Measures how much markdown content each page would deliver to an agent's context window. Implementations sample up to 10 URLs and convert each page's HTML to markdown; per-page pass = converted output ≤ 50,000 characters.
- Returns
passwhen successful samples ≥ 3, ANDpass count * 2 > successful count(clear majority pass), ANDfail count == 0(no oversize pages). - Returns
failwhen there is a failure majority (fail count * 2 > successful count); OR a mixed warn+fail outcome (conservative fail). - Returns
neutralwhen no samples could be collected; OR fewer than 3 successful samples were obtained. - Notes: Scoring interaction: when
rendering-strategyfails, thehtmlPathcoefficient cluster zeros out this check's effective weight (excluding it from score). The check still emits its normal verdict; the cluster effect is applied during scoring, not by suppressing the check itself.
markdown-negotiation
- Category:
contentReadability - Weight: 5 (scored)
- Level gate: L2 → L3. The site advances from Level 2 to Level 3 when this check returns
pass. - What it tests: Whether the site honors
Accept: text/markdowncontent negotiation on its homepage URL — serving HTML to humans and markdown to agents from a single URL (no duplicate-URL strategy). - Returns
passwhen a probe of the origin URL withAccept: text/markdownreturns a 2xx status, AND either the responseContent-Typeincludestext/markdownand the body is not a full HTML document, OR the body is markdown-shaped and not HTML-shaped. - Returns
failwhen the probe returns 2xx with an HTML response; OR returns a non-2xx status in the ranges 100–199, 300–399 (excluding 5xx — see neutral), or 400–499 (excluding 403 and 429 — see neutral). - Returns
neutralwhen the endpoint is unreachable at the network level; OR the redirect was blocked; OR the response was 403 or 429 (treated as WAF interference); OR the response was ≥ 500. - Relevant standards: llms.txt convention, Cloudflare markdown-for-agents.
redirect-behavior
- Category:
contentReadability - Weight: 4 (scored)
- What it tests: Whether the site uses agent-friendly redirect mechanisms (same-domain HTTP 3xx) versus agent-hostile ones (cross-domain redirects, JavaScript-only redirects). Implementations sample up to 5 URLs.
- Returns
passwhen successful samples ≥ 3 AND no failed samples (where each per-URL pass means the URL did not redirect, or redirected within the same eTLD+1). - Returns
failwhen successful samples ≥ 3 AND at least one failed sample (cross-eTLD redirect or JavaScript redirect detected). - Returns
neutralwhen no samples could be collected; OR fewer than 3 successful samples were obtained.
agents-md-detection
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Whether the site publishes an
/AGENTS.mdfile (a coding-agent convention; presence alone is the signal). - Returns
passwhenGET /AGENTS.mdreturns a 2xx status with aContent-Typeoftext/markdown,text/plain, ortext/x-markdown(charset suffix stripped), AND the body is non-empty, AND the body does not begin with HTML markers, AND the first line does not contain “page not found”, “404”, or “not found”, AND the body is at least 50 bytes. - Returns
failwhen the endpoint is reachable but any of the above predicates fails. - Returns
neutralwhen the endpoint is unreachable at the network level. - Notes: This check follows a community convention without a published RFC;
specUrlsis typically empty.
cache-header-hygiene
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Whether the homepage emits at least one of the standard cache-validation response headers (
Cache-Control,ETag,Last-Modified), enabling efficient agent re-fetching per RFC 7234. - Returns
passwhen at least one ofCache-Control,ETag, orLast-Modifiedis present in the homepage response headers (case-insensitive lookup). - Returns
failwhen the homepage headers are non-empty but none of the three primary headers are present. - Returns
neutralwhen the homepage header map is empty (defensively handled; in practice unreachable).
llms-txt-exists
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Whether an
llms.txtfile is reachable, probing/llms.txtfirst then/docs/llms.txt. Verdict is informational and structural details are carried in the per-checkdetailspayload (specifically adiscoveredFileslisting). - Returns
pass: This check has no pass case. Status is alwaysneutral; verdict shape is carried indetails. - Returns
fail: This check has no fail case. Status is alwaysneutral; verdict shape is carried indetails. - Returns
neutralalways. Thedetailspayload distinguishes the discovered-vs-not-discovered cases. Downstreamllms-txt-*checks read this check's cached body via dependency. - Relevant standards: llms.txt convention.
llms-txt-has-optional-section
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Whether the discovered
llms.txtbody contains an## OptionalH2 section. - Returns
passwhen a cachedllms.txtbody is present AND the body matches the regex/^##\s+Optional\b/im(an H2 line whose first word starts with “Optional”). - Returns
failwhen a body is present but no## OptionalH2 section is found. - Returns
neutralwhen nollms.txtbody was cached upstream (dependency-skip fromllms-txt-exists). - Relevant standards: llms.txt convention.
llms-txt-size
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Reports the byte size of the discovered
llms.txtagainst context-window-fit tiers. Verdict is informational and tier classification is carried indetails.tier. - Returns
pass: This check has no pass case. Status is alwaysneutral. - Returns
fail: This check has no fail case. Status is alwaysneutral. - Returns
neutralalways. Thedetails.tierfield carries the verdict:"pass"when ≤ 50,000 bytes,"warn"when 50,001–100,000 bytes,"fail"when > 100,000 bytes. When no file was discovered upstream, returnsneutralwith notier. - Relevant standards: llms.txt convention.
llms-txt-valid
- Category:
contentReadability - Weight: 0 (informational)
- What it tests: Reports the structural shape of the discovered
llms.txt. Verdict is informational and structural classification is carried indetails.verdict. - Returns
pass: This check has no pass case. Status is alwaysneutral. - Returns
fail: This check has no fail case. Status is alwaysneutral. - Returns
neutralalways. Thedetails.verdictfield carries the structural classification:"structured"when the first non-empty line is an H1 (#) AND the body contains either a blockquote (>) line or an##section heading paired with a[text](https://...)link;"minimal"when only an H1 is present;"unstructured"when the first non-empty line is not an H1;"skipped"when there is no body. When no body was cached upstream, returnsneutralwith noverdict. - Relevant standards: llms.txt convention.
5.4 agentEndpoints
mcp-server-card
- Category:
agentEndpoints - Weight: 10 (scored)
- What it tests: Whether the site advertises a Model Context Protocol (MCP) server card at one of the well-known paths, declaring its MCP endpoint to MCP clients.
- Returns
passwhen any of three candidate paths (/.well-known/mcp/server-card.json,/.well-known/mcp/server-cards.json,/.well-known/mcp.json) returns parseable JSON containing aserverInfo.name(string) ANDtransport.type(string) AND eithertransport.urlortransport.endpoint(string). - Returns
failwhen the first parseable JSON found at any candidate path is invalid (does not satisfy the required-field shape); OR when all three paths returned non-network-failure responses but no parseable JSON was found. - Returns
neutralwhen all three candidate paths failed at the network level. - Relevant standards: MCP server-card charter; the MCP Server Card discovery proposal is in flight (see PR #2127, PR #2525).
oauth-protected-resource
- Category:
agentEndpoints - Weight: 7 (scored)
- What it tests: Whether
/.well-known/oauth-protected-resourcereturns valid OAuth 2.0 Protected Resource Metadata per RFC 9728, identifying which authorization server protects the site's API. - Returns
passwhenGET /.well-known/oauth-protected-resourcereturns a 2xx status with parseable JSON containing a well-formed http(s) URL string in theresourcefield. - Returns
failwhen non-2xx status, unparseable JSON, missingresourcefield, orresourceis not a well-formed URL. - Returns
neutralwhen the endpoint is unreachable at the network level.
oauth-discovery
- Category:
agentEndpoints - Weight: 6 (scored)
- What it tests: Whether the site exposes OAuth Authorization Server Metadata (RFC 8414) at
/.well-known/oauth-authorization-serveror OpenID Connect Discovery metadata at/.well-known/openid-configurationso agents can locate the auth endpoints. - Returns
passwhen either/.well-known/oauth-authorization-serverOR the fallback/.well-known/openid-configurationreturns parseable JSON with: anissuer(https URL),response_types_supported(non-empty array), AND at least one ofauthorization_endpointortoken_endpoint. - Returns
failwhen the primary path returns parseable JSON but the contents are invalid (immediate fail); OR the primary path is unparseable AND the fallback returns parseable but invalid JSON; OR both paths returned non-404 responses but neither yielded valid metadata. - Returns
neutralwhen both paths cleanly returned 404 (no OAuth surface present at the well-known locations — the check opts out of the score denominator for this case). - Notes: This is the only scored check that can return
neutralvia a deliberate “hedge” path (clean-404-on-both-paths). The check thereby excludes itself from the score denominator when no OAuth surface exists at all, rather than penalizing sites that simply have no auth endpoint to advertise.
a2a-agent-card
- Category:
agentEndpoints - Weight: 0 (informational)
- What it tests: Whether the site publishes an A2A (Agent-to-Agent) Agent Card at
/.well-known/agent-card.jsonper the A2A protocol specification. - Returns
passwhenGET /.well-known/agent-card.jsonreturns a 2xx response with parseable JSON containing a non-emptyname, non-emptydescription, non-emptyurl, acapabilitiesobject, and askillsarray. - Returns
failwhen non-2xx status, non-JSON body, root not an object, or any of the required fields missing or invalid. - Returns
neutralwhen the endpoint is unreachable at the network level. - Relevant standards: A2A Protocol Specification, A2A Agent Discovery.
agent-skills
- Category:
agentEndpoints - Weight: 0 (informational)
- What it tests: Whether the site exposes an Agent Skills Discovery index at
/.well-known/agent-skills/index.json(or fallback/.well-known/skills/index.json). - Returns
passwhen the first parseable JSON of the two candidate paths is an object containing a$schema(string) AND askillsarray, AND the$schemamatches the canonical 0.2.0 URL or a recognized semver pattern (^(0\.\d+\.\d+|1\.\d+\.\d+)$), AND every skill entry has adigestmatching/^sha256:[a-f0-9]{64}$/. - Returns
failwhen the root parses to a non-object; OR the$schemaorskillsfield is missing; OR a recognized schema is present but digest validation fails on at least one skill; OR no parseable JSON was found across both paths AND not all paths network-failed. - Returns
neutralwhen all paths failed at the network level; OR a$schemais present but its URL/version is not recognized (forward-compat neutral). - Relevant standards: Agent Skills Discovery RFC, agentskills.io.
api-catalog
- Category:
agentEndpoints - Weight: 0 (informational)
- What it tests: Whether
/.well-known/api-catalog(RFC 9727) returns a valid Linkset (RFC 9264) pointing agents at OpenAPI specs and developer documentation. - Returns
passwhenGET /.well-known/api-catalog(withAccept: application/linkset+json, application/json) returns a 2xx response containing parseable JSON whose root contains alinksetarray, OR whose root itself is an array. - Returns
failwhen non-2xx status, non-JSON body, or no linkset shape is present in the parsed JSON. - Returns
neutralwhen the endpoint is unreachable at the network level.
6. Status
The status enum is exactly three values, normative on the wire:
6.1 Explicit non-goals (v1.0)
- No
errorstatus. v1.0 does not define a fourth status value for runtime failure modes. Implementations that experience a per-check runtime failure (parse error, internal exception, dependency unavailable) MUST map it to one of the three defined statuses:- Use
failwhen the check executed but encountered an internal error that prevented evaluation against pass criteria. - Use
neutralwhen the observation could not be made (e.g., network failure, dependency missing, infrastructure unavailable) — i.e., when the implementation has no basis for asserting either pass or fail.
detailspayload, but no part of the wire status field reveals the error. - Use
- No
skippedstatus. Dependency-skipped checks (those whose hard-declared dependencies did not pass) MUST emitneutralwith a clearmessageindicating the skip reason. - No
warnstatus. Tiered verdicts (e.g.,llms-txt-sizedistinguishing pass/warn/fail) MUST be carried inside the per-checkdetailspayload, not on the status field.
v2.0+ MAY introduce additional status values; until then, the three-value enum is closed.
6.2 How status interacts with scoring
Status is the primary input to the scoring formula defined in §7. In summary:
passof a scored check (weight > 0) contributes its weight to BOTH numerator (passWeight) and denominator (passWeight + failWeight).failof a scored check contributes its weight to denominator only.neutralof any check is excluded from both numerator and denominator.- Informational checks (weight = 0) are excluded from both regardless of status — their statuses are emitted for consumer convenience and gate evaluation but do not alter the score.
7. Scoring
Scoring in v1.0 is fully deterministic: given identical check results and identical cluster verdicts, every conforming scanner produces the same score. Two scans of the same site at the same moment SHOULD produce identical scores (subject to per-scan probe sampling for the small number of checks that sample multiple URLs).
7.1 Top-level score formula
score = round(100 * passWeight / (passWeight + failWeight))
Definitions:
passWeight— sum ofweightvalues across all scored checks (weight > 0) whosestatus === "pass"after coefficient-cluster adjustment.failWeight— sum ofweightvalues across all scored checks whosestatus === "fail"after coefficient-cluster adjustment.score— integer in the range[0, 100].
Exclusion rules:
- Checks with
status === "neutral"MUST be excluded from BOTHpassWeightandfailWeight(i.e., excluded from both numerator and denominator). - Checks with
weight === 0(informational checks) MUST be excluded entirely from scoring regardless of status. - A check whose effective weight has been zeroed by a coefficient cluster MUST be excluded from BOTH numerator and denominator (its post-coefficient weight is
0, so it contributes nothing on either side).
Rounding: half-up to nearest integer (e.g., 0.5 rounds to 1, 49.5 rounds to 50). Implementations MUST use this rounding rule.
Divide-by-zero edge case (NORMATIVE): When passWeight + failWeight === 0 — i.e., every scored check returned neutral, or every scored check had its weight zeroed by a coefficient cluster — implementations MUST emit score = 0. Implementations MUST NOT emit null, an error, or any value other than 0 in this case.
7.2 Per-category score
Each entry in categoryReports[] MUST emit an integer score field computed by the same formula scoped to that category's checks only:
categoryScore = round(100 * categoryPassWeight / (categoryPassWeight + categoryFailWeight))
Where the weights are summed only across checks belonging to that category.
Per-category scores are NOT subject to cluster math. Coefficient adjustments to per-check weights and final-score caps apply ONLY to the top-level score field, not to per-category subscores. Per-category subscores describe category posture in isolation; cluster effects are global signals about overall site shape.
The same divide-by-zero rule applies: if a category has no scored checks contributing pass/fail weight, its score MUST be 0.
7.3 Cluster math
v1.0 defines exactly three clusters, emitted in fixed order as the top-level clusters[] array:
htmlPath— kindcoefficientspaRenderingCap— kindcapnoViablePathCap— kindcap
Conforming scanners MUST emit all three cluster verdicts in this order, regardless of whether any are triggered.
7.3.1 Cluster htmlPath (coefficient)
- Trigger condition:
rendering-strategy.status === "fail". (Note: the trigger is specificallyfail, NOT!= "pass". Aneutralrendering verdict — for example, when headless rendering is unavailable — does NOT trigger this cluster.) - Effect when triggered: before scoring, multiply the effective weight of each check listed in
appliesTobycoefficient. Withcoefficient: 0,page-size-html's effective weight becomes0and the check is excluded from bothpassWeightandfailWeight. Coefficient effects are pre-aggregation, applied to per-check weights before the score formula runs. - Effect when not triggered:
coefficient: 1is a no-op; weights are unchanged.
7.3.2 Cluster spaRenderingCap (cap)
- Trigger condition:
rendering-strategy.status === "fail". (Same trigger ashtmlPath; the two clusters fire together when rendering fails.) - Severity selection (NORMATIVE):
capScore: 39(severe) when the rendering-strategy fail was caused by a SPA-shell short-circuit OR the rendered/plain content-length ratio is>= 2.0.capScore: 59(moderate) otherwise (i.e., the ratio is in(1.20, 2.0)after rendering-strategy classified the page as fail).
- Effect when triggered: the FINAL top-level
scoreis clamped tomin(score, capScore). - When not triggered: the
capScorefield MUST be omitted from the cluster object.
7.3.3 Cluster noViablePathCap (cap)
- Trigger condition (NORMATIVE): ALL FOUR of the following checks have
status !== "pass"AND at least TWO of them havestatus === "fail"(i.e., explicit fail, not merely neutral):llms-txt-existsrendering-strategymarkdown-url-supportmarkdown-negotiation
- Effect when triggered: the FINAL top-level
scoreis clamped tomin(score, 39). - Rationale for the “≥ 2 explicit fails” threshold: several of these inputs can return
neutralfor benign reasons (probe-tooling unavailability, network failure, sample-size insufficiency). Requiring at least twofailoutcomes guards against false-positive triggering when a scanner cannot make reliable observations. - When not triggered: the
capScorefield MUST be omitted from the cluster object.
7.3.4 Composition order
The composition order of cluster effects on the top-level score is normative:
1. Apply coefficient clusters to per-check weights (mutate the effective weight pool). 2. Compute base score per the §7.1 formula using adjusted weights. 3. Apply cap clusters as score = min(baseScore, lowestTriggeredCap).
Implementations that compute caps before coefficients, or interleave the operations differently, will produce non-conforming scores.
7.3.5 Cap stacking
When multiple cap clusters trigger simultaneously, the LOWEST capScore wins. Caps do NOT compound (they do not multiply, do not subtract). If both spaRenderingCap (cap 39 or 59) and noViablePathCap (cap 39) trigger, the final score is min(baseScore, 39).
7.3.6 Per-category scoring is exempt from cluster math
Per-category score values in categoryReports[].score MUST NOT have coefficients applied to them and MUST NOT be capped. Cluster math affects only the top-level score field.
7.4 Algorithm summary
A conforming implementation computes the top-level score as follows:
- Run all 25 checks; collect results with
(id, category, status, weight). - Compute the three cluster verdicts.
- For each check whose
idappears in any coefficient cluster'sappliesTo, multiply its effective weight by that cluster'scoefficient. - Compute
baseScoreusing adjusted weights. - If any cap cluster has
triggered === true, setfinalScore = min(baseScore, lowestTriggeredCapScore). OtherwisefinalScore = baseScore. - For each category, compute
categoryReports[].scorewith the ORIGINAL (un-adjusted) weights, scoped to that category's checks. NO cluster math applies here. - Emit
score = finalScoreat the top level and per-category subscores incategoryReports[].score.
A complete worked example with concrete numbers appears in Annex B.
8. Levels
8.1 Three levels
v1.0 defines exactly three levels. Conforming scanners MUST NOT emit level values outside this enum.
level is an INTEGER on the wire. It MUST NOT be emitted as a string.
8.2 Gate logic
Level is gate-driven, not score-driven. Each level transition is governed by a single named check whose status MUST equal "pass" for the transition to occur.
- Every site starts at Level 1 by default. There is no L1 entry gate.
- A site advances to L2 if and only if
content-signals.status === "pass". - A site advances to L3 if and only if
markdown-negotiation.status === "pass". (L3 also requires that the L1→L2 gate is met, by transitivity: the level system is monotonic; L3 sites are also L2 sites.) - Gate semantics are AND across
requirementsif a future spec version introduces multi-check gates. v1.0 uses single-check gates throughout.
8.3 nextLevel emission
When emitted, nextLevel is an object describing the target of the next available level transition.
Object shape:
{
"level": <integer>,
"levelName": <string>,
"requirements": [<check-id>, ...]
}level— INTEGER, enum[2, 3]. The level the site would advance TO. (Sites at L1 advance TO L2; sites at L2 advance TO L3. There is nolevel: 1here because there is nothing prior to advance from.)levelName— STRING, enum["AI-Aware", "Agent-Optimized"], corresponding 1:1 tolevel.requirements— non-empty ARRAY of kebab-case check IDs whosestatusMUST flip to"pass"for the site to advance. v1.0 always emits a single-element array per transition (single-check gates).
Conformance rule: When the site is at the highest defined level (v1.0: level === 3), conforming scanners MUST OMIT the nextLevel field entirely. Scanners MUST NOT emit nextLevel: null, MUST NOT emit nextLevel: {}, and MUST NOT emit a stub or sentinel value.
The schema reflects this requirement: nextLevel appears in properties (so its shape is constrained when present) but NOT in the top-level required array (so its absence is valid).
8.4 Score and level are independent
Score and level are decoupled observable signals. The two carry distinct, complementary information:
- A site MAY have
score === 0and still reachlevel === 2ifcontent-signals.status === "pass"(an informational, weight-0check whose pass status is the L1→L2 gate). Concretely: a site that publishes onlycontent-signalsand fails every scored check would score0at level2. - A site MAY have
score === 99and still remain atlevel === 1ifcontent-signals.status !== "pass".
Consumers of scan output SHOULD report both score and level together; treating either as a proxy for the other distorts the spec's intent.
9. Profiles
9.1 Profile concept
A Profile is a JSON document declaring which checks are active and their per-check weights for a given vertical, use case, or scanner deployment. A profile binds the abstract Specification to a concrete measurement workload.
v1.0 ships exactly one profile: b2b-saas. v1.0 does NOT define a custom-profile mechanism; all v1.0-conforming scanners MUST emit profile: "b2b-saas" on the wire.
9.2 The b2b-saas profile
Description: Default profile for v1.0 of the Agent-Adoption Specification. Applies to commercial software-as-a-service businesses targeting business customers. All 25 spec checks are applicable; the weight assigned to each check determines its contribution to the score, and checks with weight: 0 are informational.
Per-check weights in b2b-saas are normative for this profile and MUST match the catalog in §5.
9.3 Profile JSON shape (illustrative for v1.0)
The structure of profile JSON is illustrative and NOT normatively schema-validated in v1.0 (a formal profile-v1.schema.json is deferred to v1.1+ or v2.0).
Top-level fields:
checks is a keyed object, not an array, for direct lookup by check ID:
"checks": {
"robots-txt-exists": { "category": "discoverability", "weight": 7 },
"sitemap-exists": { "category": "discoverability", "weight": 4 }
}Each per-check entry has:
Casing convention notice: Profile JSON uses snake_case for top-level field names (profile_id, profile_name, spec_version). This is INTENTIONAL and distinct from the camelCase wire output; profile JSON is human-authored configuration metadata, while wire output is a machine-emitted scan result. The two artifacts use different conventions.
9.4 v1.0 profile conformance
- A v1.0-conforming scanner MUST emit
profile: "b2b-saas"as a top-level wire field. - A v1.0-conforming scanner MUST apply the per-check weights as defined in the
b2b-saasprofile (which match the catalog in §5). - A v1.0-conforming scanner MUST NOT advertise a custom or vendor-specific profile identifier in the
profilefield for v1.0 conformance. (Custom profiles are deferred to a future minor or major version.)
10. Output format
The wire format is JSON. Conforming scanners emit a single self-contained object per scan. All object keys and category enum values use camelCase; check IDs use kebab-case; timestamps use ISO 8601 with Z suffix; level, score, weight, all duration fields, and all category counts are integers.
10.1 Top-level shape (13 fields)
All 13 fields are emitted on every scan response in normal mode, with one exception: nextLevel is omitted at L3. Conforming scanners MUST emit each REQUIRED field on every scan and MUST NOT emit any other top-level fields (the schema is closed at the root via additionalProperties: false).
A complete example appears in Annex B.
10.2 Per-check object shape (10 required fields + 1 conditional)
Every check appears inside categoryReports[].checks[]. The 10 required fields MUST be present on every check; details is conditional.
Notes:
- The
detailsobject, when present, MAY contain akindfield whose value SHOULD equal the enclosing check'sid(RECOMMENDED for consumer-side discrimination). This is not required by the schema. details.bodyis FORBIDDEN. Conforming scanners MUST NOT include abodykey insidedetails. Raw HTTP response bodies do not belong on the wire.- Consumers SHOULD treat
detailsas optional and access nested fields via optional chaining or equivalent guarded reads.
10.3 Per-categoryReport object shape (7 fields)
Each entry in the top-level categoryReports array conforms to:
The four categoryReports entries MUST appear in the canonical order: discoverability, then accessControl, then contentReadability, then agentEndpoints. The schema enforces this via prefixItems with a fixed category const per position.
10.4 nextLevel object shape
When present:
OMITTED entirely at L3 (NOT nullified, NOT empty-objected).
10.5 meta object shape
Always present at top level. 4 fields:
counters shape. The value is a nested object: {<counterGroup>: {<metricName>: integer}}. Both levels of keys are open (additionalProperties: true) so implementations can add or rename groups without a schema break, provided each leaf value remains an integer ≥ 0.
The schema does NOT enumerate counter group names or metric names. Consumers MUST treat counters as opaque for conformance purposes.
10.6 clusters array
A top-level array of length exactly 3. Cluster verdicts appear in this canonical order: htmlPath, spaRenderingCap, noViablePathCap. The schema enforces both the length and the per-position name const via prefixItems. Conforming scanners MUST emit all three cluster verdicts on every scan, including non-triggered ones.
Why top-level: the cluster math is part of the normative score-computation discipline. Coefficient clusters multiply per-check weights; cap clusters clamp the final score. A consumer cannot reproduce or audit the published score without the cluster verdicts. Anything that influences scoring methodology is core spec, not vendor-extension data — so the wire surface is at the root, not buried in a vendor namespace.
Each cluster verdict object:
10.7 Vendor extension surfaces
v1.0 does NOT define a top-level vendor namespace. The wire surface is closed at the root — the schema declares additionalProperties: false on the root object, so implementations MUST NOT emit unrecognized top-level fields.
Implementations that need to surface vendor-specific data have two open surfaces:
- Per-check
detailsobject — the schema declaresadditionalProperties: trueondetailsso implementations MAY emit any per-check vendor fields. The single carve-out:details.bodyis FORBIDDEN regardless of mode. Usedetailsfor per-check evidence, debug payloads, or vendor-specific structured data tied to one check. meta.countersobject — open at both levels (additionalProperties: trueoncountersand on each counter group). Use this for telemetry (fetch counts, retry counts, internal subprocess metrics, etc.).
Consumers MUST NOT reject scan output that contains unknown keys inside details or meta.counters, provided the rest of the document validates against the schema.
A future minor version MAY introduce a top-level vendor namespace if a real cross-implementation use case emerges; v1.0 deliberately omits it to keep the wire surface minimal.
11. Conformance
11.1 MUST requirements
A conforming v1.0 scanner MUST:
- Emit all 13 top-level fields per §10.1 (with
nextLevelomitted only whenlevel === 3). - Emit all 25 checks listed in the §5 catalog under their declared categories with their declared weights.
- Use the
statusenum exactly as defined in §6: only"pass","fail","neutral". - Use
levelas an INTEGER (1,2, or3) and emitlevelNameas the corresponding string from the enum in §8.1. - Apply cluster math and the score formula exactly as specified in §7, including the divide-by-zero rule (
score = 0when no scored checks contribute) and the composition order (coefficients → base score → caps). - Emit a top-level
clustersarray with all three cluster verdicts in canonical order, including non-triggered ones. - Use camelCase for all object keys and category enum values on the wire.
- Use INTEGER types for
level,score,weight, all*DurationMsfields, and all category counts (passed,failed,neutral,checksEvaluated,checksSkipped, allmeta.countersleaf values). - Use ISO 8601 timestamps with
Zsuffix forscannedAt. - Emit
specVersionas a dotted-triple semver string identifying the version of the Specification the scanner conforms to (e.g.,"1.0.0"). - Emit
profile: "b2b-saas"as the only valid profile identifier in v1.0.
11.2 MAY allowances
A conforming v1.0 scanner MAY:
- Emit additional fields inside any per-check
detailsobject (additionalProperties: true), EXCEPT fordetails.bodywhich is FORBIDDEN. - Emit additional
meta.counters.*groups and metrics, provided the nested-object shape and integer-leaf constraint are preserved. - Emit additional URLs in any check's
specUrlsarray beyond those listed in the §5 catalog. - Choose its own request headers, retry policy, and timeout values for HTTP probes, unless explicitly constrained in a per-check definition.
- Choose its own URL sampling strategy within the sample-size bounds declared by per-check definitions in §5 (e.g., the choice of which 5 sitemap URLs to probe for
markdown-url-supportis implementation-defined; the requirement that at least 3 measurable probes be obtained is normative). - Set the
details.kindfield equal to the enclosing check'sid(RECOMMENDED for consumer discrimination but not required). - Emit any scanner
nameandversionstrings in thescannerobject; these are implementation-defined.
11.3 MUST NOT requirements
A conforming v1.0 scanner MUST NOT:
- Use snake_case for top-level wire field names.
- Emit
levelas a string (it is always an integer;levelNameis the separate string form). - Use any
statusvalue other than"pass","fail", or"neutral". Specifically,"error"and"skipped"are FORBIDDEN. - Use any
weightvalue outside the closed set{0, 4, 5, 6, 7, 8, 10}. - Add or remove checks from the 25-check registry without a
specVersionbump (additions require at minimum a MINOR bump; removals or breaking semantic changes require a MAJOR bump — see §12). - Emit
details.body. The schema explicitly forbids this property under per-checkdetails. - Emit
nextLevel: nullornextLevel: {}at level 3. The field MUST be OMITTED entirely. - Emit
clustersarray entries out of canonical order, or with fewer/more than 3 entries. - Emit
categoryReportsentries out of canonical order, or with fewer/more than 4 entries.
11.4 Optional debug-mode considerations (informative)
Implementations MAY support debug or diagnostic output modes that add non-normative fields to per-check objects (for example, raw evidence arrays, per-fetch audit fields, or fix-suggestion payloads). These modes are NOT specified in v1.0; implementations choose their own surface area.
For conformance purposes, the v1.0 spec is judged against normal-mode output only. Debug-mode output is OUT OF SCOPE for conformance testing. v2.0+ MAY standardize a debug-mode surface once implementer experience accumulates.
12. Versioning
12.1 Semver discipline
The Specification follows semantic versioning (MAJOR.MINOR.PATCH):
- MAJOR (
vN.0.0): Breaking wire-format changes. Examples: removing checks from the registry, changing the meaning of an existing enum value, changing the structure or semantics of a top-level field, dropping aMUST-level requirement. - MINOR (
v1.x.0): Non-breaking additions. Examples: adding new checks, adding new clusters, introducing a new optional top-level field, introducing a vendor-extension namespace if implementer demand emerges. - PATCH (
v1.0.x): Editorial fixes and clarifications without behavior change. Examples: typo corrections, tightening ambiguous phrasing, recalibrating threshold constants when the new constant is the empirically-correct interpretation of what was already meant.
12.2 Tag immutability
Each tagged release of the Specification is permanent and accessible at its version-suffixed canonical URL. v1.0 will remain accessible indefinitely, even after v2.0 (or later) is published.
A conforming implementation declares which version it implements via the specVersion field on the wire. An implementation that conforms to v1.0 will continue to be conformant against the v1.0 spec indefinitely; later spec versions do not retroactively invalidate earlier conforming implementations.
12.3 Erratum mechanism
Editorial bugs in published Specification text — typos, ambiguous phrasing, broken links, missing clarifications — MAY be corrected via a PATCH release (e.g., v1.0.1, v1.0.2).
A patched release MUST NOT change normative behavior. It MAY:
- Fix a typo.
- Tighten an ambiguous sentence to match the originally-intended interpretation.
- Add a clarifying example or note.
- Correct a calibration constant when the new value is demonstrably the value the spec already meant (e.g., a number that was wrong in the prose but correct in the schema).
A patched release MUST NOT:
- Change the trigger conditions of any check.
- Change the value or interpretation of any enum.
- Add or remove fields from the wire format.
- Change the score formula or composition order.
Behavioral changes belong in a MINOR (additive) or MAJOR (breaking) release.
12.4 The specVersion field
Conforming scanners MUST emit specVersion as the dotted-triple semver string identifying the Specification version they conform to. Examples: "1.0.0", "1.0.1", "1.1.0". The pattern is ^\d+\.\d+\.\d+$ (no pre-release suffixes, no build metadata).
A scanner that conforms to v1.0 emits "1.0.0". A scanner that conforms to a future patched v1.0.1 emits "1.0.1". A scanner that conforms to a future v1.1.0 (with additional checks) emits "1.1.0". Implementations MUST NOT emit a specVersion they do not actually conform to.
Annex A — Defensive framing
A.1 What the Specification measures (positive framing)
The Specification measures observable site state: the presence and well-formedness of standardized agent-readiness signals an automated HTTP client would expect to find. Specifically:
- All 25 checks are determined deterministically from HTTP-level probes (and, in the case of
rendering-strategy, an optional headless render). - All status verdicts (
pass,fail,neutral), all scores, and all level assignments are direct consequences of the observed signals. - Two scans of the same site at the same moment, run by conforming scanners with comparable network access, SHOULD produce identical scores, levels, and per-check verdicts (modulo per-scan probe sampling for the small number of checks that sample multiple URLs).
The output is a measurement contract: it answers the question “are the standardized agent-readiness signals an agent would look for present and well-formed on this site, right now?”
A.2 What the Specification does NOT measure (anti-claims)
The Specification deliberately does NOT measure, and conforming output MUST NOT be interpreted as a proxy for, any of the following:
- Predicted volume of AI traffic the site will receive.
- Conversion or revenue impact of agent-readiness investments.
- Frequency or quality of brand mentions in agent outputs (LLM citations, agent recommendations, etc.).
- Search-result rank in any search engine, crawler, or retrieval system.
- Whether any specific agent (existing or future) will visit, cite, parse, or correctly interpret the site.
- “Future-proofing” against unknown future agent behaviors or unspecified protocols.
A high score does NOT guarantee agent visibility. A low score does NOT predict agent invisibility (agents tolerate missing signals via heuristics).
A.3 Recommended consumption rules
Consumers of scan output SHOULD:
- Use the score and level as ONE input to agent-readiness decisions, not the SOLE input.
- Combine scan output with site-specific business context (audience, content strategy, regulatory posture, competitive landscape).
- Watch for spurious correlations between score and downstream metrics; the Specification is descriptive and makes no causal claim.
- NOT chase the score at the expense of legitimate site design, accessibility, performance, or user experience.
- Treat per-category scores and individual check verdicts as more actionable signals than the aggregate
score; the aggregate is a summary, not an instruction.
Annex B — Worked example
This annex walks a hypothetical scan against example.com end-to-end, demonstrating the score formula, gate logic, and cluster verdicts. The numbers are self-consistent: the score, level, and per-category subscores in the final JSON each equal what the formulas in §7 and §8 yield from the per-check inputs.
The example uses no specific implementation; the scanner object identifies a generic "Example Conforming Scanner". Per-check details payloads are minimized to a single kind field for brevity (real-world scans typically emit richer details).
B.1 Per-check status assignment
The fictional example.com scan classifies each of the 25 checks as follows. (Order: canonical category, then weight descending, then ID alphabetic.)
Bookkeeping: 4 scored pass (sum 30); 4 scored fail (sum 20); 4 scored neutral (sum 30, excluded); 13 informational (excluded). 12 + 13 = 25 ✓; sum of all scored weights = 80 ✓.
B.2 Score arithmetic
Top-level score:
passWeight = 7 + 7 + 10 + 6 = 30 failWeight = 4 + 5 + 4 + 7 = 20 weightedTotal = 30 + 20 = 50 score = round(100 * 30 / 50) = round(60.0) = 60
Per-category subscores (each computed via the same formula scoped to that category's checks; no cluster math applies):
Per-category counts:
B.3 Level calculation
Therefore level = 2, levelName = "AI-Aware", and nextLevel is emitted with the L3 advancement requirement (requirements: ["markdown-negotiation"]).
B.4 Cluster verdicts
All three clusters are NOT triggered:
htmlPathdoes not trigger becauserendering-strategy.status === "pass"(its trigger is=== "fail"). Emitted withcoefficient: 1(no effect).spaRenderingCapdoes not trigger (same reason).capScoreis omitted.noViablePathCapdoes not trigger becauserendering-strategy.status === "pass"(one of the four required-non-pass viability inputs is passing).capScoreis omitted.
Since no cap is triggered, the final score equals the base score (60).
B.5 Complete example JSON
An abbreviated form of the complete output document for this scan (one check per category shown for brevity; full document validates against schemas/output-v1.schema.json):
{
"specVersion": "1.0.0",
"profile": "b2b-saas",
"scanner": { "name": "Example Conforming Scanner", "version": "1.0.0" },
"domain": "example.com",
"finalUrl": "https://example.com/",
"scannedAt": "2026-04-28T12:00:00.000Z",
"score": 60,
"level": 2,
"levelName": "AI-Aware",
"nextLevel": {
"level": 3,
"levelName": "Agent-Optimized",
"requirements": ["markdown-negotiation"]
},
"categoryReports": [
{
"category": "discoverability",
"description": "Can agents find the signals they need to navigate your site?",
"score": 64,
"passed": 1,
"failed": 1,
"neutral": 1,
"checks": [
{
"id": "robots-txt-exists",
"category": "discoverability",
"status": "pass",
"scored": true,
"weight": 7,
"message": "robots.txt served at /robots.txt",
"description": "robots.txt is the first file crawlers and agents check for access rules; silence defaults to blanket-allow. Per RFC 9309.",
"durationMs": 50,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc9309"],
"dependsOn": [],
"details": { "kind": "robots-txt-exists" }
},
{
"id": "sitemap-exists",
"category": "discoverability",
"status": "fail",
"scored": true,
"weight": 4,
"message": "No sitemap found at /sitemap.xml or via robots.txt Sitemap directive",
"description": "An XML sitemap is the route map agents use to find your pages.",
"durationMs": 120,
"specUrls": ["https://www.sitemaps.org/protocol.html"],
"dependsOn": [],
"details": { "kind": "sitemap-exists" }
},
{
"id": "link-headers",
"category": "discoverability",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "Homepage returned no Link header — informational only",
"description": "Link: response headers expose related resources before HTML is parsed. Per RFC 8288.",
"durationMs": 0,
"specUrls": ["https://datatracker.ietf.org/doc/html/rfc8288"],
"dependsOn": [],
"details": { "kind": "link-headers" }
}
]
},
{
"category": "accessControl",
"description": "Are you telling AI systems what they may and may not do with your content?",
"score": 100,
"passed": 3,
"failed": 0,
"neutral": 1,
"checks": [
{
"id": "ai-bot-rules",
"category": "accessControl",
"status": "pass",
"scored": true,
"weight": 7,
"message": "Per-bot rules detected for 3 AI agents",
"description": "Per-bot robots.txt rules or AIPREF/Content-Signal directives declare who may train on or cite your content.",
"durationMs": 5,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc9309"],
"dependsOn": ["robots-txt-exists"],
"details": { "kind": "ai-bot-rules" }
},
{
"id": "content-signals",
"category": "accessControl",
"status": "pass",
"scored": false,
"weight": 0,
"message": "AIPREF Content-Signal directives present",
"description": "AIPREF / Content-Signal directives declare what AI systems may do post-fetch. Informational; gate for level 2.",
"durationMs": 5,
"specUrls": [
"https://datatracker.ietf.org/doc/draft-ietf-aipref-vocab/"
],
"dependsOn": ["robots-txt-exists"],
"details": { "kind": "content-signals" }
},
{
"id": "web-bot-auth",
"category": "accessControl",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "Web Bot Auth directory not published — informational only",
"description": "Operators of AI crawlers MAY publish a signing-key directory at /.well-known/http-message-signatures-directory.",
"durationMs": 90,
"specUrls": [
"https://datatracker.ietf.org/doc/draft-meunier-web-bot-auth-architecture/"
],
"dependsOn": [],
"details": { "kind": "web-bot-auth" }
},
{
"id": "robots-allow-all",
"category": "accessControl",
"status": "pass",
"scored": false,
"weight": 0,
"message": "robots.txt declares wildcard User-agent with explicit Allow: /",
"description": "A blanket-allow posture declares every crawler is welcome. Informational.",
"durationMs": 1,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc9309"],
"dependsOn": ["robots-txt-exists"],
"details": { "kind": "robots-allow-all" }
}
]
},
{
"category": "contentReadability",
"description": "Once an agent is inside, can it actually read what it fetches?",
"score": 64,
"passed": 3,
"failed": 3,
"neutral": 6,
"checks": [
{
"id": "rendering-strategy",
"category": "contentReadability",
"status": "pass",
"scored": true,
"weight": 10,
"message": "Server-side rendering confirmed — agents see content without JavaScript",
"description": "Classifies the site as SSR, hydrated, or SPA — what agents see without running JavaScript.",
"durationMs": 800,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "rendering-strategy" }
},
{
"id": "markdown-url-support",
"category": "contentReadability",
"status": "neutral",
"scored": true,
"weight": 8,
"message": "Sample inconclusive — could not classify .md twin support",
"description": "A .md twin alongside each HTML page gives agents an agent-readable fetch path.",
"durationMs": 400,
"specUrls": ["https://llmstxt.org/"],
"dependsOn": [],
"details": { "kind": "markdown-url-support" }
},
{
"id": "http-status-codes",
"category": "contentReadability",
"status": "pass",
"scored": true,
"weight": 6,
"message": "Correct HTTP 404 returned for non-existent path",
"description": "Soft-404s make agents cache garbage as canonical content. An honest 4xx tells agents the URL is dead.",
"durationMs": 80,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "http-status-codes" }
},
{
"id": "page-size-html",
"category": "contentReadability",
"status": "neutral",
"scored": true,
"weight": 6,
"message": "Sample size insufficient — informational verdict only",
"description": "Measures how much markdown each page feeds into an agent's context window.",
"durationMs": 300,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "page-size-html" }
},
{
"id": "markdown-negotiation",
"category": "contentReadability",
"status": "fail",
"scored": true,
"weight": 5,
"message": "Server ignored Accept: text/markdown — returned HTML instead",
"description": "Accept: text/markdown negotiation serves HTML to humans and markdown to agents from one URL.",
"durationMs": 100,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "markdown-negotiation" }
},
{
"id": "redirect-behavior",
"category": "contentReadability",
"status": "fail",
"scored": true,
"weight": 4,
"message": "Sampled URLs use JavaScript redirects — opaque to agents without JS",
"description": "Same-domain HTTP 3xx redirects work for agents; JavaScript redirects break agents without JS.",
"durationMs": 250,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "redirect-behavior" }
},
{
"id": "llms-txt-exists",
"category": "contentReadability",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "No llms.txt found — informational only",
"description": "An llms.txt file gives agents a curated entry point into your docs. Per llmstxt.org.",
"durationMs": 100,
"specUrls": ["https://llmstxt.org/"],
"dependsOn": [],
"details": { "kind": "llms-txt-exists" }
},
{
"id": "llms-txt-valid",
"category": "contentReadability",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "Cannot evaluate without llms.txt body",
"description": "A well-formed llms.txt parses cleanly; a malformed one is skipped silently.",
"durationMs": 0,
"specUrls": ["https://llmstxt.org/"],
"dependsOn": ["llms-txt-exists"],
"details": { "kind": "llms-txt-valid" }
},
{
"id": "llms-txt-size",
"category": "contentReadability",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "Cannot evaluate without llms.txt body",
"description": "llms.txt must fit in an agent's context window alongside the user's question.",
"durationMs": 0,
"specUrls": ["https://llmstxt.org/"],
"dependsOn": ["llms-txt-exists"],
"details": { "kind": "llms-txt-size" }
},
{
"id": "llms-txt-has-optional-section",
"category": "contentReadability",
"status": "neutral",
"scored": false,
"weight": 0,
"message": "Cannot evaluate without llms.txt body",
"description": "Reports the shape of your llms.txt — Optional section, H2 count, link count.",
"durationMs": 0,
"specUrls": ["https://llmstxt.org/"],
"dependsOn": ["llms-txt-exists"],
"details": { "kind": "llms-txt-has-optional-section" }
},
{
"id": "agents-md-detection",
"category": "contentReadability",
"status": "fail",
"scored": false,
"weight": 0,
"message": "AGENTS.md not found at /AGENTS.md — informational only",
"description": "AGENTS.md is a coding-agent convention; presence-only check. Informational.",
"durationMs": 70,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "agents-md-detection" }
},
{
"id": "cache-header-hygiene",
"category": "contentReadability",
"status": "pass",
"scored": false,
"weight": 0,
"message": "Cache-Control header observed on homepage response",
"description": "Cache-Control, ETag, and Last-Modified let agents re-fetch only what changed. Informational.",
"durationMs": 0,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc7234"],
"dependsOn": [],
"details": { "kind": "cache-header-hygiene" }
}
]
},
{
"category": "agentEndpoints",
"description": "Do you expose what agents need to call you programmatically?",
"score": 0,
"passed": 0,
"failed": 4,
"neutral": 2,
"checks": [
{
"id": "mcp-server-card",
"category": "agentEndpoints",
"status": "neutral",
"scored": true,
"weight": 10,
"message": "Detection inconclusive — known well-known paths returned ambiguous shapes",
"description": "An MCP Server Card advertises your Model Context Protocol endpoint to MCP clients.",
"durationMs": 250,
"specUrls": [],
"dependsOn": [],
"details": { "kind": "mcp-server-card" }
},
{
"id": "oauth-protected-resource",
"category": "agentEndpoints",
"status": "fail",
"scored": true,
"weight": 7,
"message": "No OAuth Protected Resource metadata at /.well-known/oauth-protected-resource",
"description": "Protected Resource metadata identifies which authorization server protects your API. Per RFC 9728.",
"durationMs": 90,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc9728"],
"dependsOn": [],
"details": { "kind": "oauth-protected-resource" }
},
{
"id": "oauth-discovery",
"category": "agentEndpoints",
"status": "neutral",
"scored": true,
"weight": 6,
"message": "No OAuth surface detected — clean 404 on both well-known paths (hedge)",
"description": "OAuth discovery metadata at /.well-known/oauth-authorization-server lets agents locate auth endpoints. Per RFC 8414.",
"durationMs": 180,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc8414"],
"dependsOn": [],
"details": { "kind": "oauth-discovery" }
},
{
"id": "api-catalog",
"category": "agentEndpoints",
"status": "fail",
"scored": false,
"weight": 0,
"message": "No /.well-known/api-catalog published — informational only",
"description": "A /.well-known/api-catalog (RFC 9727) points agents at your OpenAPI specs and developer docs.",
"durationMs": 90,
"specUrls": ["https://www.rfc-editor.org/rfc/rfc9727"],
"dependsOn": [],
"details": { "kind": "api-catalog" }
},
{
"id": "a2a-agent-card",
"category": "agentEndpoints",
"status": "fail",
"scored": false,
"weight": 0,
"message": "No A2A Agent Card at /.well-known/agent-card.json — informational only",
"description": "An A2A Agent Card describes your service to other agents.",
"durationMs": 90,
"specUrls": ["https://a2a-protocol.org/latest/specification/"],
"dependsOn": [],
"details": { "kind": "a2a-agent-card" }
},
{
"id": "agent-skills",
"category": "agentEndpoints",
"status": "fail",
"scored": false,
"weight": 0,
"message": "No Agent Skills index found at /.well-known/agent-skills/ — informational only",
"description": "An Agent Skills index exposes your capabilities as discrete skills.",
"durationMs": 180,
"specUrls": ["https://agentskills.io/"],
"dependsOn": [],
"details": { "kind": "agent-skills" }
}
]
}
],
"meta": {
"checksEvaluated": 25,
"checksSkipped": 0,
"scanDurationMs": 5000,
"counters": {
"fetches": {
"total": 20,
"success": 12,
"wafBlocked": 0,
"notFound": 8,
"failed": 0
}
}
},
"clusters": [
{
"name": "htmlPath",
"kind": "coefficient",
"triggered": false,
"coefficient": 1,
"appliesTo": ["page-size-html"],
"message": "rendering-strategy passes — page-size-html weight unscaled"
},
{
"name": "spaRenderingCap",
"kind": "cap",
"triggered": false,
"message": "No SPA rendering cap applied"
},
{
"name": "noViablePathCap",
"kind": "cap",
"triggered": false,
"message": "At least one viable agent-content path available"
}
]
}Annex C — Implementation notes
(Informative, non-normative.)
This annex collects guidance for scanner implementers. Nothing in this annex is normative; conformance is judged against §11.
C.1 HTTP probe conventions
- Implementations SHOULD use a unique
User-Agentstring identifying the scanner. This allows site operators to identify scanner traffic in their logs and apply appropriate policies. - Implementations SHOULD respect
robots.txtfor non-well-known paths during catalog probes. Probes of/.well-known/*paths androbots.txtitself need not be gated byrobots.txt(those paths are by convention publicly probable; readingrobots.txtis the prerequisite to respecting it). - Per-request timeouts and retry policy are implementation-defined. Implementations SHOULD apply reasonable timeouts so that a slow check does not block the entire scan indefinitely.
- Implementations SHOULD cap pre-parse response body sizes at a reasonable threshold (a 2 MB cap is a reasonable default for
sitemap-exists's XML parsing; other checks may apply different caps appropriate to their content type). - Implementations SHOULD apply per-host rate-limit politeness (a small minimum gap between successive requests to the same origin) to avoid triggering site-side rate limiting.
C.2 Headless rendering
- The
rendering-strategycheck requires comparing plain HTML to a JavaScript-rendered version of the homepage. Implementations MAY use any headless browser tooling for this purpose. - The neutral-when-unavailable path on
rendering-strategy(returningneutralwhen headless rendering is unavailable AND the plain body is not detected as an SPA shell) exists specifically to accommodate environments without rendering capability. Implementations without rendering MUST NOT emitfailin that case; they MUST emitneutral. - When
rendering-strategyreturnsneutral, thehtmlPathandspaRenderingCapclusters MUST NOT trigger (their trigger conditions referencerendering-strategy.status === "fail").
C.3 WAF and rate-limit handling
- For
markdown-negotiation, HTTP403and429responses are NORMATIVELY mapped toneutral(treated as WAF or rate-limit interference rather than a genuine fail signal). - For other checks where similar interference may occur, implementations SHOULD prefer
neutraloverfailwhen the observation is plausibly tainted by access-control or rate-limit responses outside the site's normative agent-readiness posture. - Implementations MAY surface the underlying cause (e.g., specific HTTP status, network timeout) inside
detailsfor debugging purposes, but the wirestatusfield MUST reflect only one of the three enum values.
C.4 Sampling
- Several checks sample multiple URLs from the sitemap or homepage rather than probing a single endpoint. v1.0 catalog entries that sample include
markdown-url-support(up to 5 URLs),redirect-behavior(up to 5 URLs), andpage-size-html(up to 10 URLs). - The minimum-sample-size thresholds for pass/fail evaluation declared in §5 are NORMATIVE (e.g.,
markdown-url-supportrequires at least 3 measurable probes before emitting pass/fail; insufficient samples emitneutral). - The selection strategy WITHIN those sample-size bounds is implementation-defined (e.g., random sampling, deterministic by sitemap order, prioritizing recently-modified URLs). Implementations SHOULD document their sampling strategy if reproducibility across runs is a requirement.
C.5 Per-check details shape
- The
detailspayload for each check is implementation-defined within the per-check semantic. The Specification does NOT prescribe an exhaustivedetailsschema for any check. - Consumers MUST parse
detailsdefensively, treating any field other thankindas potentially absent or implementation-specific. - Setting
details.kindequal to the enclosing check'sidis RECOMMENDED for consumer-side discrimination of the typed payload. details.bodyis FORBIDDEN regardless of mode. Implementations needing to surface raw response bodies for diagnostics MUST do so out-of-band (e.g., in side-channel logs or debug-mode artifacts) — never on the v1.0 normal-mode wire.
C.6 Per-host courtesy
- Implementations SHOULD avoid excessive concurrency against a single origin. A single scan typically issues fewer than 50 fetches against the target origin; with appropriate per-host rate limiting this is well within reasonable courtesy.
- Implementations SHOULD honor
Retry-Afterresponse headers on429and503responses where retry is attempted at all.
C.7 Mapping runtime errors to status
When an unexpected error occurs during a check (uncaught exception, malformed response, parse failure):
- If the check executed enough to evaluate against pass criteria but the result is unusable, implementations SHOULD emit
failwith amessagedescribing the runtime issue. - If the observation could not be made at all (network failure, dependency unavailable, infrastructure missing), implementations SHOULD emit
neutralwith amessagedescribing why no observation was made. - Either way, the status field MUST be one of the three enum values; v1.0 has no
errorstatus.
Citation
When citing this specification in research, implementations, or derived work:
Agent-Adoption Specification, Version 1.0. Respectarium, 2026-04-26.
Available at: https://respectarium.com/spec/agent-adoption/v1
Source: https://github.com/respectarium/agent-adoption-spec/releases/tag/v1.0.0
This specification is licensed under the Creative Commons Attribution 4.0 International License (CC-BY 4.0). You may copy, distribute, modify, and build upon this work for any purpose, including commercial, provided you give appropriate credit to Respectarium.
Suggested attribution:
Source: Agent-Adoption Specification by Respectarium — https://respectarium.com/spec/agent-adoption/v1
Agent-Adoption Specification, Version 1.0.0. Published 2026-04-26 by Respectarium.
Licensed under Creative Commons Attribution 4.0 International (CC-BY 4.0). Source repository: github.com/respectarium/agent-adoption-spec.