Billing strategies
Pick the plan kind that matches what customers buy, and nothing more.
Start with the simplest economic model. Every plan declares its kind up front,
and the compiler only accepts the controls that kind needs — so the shape you
pick is the shape you get. Access (grants) and structural bounds (limits)
sit beside every kind and are not repeated below. Any kind that bills usage
must carry one of them — a limits rule, maxMonthlySpendCents, or
spendPolicy: { onExhaustion: fs.exhaustion.block } — or the build fails with
PLAN_UNBOUNDED_SPEND.
The examples share one measurement and one catalog:
const units = fs.measure("units");
const usage = fs.meter("api_usage", { measures: [units] });
const usagePricing = fs.pricing("api_usage", {
meter: usage,
catalog: [fs.rate.perUnit(fs.money.usd(0.01))],
});
Free with a real bound
fs.plan("free", {
kind: fs.plan.kind.free,
grants: [status],
limits: [requests.perMinute(60)],
});
A free plan carries no economic controls. Bound it structurally: attach
requests to the granted operations and give it a rate limit.
Flat subscription
fs.plan("flat", {
kind: fs.plan.kind.flat,
price: fs.money.usd(30).monthly(),
});
Customers buy access, not a quantity. Do not invent a meter to justify the fee.
Pay as you go
fs.plan("usage", {
kind: fs.plan.kind.usage,
usagePricing: usagePricing.current(),
});
Postpaid: every reported unit is rated at the bound catalog and settled on the
invoice. Value scales directly with measured consumption. There is no funding
control because nothing is prepaid; add spendPolicy: { rail: fs.rail.x402, onExhaustion: fs.exhaustion.block } only for a pay-per-call rail where each
operation is funded before it runs.
Prepaid wallet
fs.plan("prepaid", {
kind: fs.plan.kind.prepaid,
usagePricing: usagePricing.current(),
funding: { buckets: [fs.prepaid(fs.money.usd(25), { topUp: true })] },
spendPolicy: { onExhaustion: fs.exhaustion.block },
});
The subscriber buys $25 of rated value; usage draws it down; topUp: true lets
the platform offer replenishment; the gateway blocks at zero
(credit_exhausted, 402). Prepaid plans require every unbounded measure on
their routes to declare maxOutputUnits, chunkPolicy, or a post-stream
settlementMax so admission can reserve a finite economic maximum.
Subscription plus included allowance and overage
fs.plan("hybrid", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(30).monthly(),
usagePricing: usagePricing.current(),
funding: { buckets: [fs.included(fs.money.usd(10))] },
spendPolicy: {
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
});
An included bucket is $10 of rated value reissued each period, not "N units":
the allowance is worth the same across models and dimensions, and its cost is
tracked as contra-revenue in the ledger. Overage after exhaustion is rated at
the same catalog. Use fs.exhaustion.block only on prepaid and custom
plans.
Trial
fs.plan("trial", {
kind: fs.plan.kind.trial,
price: fs.money.usd(30).monthly(),
lifecycle: { trialDays: 14 },
});
Obligation is suppressed during the trial; the recurring price applies after.
Add usagePricing when post-trial usage should be rated.
Opaque allowances (5x / 20x)
fs.plan("max-5x", {
kind: fs.plan.kind.hybrid,
price: fs.money.usd(100).monthly(),
usagePricing: usagePricing.current(),
funding: {
buckets: [
fs.included(fs.money.usd(25), {
display: fs.display.multiplier({ factor: 5 }),
}),
],
},
spendPolicy: {
disclosure: fs.disclosure.opaque,
onExhaustion: fs.exhaustion.overage(usagePricing.current()),
},
});
disclosure.opaque makes every subscriber surface — including the bill
preview API — show allowance remaining as display units or a fraction, never
rates or dollar totals. Rating stays exact underneath, so the ledger reconciles
and a "20x" plan is exactly four times the "5x" bucket on the same catalog.
Tiered and dimension-dynamic pricing
Tiers, provider/model catalogs, and modifiers are catalog concerns, not plan
concerns: see Pricing catalogs. Use
fs.rate.graduated([...]) when each band keeps its own rate and
fs.rate.volume([...]) when crossing a threshold reprices the whole window at
close.
Resource-priced products
Use fs.resource() limits for persistent inventory such as projects or seats.
Bill seats by reporting the active count as a measure from your backend rather
than summing seat-change events.
Decision checklist
- What does the customer believe they are buying: access, use, prepaid value,
or an allowance? That answer is the plan
kind. - Can the quantity be measured authoritatively from the backend?
- Does the customer need to stop at zero (
prepaid+block) or keep going (hybrid+overage)? - Should subscribers see rates (
transparent, the default) or only allowance (opaque)? - What happens at renewal, cancellation, failed payment, and plan change?
- Can support explain the invoice from the bill preview, the release, and the ledger?
If the answer requires several exceptions, simplify the plan before reaching
for custom.