Billing setup
July 12, 2026 · View on GitHub
Plutus runs fully offline until you add a Stripe key. This is the end-to-end flow to take a real (test-mode) payment and watch prepaid credit top up.
0. Prerequisites
pip install "plutus-agent[stripe]"
plutus init --org "Your Co" --tier pro
Grab your test-mode keys from the Stripe dashboard (Developers → API keys).
Always start in test mode (sk_test_…).
1. Point Plutus at Stripe
export STRIPE_SECRET_KEY=sk_test_xxx
export STRIPE_PUBLISHABLE_KEY=pk_test_xxx
export STRIPE_WEBHOOK_SECRET=whsec_xxx # from step 3
Or put them in ~/.plutus/config.yaml under billing: (env wins).
2. Create the Pro plan
plutus stripe-setup
This creates a Plutus Pro product + a $20/mo recurring price (idempotent
via the plutus_pro_monthly lookup key) and writes the price id into your
config. Credit top-ups are priced dynamically per checkout, so nothing else to
create.
3. Run the server + forward webhooks
plutus serve # dashboard at http://localhost:8420
# in another terminal (Stripe CLI):
stripe listen --forward-to localhost:8420/webhook/stripe
stripe listen prints a whsec_… — that's your STRIPE_WEBHOOK_SECRET for
step 1. Restart plutus serve after setting it.
4. Take a payment
On the dashboard's Billing panel: enter an amount → Top up → → pay with
Stripe's test card 4242 4242 4242 4242 (any future expiry / CVC). Or fire a
synthetic event:
stripe trigger checkout.session.completed
On checkout.session.completed, Plutus credits the org's ledger and the
Credit balance card updates on the next 5-second refresh. "Upgrade to Pro"
moves the org to the pro tier; the Customer Portal button manages the
subscription.
5. Go live
Swap sk_test_…/pk_test_…/whsec_… for live keys, register a production
webhook endpoint in the Stripe dashboard pointing at
https://your-host/webhook/stripe, and re-run plutus stripe-setup once
against the live key to create the live price.
How it's safe
- Webhooks are signature-verified and recorded by event id — a replay never
double-credits (
stripe_eventstable). - The credit balance is the sum of an append-only ledger, so it's auditable and can't silently drift.
- No Stripe key → Checkout is simply disabled and everything else runs offline.
6. Savings-share billing (the Perseus value path)
The Pro subscription and prepaid credit above are the subscription half of the model. The other half bills a share of the money Perseus actually saved a customer — the differentiated revenue path.
How savings are recorded. Every metered event can carry a counterfactual: what the same call would have cost without Perseus (same token counts, priced at the customer's designated baseline model). Pass it at meter time:
# actual cost \$1.00; would have been \$4.00 without Perseus routing → \$3.00 saved
plutus meter --provider anthropic --model claude-haiku-4-5 \
--cost 1.00 --baseline 4.00
or over HTTP:
POST /v1/usage
{ "provider": "anthropic", "cost_usd": 1.00, "baseline_cost_usd": 4.00 }
The baseline is folded into the usage-event hash chain, so a billed saving
is as tamper-evident as the actual cost, and it's exported alongside cost
(baseline_usd column) so a customer can reconstruct every figure.
See what's owed (read-only):
plutus savings --period 2026-07
# verified savings : \$3.5000
# share rate : 18.0%
# ── billable share : \$0.6300
Raise the invoice (dry-run without --apply):
plutus bill-savings --period 2026-07 --apply
With --apply and a live Stripe key this creates a Stripe invoice for the share
and records it in savings_invoices (idempotent per org+period — a re-run is a
no-op). Without Stripe connected it records the period as pending so nothing is
lost.
The rules that keep the number honest (see docs/savings-share.md):
- Only events with a baseline count — never a blanket "you saved 40%".
- A per-event saving is
max(0, baseline − cost); it can never go negative. - Coverage (how many events carried a baseline) is always shown, so a thin-coverage period is visible, not hidden.
- The rate is
billing.savings_share_pct(default0.10); override per-run with--rate.