Agent Card and Agent Passport: One Is On Chain, One Is a File We Serve

The two words are two objects

The passport is a token in a public registry contract. It has an identifier, an owner address and a URI string, and it is readable by anybody with a node and no account with us.

The card is a JSON document served by our API at a public path, with no authentication. It is what the passport's URI points at.

Confusing them is the common error, and it matters because they do not have the same lifetime.

What is actually stored on chain

Three things: an identifier, an owner, and a string. The registration call takes a URI and nothing else. There is no hash of the document anywhere in the transaction.

So the chain proves where to look. It proves nothing about what you find there. A reader who fetches the card is trusting our server for the contents, exactly as they would trust any web endpoint, and the on-chain record does not improve that.

Stating it plainly is better than letting "on chain" do work it cannot do.

And the pointer has no product path to change it

The registry contract does expose a method to set a new URI for an existing agent. Our backend never calls it.

The method is present in the interface we compiled against, so the search found the capability and then found nothing using it.

Consequence: if a card URI ever has to change, because a hostname moves for instance, there is no path through the product. The token owner would have to call the contract directly.

What the card contains, and why the list is short on purpose

A type and a version. The identifier and how to resolve it. The registry entry, when one exists. And a credential list.

Each credential entry carries a type, an issuance timestamp, an on-chain credential identifier, a link to a descriptor and a status reference. Revoked credentials are filtered out before the list is built.

There is no claim data in it at all. Not a name, not a date of birth, not a jurisdiction, not a score. The card is a whitelist by construction: it emits an enumerated set of fields, so a new column added to a table does not leak into a public document by default.

That is the correct shape for anything that leaves the building, and it is worth publishing as loudly as a defect.

One field is mislabelled, and the noun is the whole problem

updatedAt is the time you asked, not the time anything changed. It is generated when the card is rendered, and the card is rendered on every request.

No table stores a modification time for an agent or a credential. There is a creation timestamp and nothing else, so the field could not carry the meaning its name promises even if somebody wanted it to.

What that does to a consumer: anybody caching on it re-fetches forever, and anybody diffing it to detect a change gets a difference every time and learns nothing. A field that always changes and a field that never changes are equally uninformative, and this one manages both.

What we got right, published with the same prominence

The one configuration value that gets written irreversibly is guarded at boot. The public base URL is minted into card URIs written on chain, into issued credentials and into status-list links handed to outside verifiers, and none of those can be retracted.

The guard exists because of a near-miss, and the code says so. The value defaulted to a local address, was missing from the example configuration and was never set in production, so the very first passport anchor attempted to write a localhost URL into a public on-chain registry. It failed only because the relayer had no gas.

The fix is fail-closed: in production the service refuses to start unless the value is a public secure origin. A near-miss that was caught by an unrelated failure is exactly the kind that gets forgotten, so it is written into the guard's own comment and repeated here.

Keep reading

Agent Card and Agent Passport: One Is On Chain, One Is a File We Serve · Solidus · Solidus Agents