← all parts

Part · billing.usage

What's actually behind billing.usage

The part exactly as partkit add billing.usage vendors it into your repo — verified, locked, every byte readable. Nothing here is mocked.

billing.usagev1.1.0

✓ attested🔒 read-only↑ ctrlai upgradebilling.usage@1

Lives at parts/billing.usage/ in your repo — open, owned, readable. Not buried in node_modules. 587 lines of source you can audit.

14 conformance tests passedverified 2026-06-15adapter stripe↗ CI run
content hash bf603c07a8…51c1eapinned in parts.lockctrlai guard fails CI if a single byte changes
tested against node 25.3.0stripe 22.2.1

Public API — what your seam calls

  • usage(db: SqlExecutor): UsageMeter
  • UsageMeter { record(input: RecordUsageInput): Promise<RecordedUsage>; total(query: UsageTotalQuery): Promise<UsageTotal>; summary(query: UsageSummaryQuery): Promise<UsageTotal[]>; reportDue(opts?: ReportDueOptions): Promise<UsageReport> }
  • class UsageError extends Error { code: UsageErrorCode }
  • types: SqlExecutor, UsageMeter, RecordUsageInput, RecordedUsage, UsageTotalQuery, UsageSummaryQuery, UsageTotal, ReportDueOptions, UsageReport, UsageErrorCode

Invariants — guarantees the contract pins

  1. Importing the part performs no I/O and never throws; record/total/summary validate input with typed errors, and record NEVER calls the biller inline — it only writes the local ledger, so a biller outage never blocks or fails metering
  2. record is idempotent: the same idempotencyKey (per subject+meter) yields exactly one ledger row; the second call returns deduped:true with the original eventId. A non-finite or negative quantity is rejected before any SQL
  3. total and summary aggregate correctly over the half-open [since, until) window (since-inclusive, until-exclusive), honor the subject/meter filters, and are deterministic; an empty range yields a zero total, not an error
  4. Reporting is at-least-once toward the biller, deduplicated by the stable eventId (exactly-once within the biller's dedup window): reportDue reports each unreported event using its eventId as the biller idempotency key, then marks it reported; a biller failure leaves the event unreported for the next pass — never silently dropped — and a permanently-rejected event sinks in the drain order so it never starves fresh usage
  5. Quantities round-trip exactly for integer units (no float drift); decimal handling is documented in seams.md
  6. The biller secret never appears in error messages, in UsageError, or in any value the part returns
  7. The part operates solely through the provided SqlExecutor seam — it imports no database driver — and every statement targets only its own billing_usage_events table; every input is parameterized, so SQL metacharacters are stored literally and never executed

Owns in your Postgres

billing_usage_events

Dependencies

zero-dep — runs on your Postgres

Swappable adapters

stripe
SOURCEparts/billing.usage/20 files · click to read
parts/billing.usage/src/index.tstypescript · 1,593 bytes
/**
 * billing.usage — public interface. The ONLY legal import surface.
 * Contract: ../contract.json · What your app must provide: ../seams.md
 *
 * The vendor-neutral metered-usage ledger: record events idempotently, aggregate
 * per subject/meter/period, and report unreported usage to a biller (Stripe
 * Meters) out-of-band. The ledger is the source of truth — invoice from it
 * yourself or push to Stripe; the biller is an adapter, not the foundation.
 */
import { adapter } from "../adapters/selected/adapter";
import { makeUsage } from "./internal/usage";
import type { SqlExecutor, UsageMeter } from "./internal/types";

export { UsageError } from "./internal/errors";
export type { UsageErrorCode } from "./internal/errors";
export type {
  RecordedUsage,
  RecordUsageInput,
  ReportDueOptions,
  SqlExecutor,
  UsageMeter,
  UsageReport,
  UsageSummaryQuery,
  UsageTotal,
  UsageTotalQuery,
} from "./internal/types";

/**
 * Bind the usage meter to a database connection (the SqlExecutor seam).
 * Constructing it performs no I/O and never throws (contract invariant 1) — the
 * database is touched only when a method runs, so it is serverless-safe.
 *
 *   const meter = usage(db);
 *   await meter.record({ subjectId, meter: "api.request", quantity: 1, idempotencyKey: reqId });
 *   const t = await meter.total({ subjectId, meter: "api.request", since, until });
 *   // …then drive reportDue() from jobs.queue or a cron to push to the biller:
 *   await meter.reportDue();
 */
export function usage(db: SqlExecutor): UsageMeter {
  return makeUsage(db, adapter);
}