Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 30 additions & 1 deletion internal/embed/infrastructure/base/templates/x402.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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: []

---
Expand Down
40 changes: 40 additions & 0 deletions internal/embed/infrastructure/base/values.yaml
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions internal/embed/infrastructure/helmfile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
35 changes: 35 additions & 0 deletions internal/embed/skills/sell/references/x402-pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Loading