diff --git a/internal/embed/infrastructure/base/templates/x402.yaml b/internal/embed/infrastructure/base/templates/x402.yaml index 01e2d2a4f..91b03b30e 100644 --- a/internal/embed/infrastructure/base/templates/x402.yaml +++ b/internal/embed/infrastructure/base/templates/x402.yaml @@ -20,6 +20,11 @@ metadata: --- # Static gateway settings plus optional manual routes. In cluster mode the # gateway merges these with dynamic routes derived from ready ServiceOffers. +# +# authCaptureUnlock is rendered from values so it SURVIVES `obol stack up`. +# Hand-patching this ConfigMap works until the next sync, which restores the +# chart default and silently disables the fee split — that has already +# happened once on a live stack. apiVersion: v1 kind: ConfigMap metadata: @@ -29,10 +34,34 @@ data: pricing.yaml: | wallet: "" chain: "base" - facilitatorURL: "https://x402.gcp.obol.tech" + facilitatorURL: "{{ .Values.x402.facilitatorURL | default "https://x402.gcp.obol.tech" }}" # verifyOnly applies to the legacy /verify endpoint only. The shared seller # gateway path always settles after upstream success. verifyOnly: true + {{- with .Values.x402.authCaptureUnlock }} + {{- if .enabled }} + # Paid unlock gate. The offer named by offerPrefix must already expose a + # `gate: auth` route (its free SIWX sign-in is suppressed); buyers mint a + # session by paying once instead. minFeeBps/maxFeeBps bound the cut the + # buyer signs; the remainder goes to payTo. + authCaptureUnlock: + enabled: true + offerPrefix: {{ .offerPrefix | quote }} + price: {{ .price | quote }} + network: {{ .network | quote }} + payTo: {{ .payTo | quote }} + feeRecipient: {{ .feeRecipient | quote }} + minFeeBps: {{ .minFeeBps }} + maxFeeBps: {{ .maxFeeBps }} + captureAuthorizer: {{ .captureAuthorizer | quote }} + {{- with .captureDeadlineSecs }} + captureDeadlineSecs: {{ . }} + {{- end }} + {{- with .refundDeadlineSecs }} + refundDeadlineSecs: {{ . }} + {{- end }} + {{- end }} + {{- end }} routes: [] --- diff --git a/internal/embed/infrastructure/base/values.yaml b/internal/embed/infrastructure/base/values.yaml new file mode 100644 index 000000000..941070fb9 --- /dev/null +++ b/internal/embed/infrastructure/base/values.yaml @@ -0,0 +1,40 @@ +# Defaults for the base chart. The helmfile passes dataDir and network +# explicitly; everything below is what a stack gets when it says nothing. + +x402: + # Facilitator the verifier settles against. + # + # The hosted facilitator supports the `auth-capture` scheme on Base Sepolia + # ONLY (eip155:84532). To run the paid unlock gate on Base MAINNET, point + # this at the in-cluster facilitator sidecar instead: + # facilitatorURL: "http://localhost:8090" + facilitatorURL: "https://x402.gcp.obol.tech" + + # Paid unlock gate (opt-in, off by default — no fee is taken unless you + # configure this). Turns ONE `gate: auth` offer from free wallet sign-in + # into pay-once-to-mint-a-session, and splits that single payment on-chain + # between payTo and feeRecipient via AuthCaptureEscrow. + # + # Only one unlock offer per stack today; per-offer configuration via the + # ServiceOffer CRD is the follow-up. Set offerPrefix to the offer's public + # path — it must match the route's stripPrefix exactly. + authCaptureUnlock: + enabled: false + offerPrefix: "" + price: "" + network: "" + # Seller leg. Falls back to the verifier's own wallet when empty. + payTo: "" + # Fee leg. Required when enabled, and must be a non-zero address + # whenever maxFeeBps > 0. + feeRecipient: "" + # Bounds on the cut, in basis points, that the buyer signs. + # 50 = 0.5%. maxFeeBps must be <= 10000 and >= minFeeBps. + minFeeBps: 0 + maxFeeBps: 0 + # Address permitted to capture the escrowed authorization. Required + # when enabled. + captureAuthorizer: "" + # Optional; the verifier applies its own defaults when these are unset. + captureDeadlineSecs: null + refundDeadlineSecs: null diff --git a/internal/embed/infrastructure/helmfile.yaml b/internal/embed/infrastructure/helmfile.yaml index 98fdff4f4..ab9d1a415 100644 --- a/internal/embed/infrastructure/helmfile.yaml +++ b/internal/embed/infrastructure/helmfile.yaml @@ -23,6 +23,26 @@ values: # quick tunnel that should be preserved across the sync. - cloudflared: enabled: true + # x402 gateway settings. authCaptureUnlock is the opt-in paid unlock gate; + # off by default, so no fee is taken and the verifier's pricing config is + # byte-identical to before unless a stack configures it. Override with a + # state-values file or e.g. + # --state-values-set x402.authCaptureUnlock.enabled=true + # NOTE: the hosted facilitator only supports auth-capture on Base Sepolia. + # For Base mainnet set x402.facilitatorURL to the in-cluster sidecar + # (http://localhost:8090). + - x402: + facilitatorURL: "https://x402.gcp.obol.tech" + authCaptureUnlock: + enabled: false + offerPrefix: "" + price: "" + network: "" + payTo: "" + feeRecipient: "" + minFeeBps: 0 + maxFeeBps: 0 + captureAuthorizer: "" releases: # Monitoring stack (Prometheus operator + Prometheus). Must run before @@ -130,6 +150,10 @@ releases: values: - dataDir: /data - network: "{{ .Values.network }}" + # Passed through so the paid unlock gate is declarative and survives a + # re-sync. Off unless the stack sets it — see base/values.yaml. + - x402: + {{- toYaml .Values.x402 | nindent 10 }} # Cloudflare Tunnel (dormant until configured via obol tunnel login/provision). # `condition: cloudflared.enabled` lets `obol stack up` flip this off when an diff --git a/internal/embed/skills/sell/references/x402-pricing.md b/internal/embed/skills/sell/references/x402-pricing.md index 21fb82c3c..635f1e352 100644 --- a/internal/embed/skills/sell/references/x402-pricing.md +++ b/internal/embed/skills/sell/references/x402-pricing.md @@ -100,3 +100,38 @@ Client ## Important This Traefik `ForwardAuth` flow is a gating step, not the final settlement point for the supported production path. Final settlement belongs in a component that can observe the upstream result, such as `x402-buyer` or the standalone `obol sell inference` gateway. + +## Paid unlock gate (`authCaptureUnlock`) + +Opt-in, and **off by default** — no fee is taken unless you configure it. + +It changes **one** offer from *free wallet sign-in* to *pay once to sign in*. Normally a `gate: auth` route lets a buyer sign in with SIWX for free and then use the route on a session. With the unlock enabled, those free sign-in endpoints are suppressed and the only way to mint a session is a single inline payment on the first request. Everything after that is session-authenticated, not paid again. + +That one payment is split on-chain via `AuthCaptureEscrow`: a buyer-signed, bounded percentage (`minFeeBps`–`maxFeeBps`) goes to `feeRecipient`, the remainder to `payTo`. + +It is **not** a percentage cut of every payment your stack takes, and it cannot route a share to an upstream provider — the split has exactly two legs and fires only on the unlock. + +Configure it through chart values so it survives `obol stack up`. Hand-patching the `x402-pricing` ConfigMap works until the next sync, which restores the chart default and silently disables the fee: + +```yaml +x402: + facilitatorURL: "http://localhost:8090" + authCaptureUnlock: + enabled: true + offerPrefix: "/services/my-offer" # must equal the route's stripPrefix + price: "0.01" + network: "base" + payTo: "0x..." # seller; defaults to the verifier wallet + feeRecipient: "0x..." # required when enabled + minFeeBps: 50 # 50 = 0.5%; min <= max, max <= 10000 + maxFeeBps: 50 + captureAuthorizer: "0x..." # required when enabled +``` + +> **Base mainnet needs the in-cluster facilitator.** The hosted facilitator at +> `https://x402.gcp.obol.tech` advertises `auth-capture` on **Base Sepolia only** +> (`eip155:84532`) — check `/supported` before assuming otherwise. For mainnet, +> point `facilitatorURL` at the facilitator sidecar (`http://localhost:8090`), +> which carries the `v2-eip155-auth-capture` scheme for `eip155:8453`. + +Two current limits: only **one** unlock offer per stack (`offerPrefix` is global, matched by exact string comparison), and a `ServiceOffer` cannot declare `scheme: auth-capture` — its CRD enum permits `exact` only. Per-offer configuration is the intended follow-up.