Three models
graduated, volume (with on_crossing adjustments), package. Per-tier per, flat fees, min/max per observation, per_event_up or drift-free cumulative rounding.
You give it the tariff and the usage so far; it computes the amount and returns the plan to write. When, where and who — counting, storage, calendars, buffers, queues, balances, HTTP, UI — stay in your application.
import { buildMetering } from "pricemeter";import { z } from "zod";
export const metering = buildMetering() .context(z.object({ accountId: z.string() })) .refs(["otp_send"]) .meter("msg/free_pool") .meter("otp/sms", { country: z.string().length(2) }, { feeds: { "msg/free_pool": 1 } }) .getRate(async (meter, dims, ctx, at) => ({ rate: await myTariff(meter, dims, ctx, at), usedSoFar: await myCounter(ctx, meter) })) .commit(async (plan) => myLedger.apply(plan)); // all-or-nothing, idempotent on (refType, refId)
const r = await metering.observe("otp/sms", { country: "TR" }, 2, { type: "otp_send", id: msgId }, { accountId });// { ok: true, charged: 40000, lines: [...] } or { ok: false, reason: "insufficient_credit" | "no_price" | ... }Three models
graduated, volume (with on_crossing adjustments), package. Per-tier per, flat fees, min/max per observation, per_event_up or drift-free cumulative rounding.
Pure core
rate() and price() do no I/O. observe is a thin wrapper: getRate → price → commit.
Holds
Multi-line reservations as value objects: partial captures, extend, release. The tariff travels inside the hold.
Typed catalog
Meter ids, dimensions (zod / valibot / arktype via Standard Schema) and pools (feeds) are checked at compile time. A 500-meter catalog type-checks in ~0.6 s.
| Dependencies | Zero at runtime |
| Format | ESM + CJS, with types |
| Runtimes | Node ≥ 20, Bun, Cloudflare Workers |
| Money | MicroUsd: an integer number, 1 unit = 1,000,000; 2^53 ≈ 9 billion major units. A Rate carries no currency |
| Package | One package: pricemeter. Subpaths: /rates, /gauge, /adjustments, /calendar, /testing, /sqlite, /cloudflare |