Ir al contenido
↑↓Navigate↵SelectescClose

Managed Pass-Through Billing (Payment Provider)

How operator-managed pass-through billing funds workspace wallets via Stripe

Esta página aún no está disponible en tu idioma.

This document describes how operator-managed pass-through billing (PTB) takes real money through a payment provider to fund workspace wallets. It is only needed when an operator runs managed pass-through billing; self-hosted bring-your-own-key deployments do not use any of it.

The payment provider is Stripe. Operators may run it in test mode for evaluation.

Set these in the .env next to the justfile at the repository root (the justfile loads it for the compose stack):

Variable Purpose
NEXUS_STRIPE_API_KEY Stripe secret key (sk_test_... in test mode). Server-side only.
NEXUS_STRIPE_WEBHOOK_SECRET Webhook signing secret (whsec_...) used to verify webhook authenticity.
NEXUS_STRIPE_PUBLISHABLE_KEY Stripe publishable key (pk_test_...). Served to the console at GET /api/payments/config so the billing page can mount Stripe.js. Browser-safe.

When NEXUS_STRIPE_API_KEY is unset, the control plane disables top-ups and the managed billing surface stays inactive; BYOK traffic is unaffected.

The Stripe CLI forwards events to the local control plane and prints the signing secret to put in .env:

Terminal window
cd nexus && just stripe-webhook

This runs stripe listen --forward-to localhost:14451/api/payments/webhooks/stripe and warns if the printed secret has drifted from NEXUS_STRIPE_WEBHOOK_SECRET.

In test mode, complete the Payment Element with card 4242 4242 4242 4242, any future expiry, and any CVC.

flowchart LR
client[Payment client] -->|"POST wallet/topups"| cp[control-plane]
cp -->|"create PaymentIntent"| stripe[Stripe]
stripe -->|"client_secret"| client
client -->|"confirm intent"| stripe
stripe -->|"payment_intent.succeeded webhook"| cp
cp -->|"credit wallet once"| ledger[(workspace_wallet_transactions)]
  1. A workspace manager opens the billing page and clicks Add to credit balance, enters an amount, and the console calls POST .../wallet/topups. The control plane creates a Stripe payment intent tagged with the workspace id, records a pending workspace_wallet_topups row, and returns the intent’s client_secret.
  2. The console mounts Stripe’s Payment Element with that client_secret (using the publishable key from GET /api/payments/config) and confirms the payment.
  3. Stripe delivers a signed payment_intent.succeeded webhook. The control plane verifies the signature, records the event once (unique on (provider, provider_event_id)), and credits the wallet with a topup transaction for the recorded top-up amount, keyed on the event id.
  4. A duplicate webhook delivery is dropped once the event is processed, so the wallet is credited exactly once.

Every money-moving action emits an audit event: wallet.topup.succeeded, wallet.refund.created, and wallet.reconciliation.mismatch.

An authenticated platform administrator can issue operator-funded workspace credit through the admin control plane:

POST /api/workspaces/{workspace_id}/wallet/grants
Content-Type: application/json
{
"amount_usd": "100.00",
"reason": "Pilot credit",
"idempotency_key": "pilot-credit-001"
}

The amount must be positive, and the reason and idempotency key are required. The operation increases the workspace wallet balance, records a grant transaction, and emits wallet.grant.created with the authenticated admin as the actor. Repeating the same idempotency key for the workspace returns the original transaction without adding credit again. Unlike a top-up, a grant does not collect a payment; it authorizes operator-funded PTB usage.

A manager can refund a settled payment via POST .../wallet/refunds with the payment intent id and amount. The control plane issues the Stripe refund and records a refund transaction (idempotent on the refund id) plus an audit event.

reconcile_workspace_topups compares settled top-ups against wallet credit transactions and emits wallet.reconciliation.mismatch for any settled top-up without a matching credit. The gateway separately reconciles stale escrow reservations.

GET .../wallet/transactions/export.csv streams the wallet ledger as CSV for tenant audit. The console exposes this as an export button on the billing page.

nexus is not the system of record for tax. When the payment provider calculates tax, the provider’s receipt/invoice is canonical; nexus stores a minimal receipt reference in workspace_billing_receipts. The CSV export is the tenant-facing audit trail for wallet movements. Multi-currency wallets, provider-issued invoices, and automated tax computation are out of scope for the current implementation.

Auto-refill is off unless the deployment sets NEXUS_WALLET_AUTO_REFILL=1. It is meant for self-hosted deployments that fund each person’s usage, not for deployments where users pay. Set it the same on every process that reads it: the gateway, which applies resets when it reserves wallet funds; the control-plane, which serves the wallet API the console reads; the auth service, which creates personal workspaces at sign-up; and wherever operators run nexus ops wallet auto-refill. The compose stack passes NEXUS_WALLET_AUTO_REFILL from .env to all three services. While it is off, stored policies are ignored, setting a policy is refused (removing one still works), sign-up is unchanged, and the wallet API reports no auto_refill, so the console shows nothing about it.

Auto-refill is platform-funded credit, like a grant; it never charges a saved payment method. A policy is an allowance and a period: whether it is on, an amount in USD, and how often it resets, written as a LiteLLM-style duration (30s, 30m, 24h, 7d, 1mo; mo is a calendar month). When a platform-paid request reserves funds and a full period has passed since the workspace’s last reset (or it has never had one), the reservation first tops the available balance back up to the allowance and records an auto_refill transaction carrying the request id. Reading the wallet through the wallet API, as the console’s Billing page does, applies the same due reset without a request id, so the topped-up balance shows before the next request. Unspent credit does not accumulate past the allowance. A balance already above it, for example after an operator grant, is left alone; the reset is still recorded with amount 0 so the period starts and spending down later in it does not top up again. A reset made for a reservation that is then refused is rolled back with it.

Platform operators own the policy; workspace managers cannot change it, and the wallet API returns it read-only as auto_refill, with source set to platform_default or workspace:

Terminal window
# The platform default every workspace without its own policy follows.
nexus ops wallet auto-refill default --state on --allowance-usd 100 --period 1mo
nexus ops wallet auto-refill show
nexus ops wallet auto-refill clear-default
# One workspace's own policy, replacing the default as a whole.
nexus ops wallet auto-refill set --workspace ws_... --state on --allowance-usd 50 --period 7d
nexus ops wallet auto-refill show --workspace ws_...
nexus ops wallet auto-refill clear --workspace ws_...

Each change records wallet.auto_refill.default_updated or wallet.auto_refill.workspace_updated. While the platform default is on, a personal workspace created at sign-up has Wallet turned on for its workspace and organizations, so its users can spend without an operator switching each one on. Existing workspaces keep their Wallet setting; use nexus ops wallet set for those.

The workspace_payment_methods table holds saved-method references, but no capture flow is implemented against them.