Model SaaS pricing
Declare provider-neutral plans with base prices, usage dimensions, free allowances, graduated tiers, minimum charges, labels, and explicit rounding.
@absolutejs/billingv0.17.2betaPlatform & InfraPure-function usage billing for Bun apps — declare a priced plan and compute host-portable invoices in integer micros.
Turns metered usage into invoices without float drift or vendor lock-in. createPlan declares a priced product — flat base fee, per-dimension unit prices, graduated tiers, free allowances — and computeInvoice is a pure function from a usage snapshot to line items and a total, all in integer micros. It pairs with @absolutejs/metering for usage collection and leaves Stripe, QuickBooks, or another invoicing integration to the host, so previewing an upcoming invoice, re-pricing a past period, or dry-running a plan change is a plain function call.
bun add @absolutejs/billingProvider-neutral pricing and invoice computation used by the hosted AbsoluteJS.ai platform.
@absolutejs/billing is the pure-function layer between @absolutejs/metering (which collects usage events) and an invoicing backend (Stripe, QuickBooks, an internal billing engine).
It does two things:
createPlan(...) — declares a priced product: optional flat
base fee, per-dimension unit prices, optional graduated tiers, optional per-dimension free allowances.
computeInvoice({ plan, period, tenant, usage }) — pure
function that turns a Usage snapshot into an Invoice with line items and a total.
All money math is done in integer micros (1 micro = 1/1,000,000 of a currency unit — the same denomination Stripe stores prices in internally). Float drift is structurally impossible: a $0.0002 per-request price is 200 and rounding policy is explicit.
A PricedDimension is one of three:
Flat per-unit — { perUnitMicros: 200, unit: 1 }
Tiered (graduated) — { tiers: [{ upTo: 1_000_000, perUnitMicros: 200 }, { upTo: Infinity, perUnitMicros: 100 }] }
Custom — { price: (chargedQuantity) => micros } (escape hatch for surge / caps / non-monotonic pricing)
Optional knobs:
freeTier — units subtracted before pricing
unit — divisor so bytesEgress priced as MB ↔ unit:
10241024
label — invoice line-item display name
Plan-level knobs:
basePriceMicros — flat fee per period
minimumChargeMicros — floor; an adjustment line item fills
any gap
The control plane needs to:
Preview an upcoming invoice before the period closes
Re-price a past period under a new plan ("what would this
customer have paid on the proposed enterprise tier?")
Dry-run plan changes before publishing them
A pure cost-model function makes all three trivial — no Stripe SDK, no side effects, no IO. The Stripe push (or QuickBooks export, or mailed-PDF generator) lives outside this package, in @absolutejs/billing-adapters/.
Durable balances, reservations, capped work, and migration rules are available through the prepaid, prepaid-postgres, and credit-work subpaths.
Purchase-only handoffs provide expiring, single-use browser capabilities without a full login session.
@absolutejs/billing/reports provides customer-facing status, receipt pagination, and usage contracts. parseUsageRange uses a half-open UTC date interval of at most 90 days (default: the last 30 days including today). Receipt cursors are positions, never authorization: bind every query, reversal join, and cursor to the caller's account. Readers must project through the public helpers, which omit provider costs, payment tokens and vault references. Usage day and feature totals must reconcile exactly; purchased dollars and consumed service credits are different measures. Automatic refill is currently unsupported.
checkpointDeferred(accountId, workId, binding, expectedRevision, value) saves progress without settling or reserving more credits. The initial revision is 0; success returns the incremented revision and saved string. It verifies the bound effect and authorization, rejects stale revisions and terminal work, and limits the value to one million characters. Existing work uses the same JSON state column; no schema migration is required.
A checkpoint is not an execution lease. Claim execution separately, persist an in-flight marker before any provider call, drain metering before saving its result, and retain the reservation if the outcome is uncertain.
Uncapped embedding plans and application budgets
Pass monthlyTokenLimit: null in EmbeddingUsageSnapshot when the configured provider plan has no monthly embedding-token cap. monthlyBudgetTokens can separately describe a host-owned spending guardrail; it is never reported as provider quota. Both values are host configuration, not a live provider balance. exhausted: true reports currently refused embeddings without assuming a monthly cap caused the refusal. Member allowances remain the host application's policy.
@absolutejs/billing/provider-budget exports createProviderBudget(client, table?) using the existing CreditSqlClient transaction interface. The host owns a journaled PostgreSQL table with id text primary key, scope text, period text, reserved_micros bigint, actual_micros bigint nullable, and status text (plus a scope/period index). Reserve a defensible maximum before an external request; do not treat an estimate as a hard maximum. A repeated reservation ID never authorizes another request. Monthly request and daily cost scopes can be reserved independently; release any earlier scope if later admission fails.
Settle known fulfilled or rejected calls exactly once. An interrupted/unknown provider outcome retains its full reservation until explicitly reconciled. This ledger bounds admitted research operations, independently of member-credit pricing; it does not discover or account for provider calls made outside it. The host must account for all retries/repairs in the reservation or disable them.
Usage attribution
LedgerEntry.attribution accepts a portable UsageAttribution record (call trace, run, subject, session, funded work, release, server and tool). It passes unchanged to the commit and rollup stores and does not affect pricing. The host must retain it with the event and bind subjects after authorization. Observability identity must not change a provider event's charging/idempotency identity. Unknown external-host spend is not zero.
@absolutejs/billing/paid-work-policy exports createPaidWorkPolicy, PaidAuthorization, and PaidWorkApprovalError for Node/Bun servers. Create one policy per application with currentWork and withWork adapters to its funded execution context. The context carries userSub, optional actorId and approvalId, and a mutable closed flag. Only trusted admission code may create or attach this context.
Wrap authenticated handlers in withPaidRequester(userSub, request.method, handler, impersonatorId?). Reads (including HEAD and OPTIONS) cannot authorize spending, even through a nested write scope. Start paid work with POST; its response may stream. A GET endpoint should only read previously generated data. The transport contract and the spending contract must agree: Eden's route types alone cannot verify handler side effects.
Require a PaidAuthorization parameter at each paid transport boundary. Obtain it using authorizePaidWork() in the admitted execution scope, and call requirePaidAuthorization(authorization) immediately before starting a provider attempt. Tokens are opaque, checked at runtime, specific to the policy instance and execution scope, and rejected after their funded/deferred scope closes. bindPaidExecution preserves a scope for response iterators that outlive their handler. Do not serialize tokens, accept them from clients, or cache them across requests. A payer ID alone cannot mint a capability: authorizePaidWork requires an authenticated or funded scope. The lower-level requirePaidWorkOwner retains a trusted-server payer fallback for attribution; that fallback does not authorize a provider capability.
An impersonated request requires a funded work context whose actor matches the impersonator and which has an approval ID. withoutAutomaticSpend blocks unapproved background calls. withDeferredPaidRequester is exclusively for a worker that has already atomically claimed a durable approval; it closes that permission when its callback finishes.
This policy checks execution identity and intent. The host still authenticates sessions, validates approval ownership/expiry, reserves and bounds provider costs before calling them, meters actual usage, and settles or retains uncertain reservations. An authorization token is not a credit balance or a durable execution lease. TypeScript prevents accidental missing capabilities; runtime checks remain necessary for untrusted input and changing state.
All prices are integer micros (1/1,000,000 of a currency unit — the same denomination Stripe stores internally), so float drift is structurally impossible and rounding policy is explicit.
Each dimension is flat per-unit, graduated tiers, or a custom price function — the escape hatch for surge pricing, caps, and other non-monotonic schemes.
Per-dimension free allowances subtract before pricing, a plan-level base fee charges per period, and minimumChargeMicros floors the total with an adjustment line item.
computeInvoice has no IO and no side effects, so you can preview invoices before the period closes, re-price history under a proposed plan, and dry-run changes before publishing.
Consumes Usage snapshots from @absolutejs/metering directly, and unit divisors let you price bytesEgress per MB or cpuMs per second without pre-converting.
Outcomes
Declare provider-neutral plans with base prices, usage dimensions, free allowances, graduated tiers, minimum charges, labels, and explicit rounding.
Preview, dry-run, and re-price usage snapshots without calling Stripe, QuickBooks, or another invoicing provider.
Hardening checklist
Follow in order
# @absolutejs/billing
import { createPlan, computeInvoice, formatMicros } from "@absolutejs/billing";
const plan = createPlan({
name: "pro",
currency: "usd",
basePriceMicros: 20_000_000, // $20/mo
pricedDimensions: {
requests: { perUnitMicros: 200, freeTier: 1_000_000 },
cpuMs: { perUnitMicros: 50, unit: 1000, freeTier: 60_000 * 60 * 10 },
bytesEgress: {
perUnitMicros: 100,
unit: 1024 * 1024,
freeTier: 100 * 1024 * 1024,
},
hibernationGbSeconds: { perUnitMicros: 5 },
},
});
const invoice = computeInvoice({
plan,
tenant: "acme",
period: { start, end },
usage, // a Usage from @absolutejs/metering
});
console.log(formatMicros(invoice.totalMicros, invoice.currency));
// "27.50 USD"Declare a plan with a base fee, per-dimension prices, and free allowances, then compute an invoice from a metering snapshot.
import {
createPlan,
computeInvoice,
formatMicros,
} from '@absolutejs/billing';
const plan = createPlan({
name: 'pro',
currency: 'usd',
basePriceMicros: 20_000_000, // $20/mo
pricedDimensions: {
requests: { perUnitMicros: 200, freeTier: 1_000_000 },
cpuMs: {
perUnitMicros: 50,
unit: 1000,
freeTier: 60_000 * 60 * 10,
},
bytesEgress: {
perUnitMicros: 100,
unit: 1024 * 1024,
freeTier: 100 * 1024 * 1024,
},
},
});
const invoice = computeInvoice({
plan,
tenant: 'acme',
period: { start, end },
usage, // a Usage from @absolutejs/metering
});
console.log(formatMicros(invoice.totalMicros, invoice.currency));
// "27.50 USD"Graduated tiers price each band separately — the first million requests at one rate, everything beyond at another.
const plan = createPlan({
name: 'scale',
currency: 'usd',
pricedDimensions: {
requests: {
tiers: [
{ upTo: 1_000_000, perUnitMicros: 200 },
{ upTo: Infinity, perUnitMicros: 100 },
],
},
},
});Current package surface
Import surface · click to copy