docs(8004): add a Register a service section and fix the registration file schema - #2331
Conversation
… file schema
The page read as if ERC-8004 identities were only for autonomous agents
holding a wallet. Any callable service — an HTTP API, an MCP server, a bot —
can register one, and `register()` takes no wallet argument.
Adds a short "Register a service" section under Quick Start with a
registration file and a viem register() call, and links it from the Build
with AI overview.
The registration file example already on the page was stale: `"type":
"Agent"` with an `endpoints` array of `{type, url}`. The current schema is
`"type": "…eip-8004#registration-v1"` with a `services` array of `{name,
endpoint, version}`. Rather than leave two contradicting schemas on one page,
the old block is removed and the corrected one lives in the new section.
Also fixes the endpoint-type list (`wallet` is not a services entry; it is a
separate, optional setAgentWallet() call) and softens the claim that every
agent NFT has a linked wallet.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
palango
left a comment
There was a problem hiding this comment.
Thanks for this. A section for services is a good addition. Before it merges, though, two of the schema corrections need reverting and the snippet needs one fix.
agentWallet is set on registration. Every register() overload writes agentWallet = msg.sender (IdentityRegistryUpgradeable.sol L63, L72, L82). The spec says it "is initially set to the owner's address", so linking a wallet isn't separate or optional. The key that sends the transaction becomes the advertised payment address, and a transfer of the NFT clears it (_update, L188–195). Please restore the original intro wording at L21 and rewrite the bullet at L63. The new section should also say this outright. It tells readers they need "no keys, no balance" and it sets x402Support: true, so a reader who registers with a throwaway ops key ends up publishing that key's address as the payee.
The endpoint names aren't a closed list. The spec says "the number and type of endpoints are fully customizable". It also says agents MAY advertise "the agent's wallets on any chain". Agent 9000, which the PR description cites, uses api, mcp and an agentWallet entry. Could L62 and L159 say "for example" and put wallet back?
The snippet can print the wrong agent ID. The value returned by simulateContract is a guess at the next _lastId, not the ID that actually gets minted. If you register several services in a row, every one prints the same ID. Anyone else's registration landing first has the same effect. The snippet also logs "Registered" before the transaction is mined. Please wait for the receipt and read the ID from the Registered event, the way overview.mdx L111 does. While you're there, add feeCurrency, since the text says gas can be paid in a stablecoin and a reader with only USDC can't run the snippet as written.
Smaller things:
- The example file leaves out
registrations. The spec says agents SHOULD have one, and the cross-domain verification described at L64 depends on it. A line explaining register, then update the file with the ID, would cover it. - AGENTS.md §5 asks for every example to be run, with the command and output in the PR. Could you do one real run on Celo Sepolia? It would have caught the ID problem.
- The page now has two ways to register and get the ID: chaoschain, and viem in the new section. Consider rewriting "Register an Agent" in viem and folding the service case into it, rather than keeping both.
- The frontmatter
descriptionstill covers agents only, so an assistant looking for "register my API" won't open this page.
…event Verified against the deployed implementation behind the registry proxy (IdentityRegistryUpgradeable at 0x7274e874CA62410a93Bd8bf61c69d8045E399c02): - All three register() overloads write agentWallet = msg.sender and mint to that address. Linking a wallet is not separate or optional, so the intro wording is restored and the section now warns that the signing key becomes the agent's published payment address. - setAgentWallet() requires newWallet != address(0) and a signature, so it is a deliberate change, not a way to clear a mistake. - agentId = _lastId++ at call time, so the value register() returns under simulation is a guess. The snippet now waits for the receipt and reads the ID from the Registered event. - Endpoint names are examples, not a closed list; wallet entries are back. - The example registration file carries registrations, with a note that the ID is only known after the transaction is mined. - The snippet passes feeCurrency (mainnet USDC adapter) so a reader holding only USDC can run it, and the frontmatter description now covers services. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All four points are in. I checked the central claim against the deployed implementation rather than the reference repo, because that is what readers transact with — and it backs you. agentWallet is set on registration — confirmed, and the section now warns about itThe registry at function register(string memory agentURI) external returns (uint256 agentId) {
IdentityRegistryStorage storage $ = _getIdentityRegistryStorage();
agentId = $._lastId++;
$._metadata[agentId]["agentWallet"] = abi.encodePacked(msg.sender);
_safeMint(msg.sender, agentId);
_setTokenURI(agentId, agentURI);
emit Registered(agentId, agentURI, msg.sender);
emit MetadataSet(agentId, "agentWallet", "agentWallet", abi.encodePacked(msg.sender));
}So "no keys, no balance" is gone, the L21 intro wording is restored, and the bullet is rewritten. The section now opens with a One qualification worth recording, because it is what made me go and read the source. On-chain, I also checked The endpoint list is open again
The snippet reads the ID from the event
registrationsAdded to the example file, with the ordering explained: publish the file, register, then update the file with the ID — since the ID does not exist until the transaction is mined. The run, and the one thing still missingI extracted both blocks from the rendered page and ran them. Imports, the That run also caught a real bug in my own verification, which is worth reporting: my first Sepolia attempt reverted with Still outstanding: a real funded registration. I do not have a funded Sepolia key here, so the write itself remains unproven — same gap as last round, now narrower. Say the word and I will fund one and paste the hash. I would argue the specific defect you were worried about is now closed by construction rather than by observation, since the ID comes from the receipt, but your call. Your other three
Re-requesting review. Note CI here cannot go green until #2335 lands — |
palango
left a comment
There was a problem hiding this comment.
All the points from my last review are addressed. I checked the decode path against a real mainnet registration (agent 9862): the snippet's parseEventLogs returns the right ID and owner, and getAgentWallet returns the sender, so I'm fine without a funded run. A few accuracy points below, none blocking.
Separately, the older "Register an Agent" snippet still reads tx.events.Transfer.returnValues.tokenId (web3.js style), which I haven't verified. It predates this PR, so a follow-up issue for the viem rewrite you proposed is fine.
| An identity is not limited to an autonomous agent that acts on its own. Any callable service — an HTTP API, an [MCP server](/build-on-celo/build-with-ai/mcp/index), a bot — can hold one. The service itself needs no on-chain logic: what it gets is a record agents can find and rate, with endpoints in the Identity Registry and feedback in the Reputation Registry. | ||
|
|
||
| <Warning> | ||
| **The address you register from becomes the agent's advertised wallet.** Every `register()` overload writes `agentWallet = msg.sender` and mints the NFT to that address, so whatever key signs this transaction is published as the agent's payment address. Use the key you actually want paid, not a throwaway ops key. `setAgentWallet()` changes it afterwards, but that needs a signature from the new wallet, so it is not a way to undo a mistake quietly. |
There was a problem hiding this comment.
Small accuracy point: a wrong payee is easy to fix. unsetAgentWallet(agentId) lets the owner clear the wallet with no signature (reference contract IdentityRegistryUpgradeable.sol L167-178, deployed on mainnet too), and setAgentWallet() only needs a signature from the new wallet, which you control. What sticks is that the signing key also owns the NFT, so it controls setAgentURI and transfers. Suggest ending with something like: "Register from the key that should own the identity. To change the payee later, call setAgentWallet() with a signature from the new wallet, or unsetAgentWallet() to clear it."
| - Supports multiple endpoint types (A2A, MCP, wallet, ENS, DIDs) | ||
| - `agentURI` points to a registration file listing the service endpoints — see [Register a service](#register-a-service) | ||
| - Endpoint types are customizable — `web`, `A2A`, `MCP`, `OASF`, `ENS`, `DID`, `email` and `wallet` are examples, not a closed list | ||
| - `register()` sets the agent's wallet to the address that sent the transaction. `setAgentWallet()` changes it later, with a signature from the new wallet |
There was a problem hiding this comment.
Worth adding: "Transferring the NFT clears the wallet; the new owner sets it again." Both the spec and the contract's _update do this.
| } | ||
| ``` | ||
|
|
||
| `registrations` is what lets a caller check that the file and the on-chain entry agree, which is the cross-domain verification described above. You only learn the agent ID once the registration is mined, so the order is: publish the file without `registrations`, register, then update the file with the ID it returned. |
There was a problem hiding this comment.
If the file lives on IPFS or in a data: URI, adding the ID changes the URI, so the reader also needs setAgentURI(agentId, newURI). One clause would cover it: "...then update the file with the ID (for IPFS or data: URIs, point the registry at the new URI with setAgentURI)."
… agentWallet The page still carried two ways to register: the SDK-based "Register an Agent" and the viem one under "Register a service". Drops the SDK version so there is a single path, and moves the registration section ahead of "Install SDK", which now introduces only the feedback examples. Adds the one piece the wallet warning was missing: transferring the agent NFT clears agentWallet outright (_update, IdentityRegistryUpgradeable L187-197), alongside the existing note that setAgentWallet() needs a signature from the new wallet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All four confirmed and fixed. The registration snippet has now been run as a real transaction on Celo Sepolia, which is what settled the first and third points. agentWallet is set on registration — correct. Verified in the deployed source: all three Confirmed on-chain rather than only in source. The registration below was sent from The endpoint names are open-ended — correct. L62 and the section prose now say the names are examples and that the spec leaves the number and type of endpoints to the operator; wallet entries are back in both places. The snippet printed the wrong agent ID — correct, and it reproduced. Three back-to-back simulations all returned the same ID: Then the real run minted 444, not 443 — another registration (a Celo x402 Sepolia service using the same ops key) landed in between. Exactly the failure described. The snippet now waits for the receipt and reads the ID from the
Real run on Celo Sepolia (11142220), using the published snippet with only the network values swapped — chain Balances either side of it — CELO unchanged, USDC down 0.016599, so And the resulting agent: On the smaller things:
Still not addressed: on a 412px phone the two code blocks in this section overflow horizontally by 440px (JSON) and 525px (TS). The long lines are the spec's |
Drops the last ChaosChain SDK examples so the page uses one library. Give Feedback and Query Agent Reputation are now viem, run end to end on Celo Sepolia against a freshly registered agent. Both gained the failure paths the SDK versions hid: giveFeedback reverts with "Self-feedback not allowed" for the agent's own owner and operators, and getSummary reverts with "clientAddresses required" when handed an empty array — which is what getClients returns for an agent nobody has rated, so chaining the two turns "no reviews" into a thrown error. Code blocks are sized for a 412px phone, where a block shows 34 characters. The TypeScript blocks were 90 characters at their widest against a 57-59 page norm; they are now 51-60, so they scroll less than the examples that were already on the page. ABIs are written as objects rather than parseAbi strings: splitting a long signature across concatenated string literals keeps it running but loses the literal type, and viem's inference degrades to never — tsc --strict catches it, the runtime does not. The one line left over the norm is the registration file's agentRegistry value at 80 characters. It is a CAIP-10 identifier and breaking it would make the example wrong. Verified on Celo Sepolia, all three snippets extracted from the page with only the network values swapped: register -> agent 446, tx 0x748bb495… (agent 445 in an earlier run) giveFeedback -> tx 0x8004983b…, from a second account getSummary -> "1 reviews, score 85"; "No feedback yet" for an unrated agent tsc --noEmit --strict passes on all three. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both remaining items are closed. Neither needed your input, so here is what I decided and why. The reputation examples are viem nowA second funded account made this runnable, so the ChaosChain SDK is gone from the page entirely — one library, which closes the last part of your "two ways to do it" point. Converting them surfaced two failure paths the SDK examples hid, both now in the page:
Run end to end on Celo Sepolia, all three snippets extracted from the page with only the network values swapped: Agent 446 on-chain: The mobile overflowI measured instead of guessing. At 412px a code block on this theme shows 34 characters — 9.77px per character, 338px of visible width. Every code block on the site scrolls horizontally; the question is only how far. Against that, the useful target is the page's own norm. Before this PR the widest lines in the existing blocks were 57–59 characters. Mine were 90 (TypeScript) and 80 (JSON). Now:
The TypeScript blocks now scroll less than the examples that were already on the page. One thing worth flagging, because it nearly shipped. My first attempt at shortening split long signatures across concatenated string literals inside 'function getSummary(uint256 agentId,' +
' address[] clients, string tag1, string tag2)' + …That runs fine. But Only The JSON stays at 80 characters and I am not going to fix it. The line is Nothing outstanding from your review now. |
unsetAgentWallet(agentId) is owner-only and needs no signature, and _update clears agentWallet on transfer. Both verified against the deployed implementation at 0x7274e874CA62410a93Bd8bf61c69d8045E399c02. So the warning was wrong to imply a mis-set payee is hard to undo. The real sticking point is ownership: msg.sender also gets the NFT, and with it setAgentURI and transfers. Reframed accordingly, and the transfer-clears- wallet behaviour is now stated where a reader updating a registration will meet it. Also notes that for an IPFS or data: URI, adding the agent ID to the file changes the URI, so setAgentURI is needed to repoint the registry. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
All three accuracy points are in, and thanks for the mainnet decode check against agent 9862 — that closes the funded-run gap properly. I verified both contract claims against the deployed implementation ( function unsetAgentWallet(uint256 agentId) external {
address owner = ownerOf(agentId);
require(msg.sender == owner || isApprovedForAll(owner, msg.sender) || msg.sender == getApproved(agentId), "Not authorized");
$._metadata[agentId]["agentWallet"] = "";
...
}
function _update(address to, uint256 tokenId, address auth) internal override returns (address) {
address from = _ownerOf(tokenId);
// If this is a transfer (not mint), clear agentWallet BEFORE external call
if (from != address(0) && to != address(0)) { $._metadata[tokenId]["agentWallet"] = ""; ... }
...
}So you are right that I had the emphasis wrong. The warning now says the opposite of what it did: the payee is easy to change ( That also explains an oddity I flagged last round and could not account for: agent 1 returning a zero Added as you suggested:
On the older "Register an Agent" snippet still using Ready for another look. |
palango
left a comment
There was a problem hiding this comment.
Thanks, the warning and the IPFS/data: note both read correctly now, and moving everything to viem with a real Celo Sepolia run is a big improvement.
One thing blocks: the reputation query prints a misleading score on any agent with more than one kind of feedback. Details inline. The rest are nits.
| functionName: 'getSummary', | ||
| args: [agentId, clients, '', ''], | ||
| }); | ||
| console.log(`${count} reviews, score ${value}`); |
There was a problem hiding this comment.
Blocking. I ran this against mainnet agent 9699 and it prints "64 reviews, score 161". That agent has one starred rating of 100 (decimals 0) and 63 certificate_issued entries of 5 with decimals 2. Two things go wrong:
- tag
''averages every tag together, so ratings, response times and custom tags all get mixed; decimalsis read but never applied: the summary here comes back as161with decimals2.
getSummary(9699, clients, 'starred', '') returns 1, 100, 0, which is the answer a reader wants. Your Sepolia run printed the right thing only because the agent had one starred entry with decimals 0.
Suggest passing the tag (args: [agentId, clients, 'starred', '']) and printing formatUnits(value, decimals).
There was a problem hiding this comment.
@GigaHierz your run on 444 passes, but 444 can't show this problem. It has exactly one feedback entry (tag1 agentguard, tag2 trust-v2, value 15, decimals 0), so averaging across tags and ignoring decimals give the same number. Here is the page's "Query Agent Reputation" snippet, copied from 1d8abd7 with only as const stripped and agentId read from an env var, run against mainnet:
$ AGENT=444 node q.mjs
1 reviews, score 15
$ AGENT=9699 node q.mjs
64 reviews, score 161
The raw calls for 9699:
$ cast call 0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 'getSummary(uint256,address[],string,string)(uint64,int128,uint8)' 9699 "$(cast call … 'getClients(uint256)(address[])' 9699)" '' ''
64
161
2
$ … same with tag1 'starred'
1
100
0
So "score 161" mixes one starred rating of 100 with 63 certificate_issued entries, and the decimals (2) are dropped. The real value, 1.61, is also meaningless because it averages two different kinds of feedback.
With the change I suggested (args: [agentId, clients, 'starred', ''] and formatUnits(value, decimals)):
$ AGENT=9699 node fixed.mjs
1 reviews, score 100
$ AGENT=444 node fixed.mjs
0 reviews, score 0
The second line matters as well. Filtering by a tag can leave a non-empty clients list with a zero count, so the snippet should check count === 0n after getSummary, not only clients.length before it.
| transport: http(), | ||
| }); | ||
|
|
||
| const agentId = 444n; |
There was a problem hiding this comment.
Related: the spec says summaries over unfiltered clients are open to Sybil/spam, because anyone can call giveFeedback. Since the page puts this in front of "before delegating work to an agent", could the text say to pass the reviewer addresses you trust, and treat getClients only as a starting point?
| abi, | ||
| functionName: 'giveFeedback', | ||
| args: [ | ||
| 444n, // agentId |
There was a problem hiding this comment.
Nit: 444 is a live agent on mainnet, and this block uses the mainnet registry, so a copy-paste run rates someone else's agent. Mark it // example agentId or read it from an env var (same at L364).
| '', // tag2: optional | ||
| 'https://example.com', // endpoint used | ||
| '', // feedbackURI | ||
| keccak256(toHex('ok')), // hash of the content |
There was a problem hiding this comment.
Nit: feedbackURI is empty, so there's nothing to hash. Use zeroHash from viem here, and mention that the hash is keccak256 of the content at feedbackURI when you set one.
|
|
||
| ### 2. Reputation Registry | ||
|
|
||
| Stores feedback and attestations about agent performance. Feedback is submitted on-chain by any address that has interacted with the agent—this includes users who hired the agent, other agents that collaborated with it, or monitoring services that track uptime and responsiveness. The contract prevents agents from rating themselves (owner and operator addresses are blocked from submitting feedback on their own agent). |
There was a problem hiding this comment.
Nit, pre-existing: "submitted on-chain by any address that has interacted with the agent" suggests the contract checks for an interaction. It doesn't: any address except the owner and operators can submit. Worth fixing while you're in this section, since it's the reason the Sybil note above matters.
|
Follow-up opened for the viem rewrite: #2340. It covers rewriting |
|
Ran the two reputation snippets against Celo mainnet, since AGENTS.md §5 wants every example exercised and these had not been. All four claims hold — no changes needed. Query Agent Reputation — runs verbatim, extracted from the rendered page with only the TS Both So the warning is not theoretical: agent 9862 — the one used to verify the decode path — is exactly the empty case, and chaining Give Feedback — ABI and argument types validate, and the quoted error string is exact: The eight positional arguments encode correctly against the deployed registry, and simulating from the agent's owner reverts with precisely The Separately: #2341 renames |
The query snippet averaged every tag together and printed the raw value, so on any agent with more than one kind of feedback it reported a number that meant nothing. Verified on mainnet agent 9699: unfiltered it returns count=64, value=161, decimals=2, mixing one starred rating of 100 with 63 certificate_issued entries at 2 decimals. Now passes tag1 'starred', prints formatUnits(value, decimals), and checks count rather than clients.length - a tag filter can leave a non-empty client list with a zero count, which agent 444 demonstrates. Also, since the page puts this in front of a delegation decision: - says to choose the reviewer addresses you trust, because anyone can call giveFeedback and getClients returns all of them - corrects the Reputation Registry description, which implied the contract checks for a prior interaction; it only blocks the owner and operators - feedback example uses zeroHash, since feedbackURI is empty, and explains that the hash covers the content at feedbackURI - both example agent IDs marked as examples, as they are live on mainnet Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Blocker fixed, and you were right on both counts — I reproduced it before changing anything. So "64 reviews, score 161" was wrong twice over: it averaged one The subtler half of your comment was the more valuable one. Agent 444 has The corrected block, extracted from the rendered page with only Matches your predicted output exactly. The Sybil point — taken, and pushed a bit furtherYou were right that this sits directly in front of "before delegating work to an agent", which is what makes it matter. The section now leads with the two things that decide whether the number means anything: pass a tag, and choose the addresses yourself where the decision matters, treating Your pre-existing nit was the root cause
Other nits
Anchor |
palango
left a comment
There was a problem hiding this comment.
One regression in the query snippet, see inline. Everything else from the last round is fixed, thanks.
| const clients = await publicClient.readContract({ | ||
| address: reputationRegistry, | ||
| abi, | ||
| functionName: 'getClients', | ||
| args: [agentId], | ||
| }); | ||
|
|
||
| // getClients returns everyone who has ever rated this agent. Anyone can call | ||
| // giveFeedback, so for a decision that matters, replace this with the reviewer | ||
| // addresses you already trust and treat getClients only as a starting point. | ||
| const [count, value, decimals] = | ||
| await publicClient.readContract({ | ||
| address: reputationRegistry, | ||
| abi, | ||
| functionName: 'getSummary', | ||
| // Always pass a tag. Averaging across tags mixes ratings with | ||
| // unrelated feedback, and the result means nothing. | ||
| args: [agentId, clients, 'starred', ''], | ||
| }); | ||
|
|
||
| // A tag filter can leave a non-empty client list with a zero count, | ||
| // so check count here rather than clients.length above. | ||
| if (count === 0n) { | ||
| console.log('No starred feedback yet'); | ||
| } else { | ||
| console.log(`${count} reviews, score ${formatUnits(value, decimals)}`); | ||
| } |
There was a problem hiding this comment.
The clients.length === 0 early return needs to stay. getSummary reverts with clientAddresses required on an empty array (ReputationRegistryUpgradeable.sol L195-198), so for an agent with no feedback yet this now throws instead of printing "No starred feedback yet". Agent 9862 on mainnet reproduces it, and the note at L323 warns about exactly this. Keep both guards: return early on empty clients, then check count === 0n after the call, and please update the comment at L386-387 to match.
…ister-services-section
Restore the clients.length early return in the reputation query: getSummary reverts with clientAddresses required on an empty array, so an agent with no feedback now prints 'No starred feedback yet' instead of throwing. The count === 0n check after the call stays for tag-filtered empties. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
palango
left a comment
There was a problem hiding this comment.
Thanks, the early return is back and the comment explains why there are two guards. I ran the query against mainnet: getSummary reverts with "clientAddresses required" for an agent with no clients (9862), which the first guard now skips, and agent 9699 comes back as 1 review with score 100. All my earlier comments are addressed, and the registration file schema now matches what #2344 tells readers (services and x402Support). Approving.
|
Latest round addressed in ef17996, with Of the points in the last review, only the blocking one was still open in the current file, and it is fixed: the "Query Agent Reputation" snippet returns early with "No starred feedback yet" when The other points were already in earlier commits and are still in the file: the Checked read-only, no transactions and no keys:
|
The hole, and the fix
The ERC-8004 page read as if an on-chain identity were only for an autonomous AI agent that holds a wallet. It isn't:
register()takes no wallet argument, a wallet is a separate and optionalsetAgentWallet()call, and ~9,860 identities already registered on Celo mainnet include plain services advertisingweb,apiandmcpendpoints. Operators of an API, an MCP server or a bot had nothing on this page telling them they qualify, and nothing to be linked to.Adds a short
### Register a serviceunder Quick Start — anchor#register-a-service— with a registration file and a viemregister()call, plus one sentence on the Build with AI overview pointing at it.It also fixes a wrong schema. The registration file example already on the page was stale:
"type": "Agent"with anendpointsarray of{type, url}. The current schema is"type": "…eip-8004#registration-v1"with aservicesarray of{name, endpoint, version}. Rather than leave two contradicting JSON schemas on one page, the stale block is removed and the corrected one lives in the new section. Two smaller corrections ride along, both load-bearing for the new section's central claim that a service needs no wallet:web,A2A,MCP,OASF,ENS,DID,email— the page listed(A2A, MCP, wallet, ENS, DIDs);walletis not aservicesentry at allWhat this does NOT do / residual risk
0xd8618766ed0f054ebc6e479ce2ebfd40e78479ceac278ba08ed9f0a8224f94d9, status success, gas paid in USDC. Evidence and balances are in the review reply below.## Related Protocolsto## Related(AGENTS.md §3). Same reason.agentRegistryCAIP-10 value, which cannot be broken without making the example wrong.docs.jsonchange and no redirect: nothing was added, moved or renamed.Judgement calls
Register a service, notRegister your API, MCP server, or bot. Shorter anchor for outreach to paste; the first sentence names API, MCP server and bot. Reversal cost: one line, plus anywhere the anchor has already been sent.x402Support: true. Makes the x402 tie-in concrete rather than abstract. Fully example data.Issues
No ticket — this came out of outreach needing a link target. Not linked to the restructure epic.
Stacking / conflicts
Branched off
main, independent of my other open PRs.Shares one file with #2327 (
docs(celina): add the read-only Telegram bot): it editsoverview.mdxline 66, this edits line 62 — adjacent hunks, four lines apart, so a textual conflict is possible depending on merge order. Neither change touches the other's sentence; whichever lands second takes both paragraphs. No proposed ordering, resolution is trivial either way.Verification evidence
No test suite in this repo — it's MDX plus
docs.json, nopackage.jsonat the root. So: no unit tests, and no mutation count is possible. The checks that do exist:Link check, on this head:
Anchor resolves (
mint broken-linksdoes not validate anchors — AGENTS.md §6). Clicked the new overview link in a real browser:The
tssnippet runs. Extracted verbatim from the page, swapping only chain (Celo Sepolia) and key (throwaway, unfunded):Imports, the
parseAbistring, thesimulateContractdestructuring and the returned agent ID are all verified against the live registry. The write is unproven — that is the residual above, not a passing result.Registries are live and the ABI matches the deployment:
All four addresses in the page's deployment tables return non-empty
eth_getCodeon their networks.The schema claim is checked against the chain, not just the ERC. Decoded
tokenURI()for mainnet agents1and9000— bothregistration-v1with aservicesarray;9000advertisesweb,api,OASFandmcpentries. That is the evidence that the old block was wrong and that services, not just agents, are registering.JSON block parses:
json.loadson the extracted block, OK.Browser pass, on this head (
mint dev, Chromium):/build-on-celo/build-with-ai/overview,/build-on-celo/build-with-ai/8004#register-a-service/mcp/indexand/x402prefetches both 200.context/erc8004-register-services-8004-desktop.pngand.context/erc8004-register-services-8004-phone.png(gitignored scratch; drag them in here if you want them inline — the CLI can't upload images)Drift check (AGENTS.md §7): grepped the repo for the old schema —
"type": "Agent","endpoints",agentURI,setAgentWallet. The stale shape existed only on this page; nothing else to update.Scope, honestly: all of the above is local, on this head. Nothing has been checked against a deployed preview yet.
Remaining ops steps
Questions
#register-a-servicethe anchor you want in outreach, or should the heading spell out "API, MCP server, or bot" even at the cost of a longer link?register()transaction as evidence before this merges?@chaoschain/sdkto viem, so the page speaks one library?Latest review round (head ef17996)
Merged current
maininto the branch (no conflicts), then addressed the open review. Read against the current file first: the wallet-change guidance, "transferring the NFT clears the wallet",setAgentURIfor IPFS/data:URIs, the trusted-reviewer note, the example agent IDs,zeroHashfor the emptyfeedbackURI, and the corrected "any address except the owner and operators" wording were already in earlier commits and are still present. One change was outstanding:clients.length === 0early return.getSummaryreverts withclientAddresses requiredon an empty array, so an agent nobody has rated now printsNo starred feedback yetinstead of throwing. Thecount === 0ncheck after the call stays for tag-filtered empties, and the comment now explains both guards.Contracts read (read-only, forno and Blockscout): the Identity Registry proxy
0x8004A169...(Celo mainnet, 42220) points at implementation0x7274e874CA62410a93Bd8bf61c69d8045E399c02; the Reputation Registry0x8004BAa1...(Celo mainnet) points at0x16e0fa7f7c56b9a767e34b192b51f921be31da34. Verified source confirms theclientAddresses requiredrevert on an empty array,Self-feedback not allowedfor owner and operators only,register()writingagentWallet = msg.sender,unsetAgentWallet(agentId), and_updateclearing the wallet on transfer.Snippet runs (extracted verbatim from the MDX, only
as conststripped and theagentIdliteral substituted, Celo mainnet, read-only):Checks on this head:
No transaction was sent and no key was used in this round.
🤖 Generated with Claude Code