What the endpoint does
The route is read-only, unauthenticated and has no side effects. It serves a key document already in the store, verifies nothing and reports no verification level. The spec-required query form, GET /v1/keys?key_id=<percent-encoded logical id>, returns a byte-identical document built by the same response builder.
The path segment is the wire form of the identifier, a bare ULID, while the store is keyed on the logical form agenid:key:<ULID>. The only translation between them is parseKeyReference from @agenid/core. A key that was never published is 404 key_not_found; a malformed reference or a URI fragment is 400 invalid_key_id. Documents are re-validated against the strict KeyDocument schema before being served, and error bodies do not quote the caller's input back.
Why two paths matter
If the only place to fetch an operator's key were the registry, the registry could substitute a key it controls and mint apparently valid identities. Two-path discovery removes that position. The registry can still serve a key, but a verifier who also fetches the operator's copy will see any disagreement, and disagreement is a hard fail.
The honest boundary: the .well-known copy is published by the operator. Where an operator publishes none, a verifier has one source, not two, and should not treat the missing copy as agreement.
The double-decode defect
Measured against production, a twice-encoded key ULID returned 200, while the identical code returned 400 under a local next start. The earlier guard rejected a reference that still carried a % after the transport decode, which assumed exactly one decode had happened. On Vercel the platform normalizes the path before routing and Next decodes the dynamic segment again, so the guard was inspecting a value two decodes from the wire.
The rule is now stated on the raw request target, where it needs no decode of its own: the path segment must be the literal wire form, and a bare key ULID contains no percent sign. The ?key_id= value is decoded by the registry itself, exactly once. A repeated key_id parameter is refused, in either order, because an ambiguous request cannot promise the identical document the spec requires.
One surface, not two
The reference implementation has two registries, a Fastify one and the Next one that production serves, and they had diverged: only the Next version re-validated stored documents and kept caller input out of errors. Both now call one resolveKeyDocument. A parity test drives the real handlers of both frameworks from raw request-target fixtures and requires identical status, body and reason.
Fastify framework errors no longer reflect the request target. /v1/keys is read-only on both frameworks, with every mutating method answering 405 with an Allow header, and authority key publication moved to POST /v1/authority/keys.
What it does not do, and how to try it
Key discovery serves keys. It says nothing about levels. The reference deployment at agenid.com issues nothing above L1_REGISTERED today: L2 and higher require a VerificationAssertion signed by a root authority key, and that key has not been created yet. L5 is reserved and is not issuable at all. Which agent is acting is not the same question as what it is allowed to do. AgenID answers the first. Protocol v1.1.1 defines no signed authorization object, so permission stays with the operator and with the party on the other side.
Register an agent at /issue, open its JSON envelope from the Verification Card, and follow operator_key.discovery to both paths. The two-path key discovery entry explains the comparison step by step.