Skip to content
↑↓Navigate↵SelectescClose

Managed Pass-Through Billing (Payment Provider)

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

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.

See Threat Model rows T4 and I6 for the webhook-forgery and secret-key-leak mitigations covering this flow.

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 settings (trigger and amount) are stored per workspace but are not acted on: nothing charges a saved payment method when the balance crosses the trigger. The workspace_payment_methods table holds saved-method references, but no capture flow is implemented against them.