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.
Configuration
Section titled “Configuration”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.
Local webhook forwarding
Section titled “Local webhook forwarding”The Stripe CLI forwards events to the local control plane and prints the signing
secret to put in .env:
cd nexus && just stripe-webhookThis 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.
Money flow
Section titled “Money flow”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)]- 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 apendingworkspace_wallet_topupsrow, and returns the intent’sclient_secret. - The console mounts Stripe’s Payment Element with that
client_secret(using the publishable key fromGET /api/payments/config) and confirms the payment. - Stripe delivers a signed
payment_intent.succeededwebhook. The control plane verifies the signature, records the event once (unique on(provider, provider_event_id)), and credits the wallet with atopuptransaction for the recorded top-up amount, keyed on the event id. - 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.
Operator grants
Section titled “Operator grants”An authenticated platform administrator can issue operator-funded workspace credit through the admin control plane:
POST /api/workspaces/{workspace_id}/wallet/grantsContent-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.
Refunds
Section titled “Refunds”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.
Reconciliation
Section titled “Reconciliation”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.
CSV export
Section titled “CSV export”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.
Tax boundary
Section titled “Tax boundary”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
Section titled “Auto-refill”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:
# The platform default every workspace without its own policy follows.nexus ops wallet auto-refill default --state on --allowance-usd 100 --period 1monexus ops wallet auto-refill shownexus 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 7dnexus 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.
