@wtfalch/ledger
v0.1.0
Published
An exact money type (integer micros, currency, parsing and formatting) and a reserve -> settle -> release ledger keyed by organisation, with an append-only usage row per event and monthly periods.
Readme
@wtfalch/ledger
The estate's money primitive: an exact money type (integer micros, currency, parsing and formatting -- no floats anywhere) and a reserve -> settle -> release ledger keyed by organisation, with an append-only usage row per settled event and monthly periods. It records what was spent against a limit. It is not an accounting system, and it does not price anything -- prices, plans, invoices and payment are out of scope; billing and invoicing are built on this package's API.
Extracted from ai/packages/service/src/{money,budget}.ts and
files/packages/service/src/{money,budget}.ts (byte-identical money.ts,
divergent budget.ts; every divergence is reconciled and recorded in
.claude/campaign/2026-09-23-v1.md). Unlike either source, there is exactly
one budget per (organisation, period) -- no per-key nested budget, no
operator ceiling.
Install
pnpm add @wtfalch/ledger
pnpm exec ledger-migrations # copies migrations/*.sql into drizzle/ as the next numbersThe copy is recorded in drizzle/.ledger-migrations.json; running it again
copies nothing. Apply the copied file with the host's own migrate script, or
call migrate() against any Queryable for a quick local setup:
import { migrate } from '@wtfalch/ledger';
await migrate(db); // idempotent@wtfalch/authz is an optional peer, scaffolded per the estate's package
template: the ledger catalogue offers ledger:record (reserve, settle,
release) and ledger:read (budgets and usage). A host that does not use
@wtfalch/authz can ignore catalogue/checkLedgerRead/checkLedgerRecord
entirely and authorize calls its own way -- this package never calls them
itself.
Money
import { money, formatMoney, toMicros, fromMicros } from '@wtfalch/ledger';
const price = money('NOK', '12.50'); // { currency: 'NOK', micros: '12500000' }
formatMoney(price); // '12.500000'
toMicros('0.1'); // '100000', exact -- no float error
fromMicros('100000'); // '0.100000'Amounts are always decimal strings or bigint, never number. toMicros/
fromMicros are currency-agnostic: micros is a fixed six-decimal-place
scale, not a property of any one currency. A currency-native display
convention (2 places for USD/NOK, 0 for JPY, ...) is the caller's to apply;
this package does not carry that table.
Ledger
import { Ledger, setLimit, currentBudget } from '@wtfalch/ledger';
await setLimit(db, { organisationId, currency: 'USD', limitMicros: toMicros('500') });
// or limitMicros: null for uncapped
const ledger = new Ledger(db); // db: Database (query + transaction)
const reservation = await ledger.reserve({
organisationId,
currency: 'USD',
requestId, // idempotency key: replaying it returns the same reservation
fingerprint, // must match on replay, or the call is a conflict
amountMicros: toMicros('1.50'),
meter: 'api-calls', // optional, defaults to '' -- an unenforced tag for billing to group by
});
// ... do the work, then one of:
await ledger.settle(organisationId, reservation.id, toMicros('1.20')); // the real cost
await ledger.release(organisationId, reservation.id); // billed nothing
const budget = await currentBudget(db, organisationId);reserve rolls the organisation's budget forward into the current calendar
month automatically, carrying its currency and limit; it never originates a
budget for an organisation that has never called setLimit, which is refused
not_configured. settle never refuses at the cap -- real, already-incurred
cost is always recorded, even past the limit; a subsequent reserve refuses
instead (over_budget).
Usage
import { listUsage, usageSummary } from '@wtfalch/ledger';
const events = await listUsage(db, organisationId, periodStart); // oldest first
const perMeter = await usageSummary(db, organisationId, periodStart); // [{ meter, totalMicros }]This is what a consumer such as billing rates into a price: total micros per meter for one organisation's period. A meter with no usage that period is absent, not a zero row.
Tests
pnpm test # PGlite, in memory
TEST_DATABASE_URL=postgres://... pnpm test # a real Postgres; a scratch schema per run