Last updated 2026-04-26
How to register an ERC-8004 agent
Register your agent onchain with a single contract call: write a JSON metadata file, pin it to IPFS, and send a register(agentURI) transaction to the Identity Registry. After the tx confirms, the agent shows up on this Explorer. Allow about thirty minutes and a hundred lines of TypeScript. This walkthrough uses viem v2, but the same call works from any web3 client.
What you’ll build
By the end, you’ll have a minted Identity Registry token on the chain of your choice, owned by a wallet you control, with a metadata URI that resolves to a JSON file describing your agent. The bulk of the code is the IPFS pin and the contract call; everything else is glue. This tutorial is the read-write counterpart to /tutorials/viem, which covers the read-only path. If you’ve worked through that one, reuse the client.ts file from it directly.
Prerequisites
- An RPC endpoint. Sign up at Quicknode; the free tier is enough to register a single agent. Paid tiers unlock the archive depth and rate limits you’ll want once you start indexing reputation events.
- A funded EOA wallet that holds the chain’s native token for gas. Ethereum mainnet costs dollars. Base, Mantle, BNB, and Avalanche cost pennies.
- The Identity Registry address for your chain. See /docs/contracts. Thanks to CREATE2 it’s
0x8004A169FB4a3325136EB29fA0ceB6D2e539a432on every reference deployment. - Node 20+ and a TypeScript project. Same toolchain as /tutorials/viem; if you worked through that tutorial, reuse its
client.ts. - Quicknode IPFS for pinning and serving the metadata file. The metadata file lives off-chain; the registry only stores the URI. See Quicknode IPFS and the IPFS REST API docs.
Step 1 — Write the metadata file
Agent metadata is a small JSON document. The canonical schema is documented on the Identity Registry page; the short version is four fields, two of them optional.
{
"name": "ResearcherBot",
"description": "Summarizes academic papers on demand. Optimized for arxiv.",
"image": "ipfs://QmExampleHash/avatar.png",
"endpoints": [
{ "type": "https", "url": "https://researcher.example.com/api" },
{ "type": "websocket", "url": "wss://researcher.example.com/stream" }
]
}
Here’s what each field does:
name: human-readable, surfaced in the Explorer header and most aggregator UIs.description: one to three sentences. Searchable, and the surface most LLMs cite when asked about your agent.image(optional but recommended): anipfs://...URL pointing to an avatar. Shown on agent detail pages and in social cards.endpoints(optional): how consumers actually talk to the agent. Indexers don’t validate these; consumers do.
The schema is intentionally minimal. Fields beyond the canonical set are ignored, not rejected, so forward-compat is by additive convention rather than versioning. If you want a richer profile, add the fields and document them. Anything that conforms to the four canonical keys round-trips through every indexer.
A note on endpoints: the type field is a free-form string by convention. Consumers in the wild use https, websocket, grpc, mcp, and a handful of others. Pick whatever your callers expect; there’s no enum the contract enforces. If your agent speaks more than one protocol, list them all and let consumers pick. The Identity Registry doesn’t ping these URLs at registration time, so a typo here won’t block your tx. It’ll just leave clients staring at a dead endpoint until you update the metadata.
Step 2 — Pin the metadata to IPFS
You can pin programmatically with the Quicknode IPFS REST API or by hand from the Quicknode dashboard. Either way, the goal is a content-addressed URL of the form ipfs://<cid> that survives the host going offline.
npm install viem@^2.21.0
// src/pin-metadata.ts
const metadata = {
name: "ResearcherBot",
description: "Summarizes academic papers on demand. Optimized for arxiv.",
image: "ipfs://QmExampleHash/avatar.png",
endpoints: [{ type: "https", url: "https://researcher.example.com/api" }],
};
const body = new FormData();
body.append(
"Body",
new Blob([JSON.stringify(metadata)], { type: "application/json" }),
"metadata.json"
);
body.append("Key", "metadata.json");
body.append("ContentType", "application/json");
const response = await fetch("https://api.quicknode.com/ipfs/rest/v1/s3/put-object", {
method: "POST",
headers: { "x-api-key": "FILLME" },
body,
});
if (!response.ok) {
throw new Error(`Quicknode IPFS upload failed: ${response.status}`);
}
const payload = await response.json() as { pin?: { cid?: string }; cid?: string };
const cid = payload.pin?.cid ?? payload.cid;
if (!cid) throw new Error("Quicknode IPFS response did not include a CID");
const agentURI = `ipfs://${cid}`;
console.log("agentURI =", agentURI);
The image field above references its own IPFS hash. Pin the avatar separately first, then reference its CID inside the metadata JSON. Don’t nest the image as a base64 data URL: search-engine and aggregator crawlers don’t unpack base64 image fields, so your agent will look avatarless to the outside world.
If you’d rather click than script, the Quicknode dashboard does the same job: upload the avatar, copy its CID, paste it into the metadata JSON, upload the JSON, copy its CID. The result is identical to what the REST API returns. For one-off registrations the UI is faster; for anything you’ll repeat, the API call wins because it’s checked into git alongside the rest of your deploy script.
Whichever path you take, save the resulting agentURI somewhere you can paste it into the next step. If the URI is wrong, the registration tx might still succeed (the contract only validates shape, not reachability), and you’ll have a permanently registered agent pointing at nothing. Verify the URI resolves to your JSON in a browser before sending the tx.
Step 3 — Send the registration transaction
Now the actual write. We sign and broadcast a single register(agentURI) call against the Identity Registry. viem’s walletClient.writeContract takes care of the nonce, gas estimation, signing, and broadcast in one shot.
Verify the contract ABI before running this snippet. The example below uses
register(string agentURI) returns (uint256), which matches the canonical reference contracts at erc-8004-contracts (verified against commit0463311on 2026-04-26). The contract also exposes a no-argregister()and a three-argregister(string, MetadataEntry[])overload for advanced metadata. Cross-check the deployed bytecode at your registry address before running on mainnet — overloads make function selectors easy to mismatch.
// src/register.ts
// Function ABI verified against the canonical reference repo:
// https://github.com/erc-8004/erc-8004-contracts (commit 0463311, 2026-04-26)
// If you point this at a non-canonical deployment, re-verify before running.
import { createWalletClient, http, parseAbi } from "viem";
import { privateKeyToAccount } from "viem/accounts";
import { mainnet } from "viem/chains";
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: mainnet,
transport: http(process.env.QUICKNODE_RPC_URL!),
});
const IDENTITY_REGISTRY = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
const abi = parseAbi([
"function register(string agentURI) returns (uint256)",
]);
const hash = await walletClient.writeContract({
address: IDENTITY_REGISTRY,
abi,
functionName: "register",
args: ["ipfs://YOUR_PINNED_CID"],
});
console.log("registration tx:", hash);
Onchain, two things happen: the contract mints an ERC-721 token to your wallet and emits Registered(uint256 indexed agentId, string agentURI, address indexed owner). The minted tokenId is the agent’s permanent ID. The function returns it, but writeContract only gives you back a tx hash, not the return value, so we pull the ID out of the event log instead.
The Registered event is what every indexer in the ecosystem listens for, including this Explorer. Anything that doesn’t reach the event log doesn’t exist as far as downstream consumers are concerned, so a tx that succeeds but doesn’t emit Registered (which would only happen if you called the wrong function) leaves you with a transaction receipt and no agent. That’s why the receipt-parse step below is the real confirmation, not the bare tx hash.
waitForTransactionReceipt blocks until the tx confirms, and decoding the event off the receipt gives us the agent ID:
// receipt-parse.ts (continuation of src/register.ts; reuses `hash` from above)
import { parseEventLogs, parseAbiItem } from "viem";
import { client } from "./client.js"; // public client from /tutorials/viem
const receipt = await client.waitForTransactionReceipt({ hash });
const registeredEvent = parseAbiItem(
"event Registered(uint256 indexed agentId, string agentURI, address indexed owner)"
);
const [decoded] = parseEventLogs({
abi: [registeredEvent],
logs: receipt.logs,
eventName: "Registered",
});
if (decoded) console.log("agentId =", decoded.args.agentId);
Step 4 — Verify on this Explorer
Once the tx confirms, your agent shows up at https://erc8004.quiknode.com/agents/<network-slug>/<tokenId>, for example /agents/ethereum-mainnet/42. The page renders your agent’s name, description, IPFS avatar, and endpoints list, plus an empty feedback and validations history that fills in once clients start submitting.
Indexer lag is typically under a minute on the supported chains. A longer delay usually means the chain itself is slow to finalize on the indexer’s confirmation depth; per-chain values are listed on /status. If five minutes go by and nothing shows up, look at the receipt. A Registered event in the logs means the indexer will catch up. No event means the registration didn’t actually succeed.
Once the agent page renders, share its URL. The Explorer’s social cards pull name, description, and image from your IPFS metadata, so a Twitter or Slack paste shows the avatar and bio without any extra setup. That’s the same surface most aggregators read, which is why the metadata fields back in Step 1 matter: they’re what the rest of the world sees. If you set the bio in a hurry and want to clean it up later, that’s fine — pin a new JSON, send a URIUpdated tx, and indexers will replay it without losing your existing feedback or validations.
Common errors
- The
register()call reverts. Almost always a malformedagentURI: wrong scheme, missing CID, or past the length the contract accepts. Useipfs://,ar://, orhttps://, and keep the URI short. Other revert causes (insufficient gas, an RPC dropping the tx mid-broadcast) are recoverable with a retry; a bad URI needs a code fix. - The tx reverts with no error reason. Usually a gas estimation that came in low. Pass an explicit
gasvalue 1.5× the estimate, or wait for a calmer block. - The agent doesn’t show up on the Explorer after five minutes. Check the receipt. A
Registeredlog means the indexer will catch up. NoRegisteredlog means the tx didn’t actually register anything, even if it confirmed. Confirmation depth values are on/status; if everything looks right and the agent still hasn’t appeared, file an issue.
Where to go next
- Read agents back with viem — the read-only counterpart to this tutorial
- Production RPC for ERC-8004 — Ready to ship? Get an RPC endpoint that won’t choke under your agent’s traffic.
- Contract addresses and ABIs
- What is ERC-8004?
- Identity Registry deep dive
- Reputation formula
- The canonical EIP: https://eips.ethereum.org/EIPS/eip-8004
- Reference contracts: https://github.com/erc-8004/erc-8004-contracts
FAQ
How much does it cost to register an agent?
Gas only — there’s no protocol fee. On Base or Mantle, expect cents. On Ethereum mainnet, expect dollars depending on gas market conditions. The transaction itself is a single ERC-721 mint plus a Registered event emit, so the gas cost is comparable to any standard NFT mint.
Where should I host the metadata file?
IPFS or Arweave. Both are content-addressed, so the URL embeds the file hash and consumers can verify the metadata hasn’t been swapped. HTTPS works in a pinch, but if the server goes offline or the path changes, your agent’s identity persists onchain while its metadata becomes unreachable. Quicknode IPFS is the recommended IPFS pinning and gateway option for this walkthrough.
Can I update my agent's metadata later?
Yes — the contract emits a URIUpdated event when the owner submits a new agentURI. Indexers replay event history and use the latest URI. The original URI remains in event logs forever, which gives consumers an audit trail. Updating costs another transaction worth of gas.
What if my registration transaction reverts?
The most common cause is a malformed agentURI string (wrong scheme, missing CID, exceeded length). Check the canonical metadata schema and the contract’s input validation. Other revert causes — insufficient gas, RPC dropping the tx — are recoverable; a malformed URI requires a code fix.
How do I see my agent on this Explorer?
After the registration tx confirms, your agent appears at /agents/<network-slug>/<tokenId>. Indexer lag is typically under one minute on the supported chains. If 5 minutes pass without your agent showing up, check the transaction receipt for a Registered event — if it’s there, the indexer will catch up; if not, the registration didn’t succeed.
Can I register multiple agents from the same wallet?
Yes. The Identity Registry is a standard ERC-721, so one wallet can own any number of agent tokens. Each registration call mints a new tokenId. Some teams keep one agent per wallet for clear ownership boundaries; others register a whole fleet from one operator wallet. Both work.