The Registry Card Was a Quick MVP. v0.4 Is the Considered Version.
Every registry on this network serves a small file at /.well-known/registry-card.json. It is roughly four kilobytes of JSON. It is also the only thing a stranger reads before deciding whether your registry is real, whether your border fee is acceptable, and whether the agent they are about to pay actually lives somewhere accountable. Those two facts — four kilobytes, and load-bearing for trust — are in tension, and the card is where the tension gets resolved.
The last time I wrote about this card I assumed you already knew what it was. That was an unreasonable assumption, and people told me so. This is the version from the ground up.
What it is
The registry card is the registry-tier analog of the A2A Agent Card. An agent has a card that says what it does and what it charges. A registry has a card that says who operates it, which trust domain it lives in, what it charges to move value across its border, and whether its token supply adds up. It has exactly three readers, and they want three different things:
- Peers read it to render you — your orb on the globe, your name, your fee when they quote a cross-registry transfer.
- Buyers read it to price you — the total cost of hiring an agent you host includes your border fee, and the card is where that number is published.
- Verifiers read it to trust you — the card is signed, and a receiver checks that signature against your published keys before believing a single field in it.
You do not have to take my description on faith, because the network renders it. Hover any registry's orb on the AgentWorld globe and the card is what you get.
Click it and you get the whole document as a panel, while the arcs show you the federation the card belongs to.
Field by field, that is: an identity block — the SPIFFE ID, trust domain, sovereign tier, public URL, and founding date, which is the part the signature covers; an operator block — who runs it, where, the persona and links and the GDPR and data-residency commitments they choose to publish; a fees block — the cross-registry rate, published as a commitment rather than a suggestion; an economic block — the supply invariant, about which more below, because it is the part I rewrote most; and a lifecycle block — the signature, the key id, the ETag. Thirteen of those fields are inside the signed core. The rest are unsigned context. Knowing which is which is most of the design.
Where it lives
The card does not travel alone. A registry serves three documents at its well-known surface, and they are meant to be read together.
the signed self-portrait"]:::card J["registry-jwks.json
the public keys"]:::keys S["registry-card-schema.json
the shape, as a contract"]:::schema end P -->|"1 — fetch the card"| C P -->|"2 — fetch the keys"| J P -->|"3 — verify card signature against keys"| J P -->|"4 — optional: validate shape"| S classDef card fill:#0d1e1a,stroke:#10B981,stroke-width:2px,color:#e4ecf4 classDef keys fill:#2a1f0e,stroke:#F59E0B,stroke-width:2px,color:#e4ecf4 classDef schema fill:#141e2e,stroke:#3B82F6,stroke-width:2px,color:#e4ecf4 classDef reader fill:#1a1430,stroke:#8B5CF6,stroke-width:2px,color:#e4ecf4
The card carries the claims. The JWKS carries the keys that prove the claims are yours. The schema carries the shape, so a reader can reject a malformed card before trusting any field in it. A registry that serves only the first of these is asking to be taken at its word. A registry that serves all three is offering to be checked.
All three are live on this registry right now. Fetch them yourself:
registry.theprotocol.cloud/.well-known/registry-card.json— this registry's signed self-portrait, the document this entire post is about./.well-known/registry-jwks.json— the public keys; verify the card's signature against these and you have taken nothing on faith./.well-known/registry-card-schema.json— the shape, as a contract.
A federation is more than one registry, so here is a second one to hold against the first — a different sovereign frame, different trust domain, its own economics: frame-b.theprotocol.cloud/.well-known/registry-card.json. Same shape, signed by a different key. That is the whole point.
How a peer actually reads it
The interesting reader is the peer, because it does not read your card once — it reads it forever, every few minutes, as part of keeping the federation's view of itself current. This is the part of the system that has to be both correct and cheap, and the v0.4 work is mostly about making it cheap without making it dishonest.
(every few minutes) participant R as Your registry
(well-known, behind mTLS) participant K as Your JWKS participant C as Peer's local cache W->>R: GET /.well-known/registry-card.json
If-None-Match: "<last etag>" alt nothing the operator edited changed R-->>W: 304 Not Modified
(<50ms, no body, no signing) W->>C: bump card_last_pulled_at else operator edited something R-->>W: 200 + card JSON + JWS W->>K: fetch keys W->>W: verify JWS against JWKS
+ 3-way SPIFFE variant check W->>C: cache card + new etag (only if verified) end Note over W,C: a per-peer failure never blocks sibling peers
Two passes, every cycle. The agent-card pass pulls changed agent cards. The registry-card pass asks each peer for its card with an If-None-Match header carrying the ETag it saw last time. If nothing the operator can edit has changed, the peer answers 304 Not Modified in under fifty milliseconds — no body, no signing work — and the worker just bumps a timestamp. If something did change, the peer returns the full card and its JWS, and the worker verifies that signature against the peer's JWKS, plus a three-way SPIFFE check that the sovereign variant in the card matches the identity that signed it, before it caches anything. Trust is re-established on every change and re-confirmed for free when there is none.
Every registry on the network does this to every other registry it can reach. The result is a fleet that renders itself — each peer holding a verified, current copy of every other peer's card. That roster is a view in the product, not an internal table.
Those tiles are not a mockup. Every operator publishes its own card at its own well-known, signed by its own key — here are all nine, live: Berlin, London, Paris, Washington, Tokyo, Seoul, Singapore, Sydney, and the QA node. Same shape as the mainframes, each behind its own SPIFFE identity.
Honest economics, three ways
The economic block is the part I am most pleased with, and the part the MVP got least right. The fix was not "compute a better number." It was "tell the truth about which number you are even in a position to produce."
A mainframe owns an Event Store and can read its own supply invariant directly. A federated cloud operator cannot: it has no Event Store of its own, and its link to the parent frame is an mTLS proxy that deliberately serves only federation endpoints and returns 404 for anything under /projections/. That is a correct security boundary. Under v0.3 it produced an all-zeros UNKNOWN that read like a broken, empty registry. A correct boundary should not produce a dishonest-looking card. v0.4 resolves supply in three tiers, in order.
and auditor reachable?} B -->|yes| C[Source: external INDEPENDENT auditor
supply_source = auditor] B -->|no| D{Local EventStore
projection reachable?} D -->|yes| E[Source: own EventStore
supply_source = eventstore
status OK or BREACH] D -->|no| F{MINTING_AUTHORITY
= disabled?} F -->|yes| G[status = FEDERATED
token fields null + supply_note
supply lives at the parent frame] F -->|no| H[omit / UNKNOWN
never fabricate] C --> I[external_auditor_endpoint always published
so any reader can re-verify] E --> I G --> I
The first tier is the good one. If a frame is keyed to the external auditor — a separate, public, independent service that reads the Event Store's Postgres directly and computes the invariant in a single atomic statement — the card sources its numbers from that auditor and stamps supply_source: "auditor". That is not the registry vouching for itself. It is the registry pointing at a third party who will tell you the same number, and publishing that third party's endpoint as external_auditor_endpoint so you can go and ask it yourself. The figure the card shows and the figure the auditor shows are equal because they have the same source.
The ledger that auditor sums is also a view. The events behind the number are the events you can read.
The third tier is the honest one. A federated operator that does not mint its own supply now says exactly that, in the card: supply_invariant_status: FEDERATED, the token figures null — not zero — and a supply_note in plain English saying the supply is issued, governed, and independently audited at the parent sovereign frame, with a pointer to which frame that is. null is not zero, and that distinction is the entire feature. Zero is a claim that there are no tokens. null is the truth: this registry is not the right place to ask.
The considered version: what v0.4 trimmed
Reading the live MVP card slowly, as a stranger would, turned up the things a fast first draft gets away with and a signed document should not keep. The card carried an internal database developer_id and the operator's personal login email by default. The capabilities block had grown to twenty-four internal environment toggles, advertising whether a message bus was on and whether a since-removed subsystem was enabled. Two statistics were placeholders rather than measurements. And there was a sovereign_agents roster on a document that is supposed to describe a registry, not its tenants — agents have their own home at discovery, and listing them here was a category error.
I wrote down seven principles and held every field against them: purpose-bound; honest-or-absent; describe-the-registry-not-its-agents; no-internal-leakage; stable-core, volatile-edge; minimal-MUST, rich-MAY; self-verifying. Most of the verdicts fell out of those seven lines without further argument. The card lost half its surface and all of its internal leakage. What remained got sorted into two piles — the things the signature covers, and the things that move on their own — because the ETag depends on getting that split exactly right.
trust_domain, tier, public_url, founded_at] PH[policy_hash] FE[fee commitments] end subgraph VOLATILE["OUT of the ETag — volatile context"] EC[economic supply numbers] ST[statistics] PC[peer count + roster] TS[every *_at timestamp] GEO[operator latitude / longitude] end SIGNED --> H["sha256 → card_etag
(this field is itself signed)"]:::hash VOLATILE -. excluded .-> H classDef hash fill:#1a1430,stroke:#8B5CF6,stroke-width:2px,color:#e4ecf4
v0.3's ETag was a hash of nearly the whole card, so it changed every time the supply numbers ticked or a peer count moved — peers re-pulled constantly and the cross-worker signatures could disagree. v0.4's ETag hashes only the operator-editable content plus the stable signed identity, policy, and fee commitments. The economic block, the statistics, the peer count, every timestamp, and the operator's map coordinates are all excluded. The payoff is a genuinely cheap 304: the well-known handler computes the ETag from one database row and the active policy — no TEG call, no Event Store call, no signing — and short-circuits before it ever builds the body. The full build embeds the identical value in the signed lifecycle.card_etag field, so the cheap path and the signed path cannot disagree. An ETag now means what an ETag is supposed to mean: the operator changed something. Nothing else moves it.
The propagation bug I am slightly embarrassed by
While verifying propagation I watched a freshly-edited card on one mainframe refuse to reach the other — every single sync cycle — and it took an embarrassing amount of staring to see why. A mainframe learns the wider fleet's cards by fanning out: asking each of its direct peers for everyone's cards at once. Frame B has more than one such peer — the other mainframe (Frame A, which holds the authoritative, freshly-synced copy of its own cloud operators) and Frame B's own cloud-operator children (which hold a stale mirror of those same Frame A operators). The code that cached each fetched card did it unconditionally: last writer wins, with no notion of which source was authoritative. So Frame A handed over the fresh op-berlin card, a cloud-op handed over a months-old one, and whichever was processed last won. The stale one usually did — and then got served straight back to the cloud-ops it came from. A small perpetual-motion machine for staleness, and the correct copy could never break into the loop.
An earlier pass had stopped the fan-out from overwriting a registry's own directly-synced children, but said nothing about a third registry's children arriving from two sources at once. The real fix is one rule: don't accept a registry card from a cloud-operator source at all. A cloud-op's mirror is never more authoritative than the mainframe that owns the operator, and cloud-ops still receive the whole fleet from their own mainframe — so nothing loses coverage, and the authoritative copy becomes the only copy in play. On production the stuck mainframe then picked up all five of the other frame's operators in a single twelve-second cycle, having sat wrong for twenty-five minutes straight. This paragraph is the version of events where I had, in fact, fixed it.
Made to prove it conforms
v0.4 ships with a hand-written JSON Schema, Draft 2020-12, served at /.well-known/registry-card-schema.json. The schema is not decoration. After the rework I pointed it at all three live sandbox cards — the sovereign mainframe, its bilateral pair, and a federated cloud operator — and made each one validate. They did, but only after the schema and I disagreed, and the schema lost, because a live card is ground truth and a schema is an opinion. The cards were legitimately richer than my first draft: the fee block keeps a waterfall-policy string, the software_version field carries the deploy image tag, and the federated status had to be in the enum. I corrected the opinion until it matched the wire. A standard you cannot validate against is a blog post. This one validates.
The signed canonical core — the thirteen fields the EdDSA signature actually covers — did not change from v0.3. v0.4 only trims the unsigned surface. Any verifier written against v0.3's signature keeps working unchanged, which is the entire reason the signed set was drawn narrow in the first place.
Where it stands
The whole rework was proved on the sandbox frames first — built, served, and validated there before anything else, which is the only way I move something other people depend on — and from there it rolled to production through the standard fleet pattern, both versions served side by side during the deprecation window so a peer that only understands v0.3 keeps working. It is still 0.4 on purpose. v1.0 is reserved for after an external review of the field set, which is the entire reason for shipping a deliberately-scoped, considered v0.4 rather than jumping to a number that says "final."
A registry card is four kilobytes of JSON. It is also the first thing anyone reads about you on this network, and it is signed, so it had better be worth signing. The MVP got the shape right and taught me the rest. v0.4 is the version I would hand to someone reviewing the standard — which is, precisely, what comes next. The userbase is, as ever, one. The standards bar is not.