The problem
Agent traffic gets admitted or refused on the strength of a signature, and the signature is checked against a public key fetched from a directory on the web. That is the standard design across several specifications at once.
The Visa Trusted Agent Protocol specification, published at https://developer.visa.com/capabilities/trusted-agent-protocol/trusted-agent-protocol-specifications, publishes its keys at https://mcp.visa.com/.well-known/jwks and states the consequence plainly: "If the public key cannot be retrieved or the public key has expired, the message should be blocked". The Verifiable Intent specification, at https://verifiableintent.dev/spec/, puts a Credential Provider JWKS on the same footing. The IETF web bot authentication architecture draft, at https://www.ietf.org/archive/id/draft-meunier-web-bot-auth-architecture-05.html, describes the pattern for automated clients on the open web. The A2A specification, at https://a2a-protocol.org/latest/specification/, points at a key set through a jku header. The Mastercard agentic commerce guide, at https://developer.mastercard.com/merchant-cloud/documentation/tutorials-and-guides/agentic-commerce-guide/21/index.md, sits in the same shape.
Every one of those designs makes an admission decision depend on a document that changes without notice and keeps no history.
Now the dispute. A site protection provider blocked an agent's traffic for a day. The agent operator says its requests were correctly signed by a key that was published the whole time. The provider says the key was not in the directory when it looked. The publisher rotated its key set twice that week and serves only the current version. There is no earlier version to point at.
Both parties are describing a document that no longer exists in the form either of them saw. Each has its own logs, written by a party with an interest in the answer, about somebody else's document. Nobody can produce the document.
The reverse case is more common. A payment is disputed. The merchant says it verified the agent's signature against the scheme's key directory before accepting the order. The scheme says the key was revoked and out of the directory by then. If the merchant read the directory before the rotation landed, the merchant is right. If it cached past the rotation, the scheme is right. Telling the two apart needs what the directory served at the instant the merchant read it, and that is what nobody kept.
Autonomy makes this constant rather than occasional. A human sees a blocked page and retries. An agent fleet makes millions of signed requests, and a directory read stale by ninety seconds affects a batch of them at once, in a way nobody notices for weeks.
There is a second failure behind the first, which is time. Any party can write down the instant at which it read a document, and any party can write down a different instant. If the read instant is the reader's own clock, unbound, the record is only as good as the reader's honesty about time.
The third failure is privacy, and it is why the obvious fix does not ship. That fix is for an observer to archive the directory and publish the archive. Key directories carry key identifiers that map to customers, and an archive of who had which key when is a customer list with a timeline attached. No scheme will publish that.
What the receipt binds
The schema is at https://thehiveryiq.com/.well-known/schemas/directory-state-v1.json. The signed body has receipt_type, schema, the boundary, and one object, directory_state.
| Field | Meaning |
|---|---|
observation_id | Identifier matching ds_ plus 16 to 64 lowercase hex characters. |
recorded_at | The UTC instant this record was written. |
directory.directory_id | Identifier for the directory observed, matching dy_ plus hex. |
directory.directory_url | The https url read, up to 512 characters. |
directory.directory_class | One of jwks_well_known, payment_network_key_directory, signature_agent_directory or credential_provider_jwks. |
observed_at | The instant the directory was read. |
time_reference.reference_id | Identifier for the external time reference, matching tr_ plus hex. |
time_reference.reference_digest | Digest of the confidential time reference snapshot. |
time_reference.drift_bound_ms | The declared drift bound in milliseconds, from 1 to 86400000. |
observed_drift_ms | The widest offset measured against that reference, from 0 to 86400000. |
drift_class | Either within_declared_bound or exceeds_declared_bound. |
content_digest | Digest of the whole retrieved content record. |
key_identifier_count | How many key identifiers the directory held, from 0 to 4096. |
key_commitments | One entry per key, each with an identifier commitment, a key commitment and a key class. |
key_identifier_set_digest | Digest over the sorted commitment set. |
queried_key_commitment | Commitment to the one key identifier being asked about. |
presence_verdict | Either queried_key_present or queried_key_absent. |
observer_role | The fixed label non_publishing_observer. |
evidence_access_class | One of confidential_independent, issuer_only or public_commitment_only. |
salt_commitment | Commitment to the 32 byte salt. |
Each key commitment entry carries a key_class drawn from ed25519_verification, ecdsa_p256_verification, rsa_verification and unrecognised. The last one matters. An observer that could only record key types it recognised would drop the entries a dispute is most likely to be about.
What is deliberately not in the signed body
No directory content, no key identifier, no key material, no key material digest, no queried key identifier, no time reference snapshot, no salt. The receipt carries digests, one url, one count, enumerated classes and two instants.
Five constructions in src/typed/directory-state.js do the work. The salt commitment is SHA-256 over the UTF-8 label directory.state/salt/v1, a zero byte, and the raw 32 salt bytes.
The content digest is SHA-256 over the canonical JSON of an object carrying the domain directory.state/content/v1 and the retrieved content record. That record must hold exactly directory_id, retrieved_url, key_count and entries, and each entry exactly key_identifier, key_material_sha256 and key_class. Duplicate identifiers are refused, and the record has no field that could carry raw key material.
Per key, two keyed digests are computed with HMAC-SHA256 under the raw salt bytes. The identifier commitment uses the label directory.state/key-identifier/v1 over the canonical JSON of the key identifier alone. The key commitment uses the label directory.state/key-material/v1 over the canonical JSON of the whole entry. Splitting them makes rotation legible. When a directory rotates the material behind a stable key identifier, the identifier commitment stays the same and the key commitment changes, and a later reader sees that without learning either value.
The commitment set is then sorted ascending by identifier commitment, so the order the directory served its keys in carries nothing into the receipt. The set digest is SHA-256 over the canonical JSON of the domain directory.state/key-identifier-set/v1 and the sorted entries.
Canonical JSON is the shared rule: keys sorted recursively, arrays kept in order, no whitespace, and every code unit above U+007F escaped as a lowercase \uXXXX sequence, then digested as lowercase hex SHA-256 over the UTF-8 bytes.
The time reference snapshot is confidential too. It must hold exactly reference_id, a source_class drawn from roughtime, ntp_stratum_one, rfc3161_authority and gnss_receiver, a retrieval offset and a completion offset. The observed drift is the widest absolute value of those two offsets, which is the conservative choice: a read that started close to the reference and finished far from it is reported at its worst point.
The receipt envelope uses the shared Ed25519 construction over the ASCII string hive-receipt <receipt_id> <payload_sha256> <ts>, with hive-receipt as the domain separation label.
The gates
Verification runs in this order and stops at the first failure.
- SCHEMA. Validates the envelope against the directory state v1 schema, including the boundary constant, the four directory classes, the four key classes and the drift bounds. A failure means the artifact is malformed.
- ISSUER_KEY_MATCH. Resolves
envelope.key_idin the trusted keyring and checks the key was authorised for this receipt type at the envelope timestamp. A failure means the signer is not trusted here. - PAYLOAD_DIGEST_VALID. Recomputes the canonical digest of
signed_bodyagainstpayload_sha256. A failure means the body was altered after signing. - SIGNATURE_VALID. Requires Ed25519 and verifies the envelope signature with the resolved key. A failure means that key did not produce it.
- OBSERVATION_INSTANT_ORDERED. Requires
observed_atandrecorded_atto be valid UTC instants with the observation at or before the record. A failure means the record describes a read that had not happened yet. - SALT_BINDING. Requires the disclosed salt to be 64 lowercase hex characters and to recompute to
salt_commitment. A failure means none of the keyed digests can be checked. - DIRECTORY_REFERENCE_DECLARED. Requires the directory identifier, an https url and a declared directory class. A failure means the receipt does not say which document was read.
- TIME_REFERENCE_INTEGRITY. Requires the snapshot to hold exactly its four allowed fields with a declared source class, and recomputes its digest. A failure means the time evidence supplied is not the time evidence committed.
- OBSERVED_DRIFT_RECOMPUTE. Recomputes the drift as the widest absolute offset in the snapshot. A failure means the reported drift was supplied rather than measured.
- DRIFT_BOUND_SATISFIED. Recomputes the drift class against the declared bound and fails outright when the drift exceeds it. A failure means the read instant is not tied tightly enough to the reference to be evidence.
- CONTENT_DIGEST_RECOMPUTE. Recomputes the digest of the whole retrieved content record. A failure means the content shown is not the content observed.
- KEY_IDENTIFIER_SET_INTEGRITY. Rebuilds every commitment entry from the digested content, entry by entry and position by position, and checks the count. A failure means the committed set does not come from the digested content.
- KEY_IDENTIFIER_SET_DIGEST_RECOMPUTE. Recomputes the set digest over the sorted commitment set. A failure means the set was reordered or edited after commitment.
- CONTENT_KEY_SET_AGREEMENT. Checks the content record's own declared directory identifier, retrieved url and key count against the signed body. A failure means the content came from a different directory or url.
- QUERIED_KEY_COMMITMENT_BINDING. Recomputes the identifier commitment for the queried key identifier under the same salt. A failure means the question was changed after the fact.
- PRESENCE_VERDICT_RECOMPUTE. Recomputes the verdict as membership of the queried commitment in the committed set. Absence passes. A failure means the verdict was asserted rather than computed.
- OBSERVER_ROLE_DECLARED. Requires the observer role to be the fixed label
non_publishing_observer. A failure means the observing role was edited. - NO_DIRECTORY_CONTENT_LEAK. Walks every string in the signed body against its approved opaque form: identifier patterns, 64 character hex digests, enumerated classes, the one url. A failure means something readable reached an opaque field.
- DIRECTORYSTATE_BOUNDARY_PRESENT. Requires the boundary text to equal the fixed string. A failure means the non attestation was edited or removed.
Mint refuses every field this list recomputes, from the reference digest through to the salt commitment. It also refuses to sign a drift over the declared bound, a content record whose count disagrees with its entries, a retrieved url that does not match the directory, a duplicate key identifier, and a snapshot for a different time reference.
The boundary
This receipt attests only that a named key directory, retrieved at a declared instant within a bound drift of an external time reference, held a committed set of key identifiers, and that one queried key identifier was present in or absent from that set as recomputed by this service from the committed set rather than supplied by the caller. It does not attest that the directory content is correct. It does not attest that the publisher is entitled to publish it. It does not attest that any key in it is validly issued or under any party's custody. It does not attest that a key absent from it does not exist elsewhere. It does not attest that another observer saw the same content at the same instant. It does not attest that any signature under any key was authorised. It does not decide whether any message should have been blocked.
That string is a schema constant inside the signed bytes and it is checked again by its own gate.
The last clause is the expensive one, and it is deliberate. The specification that started this paper says a message should be blocked when a key cannot be retrieved. It would be easy for this receipt to claim it settles that call, and that claim would be the fastest way to make the instrument useless. Whether a message should have been blocked depends on policy, on the caching rules in force, and on what the reader was entitled to rely on. Those are the arguments the parties are having. A receipt that stays out of them can be handed over by either party without conceding anything.
The clause about another observer is the most honest one in the set. Directories are served from many edges and can differ between them. This receipt is one observer's read, and it says so in the signed bytes rather than in a footnote. That is what makes several receipts from several observers worth collecting, because they can disagree and the disagreement is the finding.
The clause about content correctness draws the line in the same place. The receipt fixes what was served. If a directory published a key it should have revoked, this receipt proves the key was there.
Adversarial cases
These are the real cases in test/directory-state.test.mjs, which holds 56 tests, all passing.
A content digest that does not cover the committed key set fails KEY_IDENTIFIER_SET_INTEGRITY. Four more cases fail the same gate: a forged key count, a dropped entry, a rewritten per key commitment and a rewritten key class. Every commitment is rebuilt from the digested content, so no entry survives an edit.
A forged presence verdict fails PRESENCE_VERDICT_RECOMPUTE, in both directions. Claiming presence for a key that is not in the committed set fails, and claiming absence for one that is fails too.
Edited retrieved content fails CONTENT_DIGEST_RECOMPUTE. Content read from another url fails CONTENT_KEY_SET_AGREEMENT, and so does a content record naming a different directory and one whose self declared count disagrees.
A drift over the declared bound fails DRIFT_BOUND_SATISFIED, and mint refuses that case outright. A forged drift value fails OBSERVED_DRIFT_RECOMPUTE, because the drift is recomputed as the widest offset in the snapshot. A snapshot for a different time reference fails TIME_REFERENCE_INTEGRITY, and so does an edited snapshot.
A rewritten queried key commitment fails QUERIED_KEY_COMMITMENT_BINDING, so the question cannot be swapped once the answer is known. A substituted salt fails SALT_BINDING. A raw key identifier in a signed field fails NO_DIRECTORY_CONTENT_LEAK, and a boundary mismatch fails DIRECTORYSTATE_BOUNDARY_PRESENT.
Five positive cases carry the instrument. A directory holding the queried key verifies with verdict queried_key_present. A directory without it verifies with queried_key_absent, the case a dispute usually needs. An empty directory verifies with a zero length commitment set, the state a failed publish leaves behind. A signature agent directory and a credential provider directory each verify on their own class.
Structural tests confirm the design. The minted envelope carries no key identifier, no key material digest, no queried identifier and no salt. Identifier commitments are salt scoped, so two observations under different salts share nothing. Rotated key material changes the key commitment and leaves the identifier commitment alone. The set is sorted, so directory order carries nothing. The classifiers are pure functions of their inputs, and the class lists match the schema enums.
Performance
From hive-verifier-api/benchmarks/results-latest.json, measured at 2026-08-10T08:00:47.634Z at commit a22b4d88f6e041d6f593381b6e310a8d527d5b4b over 200 iterations:
| Measure | Value |
|---|---|
| Mint p50 | 3.032 ms |
| Mint p95 | 6.546 ms |
| Verify p50 | 2.113 ms |
| Verify p95 | 2.547 ms |
| Artifact size | 3131 bytes |
Local harness measurement on one machine and one Node version. The run recorded Node v20.20.1 on linux/x64 with two Intel Xeon cores at 2.90 GHz. It is a reproducible measurement of this code, not a production latency guarantee. One caveat is specific to this type. The benchmark case observes a small directory. Verify rebuilds one identifier commitment and one key commitment per entry, so the cost grows with the number of keys observed, up to the schema limit of 4096. The receipt grows with it too, since it carries one commitment pair per key.
How to run it
This type has no mint route. It is produced as an export and only verified here. The verify route takes no credentials.
The verify route is live on the production host. It is open, needs no account, and costs nothing to call.
- Verify route:
/verify/directory-state - Demonstration route:
/demo/directory-state, carrying the keyskey_present,key_absent,rotated_directoryandsubstituted_content
curl -sS -X POST https://thehiveryiq.com/v1/verify/directory-state -H 'content-type: application/json' -d '{"receipt": <a receipt of this type>}'
The rotated_directory case is a real positive: a rotated payment network directory that no longer carries the queried key. The substituted_content case fails at KEY_IDENTIFIER_SET_INTEGRITY, and because mint refuses it, it is built by minting the honest case, editing the body and re-signing with the same demonstration key.
Where it sits in the canon
Family: network-authority. Category: typed receipt contract.
It is the observation layer under the instruments that reason about keys. The Authority Revocation Receipt settles when an authority was withdrawn and who was told. This one settles what a directory said at one instant, which is the fact a revocation argument runs on when the directory is the only place the withdrawal was expressed. The Admission Binding Receipt fixes what an admitted party was admitted as, and the Knowledge Timestamp Receipt fixes when a party could first have known something. The Screening Attestation Receipt is its nearer relative in shape, since both bind an external time reference with a drift bound and a digest of a named version of a retrieved artifact. This one is simpler: no ruleset version, no supplemental lists, no quorum, and one outcome, which is membership.
It does not replace directory monitoring and it does not decide an admission dispute. It produces the document nobody kept.
Patent pending.
Status
production_deployed.