Skip to content

x402 pre-merge follow-up: seller gateway, unsettled metric, verifyOnly warning#345

Open
bussyjd wants to merge 6 commits intofeat/x402-buyer-settlement-hold-signfrom
feat/x402-pre-merge-followup
Open

x402 pre-merge follow-up: seller gateway, unsettled metric, verifyOnly warning#345
bussyjd wants to merge 6 commits intofeat/x402-buyer-settlement-hold-signfrom
feat/x402-pre-merge-followup

Conversation

@bussyjd
Copy link
Copy Markdown
Collaborator

@bussyjd bussyjd commented Apr 17, 2026

Stacked follow-up to #343. Targets feat/x402-buyer-settlement-hold-sign; GitHub will auto-retarget to main once #343 merges.

This PR now also folds in the seller-gateway redesign that restores correct settlement ownership for sell http.

What this fixes

# Area Change
W2 / W9 Unsettled paid requests had no operator-visible signal Kept the OnPaymentUnsettled callback + obol_x402_buyer_payment_unsettled_confirmations_total metric and tests.
W4 /v1 mux behavior could drift from deployed x402-buyer behavior Kept mux symmetry coverage and image-pin linting to reduce tag drift surprises.
W7 VerifyOnly=false is unsafe on the Traefik auth hop Kept the warnings and documentation that make this invariant explicit.
W8 30s facilitator timeout stalls every paid request Kept the reduced timeout.
NEW sell http had no correct settlement owner once verifyOnly=true became permanent Route sell http through a shared seller-owned x402 gateway that verifies, proxies, settles after upstream success, and emits X-PAYMENT-RESPONSE.
NEW Cross-namespace HTTPRoute -> x402/x402-verifier backend refs were blocked by Gateway API policy Create ReferenceGrants and grant the controller RBAC to manage them.
NEW Flow-11 still assumed the old ForwardAuth -> upstream shape Updated flow/test/doc expectations and revalidated the human-path dual-stack run.

Runtime architecture

Old

  • Traefik -> ForwardAuth verifier -> upstream

New

  • Traefik -> shared seller-owned x402 gateway -> upstream

The shared gateway now owns:

  1. 402 payment requirement generation
  2. payment verification with the facilitator
  3. upstream proxying
  4. settlement after upstream success
  5. X-PAYMENT-RESPONSE emission back to the buyer

This is the important semantic fix in the stack: sell http now has the same seller-side verify -> fulfill -> settle shape that sell inference already had.

What changed

Runtime

  • Route HTTPRoute backends for ServiceOffers to the shared x402-verifier Service instead of the raw upstream Service.
  • Extend route config with upstreamURL and stripPrefix so the shared gateway can proxy to the real upstream correctly.
  • Add seller-side proxy handling in internal/x402/verifier.go:
    • match route
    • emit 402
    • verify payment
    • proxy to upstream
    • settle after upstream success
    • return X-PAYMENT-RESPONSE
  • Keep the legacy /verify endpoint, but treat verifyOnly=true as a legacy ForwardAuth safety setting instead of the hot path for sell http.
  • Create ReferenceGrants for HTTPRoute -> x402/x402-verifier backend refs.
  • Expand serviceoffer-controller RBAC to manage ReferenceGrants.

Tests and flows

  • Updated unit tests for:
    • verifier proxy path
    • route rendering
    • dynamic route derivation
  • Updated integration expectations to the shared-gateway model.
  • Updated flow scripts to stop asserting per-offer middleware resources.
  • Re-ran flow-11 successfully end-to-end.

Docs

  • Updated monetization and getting-started docs to the shared seller-gateway model.
  • Kept the detailed run report in this PR description rather than committing it into the repository.

What this does not fix

  • Image pinning by digest. This PR keeps the lint contract but does not produce actual digests.
  • Some CLI/UX findings from production-readiness testing such as wallet surfacing and model token accessor. Those should land as separate follow-ups.
  • The x402 BDD bootstrap host-port assumption. That integration path still assumes host port 8080 during bootstrap.

Validation

Unit tests

  • go test ./internal/x402
  • go test ./internal/serviceoffercontroller

Integration compile / smoke

  • go test -tags integration ./internal/openclaw -run TestDoesNotExist

Flow tests

  • flows/flow-11-dual-stack.sh
    • passed 41/41
    • run with high-port overrides to avoid occupied low ports on the local machine

Known local environment limitation

  • go test -tags integration -v -run TestBDDIntegration -timeout 20m ./internal/x402
    • blocked locally by an existing listener on host port 8080
    • I did not kill that unrelated process

Full report

Successful Flow-11 artifacts

Seller

  • Seller wallet: 0xC0De030F6C37f490594F93fB99e2756703c4297E
  • Published tunnel URL:
    • https://ten-municipal-mortgages-offerings.trycloudflare.com
  • Registered agent ID:
    • 5003

Buyer

  • Buyer discovery wallet:
    • 0x57b0eF875DeB5A37301F1640E469a2129Da9490E
  • Bob remote-signer wallet:
    • 0x5D01290Fd77EbD7a82bD316dC2d762C30B07D107
  • Purchased alias:
    • paid/qwen3.5:9b

Seller registration receipts

1. Identity registration

  • Tx hash:
    • 0x32eef92ede6779a3d05e780bf0920f4744b22ec6e85eb81d4d83371644d751b9
  • Block:
    • 40331698
  • Chain:
    • Base Sepolia (84532)
  • Function:
    • register(string)
  • Registry:
    • 0x8004A818BFB912233c491871b3d84c89A494BD9e
  • Sender / owner:
    • 0xC0De030F6C37f490594F93fB99e2756703c4297E
  • Agent ID:
    • 5003
  • Registered URI:
    • https://ten-municipal-mortgages-offerings.trycloudflare.com/.well-known/agent-registration.json

Observed receipt facts:

  • status: 1
  • gas used: 183900
  • emitted Registered(agentId=5003, agentURI=..., owner=seller)
  • emitted MetadataSet(agentWallet=0xC0De...)

2. x402 metadata write

  • Tx hash:
    • 0xab815b78ab688697ef1e39f479bf83d57b53c7afb26c00c85775105d860b1df8
  • Block:
    • 40331700
  • Function:
    • setMetadata(uint256,string,bytes)
  • Metadata key:
    • x402
  • Metadata value:
    • {"x402":true}

Observed receipt facts:

  • status: 1
  • gas used: 57552
  • sender: 0xC0De030F6C37f490594F93fB99e2756703c4297E
  • target: 0x8004A818BFB912233c491871b3d84c89A494BD9e

Buyer settlement receipt

3. x402 settlement transfer

  • Tx hash:
    • 0x847de0118110b4f056c025b9804cd82f87274a2b95926419c34ccfc0eb1216a2
  • Block:
    • 40331811
  • Function:
    • transferWithAuthorization(address,address,uint256,uint256,uint256,bytes32,bytes)
  • Chain:
    • Base Sepolia (84532)
  • Token:
    • USDC 0x036CbD53842c5426634e7929541eC2318f3dCF7e
  • Facilitator settlement sender:
    • 0xd744494E28b01073514EBC89987B305001ed257A
  • Buyer signer:
    • 0x5D01290Fd77EbD7a82bD316dC2d762C30B07D107
  • Seller payTo:
    • 0xC0De030F6C37f490594F93fB99e2756703c4297E
  • Settled amount:
    • 1000 micro-USDC (0.001 USDC)

Observed receipt facts:

  • status: 1
  • gas used: 86144
  • AuthorizationUsed emitted by USDC
  • Transfer emitted from buyer signer to seller for 1000

Supported x402 chain recipes in this repo

Chain CAIP-2 USDC
base eip155:8453 0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913
base-sepolia eip155:84532 0x036CbD53842c5426634e7929541eC2318f3dCF7e
ethereum eip155:1 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
polygon eip155:137 0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359
polygon-amoy eip155:80002 0x41E94Eb019C0762f9Bfcf9Fb1E58725BfB0e7582
avalanche eip155:43114 0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E
avalanche-fuji eip155:43113 0x5425890298aed601595a70AB815c96711a31Bc65
arbitrum-one eip155:42161 0xaf88d065e77c8cC2239327C5EDb3A432268e5831
arbitrum-sepolia eip155:421614 0x75faf114eafb1BDbe2F0316DF893fd58CE46AA4d

Public repo note

This PR body intentionally includes only public or operationally non-secret information:

  • public chain addresses
  • public transaction hashes
  • public contract addresses
  • ephemeral public tunnel URL from the successful test run

It does not include any private keys, tokens, or .env secrets.

bussyjd and others added 4 commits April 17, 2026 17:31
…a 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.
…imeout 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.
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.
@bussyjd bussyjd changed the title x402 pre-merge follow-up: unsettled metric, verifyOnly warning, image-pin lint x402 pre-merge follow-up: seller gateway, unsettled metric, verifyOnly warning Apr 17, 2026
@bussyjd bussyjd force-pushed the feat/x402-pre-merge-followup branch from a5eb379 to f56ed02 Compare April 17, 2026 13:39
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