# Metering high-frequency usage with the SDK

> Use the TypeScript SDK's LeaseManager to charge thousands of small usages a minute against a customer's prepaid wallet, with local metering, batched settles, renewals and a final settle on shutdown.

When your service charges a customer's prepaid credits for every model call, API request or batch of tokens, a call to Ownrecord per charge is too slow. [Leases](https://ownrecord.io/docs/concepts/leases.md) solve this: you reserve a block of the customer's wallet, meter against it locally, and settle in batches. The TypeScript SDK's `LeaseManager` does the bookkeeping for you. It acquires and renews leases, never grants more than a lease holds, settles on a schedule and settles everything when your service stops.

## Set it up

Create one manager per customer wallet in each process, with a secret API key that has `wallets:write`:

```ts
import { LeaseManager } from "@ownrecord/sdk";

const leases = new LeaseManager({
  apiKey: process.env.OWNRECORD_API_KEY,
  walletId: "wal_01m4ty3c2b5qx7mhd9k8w6s0b1",
  currency: "EUR",
  amount: 5_000_000n, // lease 5.00 at a time
  ttlSeconds: 300,
});
```

Every amount is **integer micros** of the wallet's currency as a `bigint`: `5_000_000n` is 5.00, `2_000n` is 0.002. Never use floating-point numbers for money.

The manager uses web standard APIs only (`fetch`, `crypto`, timers), so it runs in Node.js 18+, Deno, Bun and Cloudflare Workers.

## Charge usage

Call `consume` before you serve a request, with what it costs:

```ts
const result = await leases.consume({
  meter: "api.calls",
  quantity: 1n,
  amount: 2_000n,
  attributes: { model: "large" },
});

if (!result.ok) {
  // Don't serve the request: result.reason says why.
}
```

The manager grants the usage only when the current lease can still hold it, and records it before it answers. The first call acquires a lease; call `leases.start()` at boot to do that earlier. When a consumption doesn't fit, the manager renews the lease with room for it and the call waits for the new lease, up to `consumeTimeoutMs` (10 seconds by default). Pass `{ wait: false }` to get an answer at once instead.

| `reason`             | What it means                                                                                       |
| -------------------- | --------------------------------------------------------------------------------------------------- |
| `insufficient_lease` | The lease can't hold it right now and no bigger lease arrived in time, or you passed `wait: false`. |
| `insufficient_funds` | The wallet can't fund a lease that holds it. Offer a top-up.                                        |
| `wallet_unavailable` | The wallet is frozen or closed (`code` says which).                                                 |
| `closed`             | The manager is shutting down.                                                                       |
| `error`              | Ownrecord or your store failed, or the meter was refused. `code` and `error` have the details.      |

After a lease couldn't be funded, the manager answers `insufficient_funds` for 5 seconds before it asks Ownrecord again, so an empty wallet doesn't cost you a request per call.

Meters are names you choose, such as `api.calls`: lowercase words separated by dots. Don't use a meter that a metered price bills in arrears. Ownrecord refuses it ([`usage_meter_billed_in_arrears`](https://ownrecord.io/docs/reference/errors.md#usage_meter_billed_in_arrears)), and the manager then drops that meter's usage and refuses it from then on. Up to 10 `attributes` describe a usage, and Ownrecord keeps one usage summary per meter and set of attributes. Malformed usage, such as an invalid meter name or a negative amount, throws a `TypeError` or `RangeError`.

## Settling

The manager settles what was consumed since the last settle, with one usage summary line per meter and set of attributes. Each line becomes a [usage event](https://ownrecord.io/docs/concepts/usage.md) on the customer, with its amount. It settles:

- every `settleIntervalMs` (5 seconds by default) while there is unsettled usage;
- as soon as `settleAmount` is unsettled, if you set it;
- when it renews or closes a lease;
- when you call `await leases.flush()`.

Only one request is in flight at a time. Settles are numbered (`settleSeq`), and a request that fails is resent unchanged with its `Idempotency-Key` until Ownrecord answers, so a settle is never charged twice. If another client settles the same lease, the manager numbers its settle after theirs and sends it again.

## Renewals and sizing

A renewal is a settle that closes the lease and reserves the next one in the same request. The manager renews:

- `renewBeforeExpiryMs` before the lease expires: a quarter of the TTL by default, at most 30 seconds;
- when less than `renewAtRemaining` is left on a lease that is in use: a tenth of the lease by default;
- when a consumption doesn't fit.

While a renewal is in flight, new consumption waits for the new lease, usually for one round trip. If renewals keep failing, the manager stops granting usage on the old lease when half of `renewBeforeExpiryMs` is left, so it still has time to settle.

Renewals and later leases ask for Ownrecord's `suggestedNextAmount`, what your current rate of consumption needs, kept between `minAmount` (default `amount`) and `maxAmount` (default 10 × `amount`). A consumption larger than that still gets a lease that holds it. Set `sizing: "fixed"` to always lease `amount`. When the wallet has less than a renewal asks for, the new lease holds what there is.

A lease that nobody used is closed when it would be renewed, so its funds go back to the wallet. The next `consume` acquires a new one. Set `renewWhenIdle: true` to keep renewing instead.

## Shutting down

Settle everything before your process exits:

```ts
process.on("SIGTERM", async () => {
  const { flushed } = await leases.shutdown();
  process.exit(flushed ? 0 : 1);
});
```

`shutdown` stops granting usage, settles what is left with `final: true` and closes the lease, so its remaining funds go back to the wallet. It tries for `shutdownTimeoutMs` (15 seconds by default). If it can't reach Ownrecord in that time, `flushed` is false and `unsettled` says how much wasn't settled.

## Surviving a crash

By default the manager keeps its consumption log in memory. If the process crashes, that log is lost, and the lease's [expiry policy](https://ownrecord.io/docs/concepts/leases.md#when-a-lease-runs-out) decides what happens to the usage that wasn't settled: `release` doesn't charge it, `capture` charges the whole lease.

To charge exactly what was used, keep the log in a durable store and lease with `hold_for_late_settle`:

```ts
const leases = new LeaseManager({
  apiKey: process.env.OWNRECORD_API_KEY,
  walletId,
  currency: "EUR",
  amount: 5_000_000n,
  expiryPolicy: "hold_for_late_settle",
  lateSettleWindowSeconds: 3_600,
  store: myRedisLeaseStore,
  name: `${walletId}:${workerId}`,
});
```

When a manager starts with the same store and `name`, it first resends the request the crashed process had in flight, with the same `Idempotency-Key`, then settles what the crashed process consumed. If the lease has expired meanwhile, that is a late settle inside the lease's window. Then it continues on a new lease.

A store implements five methods. Each may return a value or a promise:

```ts
interface LeaseStore {
  get(name: string): LeaseStoreState | undefined; // the manager's saved state (plain JSON)
  set(name: string, state: LeaseStoreState): void;
  incr(name: string, leaseId: string, line: string, quantity: bigint, amount: bigint): void; // atomic
  totals(name: string, leaseId: string): LeaseUsageTotal[]; // replays the lease's log
  clear(name: string, leaseId: string): void;
}
```

`incr` must add both numbers or neither, for example two `HINCRBY` in a Redis `MULTI`, or one SQL upsert. Give each running manager its own `name`: two live managers sharing a name would settle each other's usage.

## Watching it

Pass `onEvent` to log or measure what the manager does:

| Event             | When                                                                                                                                                                                                               |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `lease_acquired`  | A new lease, at start or after one was closed.                                                                                                                                                                     |
| `settled`         | A settle was applied (`mode`: `open`, `renew` or `final`), with what it charged.                                                                                                                                   |
| `lease_renewed`   | Metering moved to the next lease.                                                                                                                                                                                  |
| `lease_closed`    | A lease closed without a successor (`reason`: `final`, `idle`, `late`, `not_renewed`, `expired` or `refused`).                                                                                                     |
| `usage_unsettled` | Usage that couldn't be charged on its lease, for example after [`lease_expired`](https://ownrecord.io/docs/reference/errors.md#lease_expired). `chargedByExpiry` says whether the expiry policy charged it anyway. |
| `recovered`       | The manager found a previous process's state in its store.                                                                                                                                                         |
| `error`           | A request or the store failed. `retrying` is true when the manager will try again.                                                                                                                                 |

`leases.lease` shows the current lease: its amount, what was consumed and settled, and what is left.

## Without the manager

`createLeaseApi({ apiKey })` gives you the three lease calls, [`leases.create`](https://ownrecord.io/docs/reference/leases-create.md), [`leases.settle`](https://ownrecord.io/docs/reference/leases-settle.md) and [`leases.get`](https://ownrecord.io/docs/reference/leases-get.md), with amounts as `bigint` micros, retries on network errors, rate limits and server errors, and the same `Idempotency-Key` on every retry. Pass your own implementation of these calls to the manager as `api` to route them through another client.
