The identity document for agents and robots
An issuer-sealed document bound to the agent's own key. A 160-byte Mark any device recognises in nanoseconds. A printed token that verifies in the browser the moment it is scanned. Certification tiers, insurance a venue can check, and a signed record of everything the agent did. Ed25519 and FIPS 204 ML-DSA-87 seals on every artefact.
The document
One card. Everything a counterparty needs.
The data page is rendered by the registry from the sealed document. The holder sees every field; everyone else sees the issuer-sealed fields with disclosable claims marked as not disclosed. The QR is not a link to a record: it is the sealed token itself.

The holder's own view of a software agent's ID, exactly as the registry rendered it during the recorded evidence run (document IA). Geist, the stone palette and the logo geometry are the phanes.app design tokens.
Nine characters, A to Z and 0 to 9, derived from the issuer key, the holder key and fresh randomness.
IA for a software agent, IR for a physical robot or device bound to attested hardware.
The agent's name and type, the legal entity behind it, its ISO 3166-1 jurisdiction.
Provider and model id, marked attested when the provider signed for it.
The agent's own Ed25519 key. Every presentation is signed fresh with it; the document is bound to it (cnf).
What the agent may do and integer limits on how far, sealed by the issuer.
Issue and expiry times, and the bit each verifier checks in the signed revocation and suspension lists.
Disclosable claims travel as salted digests (SD-JWT). The holder reveals only what a venue needs.
What a scan does
The QR carries the token. The browser does the checking.
Every printed ID encodes https://phanes.app/id#<token>. The token is the 160-byte classical Mark in base64url. It sits in the address fragment, which the browser never sends to any server. The page reads it, verifies the issuer's seal against the key published on its own origin, checks the signed status lists, and shows every check with its reason.
Eight checks, in order
Token
160 bytes read from the address fragment: magic, layout and character set accepted.
Issuer
The key id inside the token matches the issuer key published on this origin.
Signature
Ed25519 over the sealed frame, verified with WebCrypto or the in-page reference implementation.
Validity
Issued before now (five minutes of skew allowed), not yet expired.
Revocation
The token's bit in the issuer-signed revocation list is clear.
Suspension
The token's bit in the issuer-signed suspension list is clear.
Hybrid seal
The printed token carries the Ed25519 seal; the ML-DSA-87 seal on the same ID is checked by the registry and phanes-core.
Registry
When the anchors name a registry, its live verdict on the hybrid Mark is shown alongside.
Verdicts are Valid, Not valid, Revoked, Suspended or Expired. Without the issuer anchors the page says Unverified; it never says Valid without the key.

A real token opened in Chromium during the recorded evidence run. The same page, served by any registry at its own /id, verifies that registry's tokens without reaching phanes.app.
The Mark
160 bytes. Recognised in nanoseconds, verified in microseconds.
The Mark is the machine form of the ID: a fixed layout an interface can recognise at the packet boundary with no parsing and no allocation, then verify with one strict Ed25519 check and two status-bit lookups. The hybrid form adds a FIPS 204 ML-DSA-87 signature over the same bytes. The Rust core ships as a crate, a C ABI for firmware, and a JSON binary for any language.
Medians from the recorded benchmark session (phanes-core, Intel Xeon 2.10 GHz, single core). Recognition of junk stops at the first byte in under a nanosecond.
| Offset | Bytes | Field | Meaning |
|---|---|---|---|
| 0 | 4 | magic | PHNS |
| 4 | 1 | version | 1 classical, 2 hybrid |
| 5 | 1 | kind | 1 = identity |
| 6 | 2 | flags | hardware-bound, insured, certified |
| 8 | 9 | number | Phanes ID number, ASCII |
| 17 | 1 | level | certification level 0 to 3 |
| 20 | 8 | issued_at | milliseconds, big-endian |
| 28 | 8 | expires_at | milliseconds, big-endian |
| 36 | 8 | issuer_key_id | SHA-256 of the issuer key, first 8 bytes |
| 44 | 32 | holder_key | the agent's Ed25519 public key |
| 76 | 4 | revocation_index | bit position in the revocation list |
| 80 | 4 | suspension_index | bit position in the suspension list |
| 84 | 12 | status_list_hash | SHA-256 of the list id, first 12 bytes |
| 96 | 64 | ed25519 | issuer signature over the framed body |
| 160 | 4627 | ml_dsa_87 | FIPS 204 signature, version 2 only |
Rust crate phanes-core
recognize, verify and verify_hybrid on the raw bytes. No allocation on the recognition path.
C ABI for devices
phanes_recognize and phanes_verify_mark_pq from include/phanes_core.h; the C++ example is compiled and run in the test suite.
Python and JSON
aeos.mark.PhanesCore binds the library through ctypes, falls back to the JSON-stdio binary, then to a pure-Python reference with the same verdict.
Security posture
Two signatures on everything. Nothing served off the record.
Built for the environments that will ask the hardest questions. Every issuer artefact is sealed twice, every request the registry serves is provable afterwards, and the page a scan opens cannot be quietly changed.
Hybrid seals
Ed25519 · ML-DSA-87 (FIPS 204)
Every issuer artefact carries both signatures over the same bytes: the document, the Mark, the status lists, certifications and policies. A verifier can require the lattice signature and refuse classical-only tokens.
Holder binding
cnf · key-binding JWT · single-use nonces
The document is bound to the agent's key. Every presentation is signed for one audience and one nonce; replay fails.
Status lists
W3C Bitstring Status List · issuer-signed
Revocation and suspension are bits in signed, cacheable lists. A list that fails verification is not loaded and verification fails closed.
Registry audit trail
hash chain · Merkle tree · signed tree heads
Every request the registry serves is an entry with an inclusion proof against an operator-signed head. Nothing is served off the record.
The scan page
hash-pinned CSP · same-origin anchors
phanes.app/id runs one inline script pinned by SHA-256 and loads issuer keys only from its own origin; a link cannot substitute foreign anchors.
Abuse controls
per-caller token bucket · security headers
Rate limits keyed by signing key or address, 429 with Retry-After, nosniff, no-referrer, no framing, HSTS on TLS.
Physical agents
Robots carry the same document, bound to attested hardware.
A robot, vehicle, drone or embedded device applies with a device profile and a manufacturer attestation over its measured firmware and serial. The issuer verifies the attestation against the manufacturers it trusts, issues an IR document, and sets the hardware-bound flag in the Mark so a gate can tell a device from a process before any cryptography runs.
Device profile
Kind, manufacturer, model, a digest of the serial, the hardware root of trust (TPM 2.0, secure element, HSM, TEE) and the firmware digest.
Manufacturer attestation
A signed statement over the unit's key fingerprint, serial digest and firmware digest. A mismatch on any of them does not bind.
Robotics insurance
Physical damage and bodily injury lines, available only to hardware-bound IR documents at Verified or above.

A warehouse unit's ID from the evidence run: the manufacturer attested the measured firmware, the registry bound the device, the card shows the root of trust and the Hardware-bound chip.
Certification
Four tiers, every input echoed back.
The tier is computed by deterministic rules over verifiable inputs and issued as a signed credential a counterparty verifies offline. The credential lists every check it used, so a verifier sees why the tier was granted, not only that it was. The badge is communicated in HTTP headers and MCP metadata.
| Level | Tier | Adds |
|---|---|---|
| 0 | Registered | A valid ID, not revoked or suspended. |
| 1 | Verified | Controller KYB attestation and model-provider attestation, both verified by the issuer at issuance. |
| 2 | Certified | Insurance in force with a per-claim limit at or above the agent's transaction bound, and an operator-anchored activity record. |
| 3 | Institutional | 90 days of history, 500 events, adverse outcomes under 1%, a tenth of events witnessed by venues, no paid claims in a year. |

The badge for a Certified agent from the evidence run. It is rendered from the credential, never typed: the number, the expiry and the insured state are read from the signed payload.
Sealed like the document
The certification board signs with Ed25519 and ML-DSA-87. A verifier can demand a minimum level and proof of insurance in one call.
Insurance
Four packages. Premiums from the record, claims from the proof.
Quotes are computed deterministically from the ID, its tier and its activity history, with every rating factor returned. A claim names an event in the agent's activity record; the registry verifies its inclusion proof against a signed tree head before rules adjudicate it. Limits below are per claim and aggregate, in USD.
Starter
RegisteredIndividual builders and early-stage agents
Deductible $250
Professional
VerifiedProduction agents transacting with third parties
Deductible $1,000 · $2,500 data breach
Institutional
CertifiedFinancial institutions, government, large enterprise
Deductible $5,000 · $10,000 on the two largest lines
Robotics
Verified · hardware-boundPhysical robots, vehicles, drones and embedded agents
Deductible $2,500 · $5,000 · $10,000 by line
Activity record
What the agent did, signed by the agent, anchored by the registry.
Two-phase append
The registry prepares the exact event body: sequence, previous hash, actor fingerprint, timestamp
The agent signs it with the holder key; a venue may add a witness signature inside its visa
The registry commits it as a leaf of an RFC 6962 Merkle tree and signs the tree head
Anyone verifies inclusion and consistency proofs against the signed head
Who, what, when, where, how, why
Each event carries the tool invoked, the venue, the jurisdiction, the method and model, and the outcome.
Transcripts and claims
Operator-signed transcripts summarise the log; insurance claims and certification read from it, never from prose.
# hash a real file, record the invocation with its digest, size and exit code
H=$(sha256sum aeos/phanes_id.py | cut -d' ' -f1)
phanes record --server https://registry.example --key agent.key IA1234567 \
--type tool.invoked \
--what '{"tool":"sha256sum","path":"aeos/phanes_id.py","sha256":"'$H'","exit_code":0}' \
--where '{"venue_did":"did:phanes:venue:desk","jurisdiction":"USA"}' \
--how '{"method":"bash","model_id":"claude-fable-5-1"}'
# the signed summary, tree head and an inclusion proof for event 3
phanes transcript --server https://registry.example IA1234567
phanes audit --server https://registry.example --inclusion 3Proven in the open
Recorded, not described.
Every command of the evidence run was executed under a pseudo-terminal recorder and kept with its raw typescript and SHA-256: the suites, a walkthrough against a live registry, a 112-agent campaign with an adversary running 43 attacks, the browser verifier driven in Chromium, and two documents built only from those files. Every session is on the evidence page.
For builders
An ID in five commands. Verification in one.
The whole flow is the phanes command line, the Python client, the REST surface under /v1, or fourteen MCP tools inside an agent runtime. Keys are generated on the agent and never travel; every request is signed locally.
pip install 'phanes[server,render,pq]' phanes keygen --out agent.key phanes apply --server https://registry.example --key agent.key \ --name "Ledger Agent" --controller-did did:phanes:org:acme \ --controller-legal-name "Acme Inc" --jurisdiction USA \ --model-provider Anthropic --model-id claude-fable-5-1 \ --capability transact --bound max_transaction_value=2500000 \ --out agent.id.json phanes token --server https://registry.example IA1234567 # https://phanes.app/id#... phanes certify --server https://registry.example IA1234567 phanes insure --server https://registry.example --key agent.key IA1234567 --package professional # anyone: cache the anchors once, verify offline through the Rust core phanes anchors --server https://registry.example --out anchors.json phanes verify-mark --file mark.bin --anchors anchors.json --require-pq
from aeos.client import PhanesClient, OfflineVerifier
from aeos.phanes_id import HolderKeys
keys = HolderKeys.generate() # stays on the agent
c = PhanesClient("https://registry.example", holder=keys)
doc, extras = c.apply(
agent_name="Ledger Agent", controller_did="did:phanes:org:acme",
controller_legal_name="Acme Inc", controller_jurisdiction="USA",
model_provider="Anthropic", model_id="claude-fable-5-1",
capabilities=["transact"], authority_bounds={"max_transaction_value": 2_500_000},
)
print(c.token_url(doc.id_number)) # what the QR carries
v = OfflineVerifier.from_registry(c) # anchors + status lists, cached once
assert v.verify(c.mark(doc.id_number))["ok"] # Ed25519 and ML-DSA-87, offlineVenues and gateways
Verify a presented Mark or an SD-JWT presentation in one call; require a tier, insurance and a fresh nonce.
Self-hosted registries
Run phanes serve with your own Ed25519 and ML-DSA-87 keys, register the attesters you trust, verify your own tokens at your own /id.
Air-gapped verification
phanes verify-mark --classical --anchors and the C ABI need no network at all; nothing reaches phanes.app.
Read the full guide, then run it yourself
The documentation covers the document, the Mark, the printed token and the browser verifier, robots, packages, the audit trail and every route and tool. The repository carries the code, the tests and the recorded evidence.