Skip to content

feat(sell): register by default with explicit opt-out - #349

Merged
OisinKyne merged 4 commits into
feat/x402-pre-merge-followupfrom
feature/register-offchain-warning
Apr 19, 2026
Merged

feat(sell): register by default with explicit opt-out#349
OisinKyne merged 4 commits into
feat/x402-pre-merge-followupfrom
feature/register-offchain-warning

Conversation

@bussyjd

@bussyjd bussyjd commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Replaces the warning-only UX with the actual stronger design:

  • obol sell http now registers by default
  • --no-register is the explicit opt-out
  • registration is treated as part of the sell flow, not an optional afterthought
  • the controller no longer treats OffChainOnly as "registered and ready"

Why

The previous UX was not coherent:

  • obol sell http --register could still mean "only publish an off-chain document"
  • Registered=True / OffChainOnly looked like success even when nothing was on-chain
  • users had to mentally stitch together sell http and sell register as two unrelated commands

That is the wrong model.

The product model should be:

  • selling a public service registers it by default
  • if a signer is not available, that is a blocking requirement unless the user explicitly opts out

Flow

Old behavior

flowchart TD
    A[obol sell http --register] --> B[Create ServiceOffer]
    B --> C[Controller publishes off-chain registration doc]
    C --> D{Controller signing key configured?}
    D -- yes --> E[Register on ERC-8004]
    D -- no --> F[Registered=True / OffChainOnly]
Loading

New behavior

flowchart TD
    A[obol sell http] --> B[Create ServiceOffer]
    B --> C[Ensure public route is live]
    C --> D{--no-register?}
    D -- yes --> E[Skip on-chain registration intentionally]
    D -- no --> F[Resolve signer]
    F --> G[Register on ERC-8004]
    G --> H[Controller observes on-chain registration]
    H --> I[Registered=True]
Loading

Seller UX

sequenceDiagram
    participant User
    participant CLI as obol sell http
    participant Ctrl as serviceoffer-controller
    participant Chain as ERC-8004

    User->>CLI: sell http ...
    CLI->>Ctrl: create ServiceOffer
    CLI->>CLI: ensure tunnel / route
    CLI->>CLI: resolve signer
    CLI->>Chain: register agent + metadata
    Ctrl->>Chain: recover registration by owner + URI
    Ctrl-->>User: Ready with Registered=True
Loading

What changed

CLI

  • sell http now enables registration by default
  • added --no-register to skip the automatic on-chain registration step
  • retained --register only as a compatibility no-op / deprecated path
  • added --private-key-file to sell http for explicit registration signer input
  • automatic registration uses the same registration machinery as sell register
  • registration signer must match the payment wallet for the automatic path
  • sell status <name> now prints a human summary including Agent ID and Registration Tx

Controller semantics

  • OffChainOnly is no longer considered ready
  • when the controller has no signing key, it waits for external registration instead of reporting success
  • the controller now recovers externally-created registrations by owner + published URI and then marks the offer registered

Tests / flows

  • updated CLI flag coverage
  • added direct tests for:
    • registration defaults / --no-register conflicts
    • private key material parsing
    • human sell status summary output
  • updated controller phase expectations
  • updated the BDD harness to use the actual default agent wallet as the seller wallet
  • updated flow-06 to use --no-register where registration is intentionally out of scope
  • updated flow-11 to use the unified flow instead of a separate sell register step

Docs

  • updated the public monetize guide
  • updated embedded skill docs and examples that still taught the old --register flow
  • updated the embedded monetize.py help text to mark --register as legacy metadata publishing

Validation

  • go test ./cmd/obol ./internal/serviceoffercontroller ./internal/x402
  • bash -n flows/flow-06-sell-setup.sh flows/flow-11-dual-stack.sh
  • full flow-11 rerun from clean Obol/k3d state: 41/41 passed

Flow-11 artifacts from the passing run

  • seller wallet: 0xC0De030F6C37f490594F93fB99e2756703c4297E
  • seller agent ID: 5008
  • tunnel URL: https://eliminate-counting-bonus-skating.trycloudflare.com
  • buyer signer wallet: 0x9aE40E84fb587fB3B526f5542934c3B2d18D67bF
  • settlement tx: 0xca98062273d612c700dd036aa2bd68c252f1c71e5da6d2bc469e589a98cc5031

User-visible contract

From this PR onward, the intended contract is:

  • obol sell http ... means sell and register
  • obol sell http ... --no-register means sell without on-chain registration on purpose
  • obol sell register remains the advanced/manual repair path, not the normal primary flow

@bussyjd
bussyjd marked this pull request as ready for review April 19, 2026 03:45
@bussyjd bussyjd changed the title feat(sell): warn that --register is off-chain only feat(sell): register by default with explicit opt-out Apr 19, 2026

@OisinKyne OisinKyne left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Register never meaningfully shipped, its a classic ai dev over-cautiousness. Merging anyways but imo later we can just get rid of the no-op register

@OisinKyne
OisinKyne merged commit 9d7de69 into feat/x402-pre-merge-followup Apr 19, 2026
@OisinKyne
OisinKyne deleted the feature/register-offchain-warning branch April 19, 2026 20:36
OisinKyne pushed a commit that referenced this pull request Apr 19, 2026
…y warning (#345)

* feat(x402/buyer): detect 2xx without X-PAYMENT-RESPONSE and expose via metric

Post-#343, settlement moved off the Traefik ForwardAuth hop and became the
seller's responsibility. The buyer sidecar calls ConfirmSpend on any upstream
2xx regardless of whether X-PAYMENT-RESPONSE is present, so a seller that
returns 200 without settling silently consumes the payer's voucher with no
observable signal. This matches the W2/W9 gap flagged in the PR #343 review.

- Add OnPaymentUnsettled callback to replayableX402Transport. Fires exactly
  when the upstream returns 2xx but no successful X-PAYMENT-RESPONSE is
  emitted, logs a WARN, and increments a new counter.
- Add PaymentEventUnsettled event type.
- Add obol_x402_buyer_payment_unsettled_confirmations_total metric with
  upstream/remote_model labels. Operators should alert on any non-zero value.
- Pin invariant with two new tests:
  - TestProxy_UpstreamSuccessNoSettlementHeader_IncrementsUnsettledMetric
  - TestProxy_UpstreamSuccessWithSettlementHeader_DoesNotIncrementUnsettledMetric
- Pin mux symmetry invariant that both /chat/completions and
  /v1/chat/completions route identically — catches the class of regression
  that produced the PR #343 /v1 add/revert/re-add churn.

* fix(x402/forwardauth): warn on verifyOnly=false, shrink facilitator timeout to 5s

Addresses W7 and W8 from the PR #343 review.

W7 — verifyOnly=false footgun: VerifyOnly is the right name for the flag in
the in-process gateway context but is semantically load-bearing for Traefik
ForwardAuth, where the auth hop cannot observe the upstream response. If an
operator flips x402-pricing.yaml verifyOnly=false believing it enables "real"
settlement, the verifier will debit the payer before the upstream serves the
request. We cannot remove the flag without a broader refactor of
internal/inference/gateway.go, so instead:
  - NewForwardAuthMiddleware now logs a loud WARNING at construction when
    VerifyOnly=false, explaining the safe usage.
  - cmd/x402-verifier/main.go emits the same warning on startup and log-scrub
    filters will surface it.
  - ForwardAuthConfig.VerifyOnly documents the invariant ("MUST be true
    behind Traefik ForwardAuth"), so a contributor flipping it gets the
    explanation inline.

W8 — facilitator timeout: reduce http.Client.Timeout from 30s to 5s.
/verify is a cheap signature check; anything beyond 5s is a network problem
the caller should see quickly rather than having every paid request hang
for half a minute on a slow facilitator.

Tests:
- TestForwardAuth_VerifyOnlyFalse_EmitsStartupWarning pins the warning text.
- TestForwardAuth_VerifyOnlyTrue_NoStartupWarning is the negative control so
  operators don't train themselves to filter the warning out.

* chore(embed): lint :latest image tags with pin-by-digest policy

Addresses W4 from the PR #343 review. The /v1 back-and-forth on PR #343
(add → revert → re-add) was consistent with a deployed x402-buyer:latest
image lagging behind main, and the fix hardcoded /v1 in the LiteLLM template
instead of pinning the image. Same risk applies to x402-verifier and
serviceoffer-controller which also ship as :latest.

- New internal/embed/embed_image_pin_test.go scans every embedded template
  and fails when a new :latest appears without an allowlist entry. The
  allowlist currently covers the three obolnetwork images pending digest
  pinning; each entry carries a short reason. Removing an entry without
  replacing :latest in the YAML fails the test (stale-allowlist check).
- Inline TODO(image-pin) comments in llm.yaml and x402.yaml explain the
  policy at the point of violation so contributors who touch the deployment
  spec see it.

This does not pin the images (that requires GHCR access to produce the
digest) — it establishes the contract and makes drift visible.

* feat(x402): route sell http through seller gateway

* fix(obolup): harden installer writes and tty prompts

* docs: keep seller gateway report in pr body only

* feat(model): add master token accessor (#347)

* feat(sell): register by default with explicit opt-out (#349)

* feat(sell): warn that --register is off-chain only

* feat(sell): register by default with explicit opt-out

* feat(sell): show registration summary in sell status

* test(sell): cover registration defaults and sync skill docs

* feat(openclaw): surface generated agent wallet (#348)

* fix(stack): preload openclaw image in dev k3d (#352)

* feat(x402-buyer): expose confirm-spend persistence failures (#351)

---------

Co-authored-by: bussyjd <bussyjd@users.noreply.github.com>
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.

2 participants