') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ', 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); })(); feat(x402): add MPP and Stripe compatibility by bussyjd · Pull Request #651 · ObolNetwork/obol-stack · GitHub
Skip to content

feat(x402): add MPP and Stripe compatibility - #651

Open
bussyjd wants to merge 1 commit into
mainfrom
compat/mpp-payments-on-649
Open

feat(x402): add MPP and Stripe compatibility#651
bussyjd wants to merge 1 commit into
mainfrom
compat/mpp-payments-on-649

Conversation

@bussyjd

@bussyjdbussyjd commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Replaces the MPP/Stripe PR stack with one PR against main.
  • Adds MPP HTTP-auth compatibility to the x402 verifier while preserving existing PAYMENT-REQUIRED, X-PAYMENT, and X-PAYMENT-RESPONSE clients.
  • Ships Stripe SPT/card as the production card path and adds Tempo MPP as pull-only with preflight-before-proxy and broadcast-after-upstream-success semantics.
  • Keeps this PR scoped to MPP/Stripe verifier compatibility; Hermes image changes are not included.

Stripe SPT Flow

sequenceDiagram
participant Client as Hermes buyer
participant Verifier as x402 verifier
participant StripeCLI as Stripe Link CLI
participant StripeAPI as Stripe API
participant Upstream as Paid service
Client->>Verifier: Request protected route
Verifier-->>Client: 402 with PAYMENT-REQUIRED and WWW-Authenticate method=stripe
Client->>StripeCLI: Create Shared Payment Token from challenge
StripeCLI-->>Client: Shared Payment Token
Client->>Verifier: Retry with Authorization Payment or X-PAYMENT
Verifier->>StripeAPI: Authorize PaymentIntent with manual capture
Verifier->>Upstream: Proxy request
alt upstream success
Verifier->>StripeAPI: Capture PaymentIntent
Verifier-->>Client: Upstream response with payment receipt headers
else upstream failure
Verifier->>StripeAPI: Cancel authorization
Verifier-->>Client: Upstream error
end
Loading

Tempo MPP Flow

sequenceDiagram
participant Client as Hermes buyer or mppx
participant Verifier as x402 verifier
participant TempoRPC as Tempo RPC
participant Upstream as Paid service
Client->>Verifier: Request protected route
Verifier-->>Client: 402 with WWW-Authenticate method=tempo
Client->>Verifier: Retry with Authorization Payment signed pull credential
Verifier->>Verifier: Preflight challenge, request, signature, and replay guard
Verifier->>Upstream: Proxy request
alt upstream success
Verifier->>TempoRPC: Broadcast signed transaction and wait receipt
TempoRPC-->>Verifier: Verified receipt
Verifier-->>Client: Upstream response with payment receipt headers
else upstream failure
Verifier->>Verifier: Release replay reservation without broadcast
Verifier-->>Client: Upstream error
end
Loading

Compatibility Notes

  • Adds one WWW-Authenticate: Payment ... header per advertised rail using Header().Add.
  • Accepts Authorization: Payment ... alongside legacy X-PAYMENT.
  • Keeps Stripe public config on profileId / STRIPE_PROFILE_ID; the unreleased stripe-network-id compatibility alias is intentionally not included.
  • Uses Stripe wire networkId only inside the MPP challenge payload where Stripe clients expect it, derived from profileId.
  • Rejects Tempo push/hash credentials so the verifier does not charge before upstream success.
  • Does not advertise Tempo on streaming routes until a safe buffering or receipt path exists.

Coverage Follow-Up

  • Added card verifier dispatch coverage proving card routes use the card gateway and do not call the x402 facilitator.
  • Added table coverage for Stripe USD minimum validation, including malformed prices; malformed USD card prices now return validation errors instead of panicking.
  • Added MPP tests for dual WWW-Authenticate rail headers, Authorization: Payment method parsing, Tempo HandleProxy dispatch, settle-failure release, bad transaction credentials, and real gateway release/settle error branches.

Validation

  • go test -coverprofile=/tmp/obol-mpp-coverage-after.out ./cmd/obol ./internal/monetizeapi ./internal/schemas ./internal/serviceoffercontroller ./internal/x402 -count=1
  • go test -race ./cmd/obol ./internal/monetizeapi ./internal/schemas ./internal/serviceoffercontroller ./internal/x402 -count=1
  • git diff --check
  • Exact conflict-marker search returned no matches.
  • Deprecated Stripe alias scan returned no matches for stripe-network-id, STRIPE_NETWORK_ID, or Deprecated alias.

Coverage highlights after the follow-up:

  • internal/x402: 73.5%
  • validateStripeCardMinimum: 90.9%
  • addMPPAuthenticateHeaders: 88.9%
  • mppAuthorizationMethod: 100.0%
  • serveTempoMPPGated: 88.5%
  • HandleProxy: 79.4%

Supersedes

Base automatically changed from chore/hermes-agent-stripe-image to integration/v0.11.0June 17, 2026 12:08
@bussyjd
bussyjdforce-pushed the compat/mpp-payments-on-649 branch from 6b3be1f to 1cc3dd8CompareJune 17, 2026 12:17
@bussyjdbussyjd changed the title Add MPP payment compatibilityfeat(x402): add Hermes MPP and Stripe compatibilityJun 17, 2026
@bussyjd
bussyjd changed the base branch from integration/v0.11.0 to mainJune 17, 2026 12:17
@bussyjd
bussyjd requested a review from OisinKyneJune 17, 2026 12:20
@bussyjd
bussyjdforce-pushed the compat/mpp-payments-on-649 branch from 1cc3dd8 to bb0da87CompareJune 17, 2026 12:31
@bussyjdbussyjd changed the title feat(x402): add Hermes MPP and Stripe compatibilityfeat(x402): add MPP and Stripe compatibilityJun 17, 2026
@bussyjd
bussyjdforce-pushed the compat/mpp-payments-on-649 branch from bb0da87 to 9a88f54CompareJune 17, 2026 12:47
Sign up for freeto 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

@bussyjd