axp.org.ai — canonical home

The Agent eXperience Protocol

The wire contract an agent-first API publishes so that an autonomous agent can discover it, understand it, and transact with it zero-shot, on first contact — with no human reading docs in the loop.

AXP 0.9.0 draft 7 clauses RFC 2119

Status of this document

AXP is at version 0.9.0, draft. It is not 1.0 and nothing here should be read as a settled standard.

Promotion to 1.0 is gated — by owner ruling of 2026-07-21 — on substantial external pressure-testing and at least two real external binders: independent parties outside this estate that implement the contract and are bound by it. That count is currently zero. Until it is met, every clause below is a proposal that has been pressure-tested internally and published for others to break.

The protocol is authored and published by apis.ax, the standards body. Conformance is judged by api.qa, an independent verifier held outside the write access of the fleet that builds the catalog. The two roles are deliberately not the same party.

AXP is one of our own new standards — written here, in this estate. It is not a Standard in the sense that word is reserved for: the externally governed records of outside bodies. The posture is superset and engage — conform on the shared core of the protocols an agent already speaks, extend deliberately where they stop short, and invite the people who govern them to come and break this one.

Version
0.9.0
Status
draft
$id
https://apis.ax/axp
Type
Spec in https://schema.org.ai
Publisher
apis.ax — the standards body
Verifier
api.qa — independent
Dated
2026-08-23

The $id is the standard's resolver-addressable identity, cited by Clause 6 and by Appendix A as https://apis.ax/axp#wire. It is not the same thing as this document's canonical URL: axp.org.ai is the canonical home, and apis.ax/axp continues to serve the standard as a machine-register surface. Changing the $id would change what other documents cite, and is not something a rehoming decides.

§The Agent eXperience Protocol (AXP)

Publisher: apis.ax (the standards body) · Verifier: api.qa (independent)

This document is the normative text of AXP — the Agent eXperience Protocol: the set of technical surfaces an agent-first API MUST enforce — the wire contract it publishes so that an autonomous agent can discover it, understand it, and transact with it zero-shot, on first contact, with no human reading docs in the loop. apis.ax publishes this standard; api.qa verifies conformance to it from outside the building fleet's write access.

The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as described in RFC 2119.

This specification is itself linked data: its $id (https://apis.ax/axp) is the resolver-addressable identity of the standard, $type: Spec, in the https://schema.org.ai context. Every clause below names (a) the normative requirement, (b) how it is machine-verified by an independent api.qa check or pinned requirement, and (c) why an agent needs it.


§1OpenAPI 3.1 contract

Normative. A conforming API MUST publish a valid OpenAPI 3.1 document describing every operation an agent may call, its parameters, and its typed responses. The document MUST parse and MUST declare at least one operation. The contract SHOULD be discoverable at /openapi.json or linked from the capability card.

Machine-verified by: api.qa check openapi and pinned requirement openapi-valid (surface openapi, must: valid, pinned with versionPrefix: "3.1" and minOperations: 1). api.qa fetches the document, parses it, and confirms it is structurally valid, that its declared spec version begins 3.1 (a Swagger 2.0 or 3.0.x document fails), and that it declares at least one operation (an empty paths object fails).

Why an agent needs it: the OpenAPI 3.1 contract is the machine-readable shape of the API. Without it an agent must guess call signatures; with it the agent binds to the surface deterministically and calls correctly on the first try.

§2llms.txt

Normative. A conforming API MUST serve an llms.txt file at /llms.txt: markdown, with an H1 title and substantive, agent-actionable content that orients a language model to what the surface is and how to use it. It MUST NOT be an HTML page. It SHOULD cross-reference the sibling machine surfaces (agents.json, icp.json, the OpenAPI 3.1 contract).

Machine-verified by: api.qa check llms-txt and pinned requirement llms-present (surface llms.txt). api.qa confirms /llms.txt returns markdown (H1 + substantive body), not markup.

Why an agent needs it: llms.txt is the plain-language front door for a model — the one file a fresh agent reads to learn the surface without scraping a rendered site.

§3machine-legible for every client: three faces, deterministic negotiation

Normative. A conforming API MUST answer machine-legible on its resolver-addressable home and its typed bodies for every machine client, and it MUST select the representation of every dereferenceable address deterministically. Every dereferenceable address serves three faces of the same resource:

  • HTML (text/html) — the page register, for human eyes;
  • JSON (application/json) — the typed body; the JSON face SHOULD be JSON-LD carrying a resolvable context (a top-level $context or @context member whose value dereferences);
  • markdown (text/markdown) — the token-cheap agent register.

Face selection MUST follow this precedence, first match wins (the normative algorithm, including q-values, HEAD, and error behavior, is Appendix A.7):

  1. The address forces. A face address — the sibling address that names a face, canonically the path extension .html, .json, or .md — serves exactly that face, regardless of the Accept header.
  2. The Accept header infers. An Accept header naming a face media type (text/html; application/json or application/ld+json; text/markdown) selects that face.
  3. The client class defaults. When neither the address nor the Accept header selects a face (Accept: */*, absent, or unsatisfiable): a browser — detected via Sec-Fetch-* request headers, never by User-Agent sniffing — receives HTML; a known agent User-Agent (Appendix A.7 token list) receives markdown; everything else, including bare curl, receives JSON.

Every face response MUST advertise its sibling faces via HTTP Link headers with rel="alternate" and a type parameter naming each sibling's media type, so any client at any face discovers the other two without guessing.

The machine-legibility invariant is preserved and strengthened: a machine client — any request that is neither a Sec-Fetch-detected browser navigation nor an explicit request for text/htmlMUST NOT be answered HTML. An agent, a curl, or a JSON-requesting client always receives machine-legible JSON or markdown. HTML is the page face of the same resource, served only when the address, the Accept header, or a detected browser navigation selects it — it carries the same truth as the machine faces, and its Link rel="alternate" headers point straight back to them.

Revision note (0.4.0): 0.3.0 required Accept: text/html to be answered non-HTML on API paths. This version supersedes that posture with the deterministic three-face algorithm: an explicit text/html request (or a detected browser) now receives the HTML face of the same address. What a machine client receives is unchanged — never HTML — and the two obligations that made the old posture valuable (an agent never parses a DOM; a human and an agent see the same truth) are now discharged by face parity and mandatory Link alternates rather than by refusing browsers a page.

Machine-verified by: api.qa check machine-legible-home, pinned as its own requirement check-machine-legible-home (kind: check, must: pass) — the home and the sampled declared typed-body endpoints answer machine-legible non-HTML to every machine-class request (Accept: */*, Accept: application/json, and a known agent User-Agent). The negotiation algorithm itself is verified by four sibling checks, each pinned as its own requirement (kind: check, must: pass): conneg-accept (pinned check-conneg-accept) — explicit Accept: text/html / application/json / text/markdown each receive their face; conneg-client-class (pinned check-conneg-client-class) — the three */* defaults: bare client → JSON, Sec-Fetch browser navigation → HTML, known agent User-Agent → markdown; conneg-alternates (pinned check-conneg-alternates) — every face response carries Link rel="alternate" headers advertising both sibling faces; conneg-forced-face (pinned check-conneg-forced-face) — each advertised face address, fetched with a contradictory Accept header, still serves its own face (the address wins, per precedence rule 1).

Why an agent needs it: machine-legibility for every client means an agent extracts meaning directly from the response instead of parsing a DOM built for eyes; deterministic selection means the same request always yields the same face, so an agent can rely on what it will be handed before it asks; and the Link alternates mean an agent that lands on any face — even the page — can hop to the register it wants in one step, not by URL guessing.

§4typed BLOCKED/EMPTY errors that never fake success

Normative. A conforming API MUST return typed outcomes for the no-result and not-permitted paths and MUST NOT fake success. An empty result set MUST carry an explicit EMPTY type (not a bare 200 with an empty array masquerading as data); a forbidden or gated request MUST carry an explicit BLOCKED type with a non-2xx status. A conforming API MUST NOT return a 200 success envelope when the true outcome is BLOCKED/EMPTY. The outcome type is carried as a top-level type member with the closed vocabulary of Appendix A.1. So that this clause is decidable from outside, the capability card MUST declare, in its probe manifest (Appendix A.3), at least two distinct known-empty probe URLs (probes.knownEmpty) and at least two distinct known-forbidden probe URLs (probes.knownForbidden). Each declared known-empty probe MUST answer 200 with an EMPTY envelope, and each declared known-forbidden probe MUST answer 401 or 403 with a BLOCKED envelope. Additionally, each declared known-empty and known-forbidden probe URL MUST address a pathname that is also observed answering 200 with an OK envelope during the same verification run (for example via the keyless probe or the amount-0 over-ceiling control): the probed path must demonstrably branch on its query — a dedicated endpoint that can only ever answer EMPTY or BLOCKED is a decoy and does not satisfy this clause.

Machine-verified by: api.qa check claims-honesty and pinned probe requirements typed-empty and typed-blocked (kind: probe, Appendix A.4, each pinned with pathMustServeOk: true). The probes are not chosen by the verifier: api.qa reads probes.knownEmpty and probes.knownForbidden from the target's own capability card, fires every declared probe, and confirms each response is the correctly typed EMPTY / BLOCKED envelope AND that each probed pathname also answered 200 OK in the same run — a faked 200, a card that declares fewer than two of either, or a decoy pathname never observed serving OK, fails the contract.

Why an agent needs it: an agent acts on outcomes. A faked success sends the agent down a wrong branch silently; a typed BLOCKED/EMPTY lets it re-plan, escalate, or pay — deterministically.

§5hard-ceiling metered pricing

Normative. Every conforming API MUST declare its pricing as a machine-readable Pricing Document (Appendix A.2) at a probes.pricing surface that answers 200 with a closed "model" of "free" or "metered" — so an agent wallet always learns worst-case exposure before the first call, and a free API is admissible by declaring "model": "free", never by staying silent. An API that meters usage ("model": "metered") MUST additionally publish a hard-ceiling: a declared maximum spend, strictly greater than zero, the caller cannot exceed without an explicit new authorization; it MUST declare its offers and its offer boundary (monetization.offers and monetization.probe, Appendix A.5), the declared monetization.probe URL is behaviorally probed and MUST answer 402 with a typed OFFER body (Appendix A.1: "type": "OFFER" plus id/title and price); and it MUST declare an over-ceiling probe (probes.overCeiling): a same-origin declared operation plus the name of its spend parameter. The verifier — not the API — computes every probed amount from the observed hardCeiling: a request at a verifier-derived amount well above the ceiling — a seed-randomized multiple of the observed hardCeiling, so the exact amount is not precomputable from the declared ceiling — MUST be answered 402 with a typed OFFER re-authorization boundary, never silently metered past the cap, while the same probe at half the observed ceiling MUST be answered 200, and at amount 0 MUST be answered 200 — an operation that 402-walls amounts within the declared ceiling is theater, not metering. The metering obligations apply only when "model": "metered"; they do not apply to a free API.

Machine-verified by: the pinned probe requirement pricing-declared (the card-declared probes.pricing surface answers 200 with a model of free or metered) — required of every target — plus, gated on metered (appliesWhen model: "metered", so a free API passes them as not applicable), api.qa check offers-402, pinned as check-offers-402 (kind: check, must: pass) — behaviorally probing the card's monetization.probe URL (Appendix A.5) for a 402 with a typed OFFER body — and the pinned probe requirements (kind: probe, Appendix A.4): hard-ceiling-positive (the metered pricing surface publishes hardCeiling > 0), hard-ceiling-enforced (the over-ceiling operation, at a verifier-derived amount above the observed ceiling, answers a 402 OFFER re-authorization boundary, not a silent 200), hard-ceiling-below (the same operation at half the observed ceiling answers 200 — the declared ceiling comparison is real, not an any-spend-refused echo), and hard-ceiling-not-premature (the same operation at amount 0 answers 200, not 402-walled theater).

The offers-402 check is itself conditional on the observed pricing model, not only its pinned requirement: against a surface whose observed Pricing Document (the card-declared probes.pricing entry 0, Appendix A.2) declares "model": "free" and whose card declares no purchasable surface — no monetization.offers, no monetization.probe — the check reports the offer obligation vacuously satisfied and passes. A free property has no 402 boundary to prove: nothing is purchasable, so the no-ask zone is satisfied trivially, and failing the surface for the boundary it correctly does not have would punish the declaration Clause 5 demands. The rule is fail-closed on both edges. Only the observed "free" declaration earns the vacuous pass — an unobserved, non-JSON, or otherwise undetermined model keeps the hard fail, because not-applicable must be proven by the observed value, never presumed. And everything a card does declare is judged strictly: a free card that declares either monetization member has made a machine-readable claim and is held to it (Appendix A.8's defective-declaration rule, applied to the monetization surface), and the metered half of this clause is unweakened — a metered surface must still declare monetization.offers, its hard ceilings, and the behaviorally probed 402 OFFER boundary.

Revision note (0.9.0): through 0.8.0 the conditionality of the offer obligation lived only in the pinned requirement's appliesWhen gate, so a free property passed check-offers-402 as not applicable while the offers-402 check itself — and any grading ladder a verifier layers over it — still failed the property for declaring no offers. That was a verifier/ spec gap, not a property gap: the missing boundary was the correct shape for a free surface. apis-ax-axp@2.6.0 states the vacuous-satisfaction rule for the check itself; no requirement row moves, and the metered obligations are untouched.

Why an agent needs it: an autonomous wallet cannot spend safely against an open-ended meter. A hard-ceiling makes worst-case cost knowable and bounded, so the agent can commit funds without a human approving each call.

§6resolver-addressable home

Normative. A conforming API MUST have a resolver-addressable home: a stable, resolvable canonical identity (a capability card at /.well-known/agents.json and a resolvable root) that an agent can address, resolve, and cross-link from the sibling surfaces. The home MUST resolve to a valid capability card naming the surface and its interfaces. The capability card MUST carry the AXP probe manifest (probes, Appendix A.3): the card is where an API declares, on its own origin, the URLs at which the verifier exercises Clauses 4, 5, and 7. A card without a valid probe manifest leaves those clauses unverifiable, and verification fails closed.

The interfaces member is open. Beside the http and mcp interfaces a card names, a card MAY declare further named optional declared interfaces. Appendix A.8 is normative for all of them: it fixes how one is declared, what declaring it obliges, what omitting it means, and the guard that keeps every one of them additive. An optional declared interface MUST NOT relieve a surface of any obligation of Clauses 1–7; those clauses bind every conforming surface whatever its card declares or omits.

This version defines two. interfaces.digitalLink (Appendix A.8) names this origin's GS1 Digital Link resolver by pointing at the GS1 Resolver Description File at /.well-known/gs1resolver. interfaces.testSuite (Appendix A.8.5) names this origin's own published, digest-pinned conformance suite. Declaring either is OPTIONAL — a card that omits it is fully conformant and MUST NOT be failed for the omission. A card that does declare it has made a machine-readable claim and is held to it: for digitalLink, the named file MUST be retrievable, MUST validate against the description-file schema GS1 publishes, and MUST describe this origin (Appendix A.8.2). What a GS1-conformant resolver does is GS1's standard, not this one; AXP defines only how an agent discovers that this origin is one.

Machine-verified by: api.qa check agents-json, pinned as its own requirement check-agents-json (kind: check, must: pass), plus the surface requirement agents-present (surface agents.json, must: valid). api.qa resolves the capability card and confirms it is valid JSON that names the surface (name) and declares its interfaces — the clause binds to the discriminating agents-json check, not the coarse AX floor. Cross-linking of sibling surfaces is admission-pinned: the requirement check-card-interfaces-linked (api.qa check card-interfaces-linked) confirms the card names its interfaces (interfaces.http and/or interfaces.mcp, non-empty) and that llms.txt, agents.json, and the OpenAPI contract mutually cross-link — the card references its OpenAPI contract and its llms.txt, and the served llms.txt references both agents.json and the OpenAPI contract. Also pinned: the requirement probe-manifest-valid (api.qa check probe-manifest): the manifest declares every required channel, same-origin, GET-only, addressing non-templated operations the contract itself publishes, and the card also declares the monetization.probe boundary (Appendix A.5). The optional Digital Link interface is verified by api.qa check digital-link-resolver (Appendix A.8.3), which is armed by declaration: on a card that does not declare interfaces.digitalLink it returns skip, and on one that does it fetches the named description file and judges it against GS1's schema. Since apis-ax-axp@2.3.0 it is admission-pinned in the declaration-armed form, as the requirement check-digital-link-resolver (kind: check, check: digital-link-resolver, must: pass, appliesWhen: { "cardDeclares": "interfaces.digitalLink" }): a card that declares the interface MUST honour it to be admitted, and a card that omits it passes that requirement as not applicable. The card's own declaration — never this standard's ambition — is what arms the gate. The optional published test suite is verified by api.qa check published-test-suite (Appendix A.8.5), armed by declaration on exactly the same terms: on a card that does not declare interfaces.testSuite it returns skip, and on one that does it fetches the pinned suite artifact, checks its bytes against the declared pin, and runs it against this origin — in the dialect the card's runner names: api.qa/suite@1, interpreted as declarative data, or api.qa/vitest@1, executed under the Appendix A.8.6 execution invariants. Since apis-ax-axp@2.4.0 it is admission-pinned in the declaration-armed form, as the requirement check-published-test-suite (kind: check, check: published-test-suite, must: pass, appliesWhen: { "cardDeclares": "interfaces.testSuite" }): a card that declares the interface MUST honour it to be admitted, and a card that omits it passes that requirement as not applicable. Through apis-ax-axp@2.3.0 this check stood registered in the A.8 axp:optional-interfaces block and pinned by nothing — one full version in the registered-unpinned state, the recorded demonstration that eligibility and admission are different things, closed the only way this standard permits: by a ratification. The same declaration arms a second judgment since apis-ax-axp@2.5.0: capability coverage (Appendix A.8.7), verified by api.qa check capability-coverage and pinned as check-capability-coverage (kind: check, check: capability-coverage, must: pass, appliesWhen: { "cardDeclares": "interfaces.testSuite" }). On a card that declares a suite, every capability the card declares — every contract operation, every callable interface entry, every other optional declared interface — MUST be exercised by at least one passing test of that suite's run (the coverage domain and the test↔capability mapping are fixed in A.8.7); a card that omits interfaces.testSuite passes it as not applicable. One card key arming two registered checks is deliberate: the declaration is one claim, and each registered check judges one facet of it strictly — that the suite is kept (A.8.5), and that it reaches everything else the card says (A.8.7). check-card-interfaces-linked is unchanged: neither interfaces.digitalLink nor interfaces.testSuite satisfies the non-empty interfaces.http / interfaces.mcp obligation, because a resolver is a way to address this origin and a suite is a way to audit it — neither is a way to call it.

Revision note (0.6.0): 0.5.0 shipped this interface deliberately unpinned, holding the admission spec at apis-ax-axp@2.2.0, twenty-one requirements and ratified digest d9bfabee…, because no way to pin it existed that did not also fail every card which declined the option. Appendix A.8 now defines that mechanism, so the interface is pinned: the admission set is twenty-two requirements at apis-ax-axp@2.3.0 under a new ratified digest. What is superseded is the unpinned status, not the safety property. The change is breaking for exactly one population: a card that declares interfaces.digitalLink and does not honour it now loses admission. A card that omits it gains one extra not-applicable result and no new way to fail — which is the property Appendix A.8 exists to guarantee.

Revision note (0.7.0): 0.6.0 shipped interfaces.testSuite registered and deliberately unpinned, and named that pairing the standing demonstration that eligibility and admission are different things. The demonstration is complete: apis-ax-axp@2.4.0 pins check-published-test-suite in the same declaration-armed form the Digital Link interface received at 2.3.0, the admission set is twenty-three requirements under a new ratified digest, and the breaking population is again exactly one: a card that declares a suite and does not keep it now loses admission. A card that omits the interface gains one extra not-applicable result and no new way to fail. The same ratification adds the executable dialect api.qa/vitest@1 (Appendix A.8.6), superseding A.8.5's blanket never-execute rule with named execution invariants.

Why an agent needs it: the resolver-addressable home is the single address an agent resolves to find everything else. Without it the surface is an island an agent cannot reliably re-find or link. The optional Digital Link declaration extends that same address: an agent that has never heard of GS1 learns, from the one card it already reads, that this origin resolves GS1 keys — no side channel, no out-of-band knowledge, no second discovery mechanism to implement. The optional test-suite declaration extends it once more: from the same card, an agent learns which workflows this surface has committed to in public, pinned by digest so the commitment is the one it can cite later. A surface with nothing that complex to promise declares nothing and loses nothing.

§7keyless first value

Normative. A conforming API MUST deliver its first unit of real value with no API key, no signup, and no account: at least one declared, discoverable endpoint MUST answer 2xx with substantive typed content to an anonymous agent on first contact. Authentication and payment MAY gate deeper or metered use, but the surface MUST NOT require an account before an agent can obtain any value at all. The capability card MUST name one such endpoint in its probe manifest (probes.keyless); the declared probe MUST answer 200 with an OK envelope (Appendix A.1) to an anonymous caller.

Machine-verified by: api.qa check keyless-flow, pinned as its own requirement check-keyless-flow (kind: check, must: pass), plus the pinned probe requirement keyless-first-value (kind: probe: api.qa fires the card-declared probes.keyless request with no credential and requires a 200 OK envelope). api.qa samples the endpoints declared by agents.json and the OpenAPI contract and confirms at least one answers 2xx keyless.

Why an agent needs it: a fresh agent cannot negotiate an account before it knows whether the surface is useful. Keyless first value is what lets it integrate zero-shot on first contact — the whole point of the agent-first web.


§Conformance

Conformance is verified by api.qa, held outside the building fleet's write access (verifier independence); a Listing is admitted to the apis.ax catalog iff it passes this standard's conformance profile.

The mapping from each clause above to its independent api.qa verification is published, machine-readable, at conformance/profile.json. The conformance profile is executable as an api.qa PinnedSpec at conformance/apis-ax-standard.spec.json: the apis.ax curation gate expressed as a machine-checkable contract. A target passes iff api.qa returns passed: true against that pinned spec — the standards body publishes the bar, the independent verifier decides whether it is cleared, and the building fleet can edit neither verdict.

Because the verifier is held outside the fleet that builds the catalog, a Listing cannot grade itself: the gate is mechanical, not aspirational, and that independence is the whole point.

The pinned spec's ratified digest is checked out-of-band. The in-tree file conformance/apis-ax-standard.digest.txt is a hermetic-test convenience so the CLI and the test suite read the same pin locally; it is not the production independence property. Production/catalog admission runs api.qa in attested mode, which refuses to verify unless an expected digest is supplied from outside the building fleet's write access — a spec silently weakened in-tree cannot be re-ratified into a passing verdict, because the fleet does not hold the digest the attested verifier checks against.

Clause 4, 5, and 7 behavioral probes are resolved from the target's own capability card (probes.*, Appendix A.3), never hardcoded by this standard: any API, whatever its routes, is decidable against this profile by publishing a conforming probe manifest. The manifest declares only where the verifier probes; what counts as passing is fixed by the pinned spec, verbatim Appendix A.4, and expectation-like material in a manifest is ignored. If the manifest is absent, incomplete, or invalid, every requirement derived from it fails — verification fails closed, never vacuously passes.

The pinned spec contains no obligation this document does not state: every pinned requirement id appears in a clause's Machine-verified by line, the probe requirements are exactly the block printed in Appendix A.4, and the conformance tests enforce both directions structurally — including that requirement ids are unique and that every kind: check requirement names exactly the api.qa check its clause's prose names (a rewired or duplicated requirement fails the suite, not just the digest). Pricing declaration is universal: for admission every API — free or metered — declares probes.pricing, whose Pricing Document answers with the closed model of "free" or "metered" (Clause 5, Appendix A.2). The metering surfaces — probes.overCeiling and the monetization member — are required iff "model": "metered"; a free API is admitted by declaring "model": "free" and passes the metering requirements as not applicable, never by staying silent.

Optional declarations never widen admission by prose. Appendix A.8 defines the optional declarations a card may make and the mechanism that keeps every one of them additive. The rule governs every optional declaration this standard may add: it adds an obligation only to the card that makes it; it is verified by a check that returns skip when the declaration is absent, so it is never silently satisfied and never silently violated; and it enters the admission set only by a version and digest bump of the pinned spec — never by prose here. Amending this document with an optional interface therefore MUST NOT change the verdict of any surface that does not declare it, at any ratification. An optional declaration is the one kind of amendment that is safe to make between ratifications, and the reason it is safe is that a verifier judges the card's own claim rather than the standard's ambition.

Where such a requirement is pinned, it is pinned in the declaration-armed form "appliesWhen": { "cardDeclares": "interfaces.<name>" } (Appendix A.8), and two constraints bind it absolutely. The check a declaration-armed requirement names MUST be one this document registers in the Appendix A.8 axp:optional-interfaces block, against exactly the card key registered there. And it MUST NOT be a check any of Clauses 1–7 binds: a check is either always-required or declaration-armed, never both, so no Clause 1–7 obligation is ever escapable by omitting a card key. A verifier that reads a pinned requirement violating either constraint MUST refuse the whole spec rather than evaluate it — a spec that attempts to gate a MUST yields no verdict at all, never a lenient one.

Revision note (0.6.0): through 0.5.0 this section said that nothing in Appendix A.8 was pinned and that the admission set was frozen at twenty-one requirements, because no way to pin an optional interface existed that did not also fail every card which declined the option. apis-ax-axp@2.3.0 pins the first declaration-armed requirement, check-digital-link-resolver, and the admission set is twenty-two. What is superseded is the frozen count; what survives — and is now mechanically enforced rather than promised — is that a card declaring nothing optional is judged for nothing optional.

Revision note (0.7.0): apis-ax-axp@2.4.0 pins the second declaration-armed requirement, check-published-test-suite, and the admission set is twenty-three under a newly ratified digest, retiring 9063cb3e…. The mechanism is unchanged — the same appliesWhen form, the same registry row, the same one-population breaking surface (a card that declares a suite and does not keep it). What is new in the same ratification is the executable suite dialect api.qa/vitest@1 (Appendix A.8.6): the one blanket rule it supersedes, and the named invariants that replace it, are recorded in the A.8.5 revision note.

Revision note (0.8.0): apis-ax-axp@2.5.0 pins the third declaration-armed requirement, check-capability-coverage (Appendix A.8.7), and the admission set is twenty-four under a newly ratified digest, retiring dd3e5941…. The mechanism is unchanged — the same appliesWhen form, a registry row, a version+digest bump. Two things are new in kind. First, the arming key is one the registry already carries: interfaces.testSuite now arms two registered checks, which A.8 permits and A.8.7 explains — one declaration, two strictly judged facets. Second, the check is registered and pinned in the same ratification: the one-version registered-unpinned interval of 2.3.0 and 2.4.0 was each time a recorded demonstration, never an obligation, and this ratification is the recorded demonstration of that. The breaking population is again exactly one: a card that declares a suite which does not cover every capability the card declares now loses admission; a card that omits interfaces.testSuite gains one extra not-applicable result and no new way to fail.

Revision note (0.9.0): apis-ax-axp@2.6.0 moves the version and the ratified digest, retiring e4d10d4f…, while the admission set stays twenty-four requirements and no requirement row moves — the first ratification of that shape. What moves is check semantics this contract binds to: the offers-402 vacuous-satisfaction rule of Clause 5 (a free surface with no purchasable surface passes the check, not only its appliesWhen-gated pinned requirement). A semantic the verifier enforces is part of the ratified contract, so it enters by the only door this standard has — a version and digest bump, stated here and in the changelog — never by prose drifting under an unchanged pin. The breaking population is empty: every surface passing at 2.5.0 passes at 2.6.0, and the one population whose verdict improves is the free property that had no boundary to prove.


§Appendix A — AXP wire vocabulary and probe manifest (normative)

This appendix is citable as https://apis.ax/axp#wire. The outcome-type vocabulary (A.1) and the pricing model (A.2) are closed in this version; A.3 defines the probe channels this standard requires — additional channels are validated under the same rules but carry no obligations here. A.8 defines the optional interface declarations a card MAY make and the mechanism that keeps them additive; omitting all of them is full conformance, and a pinned requirement armed by one passes as not applicable on a card that makes no such claim.

§A.1 Outcome envelope

Every typed outcome body carries a top-level type member with the closed vocabulary "OK" | "EMPTY" | "BLOCKED" | "OFFER". The status code and the type are one-to-one, so an agent dispatches on type alone:

  • OK — status 200; the request succeeded and the body carries substantive content.
  • EMPTY — status 200; the request succeeded and the truthful result is an empty collection.
  • BLOCKED401 or 403; a permission boundary (not permitted, not authenticated).
  • OFFER — status 402; a payment or ceiling re-authorization boundary. The body carries an id or title plus a price, checkoutUrl, or alternatives member (Appendix A.5). A 402 MUST carry "type": "OFFER", never "BLOCKED" — payment is an offer to proceed, not a refusal.

The collection member name (e.g. results, items, terms) is the API's own choice, documented in its OpenAPI contract — it is not constrained by this standard.

{ "type": "OK", "results": [ { "id": "r1" } ] }
{ "type": "EMPTY", "results": [], "message": "no records match" }
{ "type": "BLOCKED", "reason": "not permitted for your agent class" }
{ "type": "OFFER", "id": "metered-access", "title": "Metered access", "checkoutUrl": "/checkout" }

§A.2 Pricing Document

Every conforming API declares a probes.pricing surface that answers 200 with a Pricing Document:

{ "model": "metered", "hardCeiling": 100, "unit": "usd-per-call", "price": 0.002 }
{ "model": "free" }

model is closed to "free" | "metered" in this version. When "model": "metered", hardCeiling MUST be a JSON number strictly greater than zero; unit and price are optional descriptive members. When "model": "free", no ceiling is required and the Clause 5 metering probes do not apply — the declaration itself is the obligation, so an agent wallet reads a deterministic cost answer (including "free") before its first call.

§A.3 Probe manifest

The capability card (/.well-known/agents.json) carries a top-level probes member: the manifest of URLs at which the verifier exercises Clauses 4, 5, and 7. The required channels:

channelminimummeaning
keyless1answers 200 OK to an anonymous caller (Clause 7)
knownEmpty2 distinctanswers 200 EMPTY truthfully (Clause 4)
knownForbidden2 distinct, disjoint from knownEmptyanswers 401/403 BLOCKED (Clause 4)
pricing1answers 200 with a Pricing Document — free or metered (Clause 5)
overCeiling1 (metered only)the metered operation; entry additionally carries param, the query-parameter name for requested spend, in the same unit as hardCeiling. Not required of a free API (Clause 5)

Entry shape: { "method"?: "GET", "url": "<same-origin relative or absolute>", "param"?: "<overCeiling only>" }. Single objects normalize to one-element arrays; unknown entry members are ignored. Channels beyond the required five are validated under the same rules below but carry no additional obligations in this version.

Rules, each MUST:

  • every probe URL is same-origin with the card;
  • every probe is a GET;
  • every probe pathname addresses a non-templated path declared in the OpenAPI contract, or a concrete URL declared in interfaces.http (an operation whose OpenAPI path is templated, e.g. /terms/{id}, is probeable only via such a concrete interfaces.http entry);
  • distinctness of probe URLs is judged on the fetched identity: URL fragments are stripped, so /e?a=1 and /e?a=1#dup count as one probe;
  • every knownEmpty and knownForbidden pathname is also observed answering 200 OK in the same verification run (pathMustServeOk, Clause 4) — a dedicated decoy endpoint that can only answer EMPTY/BLOCKED does not satisfy Clause 4;
  • the manifest never declares expectations — the verifier derives the over-ceiling, below-ceiling, and zero amounts from the observed hardCeiling and overwrites the declared param;
  • when probes.pricing declares multiple entries, the verifier derives amounts and applicability from entry index 0 — the first declared pricing entry is the authoritative Pricing Document.

Example:

{
  "probes": {
    "keyless": { "url": "/records" },
    "knownEmpty": [ { "url": "/records?filter=none" }, { "url": "/records?tag=gamma" } ],
    "knownForbidden": [ { "url": "/records?filter=forbidden" }, { "url": "/records?scope=admin" } ],
    "pricing": { "url": "/pricing" },
    "overCeiling": { "url": "/records", "param": "spend" }
  }
}

§A.4 Derived pinned probe requirements

The apis.ax pinned spec includes these eight requirements exactly as printed; the conformance suite asserts structural equality between this block and the ratified spec.

axp:pinned-probes
[
  { "id": "keyless-first-value", "kind": "probe", "probe": "keyless",
    "expect": { "status": 200, "paths": [ { "path": "type", "equals": "OK" } ] } },
  { "id": "typed-empty", "kind": "probe", "probe": "knownEmpty", "minDeclared": 2, "pathMustServeOk": true,
    "expect": { "status": 200, "paths": [ { "path": "type", "equals": "EMPTY" } ] } },
  { "id": "typed-blocked", "kind": "probe", "probe": "knownForbidden", "minDeclared": 2, "pathMustServeOk": true,
    "expect": { "status": [401, 403], "paths": [ { "path": "type", "equals": "BLOCKED" } ] } },
  { "id": "pricing-declared", "kind": "probe", "probe": "pricing",
    "expect": { "status": 200, "paths": [ { "path": "model", "oneOf": ["free", "metered"] } ] } },
  { "id": "hard-ceiling-positive", "kind": "probe", "probe": "pricing",
    "appliesWhen": { "fromProbe": "pricing", "path": "model", "equals": "metered" },
    "expect": { "status": 200, "paths": [ { "path": "hardCeiling", "gt": 0 } ] } },
  { "id": "hard-ceiling-enforced", "kind": "probe", "probe": "overCeiling",
    "appliesWhen": { "fromProbe": "pricing", "path": "model", "equals": "metered" },
    "paramValue": { "fromProbe": "pricing", "path": "hardCeiling", "multiplyRange": [500, 1500] },
    "expect": { "status": [402], "paths": [ { "path": "type", "equals": "OFFER" } ] } },
  { "id": "hard-ceiling-below", "kind": "probe", "probe": "overCeiling",
    "appliesWhen": { "fromProbe": "pricing", "path": "model", "equals": "metered" },
    "paramValue": { "fromProbe": "pricing", "path": "hardCeiling", "multiply": 0.5 },
    "expect": { "status": 200, "paths": [ { "path": "type", "equals": "OK" } ] } },
  { "id": "hard-ceiling-not-premature", "kind": "probe", "probe": "overCeiling",
    "appliesWhen": { "fromProbe": "pricing", "path": "model", "equals": "metered" },
    "paramValue": 0,
    "expect": { "status": 200, "paths": [ { "path": "type", "equals": "OK" } ] } }
]

§A.5 Monetization surface

A metered API carries a top-level monetization member. Both of its members below are required for admission of a metered API (a free API declares neither):

  • monetization.offers — a non-empty array of offers; each offer carries an id or title, plus a price, checkoutUrl, or alternatives member.
  • monetization.probe{ "method"?: "GET", "url": "<same-origin>" }: a URL that answers HTTP 402 with a typed OFFER body ("type": "OFFER" plus id/title plus price | checkoutUrl | alternatives). The URL MUST be a same-origin GET; an off-origin, non-GET, or private-address probe is refused without being fetched (SSRF guard) and fails admission.
{
  "monetization": {
    "offers": [ { "id": "metered-access", "title": "Metered access", "price": { "model": "metered" } } ],
    "probe": { "method": "GET", "url": "/offer" }
  }
}

The pinned requirement check-offers-402 (api.qa check offers-402) behaviorally probes the declared monetization.probe URL for the structured 402 offer, and the pinned probe-manifest check fails a metered card (one whose observed Pricing Document declares "model": "metered") that declares a probe manifest without declaring monetization.probe — so the structured-offer MUST of Clause 5 is always behaviorally verified for admitted metered targets, never satisfied by declaration alone. A free card declares neither monetization member nor probes.overCeiling, and the manifest check does not demand them of it (the metering obligations are appliesWhen-gated on the observed model, per the Conformance section). Since 0.9.0 that conditionality binds the check as well as the pinned requirement: api.qa check offers-402 passes vacuously against a surface whose observed Pricing Document declares "model": "free" and whose card declares no monetization member — no boundary exists to prove, so the no-ask zone is satisfied trivially — while a free card that declares either member is judged strictly, and a model the verifier could not observe earns no vacuous pass (fail closed; see the Clause 5 revision note).

§A.6 Derived pinned surface requirements

The apis.ax pinned spec includes these surface requirements exactly as printed; the conformance suite asserts structural equality between this block and the ratified spec, so no surface obligation (e.g. the OpenAPI version prefix or the minimum operation count) can be weakened in the spec without changing this normative text. This closes, for the kind: surface requirement BODIES, the same drift the Appendix A.4 block closes for the probes: the openapi-valid tightening (versionPrefix: "3.1", minOperations: 1) that makes an OpenAPI 3.0.x or empty-paths document fail Clause 1 is pinned here in prose, not only in the spec file.

axp:pinned-surfaces
[
  { "id": "llms-present", "kind": "surface", "surface": "llms.txt", "must": "present" },
  { "id": "agents-present", "kind": "surface", "surface": "agents.json", "must": "valid" },
  { "id": "openapi-valid", "kind": "surface", "surface": "openapi", "must": "valid", "versionPrefix": "3.1", "minOperations": 1 }
]

§A.7 Content negotiation — faces and deterministic selection (normative)

This appendix is the normative algorithm behind Clause 3. It is citable as https://apis.ax/axp#conneg.

§A.7.1 The three faces

facemedia typeregister
HTMLtext/htmlthe page, for human eyes
JSONapplication/json (the face SHOULD be JSON-LD, also answerable as application/ld+json)the typed body, for programs
markdowntext/markdownthe token-cheap agent register

Each face is a representation of the same resource: same truth, same freshness, different register. The JSON face SHOULD carry a resolvable linked-data context — a top-level $context or @context member whose value is a dereferenceable URL. The markdown face SHOULD be served as text/markdown; text/plain carrying markdown is tolerated by the verifier but SHOULD NOT be relied on.

§A.7.2 Face addresses

Every dereferenceable address has three sibling face addresses that pin a face independent of any header. Canonically these are the path extensions .html, .json, and .md appended to the resource path (for a directory-like address such as the root /, the API chooses and advertises concrete sibling addresses, e.g. /index.html). A face address:

  • MUST serve exactly its named face, regardless of the request's Accept header — the address wins over every other selection input;
  • MUST answer 404 when the underlying resource does not exist — never a differently-faced fallback;
  • MUST be discoverable from every sibling via Link rel="alternate" (A.7.5) — a client never has to guess the extension convention.

Only .html, .json, and .md are face extensions. Any other extension (.xml, .txt, …) is an ordinary path with no face-forcing semantics: if the path exists it negotiates by A.7.3 steps 2–3 like any address; if not, it 404s.

§A.7.3 The selection algorithm

For a GET or HEAD request to a dereferenceable address, first match wins:

  1. Address. The request path is a face address (A.7.2) → serve that face.
  2. Accept. Consider the face media types explicitly named in the Accept header (including via an exactly-one-face wildcard: application/* names the JSON face; text/* names more than one face and is treated as */*), each with its effective q-value. A type with q=0 is excluded. If at least one face type remains: serve the face with the highest q; on a tie, the face whose media type appears earliest in the header.
  3. Client class. No face was named (Accept: */*, an absent Accept header, only excluded/unrecognized types): a. the request is a browser navigationSec-Fetch-Mode: navigate, or Sec-Fetch-Dest: document or iframe. Browser detection MUST use Sec-Fetch-* request headers and MUST NOT be inferred from the User-Agent → serve HTML; b. the User-Agent contains a known agent token (A.7.4, case-insensitive substring) → serve markdown; c. everything else — bare curl, SDK HTTP clients, unknown callers → serve JSON.

Consequences, each MUST:

  • The algorithm is total: a face-bearing address MUST NOT answer 406 Not Acceptable — an unsatisfiable Accept falls through to step 3.
  • The response MUST declare the served face in Content-Type and SHOULD send Vary: Accept, Sec-Fetch-Mode, Sec-Fetch-Dest, User-Agent so caches never cross-serve faces.
  • HEAD MUST be answered wherever GET is, by the same algorithm, with the same status, Content-Type, and Link headers, and no body.
  • Selection MUST be deterministic: the same request (method, path, Accept, Sec-Fetch-*, User-Agent class) always yields the same face.

§A.7.4 Known agent User-Agent tokens

An implementation MUST treat a User-Agent containing any of these tokens (case-insensitive) as agent-class, and MAY extend the list:

gptbot · chatgpt-user · oai-searchbot · claudebot · claude-user ·
claude-searchbot · perplexitybot · perplexity-user · mistralai-user ·
duckassistbot · bingbot-chat · agent

The generic token agent intentionally matches any self-identifying *-Agent/* product string, so a well-behaved new agent gets markdown without a list update. The verifier probes agent-class behavior with a User-Agent drawn from this list (api.qa sends Claude-User/1.0 (+https://api.qa)), so the MUST-minimum list is what makes the clause decidable from outside.

Every face response (including HEAD, including the root) MUST carry HTTP Link headers advertising both sibling faces, each with rel="alternate" and a type parameter naming the sibling's media type:

Link: </index.html>; rel="alternate"; type="text/html",
      </index.md>; rel="alternate"; type="text/markdown"

(on the JSON face of /; each face advertises the other two). A self-referencing alternate is permitted; what is required is that the two sibling media types are each advertised at a dereferenceable same-origin URL.

§A.7.6 Verification profile

api.qa exercises the algorithm with fixed request profiles against the home and derives the face addresses from the served Link alternates — never from a guessed extension convention:

checkrequest(s)must observe
machine-legible-homehome + sampled typed bodies under Accept: */*, Accept: application/json, and an agent User-Agentmachine-legible non-HTML, every one
conneg-accepthome under Accept: text/html / application/json / text/markdownthe named face, each time (the JSON face's $context/@context is reported informationally)
conneg-client-classhome under Accept: */* three ways: bare · with Sec-Fetch-Mode: navigate + Sec-Fetch-Dest: document · with a known agent User-AgentJSON · HTML · markdown, respectively
conneg-alternatesthe three home face responseseach carries Link rel="alternate" naming both sibling faces
conneg-forced-faceeach Link-advertised face address, fetched with a contradictory Acceptthe advertised face anyway (the address wins); an advertised URL ending in a face extension must serve the extension's face

Verification fails closed: a home that advertises no resolvable alternates cannot demonstrate rule 1 and fails conneg-forced-face; it does not vacuously pass.

§A.8 Optional declared interfaces (normative)

This appendix is citable as https://apis.ax/axp#optional; the GS1 Digital Link interface it first defined stays citable as https://apis.ax/axp#digital-link. Unlike A.1–A.7 it defines optional declarations: capabilities a surface MAY offer and MAY claim. A card that claims none of them is fully conformant (Clause 6, Conformance).

How a card declares one. Clause 6's interfaces member is open. An optional declared interface is a lowerCamelCase member of interfaces whose value is a JSON object. The object MAY be empty: presence is the declaration. Every member inside it belongs to that interface's own subsection below, and unknown members inside it are ignored, exactly as in A.3.

Declaring it invokes strict verification; omitting it is conformance. These are one rule read from both ends. Each MUST binds the verifier:

  • A card that omits the key has affirmatively said "I do not offer this." The check MUST return skip — never pass, which would launder silence into a claim, and never fail, which would punish a surface for a claim it did not make. A pinned requirement armed by that key MUST report not applicable, and the surface MUST NOT be failed on its account.
  • A card that declares the key has made a machine-readable claim, and the claim is judged strictly: every MUST of that interface's subsection applies in full.
  • A defective declaration is a failed claim, not an absent one. A value that is not a JSON object, or an object whose members violate the subsection, MUST fail and MUST NOT skip. There is no value a card can write that means "no" — not null, not false, not {}, not "". A card that means no omits the key. This is what stops an optional declaration from becoming a way to buy a free skip.

Absence of the card is not absence of the claim. A verifier decides "declared" only from a capability card it actually retrieved and parsed as a JSON object. If the card was unreachable, non-2xx, not JSON, not a JSON object, or if a member on the path to the key is present but not an object, then nothing was stated and the requirement MUST apply and fail closed. Absence in a retrieved document is a statement; absence of the document is not. The asymmetry is deliberate and is the whole basis on which a declaration may skip while an unobserved probe may not: an unanswered probe is an observation failure, and absence of evidence is not evidence.

The evasion guard — an optional interface is an additive capability only. An optional declaration adds obligations to the card that makes it and removes none. Each of these is a MUST:

  • No obligation of Clauses 1–7 may be made conditional on an optional declaration. Those clauses bind every conforming surface whatever its card declares or omits.
  • A pinned requirement MAY be armed by a card declaration — the wire form is "appliesWhen": { "cardDeclares": "interfaces.<name>" }only when the api.qa check it names is registered in the axp:optional-interfaces block below against exactly that card key. A requirement whose check is unregistered, or whose cardDeclares is not the key registered against that check, is not a conforming pinned requirement of this standard; a verifier MUST refuse the whole spec rather than evaluate it.
  • A check is either always-required or declaration-armed, never both. No check that a Clause 1–7 requirement of the admission spec names may appear in the registry. This disjointness is what makes it structurally impossible to escape a Clause 1–7 obligation by omitting a card key.
  • cardDeclares is legal only on a kind: "check" requirement. The eight behavioural probe requirements of A.4 and the three surface requirements of A.6 are categorically un-armable, so no Clause 4, 5, 6 or 7 behaviour can be gated on a declaration.
  • cardDeclares tests presence only. It carries no value comparison and has no negated form: a spec cannot say "applies when the card declares this to be false". A negated form would be an evasion primitive by construction — a MUST that switches off when a key is added — so this standard does not define one.
  • An allowlisted check pinned without appliesWhen remains legal and means "I demand this of everyone"; its skip then fails closed. The registry restricts what may be skipped, never what may be required.

Eligibility is not admission. Registration below makes a check eligible to be declaration-armed; it does not pin it. An optional interface enters the admission set only by a version and digest bump of the pinned spec. digital-link-resolver is registered and pinned, as check-digital-link-resolver since apis-ax-axp@2.3.0; published-test-suite is registered and pinned, as check-published-test-suite since apis-ax-axp@2.4.0 — after standing one full version registered and unpinned, the recorded demonstration that the two states are different; capability-coverage is registered and pinned, as check-capability-coverage since apis-ax-axp@2.5.0, in the same ratification that registered it — the waiting interval was each time a demonstration, never an obligation. Both states are conformant, and the only thing that ever moves a check from the first to the second is a ratification, not prose.

One card key MAY arm more than one registered check. The registry is keyed by check — a check appears at most once, bound to exactly one card key — but nothing limits how many registered checks one key arms, because a declaration is one claim and each registered check judges one facet of it strictly. interfaces.testSuite arms two: published-test-suite (the suite is kept, A.8.5) and capability-coverage (the suite reaches everything else the card declares, A.8.7). The additive guarantee is unchanged — a card that omits the key gains one not-applicable result per armed check and no new way to fail.

axp:optional-interfaces
[
  { "cardKey": "interfaces.digitalLink", "check": "digital-link-resolver", "appendix": "A.8.1" },
  { "cardKey": "interfaces.testSuite", "check": "published-test-suite", "appendix": "A.8.5" },
  { "cardKey": "interfaces.testSuite", "check": "capability-coverage", "appendix": "A.8.7" }
]

Every cardKey above matches ^interfaces\.[A-Za-z][A-Za-z0-9]*$ — exactly two dot-separated segments, the first literally interfaces, no array indices — and the conformance suite asserts it, so a registry row can never name a path that reaches outside the interfaces member.

Clause 6's interfaces member is open. Beside http and mcp, a card MAY declare digitalLink: a JSON object naming this origin's GS1 Digital Link resolver.

{
  "interfaces": {
    "http": [ { "method": "GET", "url": "https://example.com/records" } ],
    "digitalLink": { "wellKnown": "https://example.com/.well-known/gs1resolver" }
  }
}
memberrequiredmeaning
wellKnownnoabsolute or same-origin-relative URL of the GS1 Resolver Description File. GS1 fixes its location at /.well-known/gs1resolver (RFC 8615), so when this member is absent a reader resolves that path against the card's origin. When present it MUST be same-origin with the card.

Unknown members are ignored, exactly as in A.3. The object MAY be empty ({}): presence is itself the declaration, and the default location is the one GS1 fixes.

§A.8.2 What declaring it means — and what it does not

Where interfaces.digitalLink is declared, each of these MUST hold:

  • the GS1 Resolver Description File MUST be retrievable at that address and MUST answer 200 with a JSON body;
  • that document MUST validate against the description-file schema GS1 publishes at https://ref.gs1.org/standards/resolver/description-file-schema;
  • the document's resolverRoot MUST be same-origin with the card that declared it — a card describes this origin's resolver, never a third party's.

Declaring it is not a claim of full GS1 resolver conformance. AXP asserts only that the description file exists, validates, and describes this origin. What a conformant resolver does — key-shape dispatch, linkType negotiation, RFC 9264 linksets, redirect semantics, language selection — is fixed by GS1's resolver standard at https://ref.gs1.org/standards/resolver/ and is exercised by GS1's own resolver test suite at https://ref.gs1.org/test-suites/resolver/. This standard verifies none of it and MUST NOT be cited as having done so.

AXP restates none of GS1's vocabulary. resolverRoot, supportedPrimaryKeys, supportedLinkType and every other member of the description file are defined by the cited schema; they change when GS1 changes them, not when this document does. AXP owns exactly one name here — the card key digitalLink — and one obligation: that the pointer be true.

Omission is conformance. A card that does not declare interfaces.digitalLink makes no Digital Link claim, is judged for none, and MUST NOT be failed on its account.

§A.8.3 Verification profile

checkcard statemust observe
digital-link-resolverno interfaces.digitalLinkskip — nothing is claimed, so nothing is judged; the pinned check-digital-link-resolver reports not applicable
digital-link-resolverdeclaredthe declared (or default /.well-known/gs1resolver) address answers 200 with JSON that validates against the cited schema, and its resolverRoot is same-origin with the card
digital-link-resolverpresent but not a JSON objectfail — a defective claim is a claim (A.8)
digital-link-resolvercard unreachable, non-2xx, or not a JSON objectfail — nothing was stated, so the requirement applies and fails closed (A.8)

Rules binding the verifier, each MUST:

  • the check is armed by declaration. Absent the declaration the verdict is skip — never pass, which would launder silence into a claim, and never fail, which would punish a surface for a claim it did not make. Since apis-ax-axp@2.3.0 the apis.ax admission spec turns that skip into a gate in the declaration-armed form — check-digital-link-resolver, appliesWhen: { "cardDeclares": "interfaces.digitalLink" } — so a declaring card that does not honour the declaration is not admitted while a card that omits it is untouched. A target's own pinned spec MAY additionally pin the check with no appliesWhen, making the interface mandatory for that target, where a skip fails closed.
  • a verifier MUST NOT fetch the schema from ref.gs1.org at verification time. A verdict is a function of what the target published and what the verifier pinned; a third-party document that changes underneath it is neither. The verifier validates against a copy pinned by content digest and MUST record, in the verdict, which copy it used.
  • the published schema is JSON Schema draft-07, in which format is an annotation and not an assertion. A verifier MUST NOT fail a description file on a format the schema only annotates, and MUST state in its verdict detail what it did assert — an over-strict verifier that rejects a valid document is the same defect as a lax one that admits an invalid one.

§A.8.4 One resolver, many hostnames

A single resolver deployment may answer on many hostnames. Each hostname is its own AXP surface with its own capability card, and the description file's resolverRoot names the host the caller actually reached. A verifier therefore compares resolverRoot to the origin it fetched the description file from, and MUST NOT compare it to a constant, to a card-declared value, or to any other hostname the same deployment serves.

§A.8.5 The published test suite

The published-test-suite interface is citable as https://apis.ax/axp#test-suite.

A surface with non-trivial workflows can say so in a way a machine can check. A simple CRUD or lookup API cannot, and MUST NOT be asked to: a single-call lookup has no workflow to publish, and demanding one would make ceremony out of nothing. So this interface is optional in the strong sense — omitting it is conformance, and no verdict about a card that omits it may mention it.

A card MAY declare interfaces.testSuite: this origin's own public conformance suite, published as a digest-pinned, independently runnable artifact, so a verifier can check that the surface keeps the promise the surface itself published.

{
  "interfaces": {
    "http": [ { "method": "GET", "url": "https://example.com/records" } ],
    "testSuite": {
      "url": "/.well-known/axp/suite.json",
      "digest": "sha256:1f0c000000000000000000000000000000000000000000000000000000000000",
      "environment": "public",
      "runner": "api.qa/suite@1"
    }
  }
}

An SDK-publishing property addresses its executable suite as a natively served module instead (Appendix A.8.6.6), by URL, by npm coordinate, or by both — the coordinate as an identity assertion over the served bytes:

{
  "interfaces": {
    "testSuite": {
      "url": "https://pkg.do/apis.vin@1.2.0/index.mjs",
      "package": "apis.vin",
      "version": "1.2.0",
      "export": "suite",
      "digest": "sha256:2b7d000000000000000000000000000000000000000000000000000000000000",
      "runner": "api.qa/vitest@1"
    }
  }
}
memberrequiredrule
urlconditionalabsolute URL or same-origin-relative pathname of the pinned suite artifact — a suite document, or (under api.qa/vitest@1) a natively served ES module (.mjs, Appendix A.8.6.6). At least one of url / package MUST be present. The address MAY be off-origin — a versioned module-CDN URL is the normal case for an SDK — because the digest, never the host, is the authority over the bytes (A.8.6.6). A non-routable or unparseable value is refused without being fetched and fails.
packageconditionalan npm package name. Together with version, an identity assertion (A.8.6.6): the pinned artifact's bytes are asserted byte-identical to that published version's entry module. Present without url, the artifact address derives from the native-serving obligation of A.8.6.6. A registry is never contacted. Meaningful only under runner: "api.qa/vitest@1"; refused under a declarative runner.
versionconditionalthe exact published version of package (never a range). REQUIRED when package is present; refused without it.
exportnothe name of a module export — a nullary (possibly async) function the harness calls to register tests. Meaningful only for a module artifact under api.qa/vitest@1; absent, tests register at module instantiation.
digestyes"sha256:" followed by 64 lowercase hex characters: the digest of the pinned artifact's exact bytes. Absent or mismatched MUST fail. The digest is the sole byte authority for every addressing (A.8.6.6).
environmentnothe named environment to select; defaults to "public". A name the suite does not define MUST fail. A module artifact defines no environments and carries exactly the implicit "public" (A.8.6.4).
runnernothe suite dialect; defaults to "api.qa/suite@1". This version defines exactly two: "api.qa/suite@1" (declarative, interpreted) and "api.qa/vitest@1" (executable, Appendix A.8.6). Any other value MUST fail, never skip.

Unknown members are ignored, exactly as in A.3. A.8 lets an optional interface's object be empty, because presence is the declaration; this subsection is stricter, and the object MUST NOT be: digest and an address — url, package@version, or both — are required, and a suite that names no location and no pin is not a published suite.

Which artifact kind was addressed is decided by the card, never by sniffing the bytes: the artifact is a module iff the card names a package or the resolved address's pathname ends .mjs; otherwise it is a suite document (A.8.6.1). Bytes that do not parse as the discriminated kind fail.

Revision note (0.7.0): an earlier draft of this table required url and required it same-origin with the card. Both restrictions are retired: the provenance this subsection guarantees was never the serving host, it was always the digest — a card that pins sha256:… has published exactly those bytes as its own claim no matter which host answers for them — and a same-origin rule would have made the primary SDK addressing (a versioned module-CDN URL) inexpressible. What the pin covers, the origin need not.

Two dialects, one discipline. A runner: "api.qa/suite@1" document is declarative JSON that a verifier interprets: an ordered list of requirements over public HTTP contracts — assertions on status, media type, schema and JSON paths, with response-value capture and {{var}} interpolation, so that multi-step workflows are expressible without a single line of executable text. A runner: "api.qa/vitest@1" artifact carries executable tests — as string members of the same served, digest-pinned suite document, or as a digest-pinned, natively served ES module (Appendix A.8.6.6); a verifier executes those bytes, and only those bytes, under the named execution invariants of A.8.6.3 — a fresh single-use isolate, zero ambient authority, a network floor that bars metadata and private addresses while leaving external egress open, a metered circuit-breaker deadline, and executed bytes covered by the card's own digest pin. What is executable is always digest-pinned: the three ratified addressings (inline strings, a natively served module, an npm coordinate asserted identical to the served bytes — A.8.6.6) all reduce to a pinned module the isolate instantiates, and in every one of them the digest, never the host, the registry, or the package manager, is the authority over what runs — because bytes the pin does not cover are bytes the verdict cannot cite.

Revision note (0.7.0): through 0.6.0 this subsection ruled that the suite is data, never code — a verifier MUST NOT evaluate, import, or otherwise execute code a target publishes — a blanket prohibition adopted because no execution discipline then existed under which the verdict stayed a function of digest-pinned bytes and recorded evidence. Appendix A.8.6 is that discipline, and the blanket rule is superseded by its named invariants. Every property the prohibition protected survives: the executed bytes are exactly the bytes the card's digest pins (A.8.6.3), replay is attested (A.8.6.5), and the judged party's code can reach neither the verifier's authority nor the network's private interior. What is retired is only the assumption that execution and those properties cannot coexist. Under api.qa/suite@1 nothing changes: that dialect is interpreted, never executed, exactly as before.

§A.8.5.1 What declaring it means

Where interfaces.testSuite is declared, each of these MUST hold:

  • the suite artifact MUST be retrievable at its resolved address — the declared url, or the address A.8.6.6's native-serving obligation derives from package@version — and MUST answer 200;
  • the artifact's exact bytes MUST hash to the declared digest;
  • the artifact MUST be well-formed in the declared runner dialect and the discriminated kind (document or module), and a document MUST define the selected environment;
  • every requirement in the suite MUST be one the surface actually satisfies.

The last of those is the one that makes the interface worth declaring: a check that only confirmed the file exists and hashes correctly would verify nothing about the workflows the interface is for. The verifier runs the suite.

Why the digest is required here although digitalLink.wellKnown needs none. A description file is a description, and a verdict citing the observed bytes is complete. A test suite is the target's own assertions about itself: unpinned, a target can rewrite it between advertising it and being judged against it, so the artifact a report cites would not be the artifact that was judged. The pin is also what makes the claim durable and citable — "passes suite sha256:1f0c…" survives; "passes its current suite" does not.

Omission is conformance. A card that does not declare interfaces.testSuite publishes no suite, is judged for none, and MUST NOT be failed on its account. Nor does declaring it satisfy any other obligation: a test suite is a way to audit this origin, not a way to call it, so it does not contribute to Clause 6's non-empty interfaces.http / interfaces.mcp demand, and check-card-interfaces-linked is unchanged by it.

§A.8.5.2 Verification profile
checkcard statemust observe
published-test-suiteno interfaces.testSuiteskip — nothing is claimed, so nothing is judged; the pinned check-published-test-suite reports not applicable
published-test-suitedeclaredthe suite is retrievable, hashes to the declared pin, parses, defines the selected environment, and every one of its requirements passes
published-test-suitedeclared, runner: "api.qa/vitest@1"additionally: the document's tests member parses in the A.8.6.2 subset, and every registered test passes under the A.8.6.3 execution invariants — one folded result set with the declarative rows (A.8.6.5)
published-test-suitepresent but not a JSON object, or defective in any member abovefail — a defective claim is a claim (A.8)
published-test-suitecard unreachable, non-2xx, or not a JSON objectfail — nothing was stated, so the requirement applies and fails closed (A.8)

Rules binding the verifier, each MUST:

  • the check is armed by declaration, on the same terms as A.8.3. Absent the declaration the verdict is skip — never pass, which would launder silence into a claim, and never fail, which would punish a surface for a claim it did not make.
  • a defective declaration is a failure, not an absence. A present-but-non- object value, a missing or malformed digest, a declaration with neither url nor package, a package without its exact version, a package under a declarative runner, an unknown runner, an undefined environment — each MUST fail. A card meaning "I publish no suite" omits the key; a verifier that let a broken declaration skip would have built an evasion, since any obligation could then be shed by declaring it defectively.
  • writes are refused in the declarative dialect. An api.qa/suite@1 run has writes disabled, and any requirement whose resolved method is not GET or HEAD MUST fail the check by a named reason — a card-declared suite is a stranger's document; the consent that permits a self-pinned spec to write does not exist here. The same rule binds the declarative rows of an api.qa/vitest@1 document. Executable tests are environment-gated instead (A.8.6.4): a mutating request is permitted only against an environment the suite itself declares "sandbox": true, and fails by a named reason anywhere else.
  • declarative rows stay home; executable tests roam above the floor. In the api.qa/suite@1 dialect — and for the declarative rows of an api.qa/vitest@1 document, which keep unchanged suite@1 engine semantics — every resolved URL, including one produced by interpolation or parameter injection, is re-gated same-origin with the card and publicly-routable. Executable tests are governed instead by the network floor of A.8.6.3: a request toward a metadata endpoint, a link-local, loopback, or private address, or a verifier-internal service fails the run by a named reason, and every other publicly-routable destination is permitted — cross-origin and cross-estate calls are a feature of the executable dialect, not a leak in it.
  • the suite is bounded — and the two dialects are bounded on different grounds. An api.qa/suite@1 run is free of charge on the verifier's own account, so it carries scarcity bounds: a dedicated cap of 25 requirements and the fixed 20 s deadline. An api.qa/vitest@1 run is a paid, metered capability (A.8.6.3), so its bounds are abuse circuit-breakers, never rations: 1000 across declarative rows and registered tests combined (A.8.6.1), under the metered circuit-breaker deadline of A.8.6.3 (default 5 minutes wall-clock). In both dialects a suite exceeding its cap MUST fail; it MUST NOT be truncated, because truncation would let a target hide a failing requirement past the cutoff.
  • no self-grading and no recursion. A card-declared suite's declarative rows MUST exercise only the surface's own endpoints (in the api.qa/suite@1 dialect: kind: "endpoint" and kind: "probe"); its executable tests exercise whatever the network floor permits. In both, a requirement or test that invokes the verifier's own checks or grades — including this one — MUST be refused. A surface's self-published tests are evidence about the surface, never about its grader, and a suite able to demand published-test-suite of itself would not terminate.
  • the verdict states what was judged. A passing verdict MUST name the suite digest, the environment, the requirement count, and the number of distinct pathnames exercised.
§A.8.5.3 The honest limit

A verifier that runs a published suite establishes that the surface keeps its own published promise. It does not establish that the promise is demanding: a suite asserting only GET / → 200 passes, and a verifier MUST NOT report such a result as though it were more, nor be cited as having judged a suite's rigour. Ambition is not machine-decidable, and pretending otherwise would be exactly the dishonesty this standard exists to catch.

What a verifier MUST do instead is make triviality legible. An empty suite is refused, and beyond that the verdict carries the counts A.8.5.2 requires — digest, environment, requirement count, distinct pathnames — so a reader can recognise a decorative suite for what it is. A published suite is a claim about coverage, and like every other claim in this standard it is judged by whether it is true, not by whether it is impressive.

Registered — and pinned since apis-ax-axp@2.4.0. published-test-suite appears in the axp:optional-interfaces block, which made it eligible to be declaration-armed at 0.6.0; no requirement of apis-ax-axp@2.3.0 named it, and that one version in the registered-unpinned state remains the recorded demonstration that eligibility and admission are different things. apis-ax-axp@2.4.0 pins it as check-published-test-suite, in the declaration-armed form and no other: a card that omits interfaces.testSuite gains one not-applicable result and no new way to fail, and a card that declares a suite and does not keep it loses admission — the same one-population breaking surface as every ratified optional interface before it.

§A.8.6 The executable suite dialect — api.qa/vitest@1

The executable dialect is citable as https://apis.ax/axp#vitest.

A declarative row can assert what a response is; it cannot express the judgment in the middle of a workflow — compute a check digit, compare two captures, branch on a body. A surface whose promises need that expressiveness publishes them as code, held to the same standard as data: inside a digest-pinned artifact the card names. The primary document form is the house pattern for code in a JSON-serializable document (a Code-valued member, carried as a string), and it is what makes the dialect possible with one served artifact and one pin: the card's existing digest covers the code strings byte-for-byte, so there is no second artifact to pin, no separate code digest to book-keep, and no way for the executed tests to differ from the advertised ones. A.8.6.6 ratifies the two further addressings — a natively served ES module, and an npm coordinate asserted identical to those served bytes — and both reduce to exactly the same discipline: a digest-pinned module the isolate instantiates, with the digest as the only authority.

§A.8.6.1 The document: one artifact, two natures

This subsection defines the document addressing — the primary, one-artifact form. A card MAY instead pin a natively served module (A.8.6.6), in which case this subsection's tests / module members do not apply: the pinned module is the test module, instantiated directly under A.8.6.2 and A.8.6.3.

An api.qa/vitest@1 document is an additive extension of the api.qa/suite@1 document grammar, not a new document type. The same $type: "Suite" envelope, the same environments map, the same requirements grammar where rows are present — extended by two root members:

memberrequiredrule
testsyesa string: ES module source in the A.8.6.2 subset, registering the executable tests. Absent, empty, or not a string MUST fail.
modulenoa string: ES module source in the same subset, instantiated first; its exports are importable by tests under the reserved specifier "suite:module".

The dialect is discriminated by the card's runner member, never by sniffing the document, and the choice of an additive extension over a new document type is deliberate, for two reasons this standard can state mechanically. First, the fail-closed path for older verifiers is already ratified: A.8.5 requires an unknown runner value to fail, never skip, so a verifier that predates this dialect refuses a runner: "api.qa/vitest@1" card without ever parsing — let alone executing — the code members. Second, the suite@1 parser ignores unknown root members, so one served document can carry both natures at once: its declarative rows are interpreted with unchanged suite@1 engine semantics, its tests run under this appendix, and both fold into one result set (A.8.6.5).

Rules, each MUST:

  • requirements MAY be absent or empty in an api.qa/vitest@1 document. Non-vacuity is judged over the union: at least one declarative row or one registered test, or the run fails — an all-of-nothing is refused here for the same reason an empty suite@1 suite is.
  • the cap of A.8.5.2 is combined: declarative rows plus registered tests MUST NOT exceed 1000. Exceeding it fails; it is never truncated. The number is an abuse circuit-breaker against a runaway registration loop, never a coverage ration: a real suite is bounded by the metered, billed breaker of A.8.6.3 long before it is bounded by a count.
  • the served document MUST NOT exceed 1 048 576 bytes (1 MiB). The tests and module strings live inside the document, so they are bounded — and pinned — by construction.
§A.8.6.2 The dialect subset

api.qa/vitest@1 names a guaranteed subset of the vitest API. The subset, not the vitest release, is the contract: a suite written inside it runs identically under local vitest and under the hosted verifier, and a construct outside it MUST fail — in both runners, by a named reason, never by silently diverging.

Guaranteed: describe, it (including async test functions, await, and the full ES2022 expression language), and expect with its core matchers (equality, comparison, truthiness, containment, shape, rejects/resolves). The global fetch is available, scoped as A.8.6.3 rules. Imports are closed to exactly three specifiers: "vitest" — resolving to the guaranteed subset itself, so a suite written as an ordinary vitest file imports the ordinary way; "suite:env" — exporting origin (the graded origin), vars (the selected environment's variables), seed (A.8.6.4), and sandbox (boolean); and "suite:module", available iff the artifact is a document carrying a module member (in the module addressing the specifier does not exist). In the document form the subset names are also installed as harness globals, so inline strings MAY omit the import.

Explicitly outside the subset, each MUST fail: any other import specifier — in particular every node: built-in (fs, path, child_process, …) and every bare package specifier; snapshot matchers (toMatchSnapshot, toMatchInlineSnapshot — a snapshot is state outside the pinned document); module mocking, fake timers, and the rest of the vi surface; dynamic import(); and eval / new Function (the isolate disables runtime code generation — the only code that runs is code the digest covers).

Local/hosted parity is by construction, and normative. The hosted verifier's harness and the local CLI runner MUST share one implementation of the subset — one module, never a reimplementation — so both extract the same members (or instantiate the same pinned module), resolve "vitest" and the reserved specifiers identically, and judge with identical subset semantics. A suite module that imports only "vitest" additionally runs under an unmodified local vitest, because the subset is a subset: the same file, the same tests, no translation. The npm-published verifier toolchain SHOULD ship the local runner, so "green locally, red hosted" is expressible only as a difference in the target's behavior between the two runs — never as a difference in the harness.

§A.8.6.3 Execution invariants

These invariants are the replacement for the superseded blanket prohibition (A.8.5 revision note). Each binds the verifier, and each MUST hold on every run:

  • The runner is a paid-tier capability. Execution under this dialect is metered and billed to the account that requests the run; the paid tier — not scarcity — is the gate on the capability. That gate is why this appendix's restrictions strip to the named minimum: the isolation, the network floor, and the digest pin are invariants because they protect the verifier and the network's interior, and every remaining bound is an abuse circuit-breaker priced in the requester's own money, never a rationing device.
  • Fresh, single-use isolate. Each run executes in a newly provisioned isolate (on the reference verifier: a Cloudflare Worker Loader isolate), created for the run and discarded after it. Nothing — state, globals, cache, compiled code — persists between runs or between targets.
  • Zero ambient authority. The isolate receives no secrets, no bindings, no environment beyond "suite:env", and no I/O capability except fetch. The verifier's own credentials, service bindings, and control plane are unreachable from inside: the judged party's code can never wield the verifier's authority.
  • The network floor — and nothing above it. Every fetch — including after any redirect, including URLs produced by interpolation, and re-checked after DNS resolution so a rebinding cannot slip under it — MUST be refused toward: cloud-metadata endpoints (169.254.169.254 and equivalents), link-local ranges (169.254.0.0/16, fe80::/10), loopback (127.0.0.0/8, ::1), private and reserved ranges (RFC 1918 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16; CGNAT 100.64.0.0/10; ULA fc00::/7), and the verifier's estate-internal service bindings and hostnames. Each refusal fails the run by a named reason. Every other publicly-routable destination is permitted: a suite MAY call other origins, other estates, and third-party APIs, because composed cross-estate workflows are precisely what non-trivial surfaces promise, and a runner that can only call home cannot test them. Revision note (0.7.0): an earlier draft scoped fetch to the graded origin; that posture is rejected — it protected nothing this floor does not, and it made every composed workflow untestable.
  • Metered deadline, generous circuit-breaker. A long run is a cost its requester bears, never a tax on the verifier. The circuit-breaker defaults to 300 000 ms (5 minutes) wall-clock and 60 000 ms CPU; an account MAY configure a higher breaker; the applied limits, elapsed wall-clock, and consumed CPU are recorded in the verdict. A run that trips the breaker fails — never a partial verdict over the tests that happened to finish. (api.qa/suite@1's fixed 20 s deadline is unchanged and applies to that dialect only.)
  • Caps — abuse circuit-breakers, never rations. Captured output (results, messages, logs) capped at 4 MiB; the document (1 MiB) and count (1000 combined) caps of A.8.6.1 in the document addressing; a pinned module artifact MUST NOT exceed 4 MiB (an SDK entry is a bundle, and carries more than a suite document does). Any cap breached fails the run. Each number is sized so that no legitimate suite meets it: what a cap stops is abuse, and what bounds a legitimate run is the billed breaker above.
  • Digest fail-closed. The bytes instantiated are exactly the bytes whose sha256 matched the card's declared digest — one fetch, one buffer, hash-then-instantiate: string members are extracted from that buffer, a module artifact is compiled from it, and never from a second fetch. If the bytes do not match the pin, nothing is instantiated and the check fails; there is no execute-then-check ordering under any circumstance. This is the integrity guarantee of the standard — run this exact code; local equals hosted — and unlike the deadline it is not relaxable by configuration, tier, or consent.
  • Fail-closed totality. A digest mismatch, a subset violation, a floor or breaker trip, a cap breach, an undefined or non-sandbox environment for a mutating suite, or any invariant above failing MUST yield a named fail — never skip, never a pass over the surviving remainder.

Why minutes, and metered. api.qa/suite@1's fixed 20 s is right for what it bounds: up to 25 declarative GET/HEAD probes against one origin, run free of charge on the verifier's own account. An executable run is neither free nor single-origin: it is a paid capability whose cost falls on its requester, and its tests legitimately span isolate provisioning, module instantiation, and sequential multi-origin round-trips. A deadline sized in seconds would ration exactly the composed workflows the dialect exists to make testable; a breaker sized in minutes ends only runs that have stopped making progress. Five minutes wall and one minute CPU bound a hung run to a finite, billed, recorded cost — and because time is metered and stated in the verdict, a suite pacing itself against the breaker is legible, not free.

§A.8.6.4 Determinism and politeness
  • Seeded randomness. The verifier chooses a seed per run and records it in the verdict. Inside the isolate, Math.random is replaced by a deterministic generator derived from that seed, and the same seed is exported by "suite:env" for tests that derive their own values. A replay under the recorded seed and the recorded digest is the same run.
  • Environment requirements. Additively to suite@1's { "vars": { … } }, an environment entry MAY declare "sandbox": true. Executable tests may issue requests with mutating verbs (POST, PUT, PATCH, DELETE, …) only when the selected environment declares it; in any other environment a mutating request fails the run by a named reason. Declaring an environment sandbox is the surface's own consent, in the pinned document, to be written to there — consent the verifier never infers. A pinned module artifact (A.8.6.6) declares no environments: its runs carry exactly the implicit "public" environment, sandbox: false, so a mutating verb always fails there — sandbox consent can live only in a pinned suite document.
  • Politeness. Tests execute sequentially, in registration order; the dialect defines no concurrent scheduling, so a suite cannot be a load test by accident. The run inherits the verifier's rate-limit posture toward the origin.
§A.8.6.5 Attested replay

The verdict for an api.qa/vitest@1 run MUST carry, beyond the A.8.5.2 counts: the executed artifact digest (the card pin it matched), the resolved artifact address and its discriminated kind (document or module), the runner, the environment name and its sandbox flag, the seed, the applied breaker limits with elapsed wall-clock and consumed CPU, the npm coordinate where the card asserts one (recorded as an assertion, never adjudicated — A.8.6.6), and a single folded result set — every declarative row (in document order) and every registered test (in registration order, by full describe/it name), each with its own pass/fail, duration, and, on failure, its named reason. Signed verdicts sign all of it, so a citation of a passing run names the exact bytes, the exact seed, and the exact per-test outcomes a replay must reproduce. A suite@1 verdict is unchanged by this appendix.

The honest limit of A.8.5.3 applies to this dialect unchanged: executing a suite proves the surface keeps its own published promise, and nothing about how demanding that promise is.

§A.8.6.6 Channels: every addressing is a digest-pinned module

However a card addresses its executable tests, what reaches the isolate is the same thing: a digest-pinned module the isolate instantiates. This version ratifies exactly three addressings, and no fourth:

  1. Inline — string members of the suite document (A.8.6.1). One served artifact, one digest over both natures, declarative rows and code together. The primary form for a surface whose suite is authored as a suite.
  2. Natively served module{ url, digest }: an ES module served at a versioned URL, typically the estate's module CDN (e.g. https://pkg.do/apis.vin@1.2.0/index.mjs). The primary form for an SDK: the property that publishes a package points its card at the exact versioned entry module — with its tests aboard, or with a test-registering export — and the digest pins it like any other artifact.
  3. npm identity assertion{ package, version, digest }: not a delivery channel at all, but a provenance claim — that the pinned served bytes are byte-identical to the entry module of package@version as published to npm. The registry is a verifiable mirror: anyone MAY check the assertion against the published tarball, offline and after the fact; the verifier is never required to contact a registry, and registry state MUST NOT influence a verdict. The verdict records the assertion; the digest, not npm, decided what ran.

The digest is the only authority. A host, a CDN, a registry, a version string — each is a locator or an assertion, and the digest adjudicates all of them. Two addressings that resolve to different bytes are not two channels for one suite; they are one pin kept and one claim broken, and the pin wins. A card MAY omit url only when the address is derivable from the coordinate under the native-serving scheme below (https://pkg.do/<package>@<version>/index.mjs on the reference module CDN); a verifier that cannot resolve the coordinate to an address refuses the card — fail, never skip.

Native serving is a package property's obligation. A property that publishes an npm package as part of its AXP surface (an AXP package property) MUST serve, for every published version, the package's runtime entry as an ES module (.mjs) and its type surface (.d.ts) natively — no registry, no tarball, no build step — at versioned URLs (e.g. https://pkg.do/apis.vin@1.2.0/index.mjs and https://pkg.do/apis.vin@1.2.0/index.d.ts), each answer carrying its { package, version, digest } provenance — the package name, the exact version, and the sha256 of the answer's own bytes, as response headers or an adjacent provenance document — so a reader can pin what it just fetched with no registry in the loop. A versioned URL MUST be immutable: the same address serves the same bytes, forever. What npm distributes, the property itself serves and pins; the registry mirrors the estate, never the reverse.

§A.8.7 Capability coverage — the declared-capability gate

The capability-coverage gate is citable as https://apis.ax/axp#coverage.

A published suite (A.8.5) proves the surface keeps the promises it chose to publish. What running it cannot establish — and A.8.5.3 says so — is that the published promise reaches everything the card claims: through apis-ax-axp@2.4.0 a card could declare an interface, a tool, and forty contract operations, publish a suite exercising three of them, and be admitted with every requirement green. This subsection closes exactly that gap, without touching the honest limit: it does not judge how demanding any single test is; it judges whether every capability the card declares has at least one passing test behind it. A card that declares a suite has said run this to trust me; this gate adds: and the run must reach everything else you said. Declared-but-untested is thereby inadmissible, and the top of any grading ladder a verifier layers over this spec is structurally unreachable for a surface whose own published suite does not prove what its card declares.

Arming. Verified by api.qa check capability-coverage, registered in the axp:optional-interfaces block against interfaces.testSuite and pinned since apis-ax-axp@2.5.0 as check-capability-coverage (kind: "check", must: "pass", appliesWhen: { "cardDeclares": "interfaces.testSuite" }). The arming key is the suite declaration, deliberately: this standard cannot demand a suite of a single-call lookup API (A.8.5), so it cannot demand coverage of one either — a card that omits interfaces.testSuite passes this requirement as not applicable and no existing surface's verdict changes. What the gate guarantees is conditional and total: no surface earns a suite-bearing verdict while a capability it declares stands unproven.

§A.8.7.1 The coverage domain

The domain — the set of capabilities that MUST be covered — is computed from documents the verifier already holds from the same run: the retrieved capability card and the published OpenAPI contract. It is the union of, each identified by one canonical string:

  1. Contract operations. Every operation the published OpenAPI contract declares. Identifier: openapi:<operationId>; an operation with no operationId is identified openapi:<METHOD> <path> — uppercase method, one space, the path template exactly as printed in paths. One canonical identifier per operation: the operationId form when it exists, the method-path form only otherwise.
  2. Callable interface entries. Every entry of interfaces.http MUST resolve, by method and pathname, to a contract operation — Clause 1 obliges the contract to describe every callable operation — and is covered exactly when that operation is covered; an entry that resolves to no contract operation is an uncovered capability by construction and fails. Exempt are entries addressing the faces this standard itself requires and unconditionally verifies on every run — /.well-known/agents.json, /openapi.json, /llms.txt (Clauses 6, 1, 2) — because a suite re-proving a door the verifier just verified would be ceremony. interfaces.mcp declared with a tools array contributes the identifier mcp:<toolName> for every declared tool; declared without one, the single identifier interfaces.mcp.
  3. Optional declared interfaces. Every optional declared interface of the card other than testSuite itself, identified interfaces.<name> (e.g. interfaces.digitalLink). testSuite is excluded because the suite is the instrument of proof, and A.8.5.2 already bars self-grading and recursion.

Presence-when-true makes the domain honest by construction: a contract that declares only live operations (Clause 1) yields a domain of only live capabilities, so nothing here demands a test of unshipped surface — and coverage claims about unshipped surface are refused below.

§A.8.7.2 The mapping — coverage map and coverage tags

Two mechanisms bind a suite's rows and tests to domain identifiers, and their results are unioned:

  • The coverage map (explicit). A suite document MAY carry the root member coverage: a JSON object whose keys are domain identifiers and whose values are non-empty arrays of test references. A test reference is the id of a declarative requirement row, or the full name of a registered test — the enclosing describe names then the it name, joined by " > " (space, greater-than, space) — exactly the name the folded result set records (A.8.6.5). A pinned module artifact (A.8.6.6) MAY export coverage, a plain object of the same shape, beside its tests. In either addressing the map lives inside the pinned artifact, so the card's digest covers it like everything else: a coverage claim is pinned bytes, never an out-of-band annotation.
  • Coverage tags (implicit). A declarative row or registered test whose id / full name contains a domain identifier in square brackets — [openapi:decodeVin], [mcp:lookup], [interfaces.digitalLink] — covers that identifier, and one name MAY carry several tags. A bracketed token is a tag iff it matches the identifier grammar (openapi:…, mcp:…, or interfaces.<lowerCamelName>); other bracketed text is not a tag and binds nothing. Tags let a suite authored as an ordinary vitest file carry its whole mapping in its test names, with no document member at all.

A capability is covered iff at least one row or test bound to it by either mechanism passed in this run. A failing, refused, or unexecuted row or test covers nothing.

Each of the following MUST fail the check:

  • a domain identifier with no passing bound row or test — the declared-but-untested case this subsection exists to catch, including the declared-and-tested-but-failing form;
  • a coverage member (or export) that is not a JSON object whose every value is a non-empty array of strings;
  • a coverage key or tag naming an identifier outside the domain — coverage of an undeclared capability is a claim about surface that does not exist, the exact shape presence-when-true bans;
  • a test reference that names no row and no registered test of this run.
§A.8.7.3 Verification profile
checkcard statemust observe
capability-coverageno interfaces.testSuiteskip — nothing is claimed, so nothing is judged; the pinned check-capability-coverage reports not applicable
capability-coveragedeclaredover the same single run A.8.5.2 already judges, every domain identifier (A.8.7.1) is covered by a passing row or test
capability-coveragedeclared, defective coverage member, tag, or referencefail — a defective claim is a claim (A.8)
capability-coveragedeclared, the A.8.5.2 run itself failedfail, by the named reason suite-run-failed — coverage cannot be read off a run that did not complete honestly
capability-coveragecard unreachable, non-2xx, or not a JSON objectfail — nothing was stated, so the requirement applies and fails closed (A.8)

Rules binding the verifier, each MUST:

  • one run, two judgments. capability-coverage is computed from the folded result set of the same published-test-suite execution; it MUST NOT trigger a second fetch of the artifact or a second execution. The domain is computed from the same retrieved card and contract bytes the run's other checks used, never re-fetched.
  • the verdict states the coverage. A verdict names the domain size, the covered count, and — on failure — every uncovered identifier; the coverage table (identifier → binding rows/tests with their outcomes) joins the attested verdict of A.8.6.5, and its suite@1 analogue carries the same table over declarative rows.
  • the check is armed by declaration, on the same terms as A.8.3 and A.8.5.2: absent the declaration the verdict is skip — never pass, which would launder silence into a claim, and never fail, which would punish a surface for a claim it did not make.
§A.8.7.4 What this does and does not judge

The honest limit of A.8.5.3 survives intact: one passing test per capability proves the presence of proof, not its depth, and a verifier still MUST NOT be cited as having judged a suite's rigour. Ambition is not machine-decidable. Completeness of the declaration↔proof mapping is — and since apis-ax-axp@2.5.0 it is decided: what was expressible through 2.4.0 and is inadmissible now is precisely a surface declaring a capability while publishing a suite that never exercises it.


§Changelog

  • 0.9.0 (2026-08-23) — a free property passes offers-402 vacuously. One rule, one ratification: apis-ax-axp goes 2.5.0 → 2.6.0 with the admission set unchanged at 24 → 24 requirements, retiring digest e4d10d4f… for a newly ratified one — the first ratification that moves the digest without moving a requirement row, because what moves is check semantics the contract binds to. The api.qa check offers-402 is now conditional on the observed pricing model, exactly as its pinned requirement check-offers-402 has been since 2.1.0: a surface whose observed Pricing Document (probes.pricing entry 0, A.2) declares "model": "free" and whose card declares no purchasable surface — no monetization.offers, no monetization.probe — passes the check vacuously (no 402 boundary exists to prove; the no-ask zone is satisfied trivially), so a fully conformant free property is no longer structurally barred from a clean check sheet by the boundary it correctly does not have. Fail-closed on both edges: an unobserved or undetermined model keeps the hard fail (not-applicable must be proven by the observed value), and a free card that declares either monetization member has made a claim and is judged strictly — while the metered half of Clause 5 is untouched: monetization.offers, positive hard ceilings, and the behaviorally probed 402 OFFER boundary all still bind every metered surface. The breaking population is empty; the one population whose verdict improves is the free property with nothing to sell.
  • 0.8.0 (2026-08-08) — capability coverage: an A+ is structurally impossible for an incomplete product. One change, one ratification: apis-ax-axp goes 2.4.0 → 2.5.0 and 23 → 24 requirements, retiring digest dd3e5941… for a newly ratified one. The one added requirement is check-capability-coverage (new normative Appendix A.8.7, api.qa check capability-coverage), declaration-armed on interfaces.testSuite — the key the registry already carries, because a declaration is one claim and each registered check judges one facet of it: published-test-suite that the suite is kept, capability-coverage that it reaches everything else the card declares. Declared, every capability of the card — every contract operation (openapi:<operationId> / openapi:<METHOD> <path>), every interfaces.http entry (resolving to its contract operation; the always-verified faces exempt), every declared MCP tool (mcp:<toolName>), and every other optional declared interface (interfaces.<name>) — MUST be exercised by at least one passing test of the same single A.8.5.2 run: one run, two judgments, no second execution. The test↔capability mapping is the pinned artifact's own: an explicit coverage root member (or module export) mapping identifiers to row ids / full folded test names, unioned with implicit square-bracket coverage tags in row ids and test names; a coverage key, tag, or reference naming anything outside the domain fails (presence-when-true applies to coverage claims), and uncovered, defective, or dangling always fails — never skips. The verdict carries the coverage table (domain size, covered count, every uncovered identifier on failure) in the A.8.6.5 attested replay. The honest limit of A.8.5.3 is untouched: coverage judges the completeness of the declaration↔proof mapping, never the rigour of any test. Registered and pinned in the same ratification — the registered-unpinned interval of 2.3.0/2.4.0 was a demonstration, never an obligation — and the breaking population is again exactly one: a card that declares a suite which does not cover its card's declared capabilities loses admission; a card that omits interfaces.testSuite gains one not-applicable result and no new way to fail.
  • 0.7.0 (2026-08-08) — the published test suite is pinned, and it can execute. Two changes, one ratification. The pin: apis-ax-axp goes 2.3.0 → 2.4.0 and 22 → 23 requirements, retiring digest 9063cb3e… for a newly ratified one. The one added requirement is check-published-test-suite, pinning Appendix A.8.5's interfaces.testSuite in the same declaration-armed form (appliesWhen: { "cardDeclares": "interfaces.testSuite" }) the Digital Link interface received at 2.3.0 — closing the registered-unpinned state 0.6.0 deliberately shipped as the demonstration that eligibility and admission are different things. The breaking population is again exactly one: a card that declares a suite and does not keep it loses admission; a card that omits the interface gains one not-applicable result and no new way to fail. The dialect: a second runner value, api.qa/vitest@1 (new normative Appendix A.8.6), joins api.qa/suite@1 (which is unchanged and stays valid — the extension is additive, discriminated by the card's runner, and an older verifier refuses the new value fail-closed under the already-ratified unknown-runner rule). An executable suite carries its tests as string members of the same digest-pinned JSON document (tests required, module optional, importable as "suite:module") — one served artifact, one pin covering both natures, no separate code-digest bookkeeping. The dialect is a guaranteed vitest subset (describe/it/expect + async; imports closed to "vitest", "suite:env" and "suite:module"; no node built-ins, snapshots, mocking, dynamic import, or runtime code generation), so the same file runs identically under local vitest and the hosted verifier — parity by construction, one shared harness implementation, stated normatively (A.8.6.2). Execution replaces A.8.5's blanket the-suite-is-data-never-code prohibition with named invariants (A.8.6.3): a fresh single-use isolate per run (Cloudflare Worker Loader on the reference verifier), zero ambient authority, a network floor — no cloud-metadata, link-local, loopback, private-range, or verifier-internal destination, with full external egress above the floor because cross-estate composition is what the dialect exists to test (a drafted graded-origin-only scoping is recorded as rejected) — a metered circuit-breaker deadline (default 300 s wall / 60 s CPU, billed to the requesting account, account-raisable; suite@1's fixed 20 s stays suite@1's only), because the runner is a paid-tier capability: the paid tier, not scarcity, is the gate, so every remaining bound is an abuse circuit-breaker, never a ration (4 MiB output, 1000 rows+tests combined, 1 MiB document, 4 MiB module artifact), executed bytes hash-then-instantiated from the card-pinned buffer (the non-negotiable integrity guarantee: run this exact code, local equals hosted), and fail-closed totality. Determinism and consent are normative (A.8.6.4): seeded Math.random with the seed in the verdict, sequential execution, and mutating verbs legal only against an environment the suite itself declares "sandbox": true. Verdicts are replay-attested (A.8.6.5): digest, addressing, seed, environment, breaker limits with elapsed wall and CPU, and one folded result set of declarative rows and code tests, all signed. The channels collapse (A.8.6.6): every addressing of executable tests is a digest-pinned module the isolate instantiates — inline string members of the suite document; a natively served ES module at a versioned URL ({ url, digest }, the estate module CDN, primary for the SDK case); or an npm coordinate ({ package, version, digest }) as an identity assertion that the served bytes equal the published ones — npm a verifiable mirror, never in the loop, the digest the only authority. The card seam widens accordingly: interfaces.testSuite becomes { url?, package?, version?, export?, digest, environment?, runner? }, with at least one address required, the same-origin url restriction retired (the digest, not the host, was always the provenance), and an AXP package property obliged to serve every published version's .mjs runtime and .d.ts types natively at immutable versioned URLs carrying { package, version, digest } provenance.
  • 0.6.0 (2026-08-06) — optional declared interfaces become a mechanism, and the first one is pinned. Appendix A.8 is generalized from a single interface to the normative rule for all of them: how a card declares one (a lowerCamelCase object member of interfaces, where presence is the declaration), that declaring it invokes strict verification while omitting it is full conformance, that a defective declaration fails rather than skips (there is no value meaning "no" — a card that means no omits the key), and that an unreadable card is not a declaration of anything, so the requirement applies and fails closed. The evasion guard is normative: an optional interface is an additive capability only and MUST NOT relieve a surface of any obligation of Clauses 1–7. A pinned requirement may be declaration-armed — "appliesWhen": { "cardDeclares": "interfaces.<name>" }only when its check is registered in the new machine-extractable A.8 axp:optional-interfaces block against exactly that card key, only on kind: "check" (so the A.4 probe and A.6 surface requirements are categorically un-armable), and never on a check any of Clauses 1–7 binds; a verifier refuses a spec that violates either constraint rather than returning a lenient verdict. The admission set and the ratified digest MOVE, deliberately and for a recorded reason: apis-ax-axp goes 2.2.0 → 2.3.0 and 21 → 22 requirements, retiring digest d9bfabee… for a newly ratified one. The one added requirement is check-digital-link-resolver — the Appendix A.8 Digital Link interface of 0.5.0, which 0.5.0 left unpinned only because no non-breaking way to express "verified when declared" existed. This release is that mechanism, so the interface is now enforced where it is claimed. A card that does not declare interfaces.digitalLink gains one extra result, reported not applicable, and no new way to fail; a card that does declare it and does not honour it loses admission. That single population is the entire breaking surface, and it is the intended effect. A.4, A.6, the twenty-one existing requirement ids, and check-card-interfaces-linked are untouched. Also new: interfaces.testSuite (Appendix A.8.5) — a card MAY publish its own digest-pinned, declarative conformance suite, run GET/HEAD-only with writes disabled against the card's own origin under a 25-requirement cap, never executed as code, and the verifier states what it did not judge (a suite's ambition). It is registered and not pinned, the standing example that eligibility is not admission. Appendix A.8.5 is stated in full, in the shape the rest of this document uses: A.8.5.1 what declaring it obliges — retrievable, digest-matching, parsing in the declared dialect, defining the selected environment, and every requirement in it passing, because a check that only confirmed a file exists would verify nothing about the workflows the interface is for; A.8.5.2 the verification profile, four card states against one published-test-suite verdict, with the same defective-declaration and unreadable-card rows A.8.3 carries; and A.8.5.3 the honest limit. Also normative there, and the reason the interface cannot become an obligation by the back door: omission is conformance — a simple CRUD or lookup API has no workflow to publish and MUST NOT be asked for one — and a declared suite does not contribute to Clause 6's non-empty interfaces.http / interfaces.mcp demand, because a suite is a way to audit this origin, never a way to call it, leaving check-card-interfaces-linked unchanged. Clause 6 now names published-test-suite in its Machine-verified by line with the pinned requirement it has — null — so a reader of the clause alone cannot mistake registration for admission.
  • 0.5.0 (2026-08-06) — the first optional declaration. A capability card MAY name a GS1 Digital Link resolver as a third interface, interfaces.digitalLink (Clause 6, new normative Appendix A.8), pointing at the GS1 Resolver Description File at /.well-known/gs1resolver. Declared, that file MUST be retrievable, MUST validate against GS1's published description-file schema, and MUST describe the declaring origin; AXP restates none of GS1's vocabulary and verifies none of GS1's resolution behavior — linkType negotiation, RFC 9264 linksets and redirect semantics stay GS1's standard and GS1's test suite. Not declaring it is fully conformant: api.qa check digital-link-resolver is armed by declaration and skips otherwise, and it is not pinned — the ratified admission spec stays apis-ax-axp@2.2.0 at twenty-one requirements, digest d9bfabee… unchanged, so no conforming surface begins failing and check-card-interfaces-linked is untouched. Conformance gains the general rule that an optional declaration never widens admission. A.8.3 binds the verifier too: no run-time fetch of GS1's schema, a digest-pinned copy named in the verdict, and no assertion of a draft-07 format the schema only annotates.
  • 0.4.0 (2026-07-31) — Clause 3 formalizes the estate content-negotiation law as normative AXP: three faces (HTML / JSON / markdown) on every dereferenceable address, deterministic selection (address → Accept → client-class defaults with Sec-Fetch browser detection, agent-UA markdown, JSON for everything else), mandatory Link rel="alternate" sibling advertisement, JSON face SHOULD be JSON-LD with a resolvable context; new normative Appendix A.7 (algorithm, q-values, HEAD, no-406, face extensions, agent-token list); four new pinned check requirements (check-conneg-accept, check-conneg-client-class, check-conneg-alternates, check-conneg-forced-face) — pinned spec apis-ax-axp@2.2.0. Supersedes 0.3.0's Accept: text/html-gets-non-HTML posture (see the Clause 3 revision note); the machine-client invariant is unchanged. Clarifies Appendix A.5: the probe-manifest monetization.probe demand applies to metered cards only.
  • 0.3.0 (2026-07-21) — pinned spec apis-ax-axp@2.1.0: seven RFC-2119 clauses, each bound to a discriminating check or card-declared behavioral probe; machine-legible-home and card-interfaces-linked pinned as their own requirements; metering obligations appliesWhen-gated on the observed pricing model.

How AXP relates to the surfaces around it

Only relations the source material actually states are listed. Nothing here is a roadmap.

apis.ax
Publisher of this standard and operator of the catalog gate: a Listing is admitted to the apis.ax catalog iff api.qa returns passed: true against the pinned spec. apis.ax/axp continues to serve the normative text as a machine-register surface; axp.org.ai is the canonical home it points to.
api.qa
The independent verifier. Every clause names the api.qa check or behavioral probe that decides it. api.qa is held outside the building fleet's write access — a fleet that can edit its own tests Goodharts them.
schema.org.ai
The linked-data context this document is written in. The spec declares $context: https://schema.org.ai and $type: Spec, so the standard is itself an addressable typed document, not only prose about one.
MDXLD
The document form: typed $id/$type/$context claims above, load-bearing prose below, in one markdown tree. This page is a rendering of that tree; /protocol.md is the tree.
id.org.ai
The first surface to bind itself to AXP: it holds a pinned specs/axp-acceptance.spec.json — an api.qa PinnedSpec whose Group A and Group B are the AXP discovery and honest-machine-contract surfaces — held outside its implementing fleet's write access. It is inside this estate, so it is not one of the two external binders the 1.0 gate requires.

About this page

axp.org.ai is a page surface — the human register. AXP governs API surfaces; by Clause 3 and its cited ADRs, HTML is a page concern and human-register pages live outside the protocol's scope. The machine faces of this host are /llms.txt and /protocol.md, plus the two conformance documents.

The clause text above is not a paraphrase. It is packages/apis.ax/PROTOCOL.md from the ax repository at commit 4fdc54ab7e48 (2026-07-22), vendored 2026-07-27 and rendered. Its sha256 is f4d138451b7df3ae…. The pinned conformance spec's ratified digest is a9a1197c439d708b…; production admission runs api.qa in attested mode against a digest supplied from outside the building fleet's write access.

This page makes no external requests. No fonts, no scripts, no analytics are fetched from anywhere. The type is the platform's own.