§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$contextor@contextmember 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):
- 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 theAcceptheader. - The
Acceptheader infers. AnAcceptheader naming a face media type (text/html;application/jsonorapplication/ld+json;text/markdown) selects that face. - The client class defaults. When neither the address nor the
Acceptheader selects a face (Accept: */*, absent, or unsatisfiable): a browser — detected viaSec-Fetch-*request headers, never byUser-Agentsniffing — receives HTML; a known agentUser-Agent(Appendix A.7 token list) receives markdown; everything else, including barecurl, 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/html — MUST 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— status200; the request succeeded and the body carries substantive content.EMPTY— status200; the request succeeded and the truthful result is an empty collection.BLOCKED—401or403; a permission boundary (not permitted, not authenticated).OFFER— status402; a payment or ceiling re-authorization boundary. The body carries anidortitleplus aprice,checkoutUrl, oralternativesmember (Appendix A.5). A402MUST 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:
| channel | minimum | meaning |
|---|---|---|
keyless | 1 | answers 200 OK to an anonymous caller (Clause 7) |
knownEmpty | 2 distinct | answers 200 EMPTY truthfully (Clause 4) |
knownForbidden | 2 distinct, disjoint from knownEmpty | answers 401/403 BLOCKED (Clause 4) |
pricing | 1 | answers 200 with a Pricing Document — free or metered (Clause 5) |
overCeiling | 1 (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 concreteinterfaces.httpentry); - distinctness of probe URLs is judged on the fetched identity: URL fragments
are stripped, so
/e?a=1and/e?a=1#dupcount as one probe; - every
knownEmptyandknownForbiddenpathname is also observed answering200OKin the same verification run (pathMustServeOk, Clause 4) — a dedicated decoy endpoint that can only answerEMPTY/BLOCKEDdoes not satisfy Clause 4; - the manifest never declares expectations — the verifier derives the
over-ceiling, below-ceiling, and zero amounts from the observed
hardCeilingand overwrites the declaredparam; - when
probes.pricingdeclares 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.
[
{ "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 anidortitle, plus aprice,checkoutUrl, oralternativesmember.monetization.probe—{ "method"?: "GET", "url": "<same-origin>" }: a URL that answers HTTP402with a typedOFFERbody ("type": "OFFER"plus id/title plus price | checkoutUrl | alternatives). The URL MUST be a same-originGET; 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.
[
{ "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
| face | media type | register |
|---|---|---|
| HTML | text/html | the page, for human eyes |
| JSON | application/json (the face SHOULD be JSON-LD, also answerable as application/ld+json) | the typed body, for programs |
| markdown | text/markdown | the 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
Acceptheader — the address wins over every other selection input; - MUST answer
404when 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:
- Address. The request path is a face address (A.7.2) → serve that face.
- Accept. Consider the face media types explicitly named in the
Acceptheader (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 withq=0is 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. - Client class. No face was named (
Accept: */*, an absentAcceptheader, only excluded/unrecognized types): a. the request is a browser navigation —Sec-Fetch-Mode: navigate, orSec-Fetch-Dest: documentoriframe. Browser detection MUST useSec-Fetch-*request headers and MUST NOT be inferred from theUser-Agent→ serve HTML; b. theUser-Agentcontains a known agent token (A.7.4, case-insensitive substring) → serve markdown; c. everything else — barecurl, 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 unsatisfiableAcceptfalls through to step 3. - The response MUST declare the served face in
Content-Typeand SHOULD sendVary: Accept, Sec-Fetch-Mode, Sec-Fetch-Dest, User-Agentso caches never cross-serve faces. HEADMUST be answered whereverGETis, by the same algorithm, with the same status,Content-Type, andLinkheaders, and no body.- Selection MUST be deterministic: the same request (method, path,
Accept,Sec-Fetch-*,User-Agentclass) 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 · agentThe 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.
§A.7.5 Link alternates
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:
| check | request(s) | must observe |
|---|---|---|
machine-legible-home | home + sampled typed bodies under Accept: */*, Accept: application/json, and an agent User-Agent | machine-legible non-HTML, every one |
conneg-accept | home under Accept: text/html / application/json / text/markdown | the named face, each time (the JSON face's $context/@context is reported informationally) |
conneg-client-class | home under Accept: */* three ways: bare · with Sec-Fetch-Mode: navigate + Sec-Fetch-Dest: document · with a known agent User-Agent | JSON · HTML · markdown, respectively |
conneg-alternates | the three home face responses | each carries Link rel="alternate" naming both sibling faces |
conneg-forced-face | each Link-advertised face address, fetched with a contradictory Accept | the 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— neverpass, which would launder silence into a claim, and neverfail, 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, notfalse, not{}, not"". A card that means no omits the key. This is what stops an optional declaration from becoming a way to buy a freeskip.
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 theaxp:optional-interfacesblock below against exactly that card key. A requirement whosecheckis unregistered, or whosecardDeclaresis 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.
cardDeclaresis legal only on akind: "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.cardDeclarestests 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
appliesWhenremains legal and means "I demand this of everyone"; itsskipthen 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.
[
{ "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.
§A.8.1 The GS1 Digital Link resolver — the declaration
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" }
}
}| member | required | meaning |
|---|---|---|
wellKnown | no | absolute 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
200with 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
resolverRootMUST 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
| check | card state | must observe |
|---|---|---|
digital-link-resolver | no interfaces.digitalLink | skip — nothing is claimed, so nothing is judged; the pinned check-digital-link-resolver reports not applicable |
digital-link-resolver | declared | the 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-resolver | present but not a JSON object | fail — a defective claim is a claim (A.8) |
digital-link-resolver | card unreachable, non-2xx, or not a JSON object | fail — 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— neverpass, which would launder silence into a claim, and neverfail, which would punish a surface for a claim it did not make. Sinceapis-ax-axp@2.3.0the apis.ax admission spec turns thatskipinto 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 noappliesWhen, making the interface mandatory for that target, where askipfails closed. - a verifier MUST NOT fetch the schema from
ref.gs1.orgat 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
formatis an annotation and not an assertion. A verifier MUST NOT fail a description file on aformatthe 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"
}
}
}| member | required | rule |
|---|---|---|
url | conditional | absolute 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. |
package | conditional | an 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. |
version | conditional | the exact published version of package (never a range). REQUIRED when package is present; refused without it. |
export | no | the 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. |
digest | yes | "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). |
environment | no | the 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). |
runner | no | the 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 frompackage@version— and MUST answer200; - the artifact's exact bytes MUST hash to the declared
digest; - the artifact MUST be well-formed in the declared
runnerdialect and the discriminated kind (document or module), and a document MUST define the selectedenvironment; - 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
| check | card state | must observe |
|---|---|---|
published-test-suite | no interfaces.testSuite | skip — nothing is claimed, so nothing is judged; the pinned check-published-test-suite reports not applicable |
published-test-suite | declared | the suite is retrievable, hashes to the declared pin, parses, defines the selected environment, and every one of its requirements passes |
published-test-suite | declared, 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-suite | present but not a JSON object, or defective in any member above | fail — a defective claim is a claim (A.8) |
published-test-suite | card unreachable, non-2xx, or not a JSON object | fail — 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— neverpass, which would launder silence into a claim, and neverfail, 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 neitherurlnorpackage, apackagewithout its exactversion, apackageunder a declarative runner, an unknownrunner, an undefinedenvironment— each MUST fail. A card meaning "I publish no suite" omits the key; a verifier that let a broken declarationskipwould 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@1run has writes disabled, and any requirement whose resolved method is notGETorHEADMUST 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 anapi.qa/vitest@1document. 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@1dialect — and for the declarative rows of anapi.qa/vitest@1document, which keep unchangedsuite@1engine 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@1run 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. Anapi.qa/vitest@1run 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@1dialect:kind: "endpoint"andkind: "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 demandpublished-test-suiteof 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:
| member | required | rule |
|---|---|---|
tests | yes | a string: ES module source in the A.8.6.2 subset, registering the executable tests. Absent, empty, or not a string MUST fail. |
module | no | a 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:
requirementsMAY be absent or empty in anapi.qa/vitest@1document. 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 emptysuite@1suite 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
testsandmodulestrings 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 exceptfetch. 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.254and equivalents), link-local ranges (169.254.0.0/16,fe80::/10), loopback (127.0.0.0/8,::1), private and reserved ranges (RFC 191810.0.0.0/8,172.16.0.0/12,192.168.0.0/16; CGNAT100.64.0.0/10; ULAfc00::/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— neverskip, 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.randomis 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 environmentsandboxis 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:
- 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.
- 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-registeringexport— and the digest pins it like any other artifact. - 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 ofpackage@versionas 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:
- Contract operations. Every operation the published OpenAPI contract
declares. Identifier:
openapi:<operationId>; an operation with nooperationIdis identifiedopenapi:<METHOD> <path>— uppercase method, one space, the path template exactly as printed inpaths. One canonical identifier per operation: theoperationIdform when it exists, the method-path form only otherwise. - Callable interface entries. Every entry of
interfaces.httpMUST 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.mcpdeclared with atoolsarray contributes the identifiermcp:<toolName>for every declared tool; declared without one, the single identifierinterfaces.mcp. - Optional declared interfaces. Every optional declared interface of the
card other than
testSuiteitself, identifiedinterfaces.<name>(e.g.interfaces.digitalLink).testSuiteis 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 theidof a declarative requirement row, or the full name of a registered test — the enclosingdescribenames then theitname, 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 exportcoverage, a plain object of the same shape, beside its tests. In either addressing the map lives inside the pinned artifact, so the card'sdigestcovers 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:…, orinterfaces.<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
coveragemember (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
| check | card state | must observe |
|---|---|---|
capability-coverage | no interfaces.testSuite | skip — nothing is claimed, so nothing is judged; the pinned check-capability-coverage reports not applicable |
capability-coverage | declared | over 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-coverage | declared, defective coverage member, tag, or reference | fail — a defective claim is a claim (A.8) |
capability-coverage | declared, the A.8.5.2 run itself failed | fail, by the named reason suite-run-failed — coverage cannot be read off a run that did not complete honestly |
capability-coverage | card unreachable, non-2xx, or not a JSON object | fail — nothing was stated, so the requirement applies and fails closed (A.8) |
Rules binding the verifier, each MUST:
- one run, two judgments.
capability-coverageis computed from the folded result set of the samepublished-test-suiteexecution; 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@1analogue 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— neverpass, which would launder silence into a claim, and neverfail, 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-402vacuously. One rule, one ratification:apis-ax-axpgoes 2.5.0 → 2.6.0 with the admission set unchanged at 24 → 24 requirements, retiring digeste4d10d4f…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 checkoffers-402is now conditional on the observed pricing model, exactly as its pinned requirementcheck-offers-402has been since 2.1.0: a surface whose observed Pricing Document (probes.pricingentry 0, A.2) declares"model": "free"and whose card declares no purchasable surface — nomonetization.offers, nomonetization.probe— passes the check vacuously (no402boundary 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 eithermonetizationmember 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 probed402OFFERboundary 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-axpgoes 2.4.0 → 2.5.0 and 23 → 24 requirements, retiring digestdd3e5941…for a newly ratified one. The one added requirement ischeck-capability-coverage(new normative Appendix A.8.7, api.qa checkcapability-coverage), declaration-armed oninterfaces.testSuite— the key the registry already carries, because a declaration is one claim and each registered check judges one facet of it:published-test-suitethat the suite is kept,capability-coveragethat it reaches everything else the card declares. Declared, every capability of the card — every contract operation (openapi:<operationId>/openapi:<METHOD> <path>), everyinterfaces.httpentry (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 explicitcoverageroot 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 omitsinterfaces.testSuitegains 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-axpgoes 2.3.0 → 2.4.0 and 22 → 23 requirements, retiring digest9063cb3e…for a newly ratified one. The one added requirement ischeck-published-test-suite, pinning Appendix A.8.5'sinterfaces.testSuitein 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 secondrunnervalue,api.qa/vitest@1(new normative Appendix A.8.6), joinsapi.qa/suite@1(which is unchanged and stays valid — the extension is additive, discriminated by the card'srunner, 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 (testsrequired,moduleoptional, 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): seededMath.randomwith 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.testSuitebecomes{ url?, package?, version?, export?, digest, environment?, runner? }, with at least one address required, the same-originurlrestriction retired (the digest, not the host, was always the provenance), and an AXP package property obliged to serve every published version's.mjsruntime and.d.tstypes 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.8axp:optional-interfacesblock against exactly that card key, only onkind: "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-axpgoes 2.2.0 → 2.3.0 and 21 → 22 requirements, retiring digestd9bfabee…for a newly ratified one. The one added requirement ischeck-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 declareinterfaces.digitalLinkgains 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, andcheck-card-interfaces-linkedare 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 onepublished-test-suiteverdict, 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-emptyinterfaces.http/interfaces.mcpdemand, because a suite is a way to audit this origin, never a way to call it, leavingcheck-card-interfaces-linkedunchanged. Clause 6 now namespublished-test-suitein 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 —linkTypenegotiation, RFC 9264 linksets and redirect semantics stay GS1's standard and GS1's test suite. Not declaring it is fully conformant: api.qa checkdigital-link-resolveris armed by declaration andskips otherwise, and it is not pinned — the ratified admission spec staysapis-ax-axp@2.2.0at twenty-one requirements, digestd9bfabee…unchanged, so no conforming surface begins failing andcheck-card-interfaces-linkedis 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-07formatthe 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 withSec-Fetchbrowser detection, agent-UA markdown, JSON for everything else), mandatoryLink 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 specapis-ax-axp@2.2.0. Supersedes 0.3.0'sAccept: text/html-gets-non-HTML posture (see the Clause 3 revision note); the machine-client invariant is unchanged. Clarifies Appendix A.5: the probe-manifestmonetization.probedemand 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-homeandcard-interfaces-linkedpinned as their own requirements; metering obligationsappliesWhen-gated on the observed pricingmodel.