AI Governance

Building an AI Landing Zone on Azure — Part 5: Turning tokens into euros

Raw usage records are not a bill. This is the rate card as data, cost computed at query time, the cached/cache-write/reasoning token nuances, and the report that gave finance a monthly AI bill per cost centre.

This is part 5 of a five-part series on building an AI Landing Zone on Azure. Part 1: Why every enterprise needs an AI gateway · Part 2: The platform underneath · Part 3: One front door for many models · Part 4: Metering every call · Part 5: Turning tokens into euros (this post)

Turning tokens into euros: the rate card as data and cost computed at query time

At the end of part 4 the pipeline produces one record per AI call, with the product, subscription, model and exact consumption. This final part is about the last mile: turning those records into a number in euros that a finance team will accept, per cost centre, per month, and keeping that number correct as prices and models change.

It is the least "infrastructure" part of the series and, in terms of what the client actually valued, probably the most important.

Principle: store usage, never cost

The single design rule for this layer is that the pipeline never stores a price or a cost on a usage record. Records hold tokens and pages. Prices live in a separate rate card. Cost is computed when someone looks at the report.

This sounds like an academic distinction until the first time a price changes. Microsoft adjusted rates during this build, a new model generation arrived with a pricing dimension that did not exist before, and one workload moved to a different deployment with a different rate. In a pipeline that computes cost at ingestion time, each of those is a data migration and a debate about whether to re-price history. In this design each of those is one new row in a table, and history re-prices itself on the next refresh.

The rate card as data

The rate card is a second Cosmos DB container, model-pricing, partitioned by model. Each row is a model plus everything needed to price it:

Field Meaning
model The join key, matched against the model name on the usage record
CalculationMethod tokens (input / cached / cache-write / output), tokens_total (only a total is known), or pages
CostUnit The divisor: one million for token rates, one for per-page
CostPerInputUnit, CostPerCachedInputUnit, CostPerCacheWriteUnit, CostPerOutputUnit, CostPerTotalUnit, CostPerPageUnit The rates that apply for that calculation method
Currency EUR
effectiveFrom The date this row becomes the applicable rate
isActive Whether the row is in use

The rate card is a JSON file in the repository, seeded into Cosmos by a small idempotent script that the deploy pipeline runs on the main branch. Changing a price is a merge request to a JSON file. That gives the rate card the same review, history and approval trail as the infrastructure.

Two decisions in that table were deliberate. The rates are in euros, at the Azure Data Zone / EU pricing for the deployments in use, not the USD list prices that most examples use, because a chargeback report in the wrong currency is a report nobody trusts. And effectiveFrom exists so that a new rate can be entered before it applies and old usage is still priced at the rate that was valid when it happened.

Computing cost at query time

The report is Power BI, reading both Cosmos containers. The cost measure iterates over usage records, looks up the most recent active rate row for that model with an effectiveFrom on or before the record's date, and then branches on the calculation method:

  • For tokens: uncached input tokens at the input rate, cached input tokens at the cached rate, cache-write tokens at the cache-write rate, output tokens at the output rate, all divided by the cost unit.
  • For tokens_total: total tokens at the total rate. This is used for streaming aggregates and for embeddings, where there is no meaningful input/output split.
  • For pages: pages processed at the per-page rate.

That is the entire pricing engine, one measure of about forty lines of DAX. Everything else in the report, cost per product, per subscription, per model, per day, is a slice of that measure.

Example:

{
    "BaseCost": 0,
    "CalculationMethod": "tokens",
    "CostPerCacheWriteUnit": 0.24,
    "CostPerCachedInputUnit": 0.02,
    "CostPerInputUnit": 0.19,
    "CostPerOutputUnit": 1.14,
    "CostPerPageUnit": 0,
    "CostPerTotalUnit": 0,
    "CostUnit": 1000000,
    "Currency": "EUR",
    "deploymentName": "gpt-5.6-luna",
    "effectiveFrom": "2026-08-01",
    "id": "11",
    "isActive": true,
    "model": "gpt-5.6-luna",
    "notes": "Azure OpenAI ChatGPT 5.6 Luna Data Zone Standard, EUR. Rates per 1,000,000 tokens.",
    "region": "WestEurope"
  },

The nuances that matter

Getting from "tokens times rate" to a number finance will accept involved a handful of details that I have not seen covered in any of the reference material.

Cached input tokens are not input tokens. Modern models report how many of the prompt tokens were served from a prompt cache, and those are billed at a fraction of the input rate, roughly an order of magnitude cheaper. If you price all prompt tokens at the input rate you overcharge every workload that reuses a long system prompt, which is most of them. The usage fragment captures cached tokens separately, and the measure subtracts them from the prompt count before applying the input rate.

Cache writes became a separate price mid-project. A newer model generation introduced a distinct rate for writing to the prompt cache. Because the record already had a slot for cache-write tokens and the rate card had a column for it, supporting that was a one-row change. Had cost been computed at ingestion time, it would have been a code change plus a backfill.

Reasoning tokens are output tokens. Reasoning models report the tokens spent thinking separately from the tokens in the visible answer. They are billed at the output rate, and they are already included in the output total the API returns. The record carries them as a separate field for visibility, because a team asking "why did our bill double" usually gets the answer "you turned reasoning effort up", but they are not priced twice.

Streaming estimates are priced as an upper bound. Routes that stream without buffering produce aggregate records with total tokens only. The rate card has a dedicated streaming row that prices all of those tokens at the output rate. That deliberately over-charges, because output is more expensive, and I would rather see a conservative estimate that pushes them to a buffered route if we care about accuracy than an optimistic one that surprises me later.

Model names need normalising. The API returns dated model names such as gpt-5-mini-2025-08-07, while the rate card is keyed on gpt-5-mini. The report strips the date suffix into a modelKey before the join. And when a new model appears without a rate row, it prices at a default rate and shows up on a "models without a rate card" visual so someone adds the row, rather than silently pricing at zero.

What the report shows

The dashboard has a small number of pages. The landing page is cost per product per month, because that is the chargeback view. Behind it are cost per subscription (the individual consumers inside a product), cost and volume per model, a daily trend, and a token-mix breakdown showing input versus cached versus output, which is where the optimisation conversations start.

The audience is both engineers and the finance controller who allocates the Azure bill to cost centres, and the product owners who the costs are being allocated to. For the engineers it gives them inside into their own usage for awareness.

The whole series in one paragraph

An AI Landing Zone is a private, identity-first platform (part 2) with a single API Management front door in front of every model (part 3) that meters every call into a queryable store (part 4) priced by a rate card that lives in version control (part 5). It exists because AI adoption is happening anyway, and the choice is between a paved road and several OpenAI resources nobody can account for (part 1). The build was more work than the diagram suggests, most of the difficulty was in the "private everywhere, no shared keys" constraints rather than the AI itself, and the result changed how the organisation makes decisions about AI spend.

Get in touch

I am packaging this landing zone, the network and platform, the gateway with its policy library, the metering pipeline and the chargeback report, as a Terraform-based accelerator that can be deployed into an existing Azure estate. If you are a platform lead or architect at an organisation that needs AI adoption to be governed, private and accountable, especially in financial services or another regulated sector, I would be glad to talk through what it would take in your environment.

Connect with me on LinkedIn, or reach out via ascode.nl. And thank you for sticking with me through all five parts.

Written by

Erik Christiaans

Independent cloud and AI platform architect in Velsen, NL. Twenty years of Azure, AWS, Entra ID and Kubernetes for insurers, banks and other regulated enterprises - and the control mappings that make those platforms defensible.

Book a 30-minute call