Spend · Provider spend

See what your AI cost across every provider

Provider spend puts real metered LLM spend in one ledger, from two lenses RepoOps never adds together: what each connected provider says it billed your organization, and what this install captured locally. Every provider carries a precision band, and one you have not connected reads not connected, never $0.00.

For: the engineer who wants one place to read AI spend, and the owner who has to explain the number to finance

What it does, and why it helps

Provider spend reads two things RepoOps can already see and lays them out in one ledger. The provider-reported lens pulls the cost API of each connected provider that publishes one: Anthropic, OpenAI, AWS Bedrock and Azure OpenAI. That figure is the bill your whole organization was charged. The locally-captured lens sums the LLM-call rows this install wrote into each tracked repository's .claude/brain/ai-calls day files. That figure covers only the calls this machine saw. The page shows both side by side and never adds them, because one is org-wide and the other is one install, so neither is a subset of the other.

Every provider carries a precision band saying how exact its number can be: per-token for Anthropic and OpenAI, day-level for Bedrock and Azure, per-call for OpenRouter and Cloudflare AI Gateway. Those last two publish no cost API at all, so their cards read captured only rather than inviting you to connect something that does not exist. A provider with no credential reads not connected, and a connected provider with nothing in the window reads no data. Neither is rendered as $0.00. The page flags nothing. It is a display, and recomputing what a day should have cost and arguing with the invoice is Billing Guard's job.

The pain. Spend arrives in six places, in six shapes. Adding six dashboards gives a number that looks precise and is not: two of the six publish no cost API, two report a day bucket with no model split, and an account nobody connected contributes a zero that means nothing was measured.

The point of view. A spend figure is worth only as much as its scope and its precision, so both travel with it. Two numbers that measure different things stay two numbers. A provider with no data reads as a blank, not as zero.

What gets easier. Answering what AI cost this month without opening six consoles. The card grid gives each provider a connection state, a precision band, its reported cost and its captured cost with a call count, and the day-by-day table lists every line the ledger produced with its lens and band.

When it helps. Month end, a budget question, or a check that a provider you believe is connected still is. It needs the desktop app running, and for the reported lens at least one provider credential on this machine.

Its limits. It reconciles nothing and flags nothing. OpenRouter and Cloudflare AI Gateway have no cost API, so they carry captured spend only, counted forward from the first call RepoOps saw. Bedrock and Azure report day buckets with no per-model split. And the capture path marks a rate-card estimate with an estimated flag on the stored row, but this page renders no estimate marker, so an estimate and a gateway figure look alike here.

Understand it in 30 seconds

30.1 s, captions on. Narration: Microsoft Zira Desktop (provisional voice; an approved narration source is pending).Transcript
Read the narration
  1. 0:00 Six providers, six dashboards.
  2. 0:03 So someone adds them up and guesses the total.
  3. 0:06 RepoOps shows two numbers instead of one.
  4. 0:09 The bill, and what this machine saw.
  5. 0:13 Each provider carries a precision band: per-token, day-level, or per-call.
  6. 0:18 A provider you never connected reads not connected, never zero dollars.
  7. 0:23 Read the number and its band.
  8. 0:25 Billing Guard is where you dispute it.

Synthetic example. Read the guide

Where to find it

Where to find it

  • Desktop: localhost:4000, then LLM Cost Metrics in the sidebar, then Provider spend under All tools, in the Evidence readers group.
  • Hosted: desktop only.
  • Keyboard: ⌘ K, then type “Provider spend”.

When to use it

Month end, and someone wants one number

Situation. Anthropic and OpenAI are connected, Bedrock and Azure are not, and the team also runs brain embeddings through OpenRouter.

What you do. Open Provider spend. Read the two lens cards, then the card grid, then the Notes list under them.

What you see. Provider-reported carries a dollar total and Locally-captured carries a second one, with a line under both saying they cover different scopes. The Bedrock and Azure cards read not connected, and a note names each and says connecting its cost API would populate the authoritative lens. OpenRouter reads captured only.

What it establishes. You can state what the connected providers billed the organization, what this install attributed, and which providers the first figure leaves out. You cannot state a single total, and the page will not let you pretend otherwise.

A provider you thought was connected

Situation. The Azure credentials were rotated and only three of the four variables were set again on this machine.

What you do. Read the Azure OpenAI card's Connection row, then the Notes list at the bottom of the page.

What you see. The card reads not connected and its Provider-reported row reads not connected too. A note says Azure OpenAI is not connected and no provider-reported cost is available. Nothing renders as $0.00.

What it establishes. The gap is visible. You know the reported total excludes Azure rather than believing Azure cost nothing this month.

Before you start

Supported versions
RepoOps desktop v0.3.1, the release this guide was read against. No daemon and no scheduled job is needed: the tab reads GET /api/metered-ledger while it is open and computes from stores that already exist.
Where it runs
Local only. Desktop: LLM Cost Metrics, then Provider spend under All tools, in the Evidence readers group. Hosted: repoops.ai/team/metered-spend, under All tools on LLM Cost Metrics, is a desktop-only pointer page with no data behind it, because the cost keys and the captured rows are per-machine and are not streamed to the team database.
Permissions
None on the route. The localhost aggregator gates no aggregator route, so anyone who can open localhost:4000 on this machine can read the ledger. The route never reads, logs or returns a provider key: keys are resolved inside the provider clients.
Connections
For the reported lens, one or more of: an Anthropic Admin key, an OpenAI admin key, AWS credentials (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY and AWS_REGION together, with Cost Explorer read permission), or an Azure service principal (all four AZURE variables, Cost Management Reader). The captured lens needs no key at all.
Plan
No plan gate. Provider spend is a local desktop tab and the pricing capability map carries no row for it.

Configure it

  1. Open the tab and leave it open.

    Desktop, LLM Cost Metrics, then Provider spend under All tools. The page polls every 60 seconds while it is visible and pauses while it is hidden, so nothing runs when the tab is closed. The route holds its result for five minutes by default, so a poll is cheap.

  2. Connect Anthropic or OpenAI from Billing Guard.

    Billing Guard carries the two paste boxes, Connect Anthropic account and Connect OpenAI account. RepoOps checks the key with the provider before it stores it, and stores it in the data home behind that directory's owner-only file permissions, not in an OS keychain. It must be an Anthropic Admin key rather than a regular API key, and an OpenAI admin key rather than a project key. A key already set in this machine's environment always wins over a stored one.

  3. Set Bedrock and Azure credentials in the environment.

    Neither is offered a paste box, because both take a multi-variable credential set that one box cannot honestly represent. Bedrock needs all three AWS variables and Azure needs all four AZURE ones, in the data directory's .env. Miss one and the provider stays not connected.

  4. Leave OpenRouter and Cloudflare AI Gateway alone.

    Neither publishes a cost API, so their cards read captured only, a state rather than a task. The only way their figures move is captured traffic. For OpenRouter on this install that means brain embeddings, which capture the gateway's own per-call cost when REPOOPS_BRAIN_EMBED_PROVIDER is set to openrouter. Cloudflare AI Gateway has no live capture path here.

  5. Choose the window.

    The tab asks for the default, a trailing 30 days ending today, and both lenses cover the same span. For a different span, call GET /api/metered-ledger with from and to as YYYY-MM-DD. A bound that is not a real day, such as 2026-13-45, falls back to its default rather than failing the request.

  6. Narrow to one repository if you need to.

    The dashboard shell hands the tab its repository selection and the route accepts it. It narrows the captured lens only. The Notes list says which repositories were counted and repeats that the reported lens is the org-wide bill and carries no repository dimension.

SettingWhereA sensible choiceWhy it matters
ANTHROPIC_ADMIN_KEYthe data directory's .env, or Billing Guard's Connect Anthropic account boxset it if you want Anthropic's org-wide bill in the reported lensWithout it the Anthropic card reads not connected. An Admin key reads organization usage and cost; a regular API key cannot.
OPENAI_ADMIN_KEYthe data directory's .env, or Billing Guard's Connect OpenAI account boxan organization admin key, not a project keyOnly an admin key can read organization usage and cost. Reading a large organization has not been exercised against a real account, so treat the first pull as the check.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, AWS_REGIONthe data directory's .envall three, or leave Bedrock unconnectedBedrock counts as configured only when all three are set. The pull needs Cost Explorer read permission.
AZURE_TENANT_ID, AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_SUBSCRIPTION_IDthe data directory's .envall four, or leave Azure unconnectedAzure counts as configured only when all four are set. Three out of four reads exactly like none.
from, toGET /api/metered-ledger query parametersleave both unset for a trailing 30 daysBoth lenses always cover the same span. A malformed bound is dropped back to its default, not rejected.
repo, reposthe dashboard shell's repository selection, passed through to the routeall tracked repositories unless you are attributing oneThe selection narrows the captured lens only; repos wins when both are present.
REPOOPS_METERED_LEDGER_TTL_MSthe data directory's .env300000 (the default, five minutes)How long a computed ledger is reused before the provider pulls run again. 0 recomputes on every request.
REPOOPS_METERED_LEDGER_BUDGET_MSthe data directory's .env2000 (the default)How long a request waits for a cold compute before answering with the warming shell while the real compute finishes in the background.
REPOOPS_BILLING_TTL_MSthe data directory's .env300000 (the default)The memo the provider billing clients share in front of their cost and usage reports. Errors are never cached.
REPOOPS_BRAIN_EMBED_PROVIDERthe data directory's .envopenrouter if you want OpenRouter embed spend in the captured lensIt is the one live OpenRouter capture path on this install; with any other embed provider the OpenRouter card stays at zero captured calls.
ⓘ
To stop or undo
Press Disconnect on the provider's card in Billing Guard to remove a stored key from this machine, or unset its environment variables and restart. A key set in the environment cannot be removed from the app, and the card says so instead of offering a button that would not work. Closing the tab stops the 60-second poll. Your local records stay either way.

What you should see

A fresh install with nothing connected

Configuration. No Admin key, no AWS or AZURE variables, and little or no captured traffic.

Expect. Provider-reported reads not connected where a dollar figure would go. All six provider cards still render: four read not connected and two read captured only. The day-by-day table is replaced by an explanation of what will fill it.

Verify. The Notes list names each unconnected provider and says connecting its cost API would populate the authoritative lens. No card shows a fabricated zero on the reported side.

Anthropic connected, a month of traffic

Configuration. The Anthropic account connected in Billing Guard, or ANTHROPIC_ADMIN_KEY set in the environment.

Expect. Provider-reported carries a dollar total. The Anthropic card reads connected with a per-token band, its reported cost, and its captured cost with a call count. The day-by-day table carries reported org lines and captured local lines over the same days.

Verify. The reported figure is the sum of Anthropic's own cost report across the window. If it differs from Billing Guard's Anthropic number, check REPOOPS_ANTHROPIC_ADMIN_WORKSPACE_ID: Billing Guard passes it to the Anthropic pull and this route does not, so a workspace-scoped Billing Guard reads a narrower bill on purpose.

Bedrock or Azure connected

Configuration. All three AWS variables, or all four AZURE variables.

Expect. The card reads connected with a day-level band. Its lines are day buckets carrying no model name, because the cost API returns a daily total with no per-model token counts and the ledger emits the unattributed remainder as a line with no model on it.

Verify. A note says the provider's cost is day-level only and that no per-model token breakdown is available from its cost API. The day totals still add up to the authoritative figure for that provider.

Data and cost

What is captured
Nothing new is captured for this page. It reads two stores that already exist: each tracked repository's .claude/brain/ai-calls day files for the captured lens, and each connected provider's cost report for the reported lens. The captured rows come from RepoOps' own BYOK Anthropic surfaces (chat, analyze, diary, dream, graph edges, entity extract, plan and pull-request description, each recorded only when the call returned real usage) and from OpenRouter embeddings.
Who can see it
Anyone who can open localhost:4000 on this machine. The route has no auth and no role check, which matches every aggregator route. The hosted team page for this feature is a pointer with no data behind it, so nothing here reaches the team database.
How long it is kept
Not provided as a knob. The ai-calls day files sit in each tracked repository's brain and no sweep prunes them, so shortening the history means deleting the files. Provider reports are not stored at all: they are pulled per window and held in a memo for REPOOPS_METERED_LEDGER_TTL_MS, default 300000, which clears on restart.
What leaves the machine
Read-only cost and usage report requests to the providers you connected, and nothing else. Nothing is sent to RepoOps servers. An unconnected provider generates no outbound request, and one provider's failure never takes the others down: the failure lands in a warning on the page and that provider is left out of the window rather than guessed.
What it costs
RepoOps makes no model call for this page. The only outbound requests are the read-only cost and usage report pulls to the providers you connected, and each provider prices those under its own terms. The captured figures are read from rows that were already written when those calls happened, so reading the page costs nothing new.

When the result differs

SymptomLikely causeNext action
Every provider card reads not connected.No Admin key or credential set resolves on this machine.Connect Anthropic or OpenAI in Billing Guard, or set the AWS or AZURE variables in the data directory's .env and restart the app.
A card reads captured only.OpenRouter and Cloudflare AI Gateway publish no cost API, so there is nothing to connect.Read the captured figure instead. It counts forward from the first call RepoOps saw, not from past usage.
A connected card reads no data.The provider answered, and reported no cost in this window.Widen the window with from and to, or check the Notes list, which says the provider is connected but reported nothing.
A banner says the provider pulls are still gathering.The first read of this window ran past REPOOPS_METERED_LEDGER_BUDGET_MS, default 2000, so the route answered with an empty shell while the real compute continued in the background.Wait for the next 60-second poll. The shell is never cached, so the next read serves the real ledger.
A red line says a provider pull did not complete.That provider's cost or usage request failed.Read the message, fix the credential or the permission, and refresh. The rest of the ledger still rendered; the failed provider's reported lens was left out rather than guessed.
The reported total differs from Billing Guard's.Billing Guard passes REPOOPS_ANTHROPIC_ADMIN_WORKSPACE_ID to the Anthropic pull and this route does not.Compare with that variable unset, or read the difference as the workspace narrowing Billing Guard applies and this page does not.
Selecting a repository moved only one figure.By design. The selection narrows the captured lens; the reported lens is the org-wide bill and has no repository dimension.Read the first note, which names the repositories the captured lens covered and says the reported lens is unchanged.
Disable
Press Disconnect on the provider's Billing Guard card to remove a stored key, or unset its environment variables and restart. An environment-set key is not RepoOps' to remove, and the card says where it comes from instead. Closing the tab stops the poll; there is no schedule to turn off.
Roll back
Not provided, and nothing to roll back to. The ledger is computed from the two stores on every read rather than stored, so disconnecting a provider returns its card to not connected on the next read and changes nothing else.
Revoke access
Disconnect removes the stored key from this machine and leaves your local records alone. Revoking the credential itself happens where it was issued: the Anthropic Console, the OpenAI admin keys page, AWS, or Azure. RepoOps cannot revoke a key it did not mint.
Delete
Not provided. No route deletes a captured row or a provider report. The captured rows are the day files at .claude/brain/ai-calls in each tracked repository, and deleting a file is the only way to remove its spend from the captured lens. Provider reports are not persisted beyond the route's memo.

Maintenance evidence

Feature id
metered-spend (spine leaf metered-spend)
Owner
Cross-provider metered-spend ledger program (PRs 1 to 4 plus FF1 to FF4, docs/features/cross-provider-metered-ledger.md; the Provider spend label rename, LDG-0636). Guide: LDG-0717.
Supported product version
RepoOps v0.3.1
Last verified
2026-09-15, read against origin/main at 52366bb6d; labels read from the served tab source (public/metered-spend.html and public/billing-guard.html), the credential variables read from lib/provider-credentials.mjs, lib/bedrock-billing.mjs and lib/azure-billing.mjs, the defaults read from lib/routes/metered-ledger.mjs. Not exercised against a running instance.
Example fixtures
No fixture file. lib/metered-ledger.test.mjs holds inline call rows and provider cost reports covering both lenses, the never-summed totals, the honest nulls, the precision bands and the window filter. lib/routes/metered-ledger.test.mjs writes a temporary repository whose .claude/brain/ai-calls day file feeds the captured lens, and injects provider failures to exercise the warnings passthrough and the memo bound. lib/gateways/embed-cost-capture.test.mjs covers the OpenRouter capture and its rate-card fallback.
Source references
lib/metered-ledger.mjs, lib/routes/metered-ledger.mjs, lib/billing-guard-providers.mjs, lib/provider-credentials.mjs, lib/bedrock-billing.mjs, lib/azure-billing.mjs, lib/ai-calls.mjs, lib/telemetry-chat-api.mjs, lib/gateways/embed-cost-capture.mjs, public/metered-spend.html, public/billing-guard.html
Documentation review
Independent review requested on the slice pull request; not yet recorded.
Video review
Narrated story rendered and published 2026-09-26 (render c23a1ab0c359, LDG-1012) with the breadcrumb LLM Cost Metrics, checked against main at b0bb02812. Six frames, the captions and the transcript were reviewed by the authoring agent, not an independent reviewer; the audio was not listened to by a person. Narration is the provisional Windows voice until LDG-0721.

Last updated