Credits
Add usage-based credits to your SaaS with per-plan grants, recurring re-grants, custom top-ups, and automatic refills scoped to users or organizations.
A usage meter owned by @repo/payments, configured in pricingConfig (packages/config/src/pricing.ts) and gated by pricingConfig.credits.enabled (default true). pricingConfig is a separate named export from @repo/config, not a key on the config object. With credits.enabled: false, grants and the credits UI are suppressed; balances simply stay at 0.
Storage
Balances belong to a billing entity - user or organization, per config.tenancy.billingScope. Better Auth persists them through billingAdditionalFields (packages/config/src/billing-fields.ts), all input: false so only the server mutates them via the Billing(entityId) accessor.
| Field | Default | Purpose |
|---|---|---|
credits | 0 | Current balance. |
creditsLastGrantedAt | - | Timestamp of last recurring grant. |
autoTopUpEnabled | false | Per-entity auto top-up opt-in. |
autoTopUpThreshold | - | Balance that triggers a top-up. |
autoTopUpAmount | - | Credits bought per top-up. |
The Billing(...) accessor (packages/payments/src/billing.ts) exposes addCredits, removeCredits, setCredits, and a hasCredits getter.
Per-plan grants
Each plan in pricingConfig.plans may carry these credit fields (PricePlan in packages/config/src/types/index.ts):
| Key | Type | Default | Description |
|---|---|---|---|
credits | number | 0 | Credits the plan includes per cycle. |
creditInterval | number (days) | 0 | How often credits re-grant. Omit or 0 to disable recurring grants. |
grantCreditsOnTrial | boolean | unset = grant | During a trialDays trial, credits are granted unless set to false (the runtime check is !== false). |
The shipped plans grant free: 5, starter: 10, pro: 50, all on creditInterval: 30. Resolve them with getCreditsForPlan(planId) / getCreditIntervalForPlan(planId) (return 0 when absent).
grantCreditsOnTrial is opt-out: webhook handlers grant trial credits whenever plan.grantCreditsOnTrial !== false. Set it to false to withhold credits until the first paid invoice.
Recurring re-grants
The recurring-credits job (packages/api/src/functions/billing/recurring-credits.ts) runs daily on cron 0 0 * * *.
creditInterval > 0, then load active-plan entities for the billing scope.creditsLastGrantedAt + creditInterval against now; skip those not yet due.addCredits(...), update creditsLastGrantedAt, and write a CREDITS_ADDED audit entry.See Background jobs for the scheduler and Audit logs for the entries.
Custom purchases and auto top-up
The shipped config is credits: { enabled: true } with no customPurchase block, so custom purchases and auto top-up are off. Add pricingConfig.credits.customPurchase to let users buy arbitrary credits beyond their plan; these are the fields you set when you add it:
| Key | Type | Description |
|---|---|---|
enabled | boolean | Allow custom credit purchases. |
autoTopUp | boolean | Allow users to opt into automatic refills. |
minimumCredits | number | Floor per custom purchase. |
presetCredits | number | Suggested amount in the purchase UI. |
stripeProductId / polarProductId | string? | Metered line item per provider. |
After every removeCredits call, checkAndTriggerAutoTopUp runs. With no customPurchase block it returns early and nothing fires. Once you add the block, a top-up fires only when all hold: customPurchase.enabled and customPurchase.autoTopUp are on, the active provider is "stripe", the entity has set autoTopUpEnabled/autoTopUpThreshold/autoTopUpAmount, and the balance is below autoTopUpThreshold.
Auto top-up is Stripe-only. Under Polar, users can still buy credits manually, but checkAndTriggerAutoTopUp returns early and no automatic refill runs.
Notifications
When credits land, @repo/notifications fires the credits_added trigger (notifyCreditsAdded), creating an in-app notification linking to billing settings - gated by config.notifications.enabled (default true).
notifyUser is user-scoped only - it is a no-op for organizations. Under config.tenancy.billingScope: "organization", org credits are granted but no credits_added notification fires.
Frequently asked questions
Who owns a credit balance - the user or the org?
Whichever config.tenancy.billingScope selects. The same Billing(entityId) accessor routes to the users or organizations table accordingly.
Are credits granted during a free trial?
Yes, by default - trial credits are granted unless the plan sets grantCreditsOnTrial: false.
Why don't org members see a credits notification?
The notification system is user-scoped; notifyUser no-ops for organizations, so org-scoped grants move the balance silently.