MainSDK
Search⌘K
How-to

Meter usage per tenant

Record what each tenant uses, read it back for a usage page, count a retry once and send hourly totals to your billing provider.

Building for 0.3

Status Status is where the work is: Planned (in a milestone, not started), Building (tickets in progress), Preview (usable, may change), then the stability it ships at.

Stability The promise an interface ships with.

  • Stable Follows semantic versioning; breaking changes need a new major, 12 months of overlap and a codemod.
  • Beta Usable in production; may change before it becomes stable.
  • Experimental May change or go away; needs an explicit opt-in in main.yaml.
Stability levels
Updated 2026-10-28

You will record API calls per tenant, show today's total and make a retried step count once. It takes about 15 minutes.

You need

1. Declare the metric

main.yaml
meter:
  driver: local
  metrics:
    api_calls: { unit: call, aggregate: sum }

local is for development. In production use db or redis, or mainsdk check reports MAIN-METER-0006.

2. Record after the work succeeds

lib/usage.ts
import { meter } from "mainsdk";

meter.record("api_calls", 1, { feature: "export" });   // returns { id, queued: true } at once

record never throws because the store is down: the record is buffered.

3. Read usage

app/usage/page.tsx
import { meter } from "mainsdk";

const usage = await meter.usage({ metric: "api_calls", window: "month", groupBy: ["feature"] });
for (const g of usage.groups ?? []) console.log(g.key.feature, g.value);

4. Count a retry once

lib/transcribe.ts
import { meter } from "mainsdk";

export function countMinutes(jobId: string, minutes: number) {
  meter.record("audio_minutes", minutes, { idempotencyKey: `transcribe:${jobId}` });
}

Inside a workflow step, job or inbound webhook the key defaults from that context.

5. Test it

lib/usage.test.ts
import { it } from "node:test";
import assert from "node:assert/strict";
import { createTestApp } from "@mainsdk/testing";
import { meter } from "mainsdk";

it("counts a retried record once", async () => {
  createTestApp({ main: "./main.yaml" });
  meter.record("api_calls", 1, { idempotencyKey: "req-1" });
  meter.record("api_calls", 1, { idempotencyKey: "req-1" });
  await meter.flush();
  assert.equal((await meter.usage({ metric: "api_calls", window: "day" })).value, 1);
});

Check it worked

The test passes with a value of 1. In the app, usage.stale is false when the store answered.

Troubleshooting

  • MAIN-METER-0001: the metric is not under meter.metrics. Declare it.
  • MAIN-METER-0004: usage came from the last known value because the store is unreachable (stale: true).
  • MAIN-METER-0007: no store for the tenant's region. Add meter.redis.<region>.

To bill by usage, see take a payment and the meter interface for meter.export.

Was this page helpful?

↑↓ moveEnter openEsc close

Not decided yet