Skip to content

release blocker: validate OBOL-token x402 payments for Spark2 sell path #440

@bussyjd

Description

@bussyjd

Release target

Next release horizon: sell the Spark2 external vLLM/OpenAI-compatible inference endpoint with OBOL token as the x402 settlement asset.

This is the payment-surface blocker for #439. Keep scope minimal:

Desired seller UX

Example shape:

obol sell inference spark2-qwen36 \
  --model qwen36-fast \
  --runtime openai \
  --upstream http://host.k3d.internal:8000 \
  --chain base-sepolia \
  --payment-asset obol \
  --wallet 0xSellerRecipient \
  --price 10 \
  --max-inflight 16

CRD shape:

spec:
  type: inference
  payment:
    network: base-sepolia
    asset: obol
    payTo: "0xSellerRecipient"
    scheme: exact
    price:
      perRequest: "10"

asset: obol should resolve to the configured OBOL token for the selected network, including address, decimals, symbol, and EIP-3009 domain metadata required to sign/verify TransferWithAuthorization.

Current surface validation

A quick code audit shows OBOL-token settlement is not release-ready yet. The current sell path is still USDC-shaped in several places.

Sell-side gaps

  • internal/embed/infrastructure/base/templates/serviceoffer-crd.yaml
    • spec.payment has network, payTo, price, but no asset.
    • descriptions say USDC.
  • internal/schemas/payment.go
    • PaymentTerms has no asset field.
    • PriceTable comments and conversion logic assume USDC-style human decimals.
  • cmd/obol/sell.go
    • CLI flag text says USDC everywhere.
    • no --payment-asset flag.
    • buildInferenceServiceOfferSpec() does not emit payment.asset.
  • internal/embed/skills/sell/scripts/monetize.py
    • no get_asset() accessor.
    • _add_pricing_route() writes price, payTo, network, but not asset/token metadata.
  • internal/x402/config.go
    • PricingConfig/RouteRule have no asset/token fields.
    • ResolveChain() returns x402-go ChainConfig values with USDC addresses.
  • internal/x402/verifier.go
    • always calls x402lib.NewUSDCPaymentRequirement(...).
    • this always produces a requirement whose asset is the chain's USDC address.
    • OBOL needs either direct PaymentRequirement construction or a generalized token resolver.

Buy-side gaps

The buy side is closer, but still has USDC assumptions:

  • internal/x402/buyer/config.go
    • already stores asset address in UpstreamConfig, good.
  • internal/x402/buyer/signer.go
    • CanSign() compares req.Asset, good.
    • GetTokens() hardcodes symbol USDC and decimals 6.
  • internal/embed/skills/buy-inference/scripts/buy.py
    • has USDC_CONTRACTS, USDC_DOMAIN_NAME, USDC_DOMAIN_VERSION.
    • _presign_auths() signs typed data using USDC domain metadata even if the 402 requirement asset is OBOL.
    • balance, probe, status, and budget output are USDC-worded/micro-unit-worded.
    • it currently ignores paymentRequirement.extra.name/version, which is where the verifier should publish token EIP-3009 domain metadata.

Test gaps

  • internal/testutil/eip712_signer.go hardcodes USDC contract/domain.
  • x402 integration tests and mock facilitator fixtures assume USDC/base-sepolia.
  • Need a dedicated OBOL-token fixture path, ideally using an ERC-3009-compatible test OBOL token on Anvil and/or official base-sepolia OBOL from Make offical base-sepolia OBOL + faucet #406.

Implementation checklist

1. Token/asset model

  • Add spec.payment.asset to the ServiceOffer CRD.
  • Add PaymentTerms.Asset string to Go schema.
  • Define asset aliases:
    • usdc
    • obol
    • 0x... explicit token address, optional later
  • Add a token resolver, e.g. ResolvePaymentAsset(network, asset) returning:
    • network ID
    • token address
    • symbol
    • decimals
    • EIP-3009 domain name
    • EIP-3009 domain version
  • Default payment.asset to usdc for backwards compatibility.

2. Seller/verifier path

  • Add asset/token metadata to PricingConfig and RouteRule.
  • Have monetize.py preserve payment.asset into x402-pricing routes.
  • Replace verifier's unconditional NewUSDCPaymentRequirement() with generalized payment requirement construction.
  • Ensure 402 response for asset: obol includes:
    • asset: <OBOL token address>
    • maxAmountRequired converted using OBOL decimals
    • extra.name and extra.version for the OBOL EIP-3009 domain
  • Update metrics/status labels or output to include asset symbol/address where useful.

3. CLI/status/docs

  • Add --payment-asset usdc|obol|0x... to obol sell inference and obol sell http.
  • Replace “USDC recipient wallet” language with “payment recipient wallet”.
  • obol sell status should show route asset, not only price/chain/payTo.
  • Document Spark2 OBOL example as the release-path smoke.

4. Buyer path

  • buy.py probe prints asset symbol/address and atomic amount without saying USDC unless asset is USDC.
  • buy.py buy signs typed data using the payment requirement's asset + extra.name/version, not hardcoded USDC constants.
  • Balance checks use the requirement asset address and decimals.
  • Buyer ConfigMap preserves asset symbol/decimals/domain metadata, not just asset address.
  • PreSignedSigner.GetTokens() returns the configured token symbol/decimals instead of hardcoded USDC/6.

5. Validation tests

  • Unit test: payment.asset survives CRD/schema/JSON/YAML roundtrip.
  • Unit test: asset=obol resolves to the expected network token address/domain/decimals.
  • Unit test: verifier emits an OBOL PaymentRequirement with the OBOL asset, amount, and domain metadata.
  • Unit test: buyer pre-signs authorization against the OBOL verifying contract/domain.
  • Integration test: mock seller returns OBOL 402; buyer sidecar signs/pays and request succeeds.
  • E2E release smoke: Spark2 endpoint exposed through ServiceOffer with payment.asset=obol; unpaid request gets OBOL 402; paid request reaches Spark2.

External dependency / open question

If we use base-sepolia for the release smoke, #406 is a blocker/adjacent dependency: we need an official test OBOL token + faucet or a documented temporary OBOL-compatible ERC-3009 token for e2e testing.

Also confirm the production OBOL token/payment token supports the exact EIP-3009 transferWithAuthorization flow required by x402. If OBOL does not support ERC-3009, we need a facilitator/scheme/token wrapper decision before this can ship.

Links

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestrelease-blockerMust land for the next release horizonx402

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions