Recipes

Bill AI tokens

Charge for AI usage per model and token type: one meter, a rate card or the model price list with a markup, your cost and margin, and an SDK helper that reports each model response once.

Charge your customers for the model calls your product makes: a rate for each model and token type, quoted the way model prices are, such as $3.00 per 1M input tokens. The rates go to 12 decimal places, and we round once, on the invoice line.

Read Meter usage events first for how events, meters and usage keys work.

Create the meter

One meter adds up tokens from ai_tokens events, with one total for each model and token type:

import { Tmbilling, reportAiUsage } from "@two-minute-billing/sdk";

const tmb = new Tmbilling({
  apiKey: process.env.TMBILLING_API_KEY!,
  baseUrl: process.env.TMBILLING_URL,
});

const biz = await tmb.businesses.create({ name: "MuseLane AI", default_currency: "USD" });
const meter = await tmb.meters.create(biz.id, {
  id: "ai_tokens",
  name: "AI tokens",
  event_name: "ai_tokens",
  aggregation: "sum",
  value_property: "tokens",
  group_by: ["model", "token_type"],
});

In the dashboard, Product Catalog → Meters → Create meter → Use the AI tokens setup fills in the same values.

Price it with a rate card

Rates are per 1M tokens (quantity_divisor: 1000000) and in minor units: "300" is $3.00. The price's own rate is the default, so a model that you have not priced yet still bills:

const product = await tmb.products.create(biz.id, { name: "AI usage" });
const price = await tmb.prices.create(biz.id, {
  product: product.id,
  name: "AI usage monthly",
  model: "usage",
  currency: "USD",
  interval: "month",
  meter: meter.id,
  quantity_divisor: 1000000,
  unit_amount_decimal: "300",
  rates: [
    { match: { token_type: "output" }, unit_amount_decimal: "1500" },
    { match: { token_type: "cached_input" }, unit_amount_decimal: "30" },
    { match: { model: "large-v2", token_type: "output" }, unit_amount_decimal: "2400" },
  ],
});
CombinationRow that bills itRate
any model, inputthe default$3.00 per 1M
small-v1, outputtoken_type: output$15.00 per 1M
large-v2, outputmodel: large-v2, token_type: output (names more values)$24.00 per 1M
any model, cached_inputtoken_type: cached_input$0.30 per 1M

You can change the rates later. A subscription keeps the rates it started with, so a change applies to new subscriptions. The meter, quantity_divisor and amount_source are fixed after create.

Report each model response

reportAiUsage reads the model and the token counts from a model response and sends one event for each token type, in one batch. It reads OpenAI chat completions and responses, Anthropic messages, and Vercel AI SDK results. Cached tokens are taken out of the input count, so no token bills twice.

TypeScript
const customer = await tmb.customers.create(biz.id, {
  email: "[email protected]",
  currency: "USD",
});
await tmb.paymentMethods.create(biz.id, customer.id, { token: "4242424242424242" });
await tmb.usageKeys.add(biz.id, customer.id, "workspace-4821");
await tmb.subscriptions.create(biz.id, customer.id, { items: [{ price: price.id }] });

// After your model call:
const response = {
  id: "msg_XXXXXXXXXXXX01",
  type: "message" as const,
  model: "small-v1",
  usage: { input_tokens: 1_234_567, output_tokens: 456_789 },
};
await reportAiUsage(tmb, biz.id, response, { customer: "workspace-4821" });

The event keys are <response id>:<token type>. If your code reports the same response again after a timeout, the repeat is a duplicate and bills nothing more. To build the events without sending them, call usageEventsFromAiResponse(response, { customer }).

What the invoice shows

At the end of the period, each combination is one line, rounded half-up to the cent once:

LineQuantityRateAmount
AI usage monthly · small-v1 · input1,234,567$3.00 per 1M370.3701 → $3.70
AI usage monthly · small-v1 · output456,789$15.00 per 1M685.1835 → $6.85

Total: $10.55. Read quantity_decimal and unit_amount_decimal on each line for the exact figures.

Pass through your cost instead

If you already know what each call cost you, send it on the event as cost (major units, a decimal string) and price with amount_source: "event_cost" and a markup:

TypeScript
const atCost = await tmb.products.create(biz.id, { name: "AI usage at cost" });
const passThrough = await tmb.prices.create(biz.id, {
  product: atCost.id,
  name: "AI usage at cost plus 20%",
  model: "usage",
  currency: "USD",
  interval: "month",
  meter: meter.id,
  amount_source: "event_cost",
  markup_percent: "20",
});
console.log(passThrough.id);

The line is the sum of cost × 1.20, rounded once. An event in a currency other than the price's is not billed and has the flag currency_mismatch. With amount_source: "event_amount", we bill the events' amount as sent.

Bill from the model price list

You do not have to type a rate for each model. A price can bill each model at its price in the model price list, plus a markup. We keep a dated copy of a public AI price list on our servers and update it every hour. Prices are in USD per 1M tokens (or per 1K for some units). Every invoice line names the list row that it priced from, so you can check a price later.

See the list in the dashboard at Settings → Model prices, or read it with the API:

const page = await tmb.modelPrices.list({ model: "small-v1", tokenType: "input" });
console.log(page.data[0]?.unit_amount_decimal, page.data[0]?.quantity_divisor); // "300" 1000000

Map the meter to the list

Tell the meter which event properties hold the model and the token type. Each property must be in the meter's group_by. When more than one provider lists a model, also send the provider on the events, add it to group_by, and set provider_key (or a fixed provider). Without it, that model has no list price.

await tmb.meters.update(biz.id, meter.id, {
  price_list: { model_key: "model", token_type_key: "token_type" },
  cost_currency: "USD",
});

In the dashboard, this is the Cost section of the meter: Cost from → Model price list.

Create a list-plus price

amount_source: "model_price_list" bills each model at the list price plus markup_percent. Rows in rates set a different markup or a fixed rate for some models. The default rate bills a model that has no list price and no cost, so a new model still bills:

const listPlus = await tmb.prices.create(biz.id, {
  product: product.id,
  name: "AI usage, list price plus 20%",
  model: "usage",
  currency: "USD",
  interval: "month",
  meter: meter.id,
  amount_source: "model_price_list",
  markup_percent: "20",
  markup_base: "list",
  quantity_divisor: 1000000,
  unit_amount_decimal: "500",
  rates: [
    { match: { model: "large-v2" }, markup_percent: "10" },
    { match: { model: "in-house-7b", token_type: "input" }, unit_amount_decimal: "50" },
  ],
});

For each model and token type, the first of these that applies sets the selling rate:

  1. The customer's own rate or markup on the subscription item (see below).
  2. A row in rates that matches: a fixed unit_amount_decimal or a markup_percent.
  3. The price's markup_percent.

A markup applies to the markup base. With markup_base: "list" the base is the list price, whatever your cost is. With "cost" it is your cost (cost-plus). When the base has no row, the other one is used. When neither has a row, the model bills the default rate, and the line says rating.price_source: "default_rate".

A price change in the middle of a period

A list price applies from the time we take it in, so usage that is already recorded never changes price. When a price changes inside a billing period, the invoice has one line for each price.

Example: input on small-v1 is $3.00 per 1M until 15 November 10:00 UTC, then $2.50. The price has a 20% markup on the list price. The customer used 1,234,567 tokens before the change and 456,789 after it:

LineQuantityUnit priceAmount
small-v1 · input, to 15 Nov 10:001,234,567$3.60 per 1M444.44412¢ → $4.44
small-v1 · input, from 15 Nov 10:00456,789$3.00 per 1M137.0367¢ → $1.37

Each line is rounded half-up once.

Markup on your cost

If you pay a provider less than its list price, record your cost. It applies to all businesses in the organization. The price list update never changes it. It sets the cost and margin of your usage, and it is the base of a price with markup_base: "cost":

await tmb.modelCostOverrides.create({
  model: "small-v1",
  token_type: "input",
  currency: "USD",
  unit_amount_decimal: "200",
  quantity_divisor: 1000000,
  note: "Annual contract",
});

With a list price of $3.00, your cost of $2.00 and a 20% markup:

markup_baseCustomer pays per 1MYour cost per 1MMargin per 1M
list$3.60$2.00$1.60
cost$2.40$2.00$0.40

A cost starts now or later. To change it, end it (POST …/model_cost_overrides/:id/end) and create a new one. You can delete a cost only before it starts. In the dashboard, use Set your cost on a row of Settings → Model prices.

List prices are in USD, and we never convert them to another currency. For a price in another currency, record your cost in that currency or set fixed rates. Otherwise each model bills the default rate.

Set a price for one customer

A customer can have their own markup or rates. Send them on the subscription item. They go into the item's price, so a later change to the catalog price does not change them:

curl
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/subscriptions/$SUB/change \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "changes": [{
      "item": "si_XXXXXXXXXXXX01",
      "markup_percent": "10",
      "rates": [
        { "match": { "model": "large-v2", "token_type": "input" }, "unit_amount_decimal": "250" }
      ]
    }]
  }'

The keys in match must be properties in the meter's group_by. In the dashboard, open the subscription and select Edit price under the item.

Check a price on an invoice

Each usage line from the model price list has rating.segments[]. A segment names the list row it priced from:

{
  "rating": {
    "price_source": "model_price_list",
    "segments": [{
      "from": "2026-11-01T00:00:00Z",
      "to": "2026-11-15T10:00:00Z",
      "unit_amount_decimal": "0.00036",
      "markup_percent": "20",
      "markup_base": "list",
      "rate_from": "price",
      "model_price_id": "mpr_XXXXXXXXXXXX01",
      "source": "genai-prices",
      "source_version": "\"etag-0001\"",
      "source_effective_from": "2026-10-28"
    }]
  }
}

GET /v1/organization/model_prices/mpr_XXXXXXXXXXXX01 returns that row. It has two dates: source_effective_from is the date that the source gives, and effective_from is when we started to bill with it. In the dashboard, select Price source under the line.

When a list price changes

When a list price that a business uses changes, the business gets the webhook event model_prices_changed. The event lists each change, with the old and new price, and two flags: bill_moves is true when a subscription bills from the changed price, and cost_moves is true when you have no cost for the model. Members whose role can manage model prices also get an email.

If one update changes a large part of the list, sets a price to zero, or moves a price by ten times or more, we hold it for review and do not apply it. Your bills use the last prices until we release it. A released update applies from the release time.

See your margin

The margin of a usage line is its revenue minus its cost. Revenue is the line amount after discounts and credit notes, before tax. Cost is the event's own cost, else your cost, else the list price, fixed when the line is rated. A line with no cost is counted in lines_without_cost and left out of both revenue and cost.

const margin = await tmb.usage.margin(biz.id, {
  from: "2026-11-01",
  to: "2026-11-30",
  currency: "USD",
  groupBy: "model",
});
for (const row of margin.rows) console.log(row.key, row.margin_minor, row.margin_percent);

In the dashboard, see Reports → Usage margin, and the Usage page of a customer for usage by day, cost and margin so far in the open period. Both need the usage_margin:view permission.

About the price list

The list comes from genai-prices, published under the MIT license, copyright (c) Pydantic Services Inc. 2025 to present. A self-hosted install can point MODEL_PRICES_SOURCE_URL to a copy of the file, or turn the update off with MODEL_PRICES_SYNC=off.

Copyright © 2026