Credits and billing
Credits and billing
Optional and off by default — the metering half of BenchPress, covering leases, balances, the ledger, admin adjustments, and the optional Razorpay handoff.
Optional, and off by default. enable_credits ships as 0, and nothing
on this page runs until you change it.
Who this is for. Somebody running BenchPress for a team, who needs a bench
to stop by itself rather than run for a week.
Before you start. Read this line before the rest of the page. BenchPress
is a dev-environment and onboarding tool, not a hosting platform. If you are
the only person on the box, or if everyone using it is trusted to stop their
own benches, skip this page and
Admission and limits. Neither is part
of the self-hosted path.
What switching it on changes
With enable_credits = 1:
- Every user gets a
Credit Account, opened with the signup grant. - A deploy buys a lease — a fixed window of time — and is charged for it.
- A bench stops when its lease expires.
- Concurrency is capped. See
Admission and limits. - The Credits page appears in the app, and the sidebar grows a balance
meter.
With enable_credits = 0, none of it runs. Every function in the credits
package checks the switch first, so there are no accounts, no ledger rows and
no extra queries. /frontend/credits redirects back to Labs.
Do not switch this on to try it and leave it on. Every user without a
Credit Account and a balance is refused a deploy from the next request, and
that is an outage for everybody else on the host.
Steps
-
Read the current state before changing anything.
bench --site <site> execute frappe.client.get_value \ --kwargs "{'doctype':'BenchPress Settings','fieldname':'enable_credits'}" -
Price time. Open
/app/lease-planin Desk. A plan is a duration and a
price, and one of them is the default.Screenshot: The Lease Plan list view in Frappe Desk, reading 8 of 8, in five columns: ID, Plan Label, Minutes, Credits and Is Active. The rows run 5 minutes at 5 minutes for 1 credit, through 1 week at 10080 minutes for 700 credits, and every Is Active box is ticked.
Eight plans ship, and all eight are active on this host:
Plan Minutes Credits 5 minutes 5 1 30 minutes 30 5 2 hours 120 18 8 hours 480 60 1 day 1,440 150 2 days 2,880 260 4 days 5,760 460 1 week 10,080 700 default_lease_planis 30 minutes — the plan a deploy buys when the
caller names none. The price a user actually pays is the plan's credits
times theInstance Size.price_multiplier, so a larger bench costs more
for the same wall-clock time. -
Price credits, if you are selling them. Open
/app/credit-pack.Pack Price Credits Highlighted Starter ₹499 200 no Regular ₹1,999 1,000 yes Heavy ₹6,999 4,000 no -
Set the grant.
Credit Settings.signup_grant_creditsis40— posted
once, ever, when an account is opened. Forty credits buys eight 30-minute
leases at the Small size. -
Switch it on. Set
BenchPress Settings.enable_creditsto1, in Desk
or in the app's Settings dialog. -
Switch it back off the moment you have finished evaluating, unless you
meant to keep it.
Verify
Read the flag back in a fresh process. A Single is cached, and the form you
just saved is not evidence.
bench --site <site> execute frappe.client.get_value \
--kwargs "{'doctype':'BenchPress Settings','fieldname':'enable_credits'}"
Then confirm the app agrees: with credits on, /frontend/credits renders and
the sidebar shows a meter. With credits off, that route redirects to Labs.
The accounting model
There is no meter and no accrual. A balance is a stored number, and the live
check is a read rather than a sum over the ledger.
| Entry type | Written when |
|---|---|
Grant |
an account is opened, or an operator gives credits away |
Purchase |
a payment settles |
Usage |
a lease is bought or renewed |
Refund |
a purchase is reversed |
Adjustment |
an operator corrects a balance by hand |
Four properties are worth an operator's attention.
- Every mutation runs under a row lock on the account. Two parallel
deploys cannot both read the same balance, so there is no double-spend. - Spendable is not the same as the balance. Credits an admitted deploy is
already holding are committed. A renewal that spent them would overdraw the
account the moment that deploy reachedRunning, so the guard compares
against the unreserved figure. - Amounts carry six decimal places. A price multiplier can put a lease at
7.333333 credits, and rounding at every debit would leak the difference. has_purchasedreads thePurchaserow, not the balance. A refund
clears the number, not the fact that the account paid.
This host holds 21 credit accounts and 109 ledger entries, of which 18
accounts have a zero balance. That distribution is the reason the warning
above matters: switching the flag on here would refuse almost everyone.
Moving a balance by hand
An adjustment is the one entry type no rule produced, so a year later it is
the only record of why a balance is what it is. It requires a reason and
refuses without one.
bench --site <site> execute benchpress.credits.account.adjust \
--kwargs "{'user':'[email protected]','credits':100,'reason':'Onboarding top-up, ticket 412'}"
Pass a negative number to take credits away. The call throws if credits are
off.
Taking money
The payment gateway is an optional app. razorpay_frappe is deliberately
absent from required_apps and must stay absent — nobody running BenchPress
on their own hardware should have to install a payment processor to use it.
Every entry point checks first and refuses with a sentence rather than an
import error.
Three properties define the design:
- Settlement is once-ever. Razorpay retries webhooks,
on_updatefires on
every save, and an operator pressing Sync Status in Desk is a third
delivery of the same payment. The guard checks the order reference under the
same row lock that applies the credit. - The amount is never taken from the caller.
razorpay_frappeexposes an
open endpoint on which any logged-in user names their own amount, so a paid
order is not evidence of what was bought. Settlement re-reads the price from
theCredit Packthe order points at and credits nothing unless the rupees
paid match it. - Orders only. No payment links, no subscriptions, no auto-renewal, no
dunning, no proration. Money buys credits, and credits buy time, so somebody
who wants longer renews a lease rather than holding a plan.
On this host Razorpay Settings.key_id is empty, so a purchase reaches the
gateway handoff and stops there with a missing-password error. Nothing is
charged. That is the correct state for a host that is not selling anything.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Every user is suddenly refused a deploy | Credits were switched on and most accounts hold nothing | Set enable_credits back to 0, or grant balances |
| The Credits page is missing | Credits are off | Expected. /frontend/credits redirects to Labs |
| A refusal names less than the balance shows | Credits are reserved against a running deploy | Stop an instance, or top the account up |
| A purchase fails at the gateway | No gateway keys on this site | Expected with key_id empty. Nothing was charged |
| A deploy is charged twice | It is not — settlement and charging are both once-ever under a row lock | Read the ledger. Two Usage rows mean two leases |
| A bench outlived its lease | The sweep that stops expired leases is not running | See Diagnostics |
adjust throws An adjustment needs a reason |
The reason was empty | Pass one. It is the whole value of the row |
Reference
Measured on this host.
| Setting | Value | Meaning |
|---|---|---|
enable_credits |
0 |
the master switch |
signup_grant_credits |
40 |
posted once, ever |
custom_build_credits |
40 |
flat charge for a custom image build |
low_balance_warn_percent |
20 |
warn below this share of the balance |
default_lease_plan |
30 minutes |
bought when the caller names no plan |
reap_after_days |
7 |
days a stopped instance is kept |
lease_sweep_batch |
200 |
expired leases one sweep may claim |
lease_reclaim_seconds |
60 |
before an unfinished stop is reclaimed |
lease_max_attempts |
0, falling back to 5 |
failed stops before a lease parks |
| Schedule | Job |
|---|---|
| Every 5 minutes | benchpress.credits.sweep.enforce_limits |
| Every 5 minutes | benchpress.credits.drain.sweep_expired_leases |
| Every 5 minutes | benchpress.credits.admission_repair.reconcile_admissions |
| Daily | benchpress.credits.reaper.reap_stopped_instances |
Related
- Admission and limits — the caps this switch engages.
- Self-serve signup — the third optional page, and the one that hands out grants.
- Leases and credits — the same machinery, from a user's side.
- Settings reference — every field above, in one table.