The on-chain part is tiny

ERC-8004 puts almost nothing on chain. The Identity Registry holds a struct per agent that looks roughly like this:

struct Agent {
    uint256 agentId;
    address agentOwner;
    string  agentURI;
}

Three fields. That's it. The interesting bits live behind the URI.

The function surface is just as small:

function registerAgent(string calldata agentURI) external returns (uint256 agentId);
function updateAgentURI(uint256 agentId, string calldata newURI) external;
function transferAgentOwnership(uint256 agentId, address newOwner) external;
function getAgent(uint256 agentId) external view returns (Agent memory);

Anyone can register. The owner can update the URI or hand the agent to a new owner. The agentId is a monotonic counter, and it's the only thing about your agent that's permanent.

What the agentURI actually points at

The URI is a string. It can resolve to anything that speaks HTTP-style retrieval: an https URL, an IPFS CID, an ar:// link, a data URI. Whatever's on the other end is expected to return JSON describing the agent.

The agent card JSON has a loose shape that the spec sketches and the community is still hardening:

{
  "name": "x402_deployer",
  "description": "...",
  "skills": [...],
  "endpoints": [...],
  "trustModels": [...],
  "reputation": {...}
}

Skill arrays, endpoint definitions, reputation hooks, trust model identifiers. Concrete schemas are still settling, but most reads happen off chain by a routing agent that wants to know what this agentId can do before paying for a call.

agentId 47167, the worked example

Our agent on Base is agentId 47167. The owner is the x402_deployer EOA. The agentURI is an https URL pointing at a route on our deploy worker.

When something resolves 47167, here's what happens:

1. Caller reads getAgent(47167) on the Identity Registry. 2. Contract returns the struct, including agentURI. 3. Caller GETs the URL. 4. The worker reads the live endpoint registry, the cluster metadata, the current pricing, and renders JSON on the fly. 5. JSON comes back. Routing agent picks an endpoint.

The thing to notice is step 4. It's a render, not a lookup of a static file.

Why the URI is dynamic

We ship endpoints constantly, and pricing shifts often. Off-chain reality moves faster than any pinned blob. If the agent card were a JSON file pinned to IPFS, every endpoint change would mean a new pin and an updateAgentURI transaction. Gas is cheap on Base, but the bigger cost is mental overhead and drift between what's deployed and what the card claims.

Resolving the URI to a worker route fixes that. The worker reads the same registry the deploy pipeline reads. An endpoint goes live, and the agent card reflects it on the next GET. No on-chain write needed.

This is also why you never re-mint to change metadata. The agentId is your identity across the protocol. Reputation hooks (ERC-8004's Reputation Registry, third-party trust scores, external validators) all key on the agentId. Minting a new one drops that history on the floor. If you ever need to point the card at a different host, updateAgentURI(47167, newURI) keeps the agentId intact. Re-minting is a last resort, and honestly it's a sign you've already lost the plot.

The shortest possible diagram

Routing agent
    │  getAgent(47167)
    ▼
Identity Registry (Base)
    │  returns (47167, owner, agentURI)
    ▼
Routing agent
    │  GET https://...workers.dev/.well-known/agent-card.json
    ▼
Worker
    │  reads endpoint registry, renders JSON
    ▼
Routing agent  ◀── agent card

That's the whole flow. Three contract fields, four functions, one HTTP GET. The card stays current because the worker keeps it current, and agentId 47167 stays the same forever, so the reputation built on top of it stays attached.