Skip to content

Tariffs and models

getRate returns the tariff in force: not a single unit price, but every tier. rate() decides which tier usage is in and how an observation splits across tiers. Versions, scopes, effective dates and history live in the application’s table; the library never sees them.

packages/core/src/rate.ts
export interface Tier {
/** First unit position (0-based) this tier applies to. The first tier starts at 0. */
from: number;
/** Price per `per` units (per block for `package`). */
unitPriceMicroUsd: number;
/** Price denominator; `per: 1_000_000` prices per million. Default 1. */
per?: number;
/** Added once, the first time usage enters this tier. */
flatMicroUsd?: number;
}
export interface RatePolicy {
/** `per_event_up` rounds each observation up; `cumulative` rounds the running total so the period sum never drifts. */
rounding?: "per_event_up" | "cumulative";
/** Volume only: `on_crossing` emits a (usually negative) adjustment for earlier units when a tier is crossed. */
adjustmentTiming?: "on_crossing" | "none";
/** Clamp for a single observation's charge (adjustments are not clamped). */
perObservation?: { minMicroUsd?: number; maxMicroUsd?: number };
/** Which period key `usedSoFar` belongs to. A hint for `getRate`; the library does not read it. */
tierPeriod?: string;
}
export interface Rate {
model: RateModel;
tiers: Tier[];
/** Package only: units per block. */
packageSize?: number;
policy?: RatePolicy;
/** Written to usage rows so each row can be traced to the tariff that priced it. */
id?: string;
}

A Rate carries no currency. Amounts are integer micro-units (MicroUsd, 1 = 1/1,000,000); the name is historical, and for an EUR tariff the same number is micro-EUR. It is a number: 2^53 micro-units ≈ 9 billion major units. Internally the arithmetic is exact (BigInt rationals); rounding happens only at the end.

docs/src/snippets/rate-models.test.ts
import { defineRate, rate, type Tier } from "pricemeter";
const tiers: Tier[] = [
{ from: 0, unitPriceMicroUsd: 50_000 },
{ from: 10_000, unitPriceMicroUsd: 45_000 },
];
const graduated = defineRate({ model: "graduated", tiers });
const volume = defineRate({ model: "volume", tiers, policy: { adjustmentTiming: "on_crossing" } });
const pkg = defineRate({ model: "package", packageSize: 1_000, tiers }); // price per block
rate({ rate: graduated, usedSoFar: 9_990, quantity: 20 }).totalMicroUsd; // 950_000
rate({ rate: volume, usedSoFar: 9_990, quantity: 20 }).totalMicroUsd; // 900_000
rate({ rate: volume, usedSoFar: 9_990, quantity: 20 }).adjustmentMicroUsd; // −49_950_000
rate({ rate: pkg, usedSoFar: 9_990, quantity: 20 }).totalMicroUsd; // 45_000 (1 new block)
Model Computation Example: usedSoFar 9990, quantity 20, tiers [0: 50000, 10000: 45000]
graduated Each slice at its own tier’s price; flatMicroUsd on first entry into a tier 10×50,000 + 10×45,000 = 950,000
volume The whole observation at the price of the tier its last unit lands in; with on_crossing, a separate adjustment for earlier units 20×45,000 = 900,000; adjustment −9,990×5,000 = −49,950,000
package ceil((u+q)/size) − ceil(u/size) new blocks × block price (tier prices are per block) size 1000 → 1 new block, block 10 is in tier 2 → 45,000

Three models are enough:

  • a single unit price = a one-tier graduated;
  • a flat fee = one tier, unitPriceMicroUsd: 0, flatMicroUsd (B11);
  • a percentage = quantity is the transaction amount, per: 1_000_000 (29,000 / 1,000,000 = 2.9%; B22);
  • proration = scale with per: seat-seconds / seconds-in-month (B9).

A tariff has one model. Composition comes from tier fields and multi-line observe; there are no nested tariffs.

  • per is the price’s denominator: { unitPriceMicroUsd: 150_000, per: 1_000_000 } is $0.15 per million tokens. Default 1.
  • flatMicroUsd is added once, when usage first enters that tier, i.e. by the observation with usedSoFar ≤ from < usedSoFar + quantity. A flat fee on the first tier lands with the period’s first observation. For a fixed fee on every observation, make every observation its own period: getRate returns usedSoFar: 0 (tierPeriod: "observation", A97).
docs/src/snippets/rate-models.test.ts
// $0.15 per 1M tokens = 0.15 micro-USD per token. 1,000 calls of 3 tokens each:
const perEvent = defineRate({ model: "graduated", tiers: [{ from: 0, unitPriceMicroUsd: 150_000, per: 1_000_000 }] });
const cumulative = defineRate({ ...perEvent, policy: { rounding: "cumulative" } });
let a = 0;
let b = 0;
for (let used = 0; used < 3_000; used += 3) {
a += rate({ rate: perEvent, usedSoFar: used, quantity: 3 }).totalMicroUsd; // each call rounds up to 1
b += rate({ rate: cumulative, usedSoFar: used, quantity: 3 }).totalMicroUsd; // floor(T(u+q)) − floor(T(u))
}
// a === 1_000, b === 450 (exactly 3,000 × 0.15)
rounding Computation When
per_event_up (default) Each observation rounds up Wherever the per-observation amount matters
cumulative floor(T(u+q)) − floor(T(u)), T = the period’s exact total Tiny unit prices (LLM tokens): the period total never drifts

cumulative telescopes exactly: the sum of a period’s observations equals pricing the period in one go (A83). The exact total needs usedSoFar. Where T is undefined — volume with adjustmentTiming: "none" — the library needs the remainder left by the previous observation (carry, in [0, 1)): getRate returns it as { rate, usedSoFar, carry }. Every usage row carries detail.carry.

docs/src/snippets/rate-models.test.ts
// 2.9% + $0.30, at least $0.50, at most $5 per transaction (quantity = amount in micro-USD)
const card = defineRate({
model: "graduated",
tiers: [{ from: 0, unitPriceMicroUsd: 29_000, per: 1_000_000, flatMicroUsd: 300_000 }],
policy: { perObservation: { minMicroUsd: 500_000, maxMicroUsd: 5_000_000 }, tierPeriod: "observation" },
});
rate({ rate: card, usedSoFar: 0, quantity: 1_000_000 }); // { totalMicroUsd: 500_000, clamped: "min" }
rate({ rate: card, usedSoFar: 0, quantity: 100_000_000 }); // { totalMicroUsd: 3_200_000 }
rate({ rate: card, usedSoFar: 0, quantity: 500_000_000 }); // { totalMicroUsd: 5_000_000, clamped: "max" }

policy.perObservation clamps one observation’s amount to [minMicroUsd, maxMicroUsd] and marks the row clamped: "min" | "max". Volume adjustments are not clamped; an observation of quantity 0 is not clamped.

What a hold reserves is the worst case wherever the units land (A80):

  • the most expensive tier’s unit price (unitPriceMicroUsd / per) × quantity (package: ceil(q/size) blocks),
  • + every flatMicroUsd in the tariff,
  • + (volume + on_crossing) usedSoFar × (pmax − pmin),
  • clamped by perObservation.

rate() also returns it per observation as holdUpperBound, and it is exported as holdUpperBound(rate, quantity, usedSoFar).

Units this account has used on this meter in this period so far. Tier splitting needs it, and it is never silently assumed to be 0 (A58, A82). It is required when any of these hold:

Tariff usedSoFar
More than one tier required
volume or package required
A flatMicroUsd on any tier (even one tier: first entry must be known) required
cumulative rounding without carry required
One-tier graduated without a flat fee not needed

If missing, the result is { ok: false, reason: "usage_required", meter }. Its source is getRate; observe(..., { usedSoFar }) overrides it (e.g. inside a Durable Object that already holds the counter). Lines with the same (meter, dims) in one call advance each other’s usedSoFar (A84). Which period’s counter to read is the application’s choice; policy.tierPeriod records that choice on the tariff as a hint and the library never reads it (see /calendar).

docs/src/snippets/rate-models.test.ts
import { validateRate } from "pricemeter";
validateRate({ model: "graduated", tiers: [{ from: 10, unitPriceMicroUsd: 1.5 }] });
// → { ok: false, errors: [
// { code: "first_tier_from", path: ["tiers", 0, "from"], message: "the first tier must start at 0" },
// { code: "invalid_price", path: ["tiers", 0, "unitPriceMicroUsd"], message: "unitPriceMicroUsd must be a non-negative integer" } ] }

Rules: the first tier starts at from = 0; from strictly increases; prices and flat fees are integers ≥ 0; per is an integer ≥ 1; packageSize ≥ 1, and only on package; adjustmentTiming only on volume; min ≤ max. There is no “non-increasing price” rule for graduated: B3’s [0: 0, 500: p] increases (A79).

Codes: invalid_shape, invalid_model, empty_tiers, first_tier_from, invalid_from, tiers_not_increasing, invalid_price, invalid_per, invalid_flat, invalid_package_size, package_size_not_allowed, adjustment_timing_not_allowed, invalid_policy, min_gt_max. Each error is { code, path, message }.

A successful validation returns a frozen, branded copy: ValidRate. getRate may return raw objects; callers of price() or rate() must pass a ValidRate. defineRate(rate) does the same but throws RateError; use it for tariffs written in code and in tests.