Skip to content

feat: sell and buy services in multiple currencies + networks#655

Open
OisinKyne wants to merge 4 commits into
mainfrom
oisin/agentsales
Open

feat: sell and buy services in multiple currencies + networks#655
OisinKyne wants to merge 4 commits into
mainfrom
oisin/agentsales

Conversation

@OisinKyne

@OisinKyne OisinKyne commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Problem to be solved

I want to sell services in USDC alongside their OBOL offerings, particularly to be able to sell on Base where x402scan is but OBOL is not. This PR allows service offers to list multiple payment options. It also allows people to sell in tokens other than USDC and OBOL though not fully supported best in class on the stack. This grants some flexibility to the stack users who want to sell something in their utility token, but don't want to fork and build an entire parallel Obol Stack to do it. Future updates can make the support for alternative tokens more best in class.

Summary

Lets a single service (agent or HTTP) be sold in multiple currencies/networks at once -- advertised as one x402 endpoint whose 402 response lists every accepted payment, with the buyer choosing which to pay. Previously each ServiceOffer carried exactly one payment, so the only way to offer "10 OBOL on Ethereum or 1 USDC on Base" was hand-POSTing N separate offers (N URLs, N storefront cards, duplicated registration). Now it's one offer, one URL, one card, one command.

The change is end-to-end: create -> CRD -> verifier -> catalog -> storefront -> buyer selection.

What changed

Schema (backward-compatible)

  • ServiceOffer.spec.payments[] (canonical multi-payment) added alongside the existing singular spec.payment (kept as the always-set primary = payments[0]). EffectivePayments() normalizes both, so every existing CR, the stack up resume replay, and stack import keep working untouched.
  • spec.listing{weight, category} for storefront ordering/grouping.
  • Per-option maxTimeoutSeconds (different chains, different block times).

Verifier (internal/x402) -- the protocol layer was already multi-payment (the 402 accepts is an array, and findMatchingRequirementV1//verify//settle act on whichever requirement the buyer matched). This wires it up: RouteRule carries all options, matchPaidRouteFull emits one PaymentRequirements per option, and metrics attribute revenue to the actual chain/asset paid (via a new OnPaymentMatched hook). No change to settlement correctness.

Seller CLI (obol sell agent|http) -- repeatable --accept flag:

obol sell agent bankr --accept token=OBOL,network=ethereum,price=10 \
                      --accept token=USDC,network=base,price=1

token=<symbol> resolves the registry asset; asset=0x... is an escape hatch for any ERC-20 on a supported chain. Plus --weight/--category. ERC-8004 registration uses the first option's network. sell status lists all options; sell update --accept replaces the set.

On-chain asset autofill -- for raw asset=0x..., missing decimals/symbol/EIP-712 domain are read best-effort from the chain (decimals()/symbol()/EIP-5267 eip712Domain()) and transfer defaults to Permit2; errors-to-specify if unresolvable. Registry/USDC options make zero RPC calls (no new cluster dependency for the common path).

Agent factory (factory.py) -- mirrors --accept + autofill (in-pod eRPC), --weight/--category. Sub-agent wallet creation flipped to opt-in, and --pay-to now defaults to the master Hermes wallet so a sub-agent needn't provision its own signer just to sell. Also closes the handoff pain points: --description decoupled from --register, status auto-discovers all offers, skill resolution searches both layouts.

Buyer skill (buy.py) -- pay/pay-agent/buy accept --token/--network/--payment-option to choose among advertised options (auto-selects when there's one; prompts on a TTY; errors with the list otherwise). --token/--network also guard against paying the wrong asset.

Catalog + storefront -- /api/services.json entries gain payments[] (flat fields still mirror the primary); the storefront ServiceCard renders all options with a payment selector that re-targets the buy snippets, plus copyable per-service anchor links and weight-based ordering. The demo special-casing was reworked into an ordinary category (removing bespoke isDemo code in the catalog, cards, and OpenGraph).

Operational note (CRD re-apply required)

The ServiceOffer CRD gains spec.payments/spec.listing. Strict decoding will reject offers using them until the updated CRD is applied -- re-run obol stack up (recreate the cluster if a running one doesn't refresh). Additive change; no data migration.

Testing

  • Unit: EffectivePayments fallback, verifier multi-accept + settle-the-chosen-option, --accept parser (registry/raw/dedup/errors), on-chain autofill merge (injected fetcher), catalog payments[], sell status multi-payment, CRD field presence.
  • Python: factory.py parser + EIP-5267 decode, buy.py _select_payment (token/network/index/single + error paths) verified fetcher), catalog payments[], sell status multi-payment, CRD field presence.
  • Python: factory.py parser + EIP-5267 decode, buy.py _select_payment (token/network/index/single + error paths) verified offline.
  • Frontend: tsc --noEmit clean.
  • Full go test ./... green; both skills py_compile clean.

Out of scope (intentional)

  • obol sell inference stays USDC/OBOL-curated (no arbitrary multi-currency on the standalone gateway).
  • Arbitrary eip155:N chains beyond the supported set (raw assets are limited to supported chains).
  • Optional Go obol buy inference option-selection (catalog now exposes payments[] to build on).
    Tip: Open the Command Palette (Cmd+Shift+P) and run "Shell Command: Install 'code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant