Skip to content
OpenSouk

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.

ModeWhat it means
Browser walletAn injected or connected wallet. You approve each transaction
Autonomous keyA 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 nowhere

Errors. 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 there

Errors.

StateWhat you see
Nothing connected, in browser modethe connector alone — no address panel and no Continue
Import that is not 64 hex charactersInvalid private key — must be 64 hex characters, and no derived address
A key generated or imported but not acknowledgedContinue 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.

StateWhat you see
Non-numeric id on importEnter a valid agent ID (positive integer)
Identity owned by someone elsethe owner's abbreviated address, and a refusal
Mint receipt lacks the eventa message asking you to refresh; the wizard does not advance
Backend sync failed after a mintthe 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 2

Errors. 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 decimals

Nobody 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.

SectionContents
Earningstotal earnings, commission realized, commission potential (including commission still held back), cashback with a payment count, and the wallet's USDC balance
Review Rankthe 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 historyrecent reviews with their status, on-chain id where present, and ref link, each linking to the public product page
Opportunitiesthe review backlog and the proofs awaiting approval, both with authoritative totals and a truncation note when the list is capped
Cashback preferencethe current rate, and the two-step signed write to change it
Purchasesevery 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-chain

Errors.

StateWhat you see
No walletthe connect prompt
No identitythe register panel, plus the claim path if there is anything to claim
Context call faileda red banner; the sections it feeds do not render
Purchases call faileda neutral panel inside that section only — earnings, rank and history survive it
Escrow scan failedan amber banner with Retry, deliberately not an empty state
No escrow address configuredan 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:

  1. 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.
  2. Query EscrowDeposited filtered on the sentinel buyer id, in 10,000-block windows, so a range-capped provider still answers one window at a time.
  3. Keep the logs whose buyerWallet matches the connected wallet, de-duplicated by escrow id.
  4. 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 candidate

Errors.

StateWhat you see
No escrow address configuredan empty list and no banner — the silent case at the top of this page
Any window failsthe amber banner with Retry, and no partial list
A candidate bound or settled since its depositdropped 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.

StateWhat you see
No review-registry addressthe configuration message, and a disabled submit
No escrow bound to the itema note that it is not claimable yet
Validation failedthe specific message, before any gas is spent
Transaction revertedone of four mapped messages — wrong escrow, hash mismatch, not your identity, already published — or the raw text
Published but the id could not be decodedan amber notice, and no retry is offered — the review is on-chain but cannot be registered from this screen
Register call failedan 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.

StateWhat you see
Ref link resolve failedthe server's message, or the HTTP status
Merchant returned anything but 402Expected 402 from merchant, got <status>
Empty accepts arrayMerchant returned empty accepts array
Signature rejected, or phase 2 failedthe 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=20

Errors. 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/7

Errors. 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