---
title: "Bill AI tokens"
description: "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."
canonical_url: "https://twominutebilling.com/help/developers/recipes/bill-ai-tokens"
---
# 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](https://twominutebilling.com/help/developers/recipes/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:

<code-group>

```ts [TypeScript]
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"],
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "MuseLane AI", "default_currency": "USD" }'

export BIZ=biz_…

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/meters \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "ai_tokens",
    "name": "AI tokens",
    "event_name": "ai_tokens",
    "aggregation": "sum",
    "value_property": "tokens",
    "group_by": ["model", "token_type"]
  }'
```

</code-group>

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:

<code-group>

```ts [TypeScript]
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" },
  ],
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/products \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "name": "AI usage" }'

curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "ai-usage",
    "name": "AI usage monthly",
    "model": "usage",
    "currency": "USD",
    "interval": "month",
    "meter": "ai_tokens",
    "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" }
    ]
  }'
```

</code-group>

<table>
<thead>
  <tr>
    <th>
      Combination
    </th>
    
    <th>
      Row that bills it
    </th>
    
    <th>
      Rate
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      any model, <code>
        input
      </code>
    </td>
    
    <td>
      the default
    </td>
    
    <td>
      $3.00 per 1M
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        small-v1
      </code>
      
      , <code>
        output
      </code>
    </td>
    
    <td>
      <code>
        token_type: output
      </code>
    </td>
    
    <td>
      $15.00 per 1M
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        large-v2
      </code>
      
      , <code>
        output
      </code>
    </td>
    
    <td>
      <code>
        model: large-v2, token_type: output
      </code>
      
       (names more values)
    </td>
    
    <td>
      $24.00 per 1M
    </td>
  </tr>
  
  <tr>
    <td>
      any model, <code>
        cached_input
      </code>
    </td>
    
    <td>
      <code>
        token_type: cached_input
      </code>
    </td>
    
    <td>
      $0.30 per 1M
    </td>
  </tr>
</tbody>
</table>

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.

```ts [TypeScript]
const customer = await tmb.customers.create(biz.id, {
  email: "accounts@example.com",
  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:

<table>
<thead>
  <tr>
    <th>
      Line
    </th>
    
    <th>
      Quantity
    </th>
    
    <th>
      Rate
    </th>
    
    <th>
      Amount
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      AI usage monthly · small-v1 · input
    </td>
    
    <td>
      1,234,567
    </td>
    
    <td>
      $3.00 per 1M
    </td>
    
    <td>
      370.3701 → <strong>
        $3.70
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      AI usage monthly · small-v1 · output
    </td>
    
    <td>
      456,789
    </td>
    
    <td>
      $15.00 per 1M
    </td>
    
    <td>
      685.1835 → <strong>
        $6.85
      </strong>
    </td>
  </tr>
</tbody>
</table>

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:

```ts [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:

<code-group>

```ts [TypeScript]
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
```

```bash [curl]
curl "$TMBILLING_URL/v1/organization/model_prices?model=small-v1&token_type=input" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

</code-group>

### 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.

<code-group>

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

```bash [curl]
curl -X PATCH $TMBILLING_URL/v1/businesses/$BIZ/meters/ai_tokens \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "price_list": { "model_key": "model", "token_type_key": "token_type" },
    "cost_currency": "USD"
  }'
```

</code-group>

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:

<code-group>

```ts [TypeScript]
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" },
  ],
});
```

```bash [curl]
curl -X POST $TMBILLING_URL/v1/businesses/$BIZ/prices \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "product": "ai-usage",
    "name": "AI usage, list price plus 20%",
    "model": "usage",
    "currency": "USD",
    "interval": "month",
    "meter": "ai_tokens",
    "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" }
    ]
  }'
```

</code-group>

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:

<table>
<thead>
  <tr>
    <th>
      Line
    </th>
    
    <th>
      Quantity
    </th>
    
    <th>
      Unit price
    </th>
    
    <th>
      Amount
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      small-v1 · input, to 15 Nov 10:00
    </td>
    
    <td>
      1,234,567
    </td>
    
    <td>
      $3.60 per 1M
    </td>
    
    <td>
      444.44412¢ → <strong>
        $4.44
      </strong>
    </td>
  </tr>
  
  <tr>
    <td>
      small-v1 · input, from 15 Nov 10:00
    </td>
    
    <td>
      456,789
    </td>
    
    <td>
      $3.00 per 1M
    </td>
    
    <td>
      137.0367¢ → <strong>
        $1.37
      </strong>
    </td>
  </tr>
</tbody>
</table>

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"`:

<code-group>

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

```bash [curl]
curl -X POST $TMBILLING_URL/v1/organization/model_cost_overrides \
  -H "Authorization: Bearer $TMBILLING_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "small-v1",
    "token_type": "input",
    "currency": "USD",
    "unit_amount_decimal": "200",
    "quantity_divisor": 1000000,
    "note": "Annual contract"
  }'
```

</code-group>

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

<table>
<thead>
  <tr>
    <th>
      <code>
        markup_base
      </code>
    </th>
    
    <th>
      Customer pays per 1M
    </th>
    
    <th>
      Your cost per 1M
    </th>
    
    <th>
      Margin per 1M
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        list
      </code>
    </td>
    
    <td>
      $3.60
    </td>
    
    <td>
      $2.00
    </td>
    
    <td>
      $1.60
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        cost
      </code>
    </td>
    
    <td>
      $2.40
    </td>
    
    <td>
      $2.00
    </td>
    
    <td>
      $0.40
    </td>
  </tr>
</tbody>
</table>

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**.

<note>

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.

</note>

## 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:

```bash [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:

```json
{
  "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.

<code-group>

```ts [TypeScript]
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);
```

```bash [curl]
curl "$TMBILLING_URL/v1/businesses/$BIZ/reports/usage_margin?from=2026-11-01&to=2026-11-30&currency=USD&group_by=model" \
  -H "Authorization: Bearer $TMBILLING_API_KEY"
```

</code-group>

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](https://github.com/pydantic/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`.


## Sitemap

See the full [sitemap](https://twominutebilling.com/sitemap.md) for all pages.
