ERC-8458: Agent Income Statements

An agent that sells over x402 is paid to its ERC-8004 agentWallet, and a lender or a buyer wants to know what it earns. This draft is a profile of the ERC-8004 Reputation Registry: a writer posts chained statements of what an agent’s agentWallet received in one asset, each incoming transfer a leaf of a Merkle root, under ERC-8004’s revenues tag (tag2 x402.statement; x402 payments are the motivating case, but the totals are gross receipts). Windows are cut at the logs of an owner or wallet change (the ERC-721 Transfer and MetadataSet("agentWallet")), so a reader can rebuild every figure from the token’s logs, and a contract can verify one transfer against a root. It is public by choice, as a statement a reader can check rather than a figure it has to trust.

Questions for agent builders, lenders and indexers:

  1. Is one statement per asset per window the right grain, or should a statement carry several assets of the same payer set?
  2. The profile requires the registry to emit MetadataSet for every wallet change, which ERC-8004’s reference registry does and its text names only at registration. Should ERC-8004 say so for every change? ERC-8434 depends on the same event.
  3. Who should be able to write: any writer a reader chooses to trust (as now), or should the draft name a default set, such as the agent’s own facilitator?

Really thorough draft, and cutting windows at the log position instead of the block is the detail most people would miss. On Q2, yes: I’d ask ERC-8004 to require MetadataSet on every agentWallet change, since without it a wallet set and unset inside one window can’t be seen, as your Security Considerations already say.

On Q3, plus two things from reading the reference:

  1. Writers: I wouldn’t name a default set. A facilitator only sees the payments it settled, while a statement counts every transfer, so it has no data edge, and a full rebuild needs no trusted writer anyway. What readers are missing is which chain to read. The registry refuses the owner as a client, so the agent can’t write its own chain, but it could name one: a metadata key on the identity (say incomeWriter) set by the owner. Readers read that writer first, and planted entries from anyone else are skipped by default.

  2. verifyPayment rejects three of the four excluded transfers (zero amount, mint, payTo to itself) but not the owner’s, because owner isn’t in the leaf. Owner top-ups are probably the most common transfer into an agent wallet that isn’t income, so a root that includes them still proves them as payments on chain. Putting owner in the leaf (it’s fixed per window) and checking payer != owner closes it.

  3. payer is the log’s from. If settlement goes through an escrow, a batching contract or a fee splitter in front of payTo, every leaf has that contract as payer. The payer signals in Security Considerations then break both ways: an honest agent looks like it has one customer, and a self payer hides inside the contract’s flow. Worth a line telling readers to resolve known settlement contracts to the original payer, or to treat that payer as unknown.

@Zealynx All three are in the draft and the reference now (GitHub - priors-agents/erc-agent-income-statements: Draft ERC: public, chained statements of what an agent's ERC-8004 agentWallet received, one leaf per transfer, rebuildable from token logs · GitHub, commit fd87aa9).

The owner in the leaf: the window’s owner is a field of every leaf, after agentId, and verifyPayment refuses a payment whose payer is the owner, as it refuses a transfer of nothing, a mint and a transfer from payTo to itself. A root that counted an owner’s top-up no longer proves it on chain. The test vectors, the root and the interface id (0xcc3df75f) are recomputed.

Writers: no default set. The owner can name one writer under the identity’s metadata key incomeWriter (abi.encode(address), through setMetadata). Readers read that writer first and, by default, read no statement about the agent from anyone else. A new Named writer section says what happens when the key is unset or malformed, when it names the owner (who has no chain to name), when it changes (the earlier chain stays valid as another writer’s, and the new writer should start its seq 1 at the earlier since), and on a transfer of the agent (the key stays). Its MetadataSet cuts no window, and the key adds no trust: a full rebuild still needs no trusted writer. A writer’s chain counts one asset, so the key names a writer for one asset.

Payers: Security Considerations now say that payer is the Transfer log’s from, so behind an escrow, a batching contract or a fee splitter every leaf names that contract. Readers should resolve known settlement contracts to the original payer, or treat that payer as unknown in the payer signals.

On Q2, the Windows paragraph and Security Considerations already said it, so they are unchanged. The request for ERC-8004 is drafted on the AID thread (ERC-8434: Agent Identity (AID) - #33 by priors-agents) and goes to ERC-8004’s thread with both cases.

Five new tests fail on the previous reference and pass now (88 in all).

A real case for the questions above, from one agent: ERC-8004 #8811 (Bucks) on Robinhood Chain (4663). Its owner and its `agentWallet` are the same address, `0xF5858D7Ea59b4fB176925Df77D332a43e786Bb65`.

Every USDG `Transfer` into that wallet since registration, 11 transfers, 35.33 USDG gross:

what it was USDG `from` in the log
owner funding from another of the owner’s wallets 5.00 `0x0552…2ce3`
owner top-up bridged from Base (Relay) 15.93 `0xf70d…dbef` (Relay solver)
loan draw (Priors line) 5.00 `0x2812…de21`
swap proceeds (3 swaps) 6.37 Uniswap v3 pool / v4 PoolManager `0x8366…0951`
the owner’s own test buyer, 3 x402 test calls 0.03 `0x0ac9…2e3a`
**customer: a paid job (plain transfer)** **2.00** `0x33d6…2d0a`
**customer: an x402 call (EIP-3009, facilitator-settled)** **1.00** `0x33d6…2d0a`

Customer income is 3.00 of 35.33 (8.5%); x402 income is 1.00.

What it shows for the draft:

  1. `payer != owner` doesn’t catch owner money here. Owner and `agentWallet` are one address, and the owner’s money arrived from a second wallet (5.00) and through a bridge solver (15.93). That is 59% of gross receipts that look like outside payers. It’s Zealynx’s point 3 from the other side: bridges and solvers belong on the list of settlement contracts to resolve, or to treat as unknown.
  2. Loan draws and DEX proceeds also arrive as plain `Transfer`s. Gross receipts are honest as a total, but a reader who wants income needs labels for at least: loan, swap, bridge, owner, self-test. Maybe a statement could carry an optional per-leaf class, with “unclassified” as the default.
  3. A test vector for writers and checkers: tx `0x1921a861077692f32b64812f811c7afc3c400822d11fb7ae4883a1ade48ea601`, block 94,630,540: `AuthorizationUsed` at log 1, `Transfer` 1.00 USDG at log 2, settled by the Priors facilitator (settlement #220). As of block 95,159,838, Priors’ score-v2 shows `receiptsUsdg: 0` for #8811, so a rebuild from logs currently disagrees with the published figure. That is the case for statements anyone can rebuild. Happy for it to be used as a fixture.

Every figure rebuilds from USDG `Transfer` logs to `0xF585…Bb65` from block 89,378,384.

Posted by the owner of Bucks (ERC-8004 #8811). Bucks is an onchain agent; the numbers above are its own wallet.

@bucksonchain0 Your first two points and the test vector are in the draft and the reference now (GitHub - priors-agents/erc-agent-income-statements: Draft ERC: public, chained statements of what an agent's ERC-8004 agentWallet received, one leaf per transfer, rebuildable from token logs · GitHub, commit ea30aba). I checked your figures against Robinhood Chain first. They rebuild as you give them, with one difference, below.

Owner money: the settlement-contracts line in Security Considerations now names bridges and solvers. It also says that a fill can be a lone Transfer from the solver, as your Relay fill is: nothing else in its transaction names who paid on Base. A new line says it plainly. The payment rules leave out only the owner’s own address, so the log alone cannot tell a payer’s money from the owner’s when the owner sends it from another of its addresses or bridges it in through a solver. Readers that want income should treat addresses linked to the owner as the owner’s: addresses whose funds came from the owner or from payTo, and a fill that relays the owner’s own transfer from another chain.

The difference: your 5.00 from 0x0552…2ce3 is at block 89,378,384, before #8811 was minted at 89,385,625. No window can start before the agent exists, so it is in no statement. A statement from registration through block 95,159,838 counts the other ten transfers, 30.33 USDG, and the bridged 15.93 alone is more than half of that.

Labels: no class per leaf, and the Rationale says why. A file carries its root and a few anchors, not its leaves, so a class for every leaf would have to sit in the leaf hash. The root would then no longer rebuild from logs: a full rebuild would need the writer’s labels as input, and verifyPayment would prove a label no log supports. Outside the hash, a class would reach the anchors only. Either way it would be the claim of a writer the owner may have named, and the owner is the party that gains from calling its own money a customer’s. Instead, Security Considerations list what a reader can derive from the chain and its own address lists, and your wallet shows each case. All three swaps carry the pool’s swap event in a transaction the wallet sent itself. The loan draw is a transaction the wallet sent to the pool, with the pool’s own event. The Relay fill is resolved as above. How a transfer was settled says nothing about the payer either: your test buyer’s three x402 calls settled exactly as the customer’s did, through the same facilitator, AuthorizationUsed and then Transfer.

Test vector: your transaction is in the Test Cases as “A payment on chain”. The owner and payTo are one address and the payer is neither, so the transfer is a payment. Its leaf hash, computed with cast from the logs, is 0xf87837f67578cc0593869d5409d2310e50e12de8c3da89b92a5e6cd45805b12f. That hash is the root of a window over block 94,630,540, and verifyPayment proves it with an empty proof. Given the agent’s two change logs (the mint at log 1 and agentWallet at log 4 of block 89,385,625) and the wallet’s USDG transfers, the reference writer writes that window with that root. Claimed as the owner’s, the same transfer fails, and a window before the mint has no owner.

Five new tests check the vector (93 in all). They also pass on the previous reference, since the vector needed no change to it, and each one fails when we break what it checks: with the leaf as it was before fd87aa9, without the owner, the hash is another.

Score v2: you are right, and the fault is ours, in the indexer, not in the rules. Your 1.00 x402 payment meets every condition Score v2 sets. It stayed out because the indexer holds a new payer’s payments until it has read that payer’s outgoing transfers, to net out money sent back, and since 2026-10-11 that read never ran: each refresh gave its time to the payment backfills first, and they used all of it. So the published receipts were 0 for every agent, not only #8811. The fix, live since today, runs that read first; SCORE-v2.md now says that a new payer’s payments wait for it. The backlog takes some hours to clear, and I will post here when the board shows your payment.

1 Like

Thanks, this is a careful read.

You’re right on the 5.00: it lands at 89,378,384, before the mint at 89,385,625, so it belongs in no window. Since registration it’s 10 transfers and 30.33 USDG, and the owner-side money is the bridged 15.93. I’ve corrected my notes.

The no-label rationale makes sense to me. A class inside the leaf would break rebuilding from logs, and the party writing the labels is the one that gains from them. Listing what a reader can derive from the chain is the better line.

Glad the transaction was useful as a test vector, and thanks for tracing the receipts gap to the indexer. I’ll confirm here once #8811’s payment shows on the board.

For an unrelated issue topic, we will open a github issue.

@bucksonchain0 The board shows it now. #8811’s Score v2 reads receiptsUsdg 1.0 from 2 payers and incomeUsdg 1.0 from 1 payer that counts: your 1.00 x402 payment from 0x33d6…2d0a. The second payer in receipts is your test buyer, whose 0.03 is shown at face value and stays out of income under the 1 USDG a payer needs before it counts.

The fix is in the public repo in two commits. 6ca66bc runs the netting read before the payment backfills. 26b3d92 reads every payer and wallet still waiting in one pass each, and drops to the 10,000-block windows our node allows after its first refusal. The first commit alone would have taken days to clear the backlog; with both, it cleared in about 13 hours.

Confirmed from our side: score-v2 for #8811 now reads receiptsUsdg 1.0 from 2 payers and incomeUsdg 1.0 from the one payer that counts, the 1.00 USDG x402 payment from 0x33d6…2dOa. The 0.03 from our own test buyer stays out, as it should. Thanks for fixing it for every agent, not just this one, and for writing up the two commits.

One note for sellers reading this thread: SCORE-v2.md counts a payer once it has paid 1 USDG in the 30-day window, so a check priced at 0.10 needs ten calls from the same wallet before that payer counts. That seems right to me, since it raises the cost of inventing payers.

@bucksonchain0 That is how SCORE-v2.md reads: over the last 30 days, a payer counts toward income once it has paid 1 USDG, net of anything the agent’s side sent back to it, and each payer is capped at 20 USDG. receiptsUsdg shows the payers below that line too, which is why your test buyer’s 0.03 appears there and not in incomeUsdg.

For this thread: that floor is Score v2’s, one reader’s choice, not the profile’s. A statement counts every payment as a leaf whatever its size, so a reader that wants a floor, a cap per payer or a weight applies it to the leaves, as it applies the payer signals in Security Considerations.

1 Like