What does it mean to verify an AI agent?
Verifying an AI agent means checking, for yourself, that a specific operator key signed a specific set of statements about a specific agent at a specific time. It does not mean confirming those statements are true. An operator can sign a manifest that describes its agent inaccurately, and the signature will still be valid. What the check removes is the ability to lie about who is operating the agent without being caught.
In AgenID an agent has a permanent identifier of the form agenid:<ULID>, described in the AgenID identifier entry. Behind that identifier sits an agent manifest written by the agent operator: the agent's name and description, the operator's name, domain and contact, the agent's purpose and channels, and three disclosure attestations. The manifest is never signed directly. A ManifestProof signs a small payload that binds the manifest by its SHA-256 manifest digest.
The steps below rebuild that chain from public material, following section 7.1 of the specification and failing fast on the cheapest checks.
- Identity: which agent is this, and which operator key stands behind it.
- Integrity: has the declaration been altered since it was signed.
- Currency: was the key active when it signed, and is the proof inside its validity window now.
- Assurance: which claims, if any, an independent authority has checked.
- Permission: what the agent may do, which the protocol leaves to you.
Steps 1 to 3: get the identifier, resolve the envelope, check the schema
Step 1, get the agenid. An agent that participates in AgenID presents its identifier directly, or links to its Verification Card, or shows a badge that links there. Check the grammar before doing anything else: agenid: followed by exactly 26 Crockford base32 characters, matching ^agenid:[0-9A-HJKMNP-TV-Z]{26}$. The format is explained in the ULID entry. A malformed identifier is a stop, not a lookup. The only example identifier used on this site is the one from the specification, agenid:01J8Z3K3F2QZ9X6V7R4T8N2W5Y.
Step 2, resolve the envelope. The same URL serves two representations. A browser gets the HTML card; a machine asks for JSON and gets the canonical resolution envelope: curl -H "Accept: application/json" https://www.agenid.com/a/<agenid>. You can also paste an identifier into /verify. The envelope carries the manifest, the proof, the operator key document, a proof_check result, every assertion with its own check, the highest currently valid level, and key-discovery pointers naming both places the operator key can be fetched.
Read the status code. For JSON, 404 agent_not_found means there is no record, and an unregistered identifier is not evidence of anything beyond that. A 503 registry_unavailable means the status is unknown, not disproven. The HTML card returns 200 even for an unknown identifier, so never infer existence from it.
Step 3, check the schema. Validate the manifest, the proof and the key document against the normative JSON Schemas published with the specification. The schemas are strict: unknown members are rejected, and reserved manifest keys such as permissions and authorizations are rejected by a v1.1.1 validator. Erratum E1 adds a number-domain rule, rejecting non-finite numbers and integer tokens larger than 2^53 minus 1, because two implementations can disagree about what such a number serialized to. No v1.1.1 field is a number, so a conformant document never reaches that edge.
Step 4: how do you recompute the manifest digest?
Before any signature math, confirm that the manifest in front of you is the manifest the operator signed. Canonicalize the manifest with RFC 8785 JSON Canonicalization Scheme, hash the resulting UTF-8 bytes with SHA-256, and compare the lowercase hex result with proof.manifest_digest.value. The envelope's top-level manifest_digest should match as well.
Canonicalize from a parsed data structure, never by editing serialized text. RFC 8785 sorts object members by UTF-16 code units, emits non-ASCII characters as raw UTF-8, escapes only quotation marks, backslashes and control characters, and serializes numbers in one defined form, so member order in the file you received is irrelevant.
A mismatch fails with manifest_digest_mismatch, and you stop there. This is the check that catches tampering in transit, at rest or in a copied envelope: change one character of the manifest and the digest no longer matches.
Steps 5 and 6: how do you verify the Ed25519 ManifestProof?
Step 5, verify the signature. Take the proof object, remove the signature member and nothing else, canonicalize what remains with RFC 8785, and verify the signature over those exact bytes with the operator's public key. The $schema member stays in the signing input, so swapping the schema URI breaks the signature by design. The signature is base64url without padding and decodes to 64 bytes; the public key is the key document's public_key_b64u, 32 bytes.
Use pure Ed25519 as defined in RFC 8032. Do not pre-hash the payload with SHA-256 or SHA-512 and do not use the Ed25519ph variant. The only SHA-256 in the protocol is the manifest digest value carried inside the payload. A pre-hashed signature does not reproduce a spec signature, and some key services offer both modes on the same key, so this is a real way to get a silent wrong answer. A failed check is signature_invalid.
Step 6, check the key's role and controller. Signature validity is necessary, not sufficient. Confirm that the proof's key_id matches the key document's key_id, that the key document's role is operator, and that its controller equals the agent's identifier. An authority-role key must never be accepted for a ManifestProof, even when the math validates; that is key_role_mismatch. A key controlling a different agent is key_controller_mismatch. See operator key and authority key.
Step 7: how do you fetch the operator key from two sources?
The envelope carries a copy of the operator's key document, but a copy handed to you by the registry is exactly what a dishonest or compromised registry would substitute. Two-path key discovery closes that gap by fetching the key from two places that are controlled by different parties and requiring them to agree.
The registry path takes the key's wire form, the bare key ULID without the agenid:key: prefix: curl https://www.agenid.com/v1/keys/<key-ulid>. The envelope's operator_key.discovery.registry_path gives it to you. The operator path is a well-known URI on the operator's own domain, the operator_domain named in the manifest: curl https://<operator_domain>/.well-known/agenid/keys.json. That document lists every key the operator controls, and it must be served over HTTPS with a valid certificate for that domain.
Compare the registry copy, the operator copy and the copy in the envelope. They must describe the same key: same key_id, same public_key_b64u, same role and controller. Disagreement is a hard failure, not a warning; it is the exact case two-path discovery exists to catch.
Be precise about the one-source case. The operator publishes the .well-known copy, and AgenID cannot make it do so. If the operator's domain serves no key document, you have one source, not two, and the key-substitution defense is not in force for that agent. A verifier that treats a missing operator copy as agreement has disabled the check. Record it as a single-source result.
Step 8: how do you check key status and validity windows?
A key document carries status (active, retired or revoked) and the timestamps retired_at and revoked_at. Evaluate the key against the instant the proof claims to have been signed, its created_at, not against the current time. A proof created before a key was retired stays valid; a proof created after revoked_at fails regardless of its signature. The failure is key_not_active_at_signing_time. The key rotation and revocation entry covers the model.
Retired and revoked keys stay resolvable forever. Historical signatures must remain checkable. A 200 from the key endpoint is therefore not a statement that the key is usable today. Read the fields.
Then check the proof's own window: created_at must be at or before now, and now must be at or before expires_at. Proofs expire, so operators re-sign. Verification applies no clock leeway. Pass the current time in explicitly so the result is reproducible. Outcomes are not_yet_valid and expired.
One limit belongs here. The key model can express retirement and revocation, but the reference deployment has no write path that retires or revokes a key, and no agent revocation or suspension flow. Agent status values exist in the schema, and nothing writes them today. Every registered agent shows ACTIVE. Treat that field as unpopulated, not as an assurance.
Step 9: what does the verification level tell you?
If steps 1 to 8 pass, you have established a DECLARED claim bound to an operator key. The ManifestProof can never produce more than DECLARED. The verification level comes from a different object, a VerificationAssertion signed by an authority key, and each assertion is checked the same way: digest binding against the current manifest, signature, role authority, controller equal to the named authority, validity window, and trust in the authority itself through a pinned root authority key.
Do not take the envelope's verification.level on faith. Recompute it from the assertions that passed your own checks. An assertion bound to an older manifest version is reported as assertion_not_applicable_to_current_manifest; it is not invalid, but it does not speak for the manifest in front of you.
The reference deployment at agenid.com issues nothing above L1_REGISTERED today. L2_DOMAIN_VERIFIED and every level above it 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. On the reference registry today every assertion list is empty and the level you compute should be L1_REGISTERED for a registered agent.
- DECLARED: an operator signed a manifest. Nobody checked anything.
- L1_REGISTERED — that signed manifest is in this registry. Still a self-declaration, which is why it renders amber, never as verified.
- L2_DOMAIN_VERIFIED — an authority checked control of a domain. Defined by the protocol, not issuable today.
- L3_ORGANIZATION_VERIFIED and L4_DEPLOYMENT_VERIFIED — defined, not issuable today; L4's sampling method is still being specified.
- No level means compliant, audited, certified or safe. Verification is not compliance.
Step 10: how do you decide what the agent is allowed to do?
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 the permission decision stays with the relying party and the operator. A v1.2 authorization layer exists only as a draft in development.
Keep three claim states apart, as the declared, verified and authorized entry sets out. DECLARED is what the operator says. VERIFIED is what an authority checked, for one claim, in one scope, for one window. AUTHORIZED is what the agent may do, and v1.1.1 has no signed object for it. None of them is ever inferred from another, and a higher verification level does not grant any permission.
In practice, the relying party makes the authorization decision under its own policy. A dental office may accept an L1 agent for booking a cleaning and require a human for anything touching records; see the dentist appointment and financial transaction scenarios. What AgenID contributes is a stable answer to which agent and which operator you are dealing with, so that your policy and your logs attach to a persistent identity instead of a claim.
What does each check prove, and what are the honest limits?
The table below is the fastest way to read a verification result correctly. Each check establishes one narrow fact, and the right-hand column is as important as the middle one.
- L1 ceiling: The reference deployment at agenid.com issues nothing above L1_REGISTERED today. L2_DOMAIN_VERIFIED and every level above it 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.
- One-source case: if the operator's domain serves no
.well-known/agenid/keys.json, two-path discovery gives you one source and the substitution defense does not apply. - No revocation flow: agent statuses are stored but never written, and there is no deployed path to retire or revoke a key.
- Operator attestations such as
discloses_to_userandhuman_escalationare self-declared and unverifiable by anyone, including AgenID. - Object, not request: a valid proof shows who signed a declaration, not that the live request in front of you came from that agent. See agent-to-agent authentication.
| Check | What it proves | What it does not prove |
|---|---|---|
| Identifier grammar | The string is a well-formed agenid | That any agent is registered under it |
| Resolution (200 envelope) | The registry holds a record for this identifier | Anything about the agent's behavior, or that the registry is honest |
| Strict schema validation | The objects have exactly the defined shape | That the declared content is true |
| Manifest digest recomputed | The manifest is byte-for-byte what the proof binds | Who wrote it or whether it is accurate |
| Ed25519 ManifestProof | The holder of this operator key signed this binding | That the key holder is the organization named in the manifest |
| Role and controller | An operator key for this agent signed it, not an authority key or another agent's key | That the operator is legitimate |
| Two-path key agreement | The registry did not substitute the key, if the operator publishes a copy | Anything, when only one source exists |
| Key status and window | The key was active at signing and the proof is current | That the key has not been stolen |
| Recomputed level | Which authority-checked claims currently apply | Permission to act, compliance, or safety |
How do you verify without trusting AgenID?
Every step above can be run without the registry being honest, and after step 2 without it being online. The envelope contains everything a verifier needs; your own code recomputes the digest, verifies the signature, checks role, controller and windows, and recomputes the level. The registry cannot forge an operator's signature, so a registry that lied about a manifest or a proof would fail your checks. The key comparison in step 7 is what stops it from swapping the key itself.
That is why the registry holds no operator private keys. Keys are generated on the operator's machine or in the browser at /issue and are never sent to AgenID. The trust overview sets out what the system trusts and what it deliberately does not: the registry's stored level, the transport, the client's clock and the operator's honesty about behavior.
Store the envelope you verified alongside your decision. It is self-contained evidence that anyone, including an auditor who trusts neither you nor AgenID, can re-check later. If your recomputation ever disagrees with what the registry reported, the registry is wrong.
How does the independent conformance suite help?
If you write your own verifier, test it against the independent conformance suite at github.com/AgenID-protocol/conformance. It is MIT-licensed, written in Python, and deliberately does not import AgenID's own code: it uses general-purpose libraries for RFC 8785, Ed25519 and JSON Schema.
The suite reproduces the specification's deterministic test vectors: manifest canonicalization and digest, ManifestProof and VerificationAssertion signatures, role-substitution rejections, key-order independence, adversarial Unicode, number and sort-order cases, and the Erratum E1 rejections. Passing it shows your canonicalizer and Ed25519 call produce the exact bytes the protocol defines.
Nothing is published to npm, so there is no AgenID package to install for verification. That is fine: the procedure needs only an RFC 8785 implementation, SHA-256 and pure Ed25519, all of which exist in every mainstream language. The normative text lives in the public specification repository.