Skip to content

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

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

feat: sell and buy services in multiple currencies + networks#655
OisinKyne wants to merge 5 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 offering "10 OBOL on Ethereum or 1 USDC on Base" meant hand-rolling N separate offers (N URLs, N storefront cards, duplicated registration). Now it's one offer, one URL, one card, one command � end to end: create -> CRD -> verifier -> catalog -> OpenAPI -> storefront -> buyer selection.

This branch also tightens the surrounding seller UX: a self-contained skill.md, consistent network terminology, multi-payment-aware sell status, and a replace-confirm guard.

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 (replaces the bespoke demo special-casing â�� demo is now an ordinary category).
  • Per-option maxTimeoutSeconds (different chains, different block times).

Verifier / data plane (internal/x402) � the protocol layer was already multi-payment (402 accepts is an array; 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 (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 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()); 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 + in-pod eRPC autofill, --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. 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, OpenAPI & storefront

  • /api/services.json entries gain payments[] (flat fields still mirror the primary).
  • /openapi.json x-payment-info keeps price as the primary for single-price indexers and adds accepts[] (one {mode,currency,amount,network-as-CAIP-2} per option) for multi-currency offers, so indexers can surface the cheapest.
  • 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.

skill.md rewritten to be self-contained for any LLM � added a "How to pay (x402)" section (the full 402 -> sign -> X-PAYMENT -> 200 loop, EIP-3009 vs Permit2, gasless), pointers to the machine-readable /openapi.json (Swagger) and /api/services.json, a multi-payment "Pay with" column that includes the network, and per-option detail (price, network, CAIP-2, payTo, token contract, decimals, transfer scheme) plus a concrete call hint. Copy-paste prompts on the 402 page and storefront no longer send agents to the broad obol.org/llms.txt � they point at this operator's own <tunnel>/skill.md + /openapi.json (same origin as the endpoint, one fetch to learn to pay).

Consistent network terminology � the spec field, 402 wire, buy.py, and obol network all say network; only the sell CLI said --chain. Flipped all sell flags to primary --network with --chain retained as a back-compat alias (verified: existing cmd.String("chain") reads still resolve). Examples/hints updated to --network.

sell status multi-payment � lists every accepted option, and resolves the chain-default USDC asset so offers without an explicit asset show USDC (0x833...) / 3 USDC per request instead of (not set) / 3 per request.

Replace-confirm guard � obol sell agent (and sell demo) can resolve to the same offer name+path; a second create previously kubectl apply-ed over the first silently. On a TTY it now warns and prompts before replacing; non-interactive callers (resume/flows/JSON) keep the idempotent apply.

Operational note (CRD re-apply required)

The ServiceOffer CRD gains spec.payments/spec.listing. Strict decoding rejects 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, catalog payments[], OpenAPI accepts[], sell status multi-payment + default-USDC resolution, the --network/--chain alias, CRD field presence.
  • Python: factory.py parser + EIP-5267 decode, buy.py _select_payment (token/network/index/single + error paths).
  • Frontend: tsc --noEmit clean.
  • Full go test ./... green; both skills py_compile clean.
  • Verified live on a running stack with a freshly-built controller image (unique tag, no stale cache): /skill.md renders the new format and /openapi.json resolves.

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).
  • Interactive prompting for un-supplied sell agent flags (planned as a follow-up branch).
  • Go obol buy inference option-selection (catalog already exposes payments[] to build on).

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