Agents and wallets
Where an identity is minted, funded, and put to work
Five screens. One mints an agent identity and funds it, one is the agent's own dashboard, one walks a purchase through by hand, and two are the public agent listings. The shell they all sit in — the wallet gate, the identity switcher, the session token, the two-writes-then-retry pattern — is on The console and is assumed here.
The two unset addresses
Two environment variables the console reads are set in no committed file:
NEXT_PUBLIC_COMMISSION_ESCROW_ADDRESS and NEXT_PUBLIC_REVIEW_REGISTRY_ADDRESS. The example
file declares them empty; the Base Sepolia file does not declare them at all. Both land
on /reviewer, and the two features behind them fail in opposite ways:
- Publishing a review says so. The composer checks the review-registry address and renders
Review registry is not configured. Cannot publish a review right now.with the submit button disabled. Nothing is lost and nothing is misleading. - The unclaimed-purchase scan does not. With no escrow address it returns an empty list without an error, which the screen renders as "nothing to claim" — indistinguishable from a wallet that genuinely has nothing. A buyer with unbound cashback sees no sign of it.
Both features are documented below on the assumption that the variables are supplied. Check the tracked files before relying on either:
git -C frontend grep -n \
'NEXT_PUBLIC_COMMISSION_ESCROW_ADDRESS\|NEXT_PUBLIC_REVIEW_REGISTRY_ADDRESS' -- '.env*'git grep reads tracked files only: an untracked local file can set either variable for one
developer's machine, and no deployment builds from one.
/wallet-setup — mint an identity and fund it
What it needs. Nothing to start. The wizard connects or generates a wallet itself.
What it renders. Seven labelled steps: Mode, Wallet, Identity, Commission, Cashback, Telegram, Fund. Back is available on every step, with no floor — unlike the merchant wizard, this one does not lock a step after its transaction succeeds.
Step 1 — Mode
Two choices, and the choice narrows what the rest of the wizard can do.
| Mode | What it means |
|---|---|
| Browser wallet | An injected or connected wallet. You approve each transaction |
| Autonomous key | A raw private key, generated here or pasted in. The agent signs by itself |
GET /wallet-setup (the Mode step)
→ no requests; the choice is page state and is persisted nowhereErrors. None. The mode can be changed later, which restarts the wallet step: a key and a connected wallet are not interchangeable.
Step 2 — Wallet
In browser mode: the connector, then the connected address and Continue.
In key mode: two tabs, Generate new and Import existing. Generation derives a fresh key in the browser and displays it in full alongside its address; import validates 64 hex characters, with or without the prefix, and shows the derived address. Either way a security warning appears with four points — the key is full control of the wallet, store it in a password manager, keep the balance minimal, never paste it into an untrusted site — and Continue stays disabled until you tick that you have saved it.
The key is held in page state only. Nothing writes it to storage, nothing sends it anywhere, and navigating away loses it. It is displayed in plaintext on a web page, so the tab is the secret.
GET /wallet-setup (the Wallet step, browser mode)
→ no requests; the connector reports the address, and Continue passes it to step 3
GET /wallet-setup (the Wallet step, key mode)
→ no requests; the key is generated or validated in the browser, and its address derived thereErrors.
| State | What you see |
|---|---|
| Nothing connected, in browser mode | the connector alone — no address panel and no Continue |
| Import that is not 64 hex characters | Invalid private key — must be 64 hex characters, and no derived address |
| A key generated or imported but not acknowledged | Continue stays disabled until you tick that you have saved it |
Step 3 — Identity
Two tabs again: Register new mints an ERC-8004 identity, Import existing verifies you already own one.
POST /wallet-setup (the Identity step, Register new)
→ IdentityRegistry.register() on-chain; agent id read from the Registered event
→ POST /v1/agent/register { agent_id } 409 treated as success
POST /wallet-setup (the Identity step, Import existing)
→ IdentityRegistry.ownerOf(agentId) must equal the wallet
→ POST /v1/agent/register { agent_id }In browser mode the transaction goes through the connected wallet. In key mode the wizard builds its own client, hardcoded to Base Sepolia and defaulting to that network's public RPC. There is no chain selection anywhere in this flow.
Errors.
| State | What you see |
|---|---|
| Non-numeric id on import | Enter a valid agent ID (positive integer) |
| Identity owned by someone else | the owner's abbreviated address, and a refusal |
| Mint receipt lacks the event | a message asking you to refresh; the wizard does not advance |
| Backend sync failed after a mint | the failure, and Retry sync — which never re-mints |
Step 4 — Commission
Display-only: your address, a copy button, and Continue. It states there is no separate payout address to set.
That is true of anything set up here, and not a protocol rule. On-chain, both the escrow and the router resolve a payout to "the identity's configured agent wallet, or its owner if none is configured" — so a separately configured agent wallet does receive the money. The console has no control for setting one, and its identity-registry interface does not include the function. If an agent wallet was configured elsewhere, this screen shows the wrong destination. Identity and commitments has the two keys.
GET /wallet-setup (the Commission step)
→ no requests; it displays the address from step 2Errors. None. It cannot fail and it writes nothing.
Step 5 — Cashback
The reviewer dashboard's own cashback control, reused, seeded at 0% because a fresh identity has no rate. Range 0 to 40% of commission, two decimals. Skippable.
POST /wallet-setup (the Cashback step)
→ POST /v1/cashback-rate/step1 { reviewer_agent_id, cashback_of_commission_bps }
→ (wallet signs the returned CashbackRateCommitment)
→ POST /v1/cashback-rate/step2 { …the same body, plus nonce, expiry, agent_sig }In key mode this step is skip-only. The signed write goes through the browser connector, which a pasted key is not, so the screen says so and offers Skip instead. The rate can be set later from the reviewer dashboard.
Errors. An out-of-range percentage is refused inline before step 1 is called; a failed write renders the server's message and leaves Skip available.
Step 6 — Telegram
Optional. Mints a deep link, then confirms the code it came back with.
POST /wallet-setup (the Telegram step)
→ POST /v1/telegram/link/start {} returns deep_link, code, expires_at
→ POST /v1/telegram/link/confirm { code }The code arrives prefilled in the confirmation field and the expiry is displayed. Opening the deep link, or sending the bot a start command with the code, does the same thing.
In key mode this step is also skip-only, for the same reason as cashback.
Errors. Any failure renders the server's message in a banner; Skip and Continue both stay available so the step cannot dead-end. The same flow, with status and unlink, is on Settings and simulators.
Step 7 — Fund
The end of the wizard: the agent id, the wallet address with a copy button, a link to a testnet ETH faucet, and a button that mints ten test USDC.
POST /wallet-setup (the Fund step, Mint 10 test USDC)
→ USDC.mint(address, 10000000) base units; 10 USDC at six decimalsNobody can call mint on Base USDC, whose minting is restricted to Circle's
own minters, so this button only ever works against a mock token. The faucet URL and an optional
deep link back to the calling agent both come from environment variables.
Errors. A failed mint renders the error text and the address and faucet link remain usable. The return link renders only when its variable is set.
/reviewer — the agent's dashboard
What it needs. A connected wallet through WalletGate, an owned identity, and a session token.
How it picks an identity. For a wallet with more than one, it first counts each identity's purchases and defaults to the one with the most, so a dashboard never opens on an identity with nothing in it. That probe is one authenticated request per identity — but one wallet signature total, since the session token is cached. A failing probe scores that identity zero rather than failing the page.
What it renders when there is no identity. A panel — You don't have a registered identity —
and a link to the setup wizard. The same panel appears when an id resolves but our records say it
is not registered. Above it, when the wallet has unbound purchases, a register-to-claim panel.
What it renders when there is one. Six sections, in order.
| Section | Contents |
|---|---|
| Earnings | total earnings, commission realized, commission potential (including commission still held back), cashback with a payment count, and the wallet's USDC balance |
| Review Rank | the Repeat and One-off legs to two decimals, or Cold start where no rank exists yet; a flagged badge; counts of reviews published and votes cast |
| Review history | recent reviews with their status, on-chain id where present, and ref link, each linking to the public product page |
| Opportunities | the review backlog and the proofs awaiting approval, both with authoritative totals and a truncation note when the list is capped |
| Cashback preference | the current rate, and the two-step signed write to change it |
| Purchases | every escrow this agent is party to, plus the unclaimed-purchase panel |
Cold start is not zero. The rank fields come back null for an agent with no computed rank, and the screen prints the words rather than a number. Ranks has why.
What it reads.
GET /reviewer
→ POST /v1/auth/session one signature, cached for the session
→ GET /v1/agent?owner={address} identities
→ MerchantRegistry.isActive(id) per identity
→ GET /v1/agent/{id}/escrows per identity, only when more than one is owned
→ GET /v1/agent/{id}/context the first five sections
→ GET /v1/agent/{id}/escrows the purchases list
→ CommissionEscrow.EscrowDeposited logs the unclaimed-purchase scan, on-chainErrors.
| State | What you see |
|---|---|
| No wallet | the connect prompt |
| No identity | the register panel, plus the claim path if there is anything to claim |
| Context call failed | a red banner; the sections it feeds do not render |
| Purchases call failed | a neutral panel inside that section only — earnings, rank and history survive it |
| Escrow scan failed | an amber banner with Retry, deliberately not an empty state |
| No escrow address configured | an empty list and no banner — see the top of this page |
The unclaimed-purchase scan
A purchase paid by a wallet with no agent identity is deposited against the sentinel buyer id, and its cashback cannot be released until an identity is attached. The screen finds those by scanning on-chain, because we do not index them per wallet:
- Read the current head, then a floor: the configured escrow deploy block, or head minus 100,000 blocks when that is unset. Never block zero — a from-zero scan is refused or timed out by most providers.
- Query
EscrowDepositedfiltered on the sentinel buyer id, in 10,000-block windows, so a range-capped provider still answers one window at a time. - Keep the logs whose
buyerWalletmatches the connected wallet, de-duplicated by escrow id. - Confirm each candidate with
getEscrow: still the sentinel, still this wallet, and still unsettled. A bind or a settlement since the deposit drops it from the list.
GET /reviewer (the unclaimed-purchase scan)
→ eth_blockNumber the head
→ CommissionEscrow.EscrowDeposited logs filtered on the sentinel buyer id, floor → head
in 10,000-block windows
→ CommissionEscrow.getEscrow(escrowId) once per de-duplicated candidateErrors.
| State | What you see |
|---|---|
| No escrow address configured | an empty list and no banner — the silent case at the top of this page |
| Any window fails | the amber banner with Retry, and no partial list |
| A candidate bound or settled since its deposit | dropped from the list with no notice — the confirmation read is what drops it |
Step 4's state check compares the record's state against the numeric encoding of Holding, held
as a literal in the client. That is the only form the ABI offers — the enum's member names do not
survive compilation — but it means a renumbering of EscrowState would make this filter wrong
without any error. See the note on enums on The contracts.
Binding a purchase
POST /reviewer (Bind to claim)
→ CommissionEscrow.bind(escrowId, agentId)Four contract errors are matched by name in the wallet's revert text and rewritten for a buyer: already claimed, belongs to a different wallet, you do not control that identity, and that escrow no longer exists. Anything else surfaces raw. The button is disabled with a reason when there is no identity yet or no wallet connected.
On success the screen says the purchase is bound and that writing the review is what releases the cashback — which the oracle then pays with no further action. The contract behaviour behind that is on Escrow and router.
Writing a buyer review
Reachable from a claimable backlog item. Two input modes, chosen by the product's category:
- Generic — a one-to-five star rating and a description. The description must be non-empty and may not contain angle brackets, mirroring the server's rule.
- Machine-measured — a JSON editor prefilled with the RPC-provider schema skeleton, validated against that schema and its arithmetic invariants before anything is published. A human cannot hand-author it, which is exactly why the generic form is not offered for those products.
POST /reviewer (Publish review)
→ contentHash = keccak256(abi.encode(agentId, merchantId, productId, contentJSON))
→ ReviewRegistry.publishReviewWithEscrowProof(agentId, contentHash, escrowId)
→ POST /v1/review/register { review_id, escrow_id, content_json }The content string is built once and used twice — hashed for the transaction and posted
verbatim. Re-serializing a parsed copy in between changes the bytes and the server's hash
comparison fails. The review id is read from the ReviewPublished log in the receipt.
Errors.
| State | What you see |
|---|---|
| No review-registry address | the configuration message, and a disabled submit |
| No escrow bound to the item | a note that it is not claimable yet |
| Validation failed | the specific message, before any gas is spent |
| Transaction reverted | one of four mapped messages — wrong escrow, hash mismatch, not your identity, already published — or the raw text |
| Published but the id could not be decoded | an amber notice, and no retry is offered — the review is on-chain but cannot be registered from this screen |
| Register call failed | an amber panel naming the on-chain review id, and Retry register — never re-publish |
The row above it has no retry at all: the register call needs the review id, and that is the one thing that step failed to read.
/buy — the manual purchase walkthrough
What it needs. An attribution token, a wallet, and a buyer agent id. The screen calls itself a test payment, and it is: nothing resolves the agent id for you, and only the first payment option a merchant offers is ever used.
What it renders. A token field, prefilled from a token query parameter when present; a
browser-wallet or pasted-key choice; a buyer agent id field; then, once loaded, the payment
requirements and a pay button.
What it does.
GET /buy?token=<attribution token>
→ GET {api}/r/{token} resolves to merchant endpoint, token, router, facilitator
POST /buy (Load payment details — phase 1)
→ POST {merchant endpoint}
X-Referrer-Token: <token>
X-Referrer-Buyer-Agent-Id: <agent id>
expects HTTP 402 and a non-empty accepts array
POST /buy (Pay — phase 2)
→ sign ReceiveWithAuthorization { from, to, value, validAfter, validBefore, nonce }
→ POST {merchant endpoint}
X-Referrer-Token, X-Referrer-Buyer-Agent-Id
PAYMENT-SIGNATURE: <base64url of the x402 payment payload>The typed-data domain takes its name and version from the merchant's own payment requirements and
its verifying contract from the asset address — but the chain id is a literal in the source,
fixed to Base Sepolia's. validAfter is zero, validBefore is five minutes out, and the
nonce is 32 random bytes. On success the returned transaction is displayed.
This screen signs a ReceiveWithAuthorization. The router settles by calling
receiveWithAuthorization, which validates that type and no other; a
TransferWithAuthorization over the same field values hashes differently and is rejected inside
the token. See Escrow and router.
Errors.
| State | What you see |
|---|---|
| Ref link resolve failed | the server's message, or the HTTP status |
Merchant returned anything but 402 | Expected 402 from merchant, got <status> |
| Empty accepts array | Merchant returned empty accepts array |
| Signature rejected, or phase 2 failed | the error, with the token and agent id kept so you need not retype |
This screen is the hand-driven version of what x402 and MPP describes and what the signer's payment tool does in one call — see Signer tools.
The two public agent screens
Neither needs a wallet, a session, or a signature.
/agents
What it needs. An optional page query parameter, floored at 1. The page size is fixed at 20.
What it renders. A count of registered agents, a card each, and pagination.
GET /agents?page=3
→ GET /v1/agent?page=3&limit=20Errors. A failed fetch renders the message in a red panel; an empty result renders
No agents registered yet.
/agents/[id]
What it needs. An agent id in the path.
What it renders. The agent id with a flagged badge where applicable; owner address; payment
wallet when one is recorded; the Repeat and One-off Review Rank legs to four decimals or an em
dash where null; review count; registration time; agent URI when present. Then a list of reviewed
products, each linking to the public product page.
GET /agents/7
→ GET /v1/agent/7Errors. Anything that fails renders Agent not found. and a link back to the listing — a
missing agent and a failed request are indistinguishable here.
The rank legs are shown to four decimals on this screen and two on the reviewer dashboard, from the same underlying value.
Next steps
- The console — the shell, and the merchant screens
- Settings and simulators — caps, Telegram, the rank explorers
- Agents — the routes behind these screens