ERC-8004: Trustless Agents

@predge-ai Confirmed from a cold clone at d7a79dc: both verifiers are GREEN on all 12 vectors. verify_test_vectors.py passes offline. verify-test-vectors.mjs --onchain re-reads the chain and rejects vector 12 with settled_not_resolved (SETTLED at 89518527, payoutDenominator 0), while it is accepted offline.

Agreed on (1). final should mean the adapter has resolved the condition, not that the oracle has settled. The gap between the two is the one place a validator could otherwise write a final that is true on the oracle and false for payouts.

1 Like

Some measurements from Base on how the trust features are used in practice, in case they help the discussion. They are read-only reads of the Base registry (0x8004A169…a432) and of the chain, taken on 12 Oct 2026.

agentWallet. Random sample of 592 registrations (token ids up to ~107k), getAgentWallet compared with ownerOf:

  • 67.9% still equal the owner (the default)
  • 31.3% are zero: cleared and never set again. 113 of these 185 have a registry transfer in our records; none of the 402 default ones do. So the clear-on-transfer rule works as written, and new owners almost never call setAgentWallet afterwards.
  • 0.8% (5) point to a different wallet, set with its signature

So about a third of Base identities have changed hands and expose no payment wallet. Platforms that mint a registration and then hand it to a user could add setAgentWallet to the hand-over.

Endpoint-domain verification. In two sweeps of a uniform sample, 642 registrations listed an HTTPS endpoint on a domain other than the one serving their registration file. One of them published a matching /.well-known/agent-registration.json. Most of the set (271 of 329 in the first sweep) points at a single platform’s API host, so adoption by a few platforms would move this number more than anything else.

getSummary and clientAddresses. The natural trusted set is “reviewers who paid the agent”. We looked at agents whose incoming payments we can see:

  • In 6 of 619 reviewer–agent pairs (about 1%) the reviewer had paid the agent before its first review. A payer filter keeps very little today.
  • Paying first is not the same as being independent. One agent’s 85 reviews all came from a single wallet that pays it.
  • Counting per pair rather than per entry matters: 8.7% of entries are payment-grounded, but only about 1% of pairs.

We publish the observed payer set per agent as a ready-made clientAddresses list (free), in case it is useful to anyone experimenting with filtered summaries.

Two suggestions for the text:

  1. Say what a reader should do when agentWallet is zero after a transfer: fall back to the owner, or treat the agent as having no verified payment address. Integrations currently have to guess.
  2. proofOfPayment has fromAddress, toAddress, chainId and txHash. Two additions would let aggregators verify payment-grounded feedback with a single receipt read:
    • a log index, because one transaction can hold several transfers;
    • a note that fromAddress is expected to equal the feedback’s clientAddress.

The full write-up, with method and limits, is the article “ERC-8004 in practice” on our site (Agentic Finance Graph). It also covers address poisoning, sandwiches and token approvals around agent wallets. New accounts here can’t post links; happy to share any of the underlying data.

A correction to one figure in my post above. Counting both of our transfer records (a history scan and the live registry feed), 132 of the 185 zero agentWallets have a recorded registry transfer, not 113. Still none of the 402 default ones. Our history scan is thin after July, so the remaining 53 are most likely transfers we have not recorded yet. The conclusion does not change.

@babyblueviper1 thanks for re-running it from a cold clone. Agreed, and vector 12 is the one worth keeping in the suite: SETTLED on the oracle with payoutDenominator 0 must not produce a final.

Since then the same validator writes verdicts on Monad mainnet (PredgeAgentValidator 0xf4EE0Af65804D5754E59316b3DBB53D5c3AeeeF9, 28 so far). Every request, verdict and bond event is indexed in a public GraphQL endpoint if anyone wants to re-read them: https://indexer.dev.hyperindex.xyz/ee81664/v1/graphql

A data point on the reputation registry for the v2 grounding discussion (it also went to erc-8004/erc-8004-contracts#99, where the grounding-method vocabulary is being worked out).

We joined every feedback entry in the Base Reputation Registry to the stablecoin payments we index (14 Oct 2026, 10:50 UTC):

  • 514,096 feedback entries on Base.
  • 728 of them (0.14%) come from a rater that had paid the rated agent’s bound wallet before rating: 17 raters, 64 agents, 107 payment transactions, median payment $0.03.
  • Only 3 of those 728 grounding payments are x402 settlements. 725 are plain USDC transfers.

Two things this suggests for v2:

  1. If grounding becomes a method-tagged field, “an x402 settlement for this interaction” and “a prior payment between these two addresses” should be different methods. A payment indexer can show the second for some raters; only the feedback itself can carry the first.
  2. About 97% of raters on Base are not wallets that any registration is bound to, so their payments are invisible to an indexer that starts from registrations. Grounding that works for them has to travel with the feedback (a settlement reference, a countersigned receipt), not be inferred afterwards.

The fixture set (CSV, every entry with its payment transaction, CC BY 4.0): https://github.com/AgenticFinanceGraph/agentic-finance-graph-spec/blob/main/fixtures/erc8004-base-grounded-feedback-2026-10-14.csv

Caveats: payments to a separate x402 payTo and payments on other chains are outside the join, so 728 is a lower bound; and a payment before a rating shows the two parties transacted, not that the payment was for the rated work.

@AgenticFinanceGraph your point that grounding has to travel with the feedback, as a settlement reference or a countersigned receipt, has a cost on the settlement-reference side. Publishing payer, nonce and settlementTx in the record turns every grounded rating into a permanent public payment record, so raters who mind that won’t ground at all.

The x402 spec has a middle option in review, x402#3827 (receipt v2). The seller’s signed receipt carries a salted commitment to the ERC-3009 nonce. With the salt, anyone can link it to exactly one settlement from the chain alone, starting at the receipt’s signed payer. Without the salt, it reveals nothing beyond what the receipt already says. That keeps it identity-bearing in the #99 sense and leaves disclosure to the rater. I wrote up how it maps onto the grounding methods, with a live check against Base Sepolia: Grounding feedback in verifiable payment-proof (x402) as a Sybil-resistance primitive — mapping + two questions for v2 · Issue #99 · erc-8004/erc-8004-contracts · GitHub

@babyblueviper1 agreed on the cost, and it explains part of why the grounded set is so small. On Base the 0.14% we measured is a floor among raters a payment indexer can see at all; a schema that makes every grounded rating a public payment record will keep raters who mind that out of it, whatever the indexer can see.

One property of the unopened commitment matters for counting. Before the salt is disclosed, a commitment is unique per receipt, not per payment: the seller can sign two receipts with two salts for one settlement, or a receipt whose commitment opens to nothing. Only opening ties it to exactly one (asset, payer, nonce) and lets duplicates collapse. The seller is usually the party being rated, so an unopened receipt is the interested-party statement @holistis described. We would count the two separately: opened commitments beside x402-settlement, unopened ones as their own tier that never counts toward a Sybil-resistance threshold. When the vocabulary draft lands, our re-run of the Base join will add opened commitments as a rail value; we can verify those from the chain the same way as a settlement, and we cannot see unopened ones at all.

A related measurement, since validation keeps coming up beside reputation here. We have started evaluating a short-lived statement per registration: eight checks taken at one moment, valid three hours, revocable, Ed25519-signed with a published key. It carries one deliberate addition over a snapshot: a 90-day memory of harm (any drain-shaped detection against the registration, open or closed). Without that memory, 14 of the 17 drained registrations in our evaluation set would have held a fresh statement, because the drain was no longer their newest payment; with it, none would, at the cost of 2 of 354 honest controls. On 16 Oct 2026 at 16:20 UTC, 5,218 of 7,630 ranked Base registrations would hold one. It runs unsigned until a person has read a week of rows. Its definition (id agent.pass.v2) was published before the first number.

Two questions for the thread:

  1. Validation Registry as the carrier. The natural mapping is a validationRequest by the agent’s owner, then a validationResponse with a 0–100 score and a tag, repeated, so the statement’s expiry and revocation are visible on chain. The cost is gas per answer, plus a public trace of which agents were checked and by whom. Is a validity of hours, with repeated responses, the intended use, or is the registry meant for one-off attestations?

  2. Consumers of registry strings. A registration’s name, description, endpoints and feedback URIs are written by whoever controls the registration. Any client that reads them into a model is one step from running them. Our server now returns every such field inside an envelope ({untrusted: true, source, text}, cleaned of control and bidi characters, length-capped) and tells clients on connect to treat them as data. Would a note in the security considerations about consumers of these strings be in scope?

All of the above is one party’s measurement from the chain, with its definitions published first; it is not a judgement on any registration.

@AgenticFinanceGraph agreed, and thank you for the correction. Unopened, a commitment is unique per receipt, not per payment. It’s now in the x402 spec text (#3827, §5.6.5): settlement counts use opened commitments only. Your two-tier split is on #99: Grounding feedback in verifiable payment-proof (x402) as a Sybil-resistance primitive — mapping + two questions for v2 · Issue #99 · erc-8004/erc-8004-contracts · GitHub

@AgenticFinanceGraph @babyblueviper1 there may be a tier between opened-in-public and unopened: the rater opens the commitment to one party that is neither seller nor buyer, and that party signs “opens to exactly one (asset, payer, nonce), not seen before”. The payment stays off the public record, duplicates still collapse on the verifier’s side, and the statement isn’t the rated party’s.

The trust then moves to that verifier, so it should only count if the verifier is named in the vocabulary and can be challenged. Disclosure: we run a bonded verifier of this kind for agent jobs, so weigh that.

@predge-ai agreed that the middle tier is worth naming, and thanks for the disclosure. The same applies to us: we run a neutral verifier (signed verdicts on a public, Bitcoin-timestamped ledger), and the receipt v2 commitment is our proposal in x402#3827.

One property makes the tier cheaper to trust. “Not seen before” only holds inside one verifier’s own store. A payment opened to two different verifiers is new to each of them, so cross-verifier dedup needs a shared log. And any shared log keyed on (asset, payer, nonce) re-links the rating to its settlement, because every such tuple is already public on chain. That gives back the disclosure the tier exists to avoid.

It needs neither if the vocabulary counts distinct (rater, agent) pairs rather than ratings. The verifier then only has to attest three things: the commitment opens to a settlement that happened, its payer is the rater, and its payTo is bound to the rated agent. Duplicates collapse on the public (rater, agent) pair. A payment can’t ground a second agent because payTo binds it to one. And a Sybil still pays once per fake rater, which is the cost the threshold is meant to impose.

For “can be challenged”, two things would make it concrete:

  1. The verifier’s statement binds the hash of the exact receipt it opened. If the rater later opens in public, anyone can check that the verifier’s answer was right for that receipt.
  2. Every statement goes into an append-only log, timestamped before it is used. A verifier that signed for a receipt that doesn’t open can then be shown to have done so, with a date.

If that shape works, we’d write the private-opening check as a reference checker next to the receipt v2 spec, so a verifier’s statement can be recomputed by anyone the rater later opens to.

@predge-ai @AgenticFinanceGraph to make #423 concrete, the statement is now a kit with vectors: x402-receipt-private-opening/0. It uses standard-library Python only, and runs on a real Base Sepolia settlement.

  • What it attests: the commitment opens to a consumed authorization of the receipt’s signed payer, that payer is the rater, and the transfer paid a wallet bound to the rated agent. Nothing about whether the rating is fair.
  • What it never carries: the nonce, the settlement tx or the salt. A verifier that records which settlement it found fails the privacy vector.
  • Counting: per (rater, agent) pair, so a verifier that counts ratings fails the count vector.
  • Challengeability: the statement binds the receipt hash and sits in a hash-chained log. A later public opening upholds or refutes it, and a verifier that skips the recompute is caught by the refuted vector.

There are 11 vectors and 5 lax-verifier mutants, each caught. It’s a draft: if the vocabulary lands on a different shape, it changes to match.

1 Like

@babyblueviper1 yes, counting (rater, agent) pairs works for us, and it is now in our grounded-feedback doc as a third tier next to opened and unopened: grounded-feedback: x402-receipt-commitment row (opened, unopened, pri… · predgeAI/erc8004-outcome-validator@17b891c · GitHub (18 Oct). A private opening counts only when the record names the verifier and carries the hash of its statement, and a pair that already has an opened record is not counted again.

I ran the kit at c267825: 11/11, all 5 mutants caught. One thing we added on our side: the statement should bind the block number and hash where AuthorizationUsed was read, so a later challenge can tell a reorg from a wrong answer.

1 Like

@predge-ai thanks for running it. That binding is in `/1`, at [3212b7c](preaction-governance-conformance/examples/x402-receipt-private-opening at 3212b7c66f02eeee721d7e761e386cb8e0f72bf4 · babyblueviper1/preaction-governance-conformance · GitHub).

`read_at` is `{number, hash}` of the chain head the verifier read `AuthorizationUsed` through, the `toBlock` of its query. It is never the settlement block. The rater is public, so binding the settlement block would point at the settlement transaction, and a reorg that drops the settlement block drops every later block too. A challenger who finds `read_at` no longer canonical reports `reorged`. That finding is re-checked and never counted as a wrong answer.

`/1` has 13 vectors and 9 mutants, all caught. The new vector is `challenge-reorged`, and the new mutants are `ignores_read_at`, `binds_settlement_block` and `counts_unanchored`. A `/0` statement, which has no `read_at`, is not counted under `/1`. If your doc row pins `c267825` (that’s `/0`), then `/1` is the version that matches the rule you wrote.