
197 ERC-8004 agents, and where their prices live
We indexed real ERC-8004 agent registrations on Base, Ethereum mainnet and Sepolia with Substreams, The Graph's block-streaming indexer, using one Rust module for all three chains. The registry tells you who exists. What they cost is somewhere else.
TL;DR
I built Turnstile, a marketplace where AI agents sell answers and other agents pay for them.
Step one of any purchase is discovery: which agents exist, what do they do, and where do they get paid. ERC-8004 ("Trustless Agents") is the obvious place to look. I indexed it with Substreams, The Graph's engine for processing blocks with Rust code compiled to WASM. I wrote one module that decodes the ERC-8004 Identity Registry and ran the same binary on every chain.
- Streamed 197 real agent registrations from Base (119), Ethereum mainnet (38) and Sepolia (40) into one SQLite store, joined on a cross-chain id. That is a sample of short block windows, not a full index: Base alone has about 85,000 agents.
- ERC-8004 has no price field, by design, so the prices that exist live elsewhere, in three formats. 76 Virtuals agents follow ACP, Virtuals' Agent Commerce Protocol, and put a bare number with no currency in its
jobOfferingsfield. One agent, Capminal, answers with a live x402 quote: 0.50 USDC on Base. And one is ours: agent 10127 on Sepolia, whose registry entry points to the ENS nameliquidity.turnstile.eth, with the price in that name's text records. - 97 agents set
x402Support, but 79 of those are Virtuals agents with no HTTP endpoint at all. Of the rest, only a handful list a real endpoint, and most of those are dead.
So finding a seller takes more than the registry. The registry tells you which agents exist. The price comes from a different place depending on who you ask: the seller's ENS name, the 402 its endpoint returns if the endpoint is up, or a marketplace's own format.
The code can be found on Github.
Part 4 of 9 in the Turnstile series: building a paid lane for onchain data agents.
What ERC-8004 standardizes, and what it leaves out
I first read the EIP8004 in February, when Base announced support. It defines three lightweight registries.
- Identity Registry. An ERC-721 with URIStorage. Each agent is a token, and its
agentURIpoints to a registration file. - Reputation Registry. A standard way to post and read feedback from clients.
- Validation Registry. Hooks for independent checks: stake-secured re-execution, zkML verifiers, TEE oracles.
Identity Registry: who exists
The registration file is JSON. Here is a real one, from Capminal's agent (number 1380 on Base), trimmed:
{
"type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
"name": "Captain Dackie",
"description": "I am Captain Dackie | A DeFAI and x402 AI Agent from Capminal | ...",
"image": "https://blob.8004scan.app/cd88d8d9….jpg",
"active": true,
"services": [
{ "name": "web", "endpoint": "https://app.virtuals.io/virtuals/23397" },
{ "name": "MCP", "endpoint": "https://www.capminal.ai/mcp", "version": "2025-01-15" },
{ "name": "A2A", "endpoint": "https://www.capminal.ai/.well-known/agent-card.json", "version": "0.3.0" },
{ "name": "custom", "endpoint": "https://www.capminal.ai/api/x402/deploy" },
{ "name": "email", "endpoint": "dackie@capminal.ai" }
],
"registrations": [
{ "agentId": 9382, "agentRegistry": "eip155:1:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" },
{ "agentId": 1380, "agentRegistry": "eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432" }
],
"supportedTrust": ["crypto-economic", "tee-attestation", "reputation"],
"x402Support": true
}
services lists the ways to reach the agent: a website, an MCP server, an A2A agent card, plain endpoints. registrations links the same agent across chains, here Ethereum mainnet and Base. x402Support: true says the agent takes x402 payments, but nothing in the file says how much it charges.
For discovery, the Identity Registry emits three events.:
event Registered(uint256 indexed agentId, string agentURI, address indexed owner)
event URIUpdated(uint256 indexed agentId, string newURI, address indexed updatedBy)
event MetadataSet(uint256 indexed agentId, string indexed indexedMetadataKey, string metadataKey, bytes metadataValue)
One metadata key is reserved, agentWallet, and it starts out as the owner's address. For a marketplace that is the most important field in the standard, because it is where the agent gets paid.
Reputation Registry: what clients report
Any address can leave feedback on an agent with giveFeedback, except the agent's owner and its operators; the contract reverts on self-review. Each piece of feedback stores a signed number with its decimal count, two free-form tags that say what the number measures, and a revoked flag. The spec's examples give a sense of the range:
tag1 |
Meaning | value |
valueDecimals |
|---|---|---|---|
starred |
87 out of 100 | 87 |
0 |
uptime |
99.77% | 9977 |
2 |
responseTime |
560 ms | 560 |
0 |
A written review, if there is one, lives offchain at a feedbackURI, and the contract keeps its keccak-256 hash so the file can't be swapped later. The reviewer can revoke their feedback, and anyone can attach a response to it: the agent pointing to a refund, or an aggregator flagging spam.
To read an agent's score, you call getSummary with a list of reviewers you trust, and the contract averages only their feedback. The list is mandatory because new addresses cost nothing: anyone could post a hundred five-star reviews from a hundred fresh wallets. Smarter scoring, such as trusting reviewers with a long track record, is left to offchain tools.
Validation Registry: who checked the work
Reputation is opinion. Validation is a check. An agent's owner calls validationRequest, naming a validator contract and pointing to the inputs and outputs of a piece of work stored offchain. The validator answers with validationResponse, a score from 0 to 100, and can answer again as the check progresses, for example tagging a soft result and later a final one. How the validator checks is its own business: re-running the job with stake at risk, verifying a zkML proof, reading a TEE attestation. Staking and slashing sit outside the standard.
You can think about the validator as an oracle the agent chooses, and the registry is where its answers are published. The registry checks nothing itself, so a score is only worth as much as the validator that gave it.
This registry isn't live. The contract is in the official repository, but the deployment list has no Validation Registry address on any chain, and the README says that part of the spec is still being revised with the TEE community. The deployed ERC-8004 is identity and reputation.
No price
What the standard does not have is a price. The EIP states: "Payments are orthogonal to this protocol and not covered here." That is fine as the standard needs to be as generic as possible so we can compose over it, but for someone building a buyer's agent it means the registry can be used to discover seller agents and their services but it can never answer "what does this cost?"
Why Substreams, when a subgraph would have worked
ERC-8004 deploys to vanity addresses: 0x8004A169…a432 on mainnets and 0x8004A818…BD9e on testnets, per the official deployments list. Ethereum mainnet and Base share an address, and so do Sepolia and Base Sepolia. The event ABI is identical everywhere. What varies between chains is the chain id, which address applies, and the deployment block.
So the decoding code is the same on every chain, and only those three values change.
A subgraph would have handled that fine. You write one mapping, list the address and start block per network in networks.json, and deploy it once per chain. The Graph's Agent0 subgraphs already index ERC-8004 that way across many networks, and we named our fields to match their schema.
We picked Substreams for two reasons. The first is where the data ends up. Each subgraph deployment is its own GraphQL API, so three chains means three endpoints to query and merge. We wanted every chain's registrations to land in one SQLite store, joined on a cross-chain id, next to the ENS offers, x402 quotes and settlement receipts the rest of Turnstile reads. The second is that we had built subgraphs before and wanted to try The Graph's other indexing tool, for fun.
Substreams is The Graph's other indexing engine, originally built by StreamingFast. You write modules in Rust. Each one receives a full block (header, transactions, receipts, every event log, internal calls and state changes) or the output of another module, picks what it needs, and returns it in a format you define. Ours only reads event logs: it keeps those emitted by the registry and decodes the three events above. The engine runs modules across the chain's history in parallel and streams the results to a sink: a database, a file, your own program, or a subgraph.
Two features did the multi-chain work. The manifest's networks: section "allows specifying per-network params and initialBlock for each module", so the chain-specific facts become params and every chain runs the same WASM. And the output is a protobuf message we define, so registrations from every chain come out in one shape, ready for the same table. The package ships as a single .spkg that anyone can import and build on.
This is how the multi-chain part looks in practice:
networks:
mainnet:
initialBlock:
map_agent_registrations: 24339871
params:
map_agent_registrations: "registry=0x8004a169fb4a3325136eb29fa0ceb6d2e539a432&chain_id=1&network=mainnet"
base:
initialBlock:
map_agent_registrations: 41663783
params:
map_agent_registrations: "registry=0x8004a169fb4a3325136eb29fa0ceb6d2e539a432&chain_id=8453&network=base"
sepolia:
initialBlock:
map_agent_registrations: 9989393
params:
map_agent_registrations: "registry=0x8004a818bfb912233c491871b3d84c89a494bd9e&chain_id=11155111&network=sepolia"
Trimmed (store modules and base-sepolia omitted). graph/substreams/substreams.yaml
substreams run erc8004-agent-registry-v0.1.0.spkg map_agent_registrations --network base and --network mainnet run the same WASM. The manifest declares four networks: mainnet, Base, Sepolia and Base Sepolia.
The module
The package has three modules:
| Module | Kind | Purpose |
|---|---|---|
map_agent_registrations |
map | Decodes one registry, one block, no state |
store_agent_wallets |
store (set) |
Latest agentWallet per agent |
map_agent_directory |
map | The map joined against the store, so the payout address is right even when the wallet changed in an earlier block |
The core is the map handler. For each log from the configured registry, it matches Registered or URIUpdated, builds a cross-chain id, picks an operator, and tries to parse the registration file:
#[substreams::handlers::map]
fn map_agent_registrations(
raw_params: String,
blk: eth::Block,
) -> Result<out::AgentRegistrations, Error> {
let params = Params::parse(&raw_params)?;
// ...
for view in blk.logs() {
let log = view.log;
if log.address != params.registry_bytes {
continue;
}
let (event, agent_id, uri, owner) =
if let Some(e) = events::Registered::match_and_decode(log) {
(out::RegistrationEvent::Registered, e.agent_id, e.agent_uri, e.owner)
} else if let Some(e) = events::UriUpdated::match_and_decode(log) {
(out::RegistrationEvent::UriUpdated, e.agent_id, e.new_uri, e.updated_by)
} else {
continue;
};
let agent_id = agent_id.to_string();
let agent_uid = agent_uid(¶ms, &agent_id);
let owner = to_hex(&owner);
let (operator, operator_source) = match wallet_for(&agent_uid) {
Some(wallet) => (wallet, out::OperatorSource::AgentWallet),
// EIP-8004 initialises agentWallet to the owner's address.
None => (owner.clone(), out::OperatorSource::OwnerDefault),
};
let (uri_scheme, document) = registration::parse_uri(&uri);
let parsed = document.as_ref().map(registration::parse_document);
// ... push AgentRegistration { agent_uid, chain_id, operator, uri_scheme,
// registration_resolved: parsed.is_some(), x402_support, price, ... }
}
// ...
}
Trimmed. graph/substreams/src/lib.rs
Three design decisions matter here.
The join key. agent_uid is eip155:<chain_id>:<registry>/<agent_id>, a CAIP-10-shaped id extended with the token id. Agent 1411 on Base and agent 1411 on mainnet share a registry address but are different agents, and the chain id keeps them apart. That is what makes a cross-chain union safe.
What a module cannot resolve. A module is a pure function of the block, so it cannot fetch https:// or ipfs://. It resolves what the chain already carries (data: URIs and, because people do it, bare JSON literals in the URI field) and marks everything else with registration_resolved: false. In our store that covered 15 of 197 agents in-module, 7.6%.
Tolerant parsing. Real registrations drift from the spec. In a survey of 1,200 registrations across mainnet, Sepolia and Base Sepolia, 31 of 441 service arrays were named endpoints instead of services, x402Support showed up as x402support 41 times, and an endpoint's URL showed up under five different keys. The parser reads all of them.
That survey found no document with a price field, but it didn't cover Base, where the Virtuals agents keep their prices inside jobOfferings (see The measured finding). The module still has a Price message, shaped like an x402 payment requirement, for the price, pricing and x402Price fields some agents add on their own. It doesn't read Virtuals' format; the sink handles that.
The sink: landing registrations in a store
A sink is the program on the receiving end of a Substreams module: it takes the module's output and writes it somewhere. The Graph ships an official SQL sink for Postgres and ClickHouse. We wrote a small one instead, because we wanted SQLite: it ships with Node, so the store goes from git clone to a query with no server to run. graph/sink/sink.ts runs substreams run ... -o jsonl, reads one JSON line per block, and upserts rows in batches of 500 blocks.
The package has two outputs for two kinds of reader. map_agent_registrations looks at each block on its own, so it can stream any window of blocks cheaply. map_agent_directory adds a running memory of every agentWallet change.
An agent can set its payout wallet blocks after it registers, and only a reader with memory can connect the two. That memory is built by replaying every block from the registry's deployment. On Base the registry was deployed about 37,000 blocks before the window we wanted, so a 200-block read came back as this request needs to process 37,200 blocks (37,000 of them to prepare the stores). That suits someone indexing a chain from the start but we were reading short windows, so the sink takes the plain output, stores every wallet change as its own row, and joins them in SQL (foldWallets). The join moved 10 Sepolia agents onto a wallet they set after registering.
The store is a sample, not a full index. Our Graph Market plan allows two streams at a time, so we streamed four short windows, one chain at a time:
| Network | Block range | Covers | Agents |
|---|---|---|---|
| Base | 41,700,000 to 41,715,999 | about 9 hours on 4 February 2026, a day after the registry deployed | 119 |
| Ethereum mainnet | 25,000,000 to 25,014,999 | about 2 days, 1 to 3 May 2026 | 38 |
| Sepolia | 10,000,000 to 10,009,999 | about 38 hours, 8 to 9 January 2026 | 39 |
| Sepolia | 11,653,700 to 11,653,729 | the block where ours registered | 1 (agent 10127) |
For scale, Base alone had about 85,000 agents registered by 2026-09-07, counted from RPC logs. 197 is a small slice, and every number in this article describes that slice. Indexing everything is a longer run of the same code: the sink keeps a cursor per chain and picks up where it stopped.
Fetching the registration files
The registry stores each agent's agentURI, which is usually a link. For most agents the registration file sits behind it, on a web server or IPFS, and a Substreams module can't make network calls. So once the stream has landed, resolve-cards.ts follows every link the module couldn't read and stores the file it finds. It records every outcome, failures included, because "this agent advertises nothing" and "we couldn't reach its file" are different answers for a buyer. Each agent ends up in one state:
document_state |
Meaning | Agents |
|---|---|---|
off_module |
fetched by resolve-cards.ts |
126 |
in_module |
the file was onchain, decoded by the module | 15 |
failed |
the link gave a 404, a DNS error or a timeout | 25 |
no_uri |
registered with no URI at all | 30 |
not_fetchable |
a URI nothing can fetch over HTTP; ours, which is an ENS name | 1 |
Anyone can register an agent with any link, and you can see anything and everything here. The fetcher refuses links into private networks: localhost, office and home ranges such as 10.x and 192.168.x, carrier ranges, and link-local addresses such as 169.254.169.254, where cloud servers expose their own credentials. It re-checks after every redirect, so a public link can't bounce it into one.
Where the price comes from
Price is not a column on the agent table. It is derived in the read model, and every row says where its price came from, or that there is none:
CASE
WHEN s.price IS NOT NULL THEN 'turnstile'
WHEN q.amount IS NOT NULL THEN 'x402'
WHEN COALESCE(c.price_amount, a.price_amount) IS NOT NULL THEN 'document'
WHEN o.acp_offer_count > 0 THEN 'acp'
WHEN COALESCE(c.x402_support, a.x402_support, 0) = 1 THEN 'ask_x402'
ELSE 'none'
END AS price_source,
From the agent_current view. graph/sink/schema.sql
s is a Turnstile seller's ENS offer, q is a live 402 quote, c and a are the fetched and in-module documents, and o counts a Virtuals agent's priced job offerings. ask_x402 means "no price, but this agent takes x402, so the price is knowable if you ask".
The measured finding
This query shaped the rest of the design:
SELECT price_source, COUNT(*)
FROM agent_current
GROUP BY price_source;
It returned:
| Price source | Agents |
|---|---|
turnstile (an ENS turnstile:price record) |
1 |
x402 (a live 402 quote) |
1 |
acp (a Virtuals job offering) |
76 |
document (a price field in the registration file) |
0 |
ask_x402 (says it takes x402, no quote) |
20 |
none |
99 |
The document row counts agents that added their own price field to the registration file. None did. The prices that exist come from three other places, and each tells a buyer something different.
Ours is in ENS. turnstile:price on liquidity.turnstile.eth reads 0.07, in US dollars, and a buyer can read it without calling the seller.
Capminal's is in its 402. probe-x402.ts asks every endpoint an agent lists and reads x402 v2 quotes from the PAYMENT-REQUIRED response header. Capminal's registration file lists https://www.capminal.ai/api/x402/deploy as a custom service, and a POST there returns 500000 units of USDC on Base, fifty cents. A buyer learns it only by calling.
Virtuals' 76 are in a jobOfferings array defined by Virtuals' ACP protocol: 227 offerings priced from 0.0003 to 79.99, written as { "type": "fixed", "value": 0.02 }. No currency or asset is named, so discovery keeps them as raw numbers and never ranks them against dollar prices. A buyer that doesn't use ACP can't act on them.
The 20 ask_x402 agents say they take x402 payments, but none answered with a quote. Of the agents that list any service, six point at the OASF specification on GitHub, one at api.example.com, and the gekkoterminal domains no longer resolve.
Where x402 fits, and where ENS fits
x402 is a payment protocol built on HTTP: the server answers a request with 402 Payment Required and the payments it accepts, each with an amount. It builds that answer per request, so it can price by resource, by caller or by the minute. The spec lists dynamic pricing as one of its patterns, and our Hedera rail reprices with the HBAR rate. For an agent you found in ERC-8004, the price you can rely on is the one in the 402 you get right before paying.
Turnstile's price is published before anyone asks. It lives in ENS text records on liquidity.turnstile.eth: turnstile:price holds 0.07, in US dollars, for the standard tier, and turnstile:price-ceiling holds 0.50, the most any tier may charge. The premium tier sells at 0.35, under that ceiling. We put it onchain so a buyer's agent can read it without calling us, even when our server is down, and can check who is allowed to change it: the seller's hot key can reprice, and only the cold key can raise the ceiling (see The offer is the name). The seller service reads the same record, so the 402 charges what ENS says. The 402 still moves in one way: each payment option converts the dollar price into its own asset, so the HBAR amount follows the exchange rate.
The two prices come in different units. turnstile:price is decimal dollars, "0.07". An x402 amount is an integer in the asset's smallest unit, so "70000" USDC is seven cents. normalizePriceUsd converts both to dollars when it knows the asset and returns usd: null when it doesn't, rather than compare a raw 70000 with 0.07. Virtuals prices always come back null, since they name no currency.
Linking the ENS name to the ERC-8004 agent
Discovery starts from ERC-8004, and our price lives in ENS, so something has to join an agent to its ENS name. Two records point at each other. In ERC-8004, agent 10127's agentURI is liquidity.turnstile.eth. In ENS, the name carries a text record defined by ENSIP-25, agent-registration[<registry>][<agentId>], naming that registry and agent id. The registry part is an ERC-7930 address, which packs the chain id in with the contract address, so one string means "this contract on Sepolia". A non-empty value is the name's owner claiming that agent.
A one-way link proves little: anyone can register an agent whose URI is our name, and a name's record can list any agent id. So hydrate-sellers.ts, the script that copies our sellers' ENS records into the store, links the two only when each points at the other:
const backlink = await readAgentBacklink(client, seller.identityRegistry, seller.agentId);
const backlinkOk = backlink.tokenUri?.toLowerCase() === seller.ensName.toLowerCase();
const linked = offer.agentRegistrationConfirmed && backlinkOk;
const uid = agentUid(seller.chainId, seller.identityRegistry, seller.agentId);
For our seller both checks pass: the name carries agent-registration[…][10127], and the Sepolia registry's tokenURI(10127) returns liquidity.turnstile.eth. Only then does the seller's price join to agent eip155:11155111:0x8004a818bfb912233c491871b3d84c89a494bd9e/10127.
The record's key is a long string. The ENSIP's own example is agent-registration[0x000100000101148004a169fb4a3325136eb29fa0ceb6d2e539a432][42]. We build it twice, in Solidity when we write the record and in TypeScript when we read it. If both builders made the same mistake, our reader would still find our record and everything would look fine, but no other app following ENSIP-25 would. So both are tested against that example string from the ENSIP.
Ranking sellers by what buyers paid
Turnstile runs one seller of its own, liquidity.turnstile.eth, but it's also a marketplace. Its store lists every ERC-8004 agent it has indexed, and any buyer's agent can search it to find who sells a given service and at what price. A seller doesn't have to be ours to appear. It only needs an ERC-8004 registration.
Everything so far fills the store. Buyers' agents query it through one function, findSellers in seller/service/discovery.ts, which Turnstile exposes three ways:
- the
find_sellerstool on its MCP server, - the
/api/sellersendpoint behind the market page - a command-line tool.
A buyer asks for a capability, such as liquidity, optionally with a chain and a maximum price, and gets back a ranked list of sellers. The ranking uses settled volume: the total of payments buyers have actually made to each seller. A seller can't inflate that number on its own. Every payment Turnstile settles writes a receipt to the Hedera Consensus Service topic 0.0.10408013, and ingest-receipts.ts reads that topic back from Hedera's public mirror node, not from the seller's own database, so anyone reading the topic gets the same totals.
A store with no receipts yet, such as a fresh local checkout, ranks by registration date instead:
export function rankingBasis(db: DatabaseSync): { basis: RankBasis; placeholder: boolean; note: string } {
const row = db.prepare('SELECT COUNT(*) AS n FROM settlement_receipt').get() as { n: number };
if (row.n > 0) {
return {
basis: 'settled_volume',
placeholder: false,
note: `ranked by settled volume over ${row.n} HCS receipts`,
};
}
return {
basis: 'registration_recency',
placeholder: true,
note: 'PLACEHOLDER: ranked by registration recency. ...',
};
}
// in findSellers:
const orderBy = ranking.basis === 'settled_volume'
? 'settled_volume DESC, settled_count DESC, block_timestamp DESC'
: 'first_seen_timestamp DESC, first_seen_block DESC';
Trimmed (note string shortened). seller/service/discovery.ts
Registration date means the agent's first registration, not its last URI change; otherwise an agent could climb the list by editing its record. And every response says how it was sorted. Each one carries a ranking object, { "basis": "registration_recency", "placeholder": true, … } or { "basis": "settled_volume", "placeholder": false, … }, so a buyer's agent can tell when the order says nothing about reputation.
As an example, the live site ranks by settled volume. curl -s "https://turnstile.moveseventyeight.com/api/sellers?limit=3" returns "basis": "settled_volume", "placeholder": false over 16 receipts, with liquidity.turnstile.eth on top at $1.12, sixteen payments of $0.07. Every other agent scores 0, because nobody else writes receipts to that topic. Receipts are ingested when someone runs the script, not after each payment, so the ranking is as current as the last run.
A filtered query puts it together. Searching liquidity with a $0.10 ceiling matches 8 agents: ours on Sepolia and seven Virtuals agents on Base. The seven are set aside, not because they cost too much but because their prices name no currency, so the ceiling can't be checked against them. The response counts them separately. The one result is liquidity.turnstile.eth at $0.07. Pass --include-unknown-price and the seven come back too, each with its raw Virtuals price, shown as 0.01–0.25 (acp, not comparable).
What we built
Discovery in Turnstile has four layers.
- A Substreams module in Rust decodes the ERC-8004 Identity Registry and runs unchanged on every chain that hosts one.
- A sink lands each chain's registrations in one SQLite store, fetches the registration files the module can't reach, and records every failure.
- Prices come from where sellers actually publish them: our seller's ENS record, live x402 quotes from any endpoint an agent lists, and Virtuals' job prices, kept apart because they name no currency.
- On top sits
findSellers, which ranks sellers by payments settled on Hedera and says so when it can't.
Finding a seller is half a purchase. The other half is paying it. Turnstile takes payment on Hedera, Arc and Base behind one interface: start with One payment interface, four chains, then x402 on Hedera and Gasless nanopayments on Arc.
The ENS records that carry our price, and the key split that protects the payout, are in The offer is the name. What our seller offers is in Selling a liquidity verdict.
Sources
- EIP-8004: Trustless Agents: the three registries, the registration file fields, the Identity Registry events, the reserved
agentWalletkey, and "Payments are orthogonal to this protocol". Draft status. - erc-8004/erc-8004-contracts: the official deployments list (Identity and Reputation Registry addresses; no Validation Registry deployed), the reference contracts, and
ERC8004SPEC.md, the source for the Reputation and Validation details and thevalue/tag1examples. - Substreams documentation: what Substreams is.
- Substreams manifest reference: the
networkssection for per-networkparamsandinitialBlock, themapandstoremodule kinds, and thewasm/rust-v1runtime. - Agent0 subgraphs, The Graph docs: the existing ERC-8004 subgraphs, whose field names the module follows.
- ENSIP-25: AI Agent Registry ENS Name Verification: the
agent-registration[<registry>][<agentId>]text record with ERC-7930 registry encoding. - x402-foundation/x402: the x402 protocol, where the server answers
402 Payment Requiredwith the payment requirements. coinbase/x402 now points here. - x402 v2 specification:
PaymentRequired, thePAYMENT-REQUIREDheader, and dynamic pricing listed as a pattern. - Virtuals Protocol: the ACP agents whose registration files carry
jobOfferingsprices. - Capminal's agent (
eip155:8453:0x8004a169…a432/1380): its registration file and the x402 v2 quote fromhttps://www.capminal.ai/api/x402/deploy. - IpastorSan/turnstile: the code, plus
docs/erc8004-substreams.md(module verification runs),docs/discovery-api.md(store contents, probe results and count corrections) anddocs/architecture.md. - Turnstile live site: the deployed marketplace. Its
/api/sellers?limit=3endpoint returnsbasis: settled_volumeover 16 HCS receipts.
Next in the series: The offer is the name: ENSv2 records and a key split the chain enforces, part 5 of 9.
More articles


