Last updated 2026-04-26
Read ERC-8004 onchain with TypeScript and viem
Reading ERC-8004 onchain with viem is mostly a matter of pointing a PublicClient at an RPC and calling getLogs against the right registry addresses. This walkthrough does that on any EVM chain with viem v2 and a Quicknode endpoint. We start with a client, list recent agents, fetch one agent’s metadata off IPFS, and read its feedback. Roughly 200 lines, no wallet needed.
Prerequisites
- Node 20+ and a TypeScript project (or
tsxfor one-off runs) - An RPC URL — sign up at Quicknode for the chain you want to read from
- The Identity / Reputation / Validation Registry addresses for that chain — see /docs/contracts
- Patience for IPFS — agent metadata is content-addressed and fetched off-chain, so the first fetch from a cold gateway can take a couple of seconds
Install
npm install viem@^2.21.0
viem is the only runtime dep needed for this tutorial. The pin at major 2 is intentional — the v1 → v2 migration changed the public API enough that older snippets won’t compile against v2, and latest would mean a future v3 silently breaks everything below.
Connect to the chain
createPublicClient is viem’s read-only client. You give it a chain (one of viem’s named exports) and a transport (here, plain HTTP pointed at your Quicknode endpoint). One client handles every read below.
To switch chains, swap the chain import. viem ships named exports for mainnet, base, bsc, avalanche, mantle, and most other EVM chains. The RPC URL is the only other thing that has to change.
// src/client.ts
import { createPublicClient, http } from "viem";
import { mainnet, base, bsc, avalanche, mantle } from "viem/chains";
const QUICKNODE_RPC_URL = process.env.QUICKNODE_RPC_URL!; // e.g. https://abc.quiknode.pro/xyz/
export const client = createPublicClient({
chain: mainnet, // swap for base / bsc / avalanche / mantle
transport: http(QUICKNODE_RPC_URL),
});
List recent agent registrations
Every agent registration emits a Registered(uint256 indexed agentId, string agentURI, address indexed owner) event from the Identity Registry. To list agents, call getLogs with that event signature and the registry address.
For Ethereum mainnet, the Identity Registry is at 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432. CREATE2 means it’s byte-identical on every reference-deployment chain. The other addresses live in /docs/contracts.
The fromBlock choice is the main knob. Use the deploy block (24339871 on Ethereum mainnet) to backfill the whole registry. Use something like head - 100_000n for a “what’s new lately” view — roughly two weeks of mainnet history. Going earlier than the deploy block is wasted RPC budget.
viem decodes indexed topics and the non-indexed event data, so agentId, owner, and agentURI all come out of log.args:
// src/list-agents.ts
import { parseAbiItem } from "viem";
import { client } from "./client.js";
const IDENTITY_REGISTRY = "0x8004A169FB4a3325136EB29fA0ceB6D2e539a432";
const event = parseAbiItem(
"event Registered(uint256 indexed agentId, string agentURI, address indexed owner)"
);
const head = await client.getBlockNumber();
const logs = await client.getLogs({
address: IDENTITY_REGISTRY,
event,
fromBlock: head - 100_000n, // last ~100k blocks; use 24339871n to backfill from deploy
toBlock: "latest",
});
for (const log of logs) {
console.log(log.args.agentId, "→", log.args.agentURI, "(owner", log.args.owner, ")");
}
Fetch one agent’s metadata
The agentURI field is typically ipfs://<cid>. Browsers and Node can’t fetch ipfs:// directly, so rewrite it to your Quicknode IPFS gateway first, then fall back to a public gateway like https://ipfs.io/ipfs/<cid> if needed. In production, set a short AbortSignal.timeout(...) on the fetch and fall back to a second gateway on timeout — gateways flap independently, so a retry on a different host fixes most transient failures.
The metadata JSON shape is name, description, image, and endpoints. The full schema is documented on the Identity Registry page.
// src/fetch-metadata.ts
async function fetchAgentMetadata(agentURI: string) {
const quicknodeIpfsGateway = (process.env.QUICKNODE_IPFS_GATEWAY_URL ?? "https://ipfs.io/ipfs/")
.replace(/\/?$/, "/");
const url = agentURI.startsWith("ipfs://")
? agentURI.replace("ipfs://", quicknodeIpfsGateway)
: agentURI;
const res = await fetch(url);
if (!res.ok) throw new Error(`metadata fetch failed: ${res.status}`);
return res.json() as Promise<{
name: string;
description: string;
image?: string;
endpoints?: Array<{ type: string; url: string }>;
}>;
}
Read feedback for an agent
Feedback events come from the Reputation Registry. The signature is long but the shape isn’t bad. agentId and clientAddress are indexed. The third indexed slot is keccak256(tag1), which is handy if you want to prefilter by tag without scanning every row. The numeric rating is value (signed int128) divided by 10 ** valueDecimals, intended to land in [0, 1].
The Reputation Registry on Ethereum mainnet is at 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63. viem’s args filter narrows the query to a specific agent without you having to hash the topic by hand:
// src/read-feedback.ts
import { parseAbiItem } from "viem";
import { client } from "./client.js";
const REPUTATION_REGISTRY = "0x8004BAa17C55a88189AE136b182e5fdA19dE9b63";
const event = parseAbiItem(
"event NewFeedback(uint256 indexed agentId, address indexed clientAddress, uint64 feedbackIndex, int128 value, uint8 valueDecimals, string indexed indexedTag1, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash)"
);
async function listFeedback(agentId: bigint, fromBlock: bigint) {
return client.getLogs({
address: REPUTATION_REGISTRY,
event,
args: { agentId },
// To prefilter by tag, also pass: indexedTag1: "quality"
// viem hashes the string into a bytes32 topic for you.
fromBlock,
toBlock: "latest",
});
}
Each feedback row’s normalized rating is Number(value) / 10 ** valueDecimals. The contract intends this to land in [0, 1], but value is int128 (signed), so a non-conforming submission could carry a negative number — clamp before displaying. The Reputation Registry article explains how non-revoked rows roll into the explorer’s composite score.
One thing to handle on the read side: clients can pull a feedback record back by emitting FeedbackRevoked(uint256 indexed agentId, address indexed clientAddress, uint64 indexed feedbackIndex). Any indexer that aggregates feedback has to subtract those revocations, otherwise the score lies. For a one-off read it’s fine to ignore; for a real index, listen to both events and reconcile by (agentId, clientAddress, feedbackIndex).
Pagination and archive depth
Two things tend to bite once you move from prototype to production reads.
- Block-range cap and retries. RPC providers cap how wide a window a single
getLogscall can sweep. Quicknode’s mainnet cap is generous, but if you hit “block range too wide”, chunkfromBlock/toBlockinto 50k-block windows and concatenate. While you’re there, wrap each call in a 3-attempt retry with exponential backoff — RPCs flap, and viem doesn’t retry by default. - Archive node. Backfilling from the deploy block needs archive depth. Quicknode’s paid tiers include archive (see /quicknode). Without archive history you’re stuck reading from the chain head forward, so a brand-new index can never catch up to events older than your provider’s retention window.
Where to go next
- What is ERC-8004? — the conceptual overview if you arrived here from a search
- Register your first agent — the write path
- Get RPC for ERC-8004 — the Quicknode signup destination
- Contract addresses
- The Reputation formula: /reputation-v1
- viem docs: https://viem.sh
- The canonical EIP: https://eips.ethereum.org/EIPS/eip-8004
FAQ
Why viem instead of ethers.js?
viem v2 is the current standard for TypeScript Ethereum clients — smaller bundle, first-class type inference from ABIs, and a clean tree-shakable API. ethers.js still works, but viem is what most new projects pick.
Do I need an archive node?
For full-history reads (every Registered event since deploy), yes — you need archive depth to call eth_getLogs from the deploy block. Quicknode’s paid tiers include archive. The free tier indexes the most recent few thousand blocks, which is fine for reading current state but won’t backfill an index from genesis.
How fast is getLogs?
Depends on your RPC provider’s block-range cap and the density of events. ERC-8004 events are sparse on most chains today (the standard is new), so a single getLogs call from deploy block to head usually returns in under a second on Quicknode mainnet RPCs. Once volume picks up, paginate by block range.
Can I use viem in a browser?
Yes. Everything in this tutorial works the same in a browser as in Node — switch the http() transport for window.ethereum if you need wallet signing, but reads don’t require a wallet. Be careful with putting your RPC URL in client-side code; rotate keys you’ve exposed.
Where do agent metadata files live?
Most agents host metadata on IPFS. Quicknode IPFS can pin and serve those files through a dedicated gateway; public gateways like ipfs.io work for fallback fetching. Some agents host on Arweave or HTTPS. The agentURI field in the Registered event tells you the scheme; use a small fetcher that supports ipfs:// gateway rewriting if you don’t already have one.