Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', '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('^' + ".*" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, '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" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, '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('^' + ".*" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, '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); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Closed
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
13 changes: 13 additions & 0 deletions .env.example
Original file line numberDiff line numberDiff line change
Expand Up@@ -11,3 +11,16 @@ ANTHROPIC_API_KEY=

# Required for TestIntegration_OpenAIInference
OPENAI_API_KEY=

# ── MPP credit-card payments (Stripe) ──────────────────────────────────────
# Seller-side credit-card settlement via the Machine Payments Protocol (MPP).
# Requires a Stripe account with "Machine payments" enabled. See the
# "Credit-card payments (MPP)" section of README.md.
#
# Consumed by the x402-verifier (sourced from the x402-secrets Secret in the
# `x402` namespace) to authorize/capture Stripe PaymentIntents for card offers.
STRIPE_SECRET_KEY=
# Your Stripe "machine payments" network id, advertised in the 402 challenge so
# card clients can mint a Shared Payment Token. Default for
# `obol sell http --pay-with card --stripe-network-id`.
STRIPE_NETWORK_ID=
54 changes: 54 additions & 0 deletions README.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -232,6 +232,60 @@ obol openclaw skills remove <name> # remove via openclaw CLI in pod

Skills are delivered via host-path PVC injection — no ConfigMap size limits, works before pod readiness, and survives pod restarts.

## Credit-card payments (MPP)

Alongside the default x402 on-chain (stablecoin) payment path, sellers can accept
**credit-card** payments via the [Machine Payments Protocol](https://mpp.dev) (MPP,
the Stripe + Tempo HTTP-402 standard). A card offer is gated on the same
`/services/<name>/*` route as a crypto offer — the payment method is selected per
offer.

```bash
# Expose an upstream as a card-paid endpoint (Stripe stripe.charge).
obol sell http my-api \
--pay-with card \
--stripe-account acct_1A2b3C4d \ # Stripe destination account (card analog of --pay-to)
--stripe-network-id stripenet_...\ # Stripe "machine payments" network id (or STRIPE_NETWORK_ID)
--card-currency usd \
--upstream my-svc --port 8080 --price 0.01
```

How it works:

- The offer advertises a `card` option in its `402` challenge (amount in the
currency's **minor units** — cents for `usd`, whole yen for `jpy`, etc.).
- A card-capable buyer presents a Stripe **Shared Payment Token** (`spt_…`) in the
`X-PAYMENT` header.
- The verifier **authorizes** a manual-capture Stripe PaymentIntent before serving,
proxies to the upstream, then **captures** only after a successful (`<400`)
response — a failed upstream **cancels** the hold, so a buyer is never charged for
nothing. Each SPT is single-use (replay-guarded).

### Requirements & configuration

- A **Stripe account with "Machine payments" enabled** (a gated Stripe feature).
- `STRIPE_SECRET_KEY` — used by the `x402-verifier` to authorize/capture
PaymentIntents. It is read from the `x402-secrets` Secret in the `x402` namespace;
populate it before taking card payments:

```bash
kubectl -n x402 patch secret x402-secrets --type merge \
-p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'
kubectl -n x402 rollout restart deploy/x402-verifier
```

- `STRIPE_NETWORK_ID` — your Stripe "machine payments" network id, advertised in the
402 challenge so clients can mint an SPT. It is a host/CLI value (default for
`--stripe-network-id`); add both to your `.env` from `.env.example`.

> **Note on scope.** Card offers are not ERC-8004 registered (no on-chain identity).
> The Stripe key is currently a single cluster-wide value in `x402-secrets`; a
> per-offer/per-namespace Secret is the production direction but is gated on widening
> the verifier's deliberately `resourceName`-scoped Secret RBAC. The SPT replay guard
> is per-pod (the verifier runs single-replica). The SPT is passed as the top-level
> Stripe form field `shared_payment_granted_token` per the `cp0x-org/mppx` reference —
> validate against your live Stripe account before relying on it in production.

## Public Access (Cloudflare Tunnel)

Expose your stack to the internet via Cloudflare Tunnel:
Expand Down
24 changes: 18 additions & 6 deletions cmd/obol/sell.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -126,13 +126,20 @@ func resolveCardPayment(cmd *cli.Command, price map[string]any) (map[string]any,
if !currencyRe.MatchString(currency) {
return nil, fmt.Errorf("invalid --card-currency %q: expected a 3-letter ISO-4217 code like usd", currency)
}
card := map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
}
// The Stripe "machine payments" network id is advertised in the 402
// challenge so MPP card clients can mint a Shared Payment Token. Defaults
// from the STRIPE_NETWORK_ID env var.
if networkID := strings.TrimSpace(cmd.String("stripe-network-id")); networkID != "" {
card["networkId"] = networkID
}
return map[string]any{
"method": payMethodCard,
"card": map[string]any{
"provider": "stripe",
"account": account,
"currency": currency,
},
"method": payMethodCard,
"card": card,
"maxTimeoutSeconds": cmd.Int("max-timeout"),
"price": price,
}, nil
Expand DownExpand Up@@ -647,6 +654,11 @@ Examples:
Usage: "ISO-4217 currency for card charges",
Value: "usd",
},
&cli.StringFlag{
Name: "stripe-network-id",
Usage: "Stripe \"machine payments\" network id advertised in the 402 challenge (so MPP card clients can mint a Shared Payment Token)",
Sources: cli.EnvVars("STRIPE_NETWORK_ID"),
},
&cli.StringFlag{
Name: "upstream",
Usage: "Upstream service name",
Expand Down
8 changes: 6 additions & 2 deletions cmd/obol/sell_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -262,7 +262,7 @@ func TestSellHTTP_Flags(t *testing.T) {
"namespace", "upstream", "port", "health-path", "path",
"max-timeout",
"register", "no-register", "register-name", "register-description", "register-image",
"pay-with", "stripe-account", "card-currency",
"pay-with", "stripe-account", "card-currency", "stripe-network-id",
)

assertStringDefault(t, flags, "chain", "base")
Expand DownExpand Up@@ -306,6 +306,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
&cli.StringFlag{Name: "pay-with", Value: payMethodCard},
&cli.StringFlag{Name: "stripe-account"},
&cli.StringFlag{Name: "card-currency", Value: "usd"},
&cli.StringFlag{Name: "stripe-network-id"},
&cli.IntFlag{Name: "max-timeout", Value: 300},
},
Action: func(_ context.Context, c *cli.Command) error {
Expand All@@ -320,7 +321,7 @@ func runCardResolve(t *testing.T, args ...string) (map[string]any, error) {
}

func TestResolveCardPayment_Valid(t *testing.T) {
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur")
out, err := runCardResolve(t, "--stripe-account", "acct_1A2b3C4d", "--card-currency", "eur", "--stripe-network-id", "stripenet_test")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
Expand All@@ -340,6 +341,9 @@ func TestResolveCardPayment_Valid(t *testing.T) {
if card["currency"] != "eur" {
t.Errorf("card.currency = %v, want eur", card["currency"])
}
if card["networkId"] != "stripenet_test" {
t.Errorf("card.networkId = %v, want stripenet_test", card["networkId"])
}
if _, ok := out["price"].(map[string]any); !ok {
t.Errorf("price block missing: %v", out["price"])
}
Expand Down
16 changes: 16 additions & 0 deletions internal/embed/infrastructure/base/templates/x402.yaml
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,12 @@ metadata:
type: Opaque
stringData:
WALLET_ADDRESS: ""
# Stripe secret key for MPP credit-card (stripe.charge) offers. Empty on a
# crypto-only stack; populate it (e.g. `kubectl -n x402 patch secret
# x402-secrets --type merge -p '{"stringData":{"STRIPE_SECRET_KEY":"sk_live_..."}}'`)
# to let the verifier authorize/capture card PaymentIntents. See README
# "Credit-card payments (MPP)".
STRIPE_SECRET_KEY: ""

---
apiVersion: v1
Expand DownExpand Up@@ -267,6 +273,16 @@ spec:
- --config=/config/pricing.yaml
- --listen=:8080
- --route-source=kube
env:
# MPP credit-card (Stripe) settlement key. Empty unless card offers
# are in use; optional=true keeps the verifier starting on
# crypto-only stacks where the key is unset.
- name: STRIPE_SECRET_KEY
valueFrom:
secretKeyRef:
name: x402-secrets
key: STRIPE_SECRET_KEY
optional: true
volumeMounts:
- name: pricing-config
mountPath: /config
Expand Down
Loading