subscriptions.usage-tiered
Price metered usage against tiers, graduated or volume, with an optional flat fee per tier.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 25 tests, run in TypeScript, Python and Rust.
What it does
Tiered pricing for metered usage, in the two modes Stripe calls `graduated` and `volume`, with Stripe's `up_to` and `flat_amount` semantics. With tiers of 1-5 at 7.00, 6-10 at 6.50 and 11+ at 6.00 (Stripe's own worked example):
- **graduated**: each unit is priced in the tier it falls in. 6 units are 5 x 7.00 + 1 x 6.50 = 41.50; 20 units are 127.50. - **volume**: every unit is priced at the tier the whole quantity falls in. 6 units are 6 x 6.50 = 39.00, which is less than 5 units at 35.00 would suggest by extrapolation: volume totals can fall as usage rises across a boundary, and that is correct.
For example
priceUsage(1, tiers ×3, volume)→ lines ×1, total $7.00 Stripe volume example: 1 font at 7.00priceUsage(5, tiers ×3, volume)→ lines ×1, total $35.00 Stripe volume example: 5 fonts, the top of the first tierpriceUsage(6, tiers ×3, volume)→ lines ×1, total $39.00 Stripe volume example: 6 fonts, all at the second tier's 6.50
The function
The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.
export function priceUsage(quantity: number, tiers: readonly UsageTier[], mode: TierMode): UsageCharge
| quantity | int | units used in the period, 0 or more |
| tiers | UsageTier[] | in ascending order; only the last may be open-ended |
| mode | TierMode | graduated: each unit at its own tier's price; volume: every unit at the price of the tier the total falls in |
| returns | UsageCharge |
The types it declares, generated into your project
export type TierMode = "graduated" | "volume";
/** One band of a tiered price, as Stripe's tiers spell it. */
export interface UsageTier {
/** the last unit in this tier, inclusive; null for an open-ended top tier */
readonly upTo: number | null;
/** the price of each unit priced in this tier */
readonly unitPrice: Money;
/** charged once when usage reaches this tier; zero for none */
readonly flatFee: Money;
}
/** What one tier contributed to the charge. */
export interface TierCharge {
/** 1-based position in the tiers list */
readonly tier: number;
/** units priced in this tier */
readonly quantity: number;
readonly unitPrice: Money;
readonly flatFee: Money;
/** quantity x unitPrice + flatFee */
readonly amount: Money;
}
/** The charge and the tiers it came from, ready to print as invoice lines. */
export interface UsageCharge {
readonly lines: readonly TierCharge[];
readonly total: Money;
}
Your code names it in one line, in the file that uses it
import { priceUsage } from "#fune/subscriptions.usage-tiered@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { assertSameCurrency, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts"; ← from money.sum ^1.0.0 · built alongside by fune
import { type TierCharge, type TierMode, type UsageCharge, type UsageTier } from "./subscriptions_usage_tiered_types.ts";
function charge(tier: number, quantity: number, t: UsageTier): TierCharge {
const amount = money(quantity * t.unitPrice.minor + t.flatFee.minor, t.unitPrice.currency);
return { tier, quantity, unitPrice: t.unitPrice, flatFee: t.flatFee, amount };
}
/**
* Price `quantity` units against ascending tiers. Graduated prices each unit
* in its own tier and charges the flat fee of every tier reached; volume
* prices every unit at the tier the quantity falls in, plus that tier's flat
* fee. The first tier is always reached, so its flat fee applies at zero.
*/
export function priceUsage(quantity: number, tiers: readonly UsageTier[], mode: TierMode): UsageCharge {
if (!Number.isInteger(quantity) || quantity < 0) {
throw new RangeError(`quantity must be a whole number of 0 or more, received ${quantity}`);
}
if (mode !== "graduated" && mode !== "volume") {
throw new RangeError(`unknown tier mode "${mode}": expected graduated or volume`);
}
if (tiers.length === 0) {
throw new RangeError("tiered pricing needs at least one tier");
}
const currency = tiers[0].unitPrice.currency;
let previous = 0;
tiers.forEach((t, i) => {
assertSameCurrency(tiers[0].unitPrice, t.unitPrice);
assertSameCurrency(tiers[0].unitPrice, t.flatFee);
if (t.upTo === null) {
if (i !== tiers.length - 1) throw new RangeError("only the last tier may be open-ended (upTo null)");
} else {
if (!Number.isInteger(t.upTo) || t.upTo <= previous) {
throw new RangeError(`tier upTo values must be positive and strictly increasing, received ${t.upTo} after ${previous}`);
}
previous = t.upTo;
}
});
const last = tiers[tiers.length - 1];
if (last.upTo !== null && quantity > last.upTo) {
throw new RangeError(`quantity ${quantity} exceeds the last tier, which ends at ${last.upTo}`);
}
const lines: TierCharge[] = [];
if (mode === "volume") {
const index = tiers.findIndex((t) => t.upTo === null || quantity <= t.upTo);
lines.push(charge(index + 1, quantity, tiers[index]));
} else {
let floor = 0;
for (let i = 0; i < tiers.length; i++) {
// Tier 1 is always reached; a later tier only once usage passes the
// previous tier's last unit.
if (i > 0 && quantity <= floor) break;
const t = tiers[i];
const top = t.upTo === null ? quantity : Math.min(quantity, t.upTo);
lines.push(charge(i + 1, top - floor, t));
if (t.upTo === null) break;
floor = t.upTo;
}
}
return { lines, total: sumMoney(lines.map((l) => l.amount), currency) };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:
fune add subscriptions.usage-tiered
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./subscriptions.usage-tiered-1.0.0-typescript.fune, or fetch it from a terminal with fune pull subscriptions.usage-tiered@1.0.0:typescript.
The whole function, every language, is one file too: subscriptions.usage-tiered-1.0.0.fune, 36,389 bytes, sha256 a95d14505f62a99c7864d1763d9cb2cef7fb5c178ad87c07060a7fe269a41a97. It installs into a project of any language.
Customise it in your app
The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.
before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.
// fune: before subscriptions.usage-tiered
after — your function gets the result and the arguments, and returns the final result.
// fune: after subscriptions.usage-tiered
replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.
// fune: replace money.amount in subscriptions.usage-tiered
// fune: replace money.sum in subscriptions.usage-tiered
step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show subscriptions.usage-tiered --steps.
// fune: step subscriptions.usage-tiered after <n|label>
Tests
A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.
| Case | Arguments | Expected | |
|---|---|---|---|
| Stripe volume example: 1 font at 7.00 | 1, tiers ×3, volume | → | lines ×1, total $7.00 |
| Stripe volume example: 5 fonts, the top of the first tier | 5, tiers ×3, volume | → | lines ×1, total $35.00 |
| Stripe volume example: 6 fonts, all at the second tier's 6.50 | 6, tiers ×3, volume | → | lines ×1, total $39.00 |
| Stripe volume example: 20 fonts, all at 6.00 | 20, tiers ×3, volume | → | lines ×1, total $120.00 |
| Stripe volume example: 25 fonts | 25, tiers ×3, volume | → | lines ×1, total $150.00 |
| Stripe graduated example: 5 fonts | 5, tiers ×3, graduated | → | lines ×1, total $35.00 |
| Stripe graduated example: 6 fonts is 35.00 plus one at 6.50 | 6, tiers ×3, graduated | → | lines ×2, total $41.50 |
| Stripe graduated example: 20 fonts | 20, tiers ×3, graduated | → | lines ×3, total $127.50 |
| Stripe graduated example: 25 fonts | 25, tiers ×3, graduated | → | lines ×3, total $157.50 |
| Stripe flat-rate example, volume: 12 x 3.00 + 30.00 | 12, tiers ×5, volume | → | lines ×1, total $66.00 |
Show the other 15 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| Stripe flat-rate example, graduated: three tiers and three flat fees | 12, tiers ×5, graduated | → | lines ×3, total $111.00 |
| no usage still bills the first tier's flat fee, graduated | 0, tiers ×5, graduated | → | lines ×1, total $10.00 |
| no usage still bills the first tier's flat fee, volume | 0, tiers ×5, volume | → | lines ×1, total $10.00 |
| usage of exactly a tier's upTo does not reach the next tier or its flat fee | 10, tiers ×5, graduated | → | lines ×2, total $75.00 |
| one unit past a boundary reaches the next tier and its flat fee | 11, tiers ×5, graduated | → | lines ×3, total $108.00 |
| a free allowance, then 2p a unit, then 1p a unit | 25,000, tiers ×3, graduated | → | lines ×3, total £330.00 |
| a single open-ended tier is plain per-unit pricing | 42, tiers ×1, graduated | → | lines ×1, total £10.50 |
| a closed last tier prices usage right up to its end | 200, tiers ×2, volume | → | lines ×1, total £10.00 |
| usage past a closed last tier is an error | 201, tiers ×2, graduated | → | error: exceeds the last tier |
| a negative quantity is an error | -1, tiers ×3, graduated | → | error: quantity must be a whole number of 0 or more |
| no tiers is an error | 5, , volume | → | error: at least one tier |
| tiers out of order are an error | 5, tiers ×3, graduated | → | error: strictly increasing |
| an open-ended tier before the last is an error | 5, tiers ×2, graduated | → | error: only the last tier may be open-ended |
| tiers in two currencies are an error | 5, tiers ×2, graduated | → | error: currency mismatch |
| an unknown mode is an error | 5, tiers ×3, stairstep | → | error: unknown tier mode |
More from the author
A tier's `upTo` is the last unit in it, inclusive, so usage of exactly 10 against tiers ending at 5 and 10 is wholly inside the first two tiers and does not reach the third, flat fee included. The last tier may be open-ended (`upTo` null); if it is not, usage beyond it is an error rather than being silently priced at the last rate.
Flat fees. In graduated mode a tier's flat fee is charged once when usage reaches the tier; in volume mode only the flat fee of the tier the quantity falls in is charged. The first tier is always reached: usage of 0 still charges the first tier's flat fee, in both modes, which is what Stripe does ("Stripe always bills the first flat rate tier when quantity=0"). To charge nothing for no usage, make the first tier `upTo` 1 with a unit price instead of a flat fee.
The result lists one line per tier that was charged, in tier order, with the units priced in it, so an invoice can show the breakdown; `total` is their sum. Everything is exact integer arithmetic in minor units - no rounding happens anywhere. A per-unit price smaller than one minor unit (0.1p per API call) cannot be written as Money; meter such usage in packages (per 1,000 calls) instead.
Errors: no tiers, a negative quantity, `upTo` values that are not positive and strictly increasing, an open-ended tier before the last, usage beyond a closed last tier, and prices in more than one currency.
Source: Stripe, "Set up tiered pricing", https://docs.stripe.com/subscriptions/pricing-models/tiered-pricing (the vectors reproduce its volume, graduated and flat-rate examples).
Files
| Path | Bytes |
|---|---|
| README.md | 2,226 |
| impl/python.py | 2,984 |
| impl/rust.rs | 4,621 |
| impl/typescript.ts | 2,809 |
| vectors.json | 17,155 |