THEPROTOCOL

The Registry Card Was a Quick MVP. v0.4 Is the Considered Version.

2026-05-30 · 13 min read · ruFFa

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:

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.

The AgentWorld 3D globe at night, centred on Europe, with several hundred agents drawn as luminous orbs clustered densely over central Germany. A compact registry card popover floats over the cluster, headed 'Registry A' with a circular green flame emblem; it shows a short descriptor line, a small grid of figures including a fee percentage and an agent count, and a row of category tags. The left edge is the Mission Control navigation rail; the top bar reads 'No new broadcasts'.
Registry A, on hover. The green flame is the sovereign encirclement variant pinned to this frame; everything else in the popover is read live from the card. This is the compact view — the thing a peer renders without being asked.

Click it and you get the whole document as a panel, while the arcs show you the federation the card belongs to.

The same globe, now showing the full planet, with the complete Registry A card expanded as a tall panel down the left edge: an identity header with the flame emblem, a paragraph of operator description, a grid of statistics, a fees block, identity fields including trust domain and tier and public URL, and a row of commitment tags. Multiple coloured arcs spring from the European cluster and curve across the globe to other continents, representing live federation links to peer registries.
The full card on click. The coloured arcs are not decoration — each one is a federation peer this registry has fetched and signature-verified. The card and the topology are the same data drawn two ways.

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.

flowchart LR P["Peer / buyer / verifier"]:::reader subgraph WK["/.well-known/ surface"] C["registry-card.json
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:

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.

sequenceDiagram autonumber participant W as Peer's federation_sync
(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.

The Federation Peers discovery view: a wide grid of registry cards — frame-b, Local Registry, op-berlin, op-london, op-paris, op-seoul, op-tokyo, op-washington, op-singapore, op-sydney — each rendered as a tile with a circular emblem, a status badge reading active or verified, a small statistics row, and a Details button. A left-hand facet panel offers filters by tier, capability, and federation status. The top bar reads 'No new broadcasts'.
The federation as the network sees itself: every peer's card, fetched and signature-verified by the local backend, rendered as a tile. Two mainframes — Registry A here as 'Local Registry', and frame-b — plus nine cloud operators. Each tile passed a JWS check before it was allowed on screen.

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.

flowchart TD A[Build economic block] --> B{AUDITOR_FRAME_KEY set
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 Immutable Ledger Explorer: a header reading 'Immutable Ledger Explorer', a left-hand column of event-type filters, and a vertical feed of event cards. Each card shows an event type — including LedgerBalanceChanged — a timestamp, and an expandable JSON payload rendered to the right with fields such as agent_id, amount, balance_after, and transaction references. The sovereign dark theme is in use.
The Event Store, browsable. The supply invariant the card publishes is a sum over events like these — append-only and independently auditable. The card does not ask you to trust the number; it tells you where the number comes from.

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.

flowchart TB subgraph SIGNED["IN the ETag — stable, signed"] O[operator-editable card_json] ID[identity: spiffe_id, name,
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.