Billing strategies
Subscription, usage, overage, and prepaid-credit shapes — each as a @Plan.
Subscription, usage, overage, and prepaid-credit shapes — each as a @Plan.
There is no "billing model" setting to pick. A pricing strategy is just the shape
of a @Plan — combine a recurring price, per-unit meter overage, a spend cap,
and credit grants, and you get flat subscriptions, usage-based, hybrid, or
prepaid. Below are the four canonical shapes, each as the exact @Plan that
expresses it. They all sit on the same CronCloud product, so you can mix them
across a plan ladder.
All money is integer-only: price.amount and maxMonthlySpendCents are
cents; meter micros is micro-dollars (1,000,000 µ = $1.00); credit
amount_cents is cents.
A fixed recurring fee. Capacity is bounded by rate limits; usage is not billed per unit. The simplest shape — and the floor every plan needs (at least one limit).
@Plan("starter", {
name: "Starter",
price: { amount: 2900, currency: "usd", interval: "month" }, // $29/mo flat
limits: {
requests: { rate: 600, interval: "minute", enforcement: "enforce" },
},
// Bound a counted resource, e.g. 10 cron jobs, via a capability grant:
grants: [capabilityGrant("managed-cron", { limits: { cron_jobs: 10 } })],
})
starter!: unknown;
Over-limit calls are rejected by the gateway with 429 (rate_limited)
because enforcement: "enforce". No metered charges ever appear on the invoice.
No (or minimal) recurring fee; the customer pays per unit of a metered dimension.
Declare the @Meter, report real usage from a route, and price it with meter.
includedUnits gives a free allotment before billing starts.
@Meter("tokens_used", { unit: "token", estimate: 500 })
tokensUsed!: unknown;
@Feature("runs", {
routes: { "POST /v1/chat": { reports: "tokens_used" } }, // upstream reports actual
})
runs!: unknown;
@Plan("payg", {
name: "Pay as you go",
price: { free: true }, // no base fee
limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
// $0.000002/token after the first 100,000 free tokens each period:
meter: { tokens_used: { micros: 2, includedUnits: 100_000 } },
})
payg!: unknown;
estimate: 500 on the meter is the admission estimate — what the gateway
reserves before the upstream reports the true tokens_used. A reported meter
needs an estimate, or compilation fails.
A base fee that includes an allotment, then per-unit billing beyond it — the common SaaS hybrid. Add a spend cap so a runaway month can't surprise the customer.
@Plan("pro", {
name: "Pro",
price: { amount: 19900, currency: "usd", interval: "month" }, // $199 base
limits: { requests: { rate: 6000, interval: "minute", enforcement: "enforce" } },
// 1M tokens included in the base fee, then $0.000002 each:
meter: { tokens_used: { micros: 2, includedUnits: 1_000_000 } },
maxMonthlySpendCents: 50_000, // cap overage at $500/mo
overageBehavior: "allow_and_bill", // let it through and charge, up to the cap
})
pro!: unknown;
Set overageBehavior: "block" instead to deny calls past the included
allotment (a hard ceiling, no surprise bill). Use minMonthlySpendCents to
invoice a floor for committed-spend deals.
The customer holds a credit balance that usage draws down; you grant credits with
the subscription and/or sell top-up packs. Credit grants are authored on the plan
via grants:
@Plan("credits", {
name: "Credits",
price: { amount: 2000, currency: "usd", interval: "month" },
limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
grants: [
// $20 of credit granted every period (recurs); usage burns it down:
{ kind: "credit", amount_cents: 2000, recurs: true },
// Purchasable pack (sku + label required): pay $50, get $60 of credit:
{
kind: "top_up",
sku: "credit-50",
label: "$50 credit pack",
price_cents: 5000,
credit_cents: 6000,
},
],
meter: { tokens_used: { micros: 2 } }, // priced against the balance
})
credits!: unknown;
Credit-policy knobs (rollover, auto-recharge) ride through the plan's raw
escape hatch, mirroring the platform creditPolicy schema:
@Plan("credits", {
name: "Credits",
price: { amount: 2000, currency: "usd", interval: "month" },
limits: { requests: { rate: 600, interval: "minute", enforcement: "enforce" } },
grants: [{ kind: "credit", amount_cents: 2000, recurs: true }],
raw: {
creditPolicy: {
rollover: { percent: 50 }, // carry 50% of unused
auto_recharge: { threshold_cents: 500, refill_cents: 2000 }, // refill $20 at $5 left
},
},
})
credits!: unknown;
recurs: true — granted every period (free-then-bill). recurs: false
(default) grants once at subscription start (a signup credit).label (+ optional expires_after_days) marks a campaign/promotional grant.On the customer side, fs.billing.creditBalance() reads the balance and
fs.billing.createTopUpCheckout({ amountCents }) starts a top-up; the zero-prop
<CreditBalance/> and <TopUpPrompt/> components render both.
A real product usually stacks these: a free plan, a flat Starter, a Pro with
overage, and a credit-funded tier — all on one @Product class. Because each is
just a @Plan shape, you compose them freely; the gateway enforces limits and
spend caps per subscriber's active plan, and Stripe handles the invoicing.