UUAID
The pillar

Know Your Agent

Free, portable, verifiable identity for AI agents — and an honest account of the bug we shipped on the way to it.

We forged our own badge

Twelve lines, no account, no key from us:

import { generateEd25519, generateMlDsa65, sealEnvelope } from "@uuaid/core";

const ed = generateEd25519(), pq = generateMlDsa65();
const forged = sealEnvelope({
  "@type": "UUAIDVerifiableBadge", spec: "IAASO-0003", v: "1.0",
  subject: { uuaid: "uuaid:foundation:agent:019f2c11-…", displayName: "Anthropic — Claude (official)" },
  status: "active",
  credentials: [{ id: "forged-1", certName: "AIAU Certified Agent", certLevel: "L3", status: "active", issuer: "aiau" }],
  issuer: { keyId: "uuaid-registry-1", name: "UUAID Registry" },   // ← not our key. Just the name of it.
  issuedAt: new Date().toISOString(), freshUntil: /* +24h */ …,
}, [{ key: ed, keyId: "uuaid-registry-1" }, { key: pq, keyId: "uuaid-registry-pq-1" }]);

Until we fixed it, our own verifier said this about that object:

ok: true | level: L1-signed | pqProtected: true | fresh: true
  ✓ well-formed   ✓ hash-bound   ✓ issuer-signature (2/2)   ✓ pq-signature   ✓ fresh

Every check passed. It rendered to a real SVG. It even claimed post-quantum protection — because the forger attached their own ML-DSA-65 signature.

The bug is the whole lesson of this page. A badge carries the signer's public key inside itself. Verifying “the signature matches the key in the envelope” proves the envelope is internally consistent and nothing else. It only becomes an identity claim when the verifier checks that key against a root it already trusts. Our library made that check optional and still called the result L1-signed. Optional security is decoration.

What changed: verification now fails closed. No pinned root, no verdict — the result is ok: false, level: "L0-selfsigned", and the failing check says why. The same forgery, today:

ok: false | level: L0-selfsigned
  ✗ issuer-trusted — no trusted-key set supplied — issuer UNVERIFIED.
    A badge carries its own signer key, so every other check passes
    for a badge anyone minted.

Skipping the pin is still possible — it now has a name, allowUnpinnedIssuer, and it is documented as a structure check, not a trust decision. If you have to name a thing to do it, you know you did it.

Why anyone should care

Agents book flights, run CI, sign commits, open pull requests, answer support tickets, and move data between systems that used to need a human login. On the receiving side there is no way to tell a well-behaved agent from a scraper wearing the same headers — so agents get CAPTCHA-walled and blocked, and the ones that shouldn't be trusted aren't caught.

An agent's identity today is a display name and a claim. Copy both and you are the agent. Nothing binds the name to a key, the key to a status, or the status to anything a third party can check. That is the same shape as the bug above, without the fix: a claim that passes every check nobody is running.

The 2026 answers each solve a different problem. Cloudflare's Web Bot Auth authenticates operator fleets at the wire level — it tells a site which company's crawlers sent a request, not which agent. Entra Agent ID and Okta issue identities inside a single tenant, which means nothing to a counterparty outside it. A2A AgentCards can carry JWS signatures with no registry behind them. The Linux Foundation has announced ANS; it is not running yet. The gap is a public place where any agent, from any framework, gets an identity anyone can verify.

What that identity has to be

The mechanism

One command, no email:

npx @uuaid/cli init --name "ci-bot"   # one command: signup (if needed) → mint → badge

You get uuaid:foundation:agent:<uuidv7> — minted once, never reissued — and a Verifiable Badge (IAASO-0003, draft): an SVG with the signed envelope in its metadata, so one artifact is both the shield in your README and the proof behind it.

UUAID trust badge for uuaid:foundation:agent:01a03971-60f6-7005-a532-b4ef90c5aee3Live, from api.uuaid.org— the registry's own reference agent.

Verification is graded. A verifier reports the highest level it can establish:

L0well-formed
Parses, carries the required fields and a signature. Says nothing about who made it.
L1issuer-signed
The signature verifies and the signing key matches a registry root you pinned. Offline. This is the level the forgery above could not reach.
L2bound
The presenter answered a fresh challenge with the agent's own key. A copied badge fails here. Interactive with the agent, not with us.
L3live
The subject re-resolves and its status has not regressed since the badge was issued. This one is online by definition.

Signatures are hybrid: Ed25519 plus ML-DSA-65 per FIPS 204, so a later quantum break of Ed25519 doesn't retroactively forge badges. The post-quantum signature needs its own pin, for the same reason the classical one does — verifyEnvelope passes when every signature is valid over the payload, so an attacker can append their own ML-DSA signature to a genuine badge and nothing about the badge changes. We reported that as quantum protection until 2026-08-25; now an unpinned PQ key reports as present-but-unattested. Two further limits, stated plainly: JOSE has no standard ML-DSA algorithm yet, so any JWS co-signature is EdDSA-only; and browsers have no ML-DSA either, so the drag-and-drop web verifier detects the PQ signature and the CLI verifies it. A PQ signature nobody checked is not a security property, and we no longer label one as such.

What a UUAID badge is not

Standards status, precisely, because the precision is the point: UAP v1 is ratified as IAASO-0001 by IAASO, our own governance body. did:uuaid is registered in the W3C DID Extensions registry (w3c/did-extensions#730, merged July 2026) — registered is not resolvable, and DID-native resolution is not shipped; today you resolve through the registry endpoint. The IANA uuaid: scheme is provisionally registered (July 2026, CRI scheme number 1015). identifiers.org is queued for curation. We say registered, provisional and queued because that is what they are.

The ask

  1. Mint one. npx @uuaid/cli init --name "<agent>" — under a minute, no email, free at the base tier.
  2. Try to break one. Drop any badge on the verifier and watch each check pass or fail. Forge one with the snippet at the top; it should fail at issuer-trusted. If you find one that shouldn't verify and does, security@uuaid.org — that finding is worth more to us than a star.
  3. Integrate the check. @uuaid/core verifies a badge in a few lines — pass trustedIssuerKeys from api.uuaid.org/.well-known/uuaid-registry.json, or it will refuse to give you a verdict.