Skip to content
NEWClaude Opus 5.5 and GPT-6 Sol / Luna are live — Opus 5.5 at $4 / $20 per 1M tokens (20% below Opus 5), GPT-6 Sol at $2 / $10 and Luna at $0.10 / $0.50, all at official list priceView pricing
HopBase
← Back to blog

LLM API Usage Reconciliation and Department Chargeback | HopBase

The short answer: reconciling an LLM API bill takes three layers of data you can recompute yourself, not a vendor's promise of "transparent billing". Layer one is the individual task: on HopBase, once a video or asynchronous image task finishes, its query response carries usage.cost, the amount actually deducted from your balance for that task, and usage.currency; an uncharged failure reports 0, and one curl command checks it. Layer two is the itemised record: the console Usage page exports a CSV filtered by date, key, member, department or key creator, and every row carries the official cost, discount, exchange rate and charged amount to verify official cost × discount ÷ exchange rate = charged amount line by line. Layer three is allocation: every key shows its creator, per-key spending charts exist, department managers see and manage their own department's keys and usage, and quotas can be set per key, member and department. With all three in place, a department chargeback is just a filter and an export.

Why LLM bills are hard to reconcile

A company that runs chat, image and video models at the same time deals with at least four billing units: tokens for chat, images for most image models, seconds or video tokens for video, and flat per-call fees for a few others. Add a handful of practical problems and the numbers stop lining up:

  • Asynchronous tasks are priced when they end. A video submission only carries an estimate; the real charge is known once the task finishes, and vendors differ on whether and how much a failed task costs.
  • Failed requests. A failed request is normally free, but when the client disconnects after output has already been generated, the generated part is usually billed. If the bill does not say so, you get the classic "it errored and I was still charged" dispute.
  • Currencies. Official list prices are mostly in US dollars while the account balance may be kept in another currency. Without the exchange rate in between, nobody can recompute the charge.
  • Shared keys. Once several projects share one key, nobody can tell afterwards who spent what.

So the goal is concrete: every charge should trace back to a task or request, a key, the person who created that key and a department, and the amount should be recomputable from published list prices.

Layer 1: the actual charge, straight from the task query

Once an asynchronous task (video, async image and so on) is completed, failed or cancelled, the single-task query response gains a top-level usage object:

  • usage.cost is the amount actually deducted from your balance for that task — the same number as in the usage record;
  • usage.currency is your balance currency, currently CNY. Do not assume US dollars; read this field;
  • a failed task that was not charged reports 0; in the rare case an image task fails after its charge was settled (for example, the model returned no usable image), it reports the real charge;
  • the field is absent while the task is running, and only the key that created the task sees it.

It applies to GET /v1/video/tasks/{task_id} (Seedance, Grok Imagine, Kling, MiniMax, Midjourney), GET /api/v1/tasks/{task_id} (Wan, HappyHorse) and GET /v1/images/tasks?task_id=... (GPT Image and Gemini image async tasks). Checking it takes one command:

# Query with the key that submitted the task; jq keeps only status and cost
curl -s https://api.hop-base.com/v1/video/tasks/$TASK_ID \
  -H "Authorization: Bearer $HOPBASE_API_KEY" \
  | jq '{status: (.task.status // .status), usage}'

# Once the task has finished you get something like (amount illustrative):
# { "status": "completed", "usage": { "cost": 2.4, "currency": "CNY" } }

In a Seedance response, task.usage is the billable token count and the top-level usage.cost is the money — do not mix them up. For MiniMax and Wan, which already return a top-level usage, cost and currency are merged into it. The practical benefit: your own system can write the real charge into its order or job record the moment the task ends, instead of polling the console or booking the submission estimate as cost.

Layer 2: itemised records you can recompute row by row

Synchronous chat requests create no task and their responses carry token counts only, so the money lives in the usage records. The export button on the console Usage page downloads a CSV that follows the filters on the page (date range, key, member, department, key creator), up to 50,000 rows and 400 days per file. Each row has nine columns:

  1. time, model, usage;
  2. official list-price currency, official cost in that currency;
  3. discount, exchange rate, charged amount in the balance currency;
  4. note.

Read left to right, four of those columns are the verification formula itself: official cost × discount ÷ exchange rate = charged amount. When the list-price currency and the balance currency are the same, the exchange-rate cell is empty and the formula becomes official cost × discount = charged amount. You can compute the official cost from the vendor's own price page, and the discount is your plan group's discount, so every row can be checked independently without trusting anyone's totals. Early records without a list-price snapshot leave those columns empty.

Failed requests are noted as "Request failed, not charged"; a request interrupted after output was generated is noted as "Request interrupted, charged for the usage already generated", and the end of the file counts both. Those are exactly the rows that cause disputes, so stating the rule in the record beats explaining it afterwards. For automation, GET /v1/usage with an API key returns the balance and that key's quota usage.

Layer 3: allocating by key and by department

Allocation starts with one key per cost centre. HopBase enterprise accounts have members and departments, and a few facts decide whether allocation actually works:

  • Every key shows its creator. Keys a member created and keys the owner or a department manager created and assigned to a member stay distinguishable, and exports can filter by creator.
  • Spending charts per key. The console shows spending trend, request count and model mix per key, so unusual spend points straight at a key.
  • Department manager scope. A department manager sees and manages the keys and usage of members in their own department, can create keys for those members, and cannot see other departments. The owner sees the whole company. When a member moves department, the scope follows.
  • Layered quotas. Quotas can be set per key, per member and per department, and hitting any of them blocks the request with a 402 whose message says whether the key, member or department quota ran out — not a vague "insufficient balance".

A monthly department chargeback is then: pick the department and the month on the Usage page, export. Each department gets its own itemised file and can recompute it with the formula above.

A reconciliation checklist you can use on any vendor

  1. After an async task ends, can you get its actual charge from the API — not an estimate or a base cost?
  2. Can you export itemised records with official cost, discount and exchange rate, and recompute each row?
  3. Are failed requests and "interrupted after output was generated" requests labelled separately?
  4. Does every amount state its currency, and is the exchange rate visible when list-price and balance currencies differ?
  5. Can you filter exports by key, member and department, and does each key record its creator?
  6. Can quotas be layered, and does a quota error tell you which layer ran out?
  7. Are you actually getting the model you pay for? Test that yourself with our model fidelity verification method.

Every item can be checked with curl or an export file, which makes the list usable against any vendor — including us.

FAQ

Is usage.cost in the task query the same as the charged amount in the console?

Yes. usage.cost is the charged amount from that task's usage record — the amount by which your balance actually went down — so you can match it one-to-one against the exported records.

Why is there no usage.cost while the task is still running?

The final charge of an async task is only known when it ends; any number returned earlier could only be an estimate. The field therefore appears once the task is completed, failed or cancelled. While it runs, keep polling as before.

Are failed tasks charged?

Failed video tasks are never charged, and the query reports usage.cost as 0. In rare cases an image task fails after its charge was settled; the query then reports the real charge, and the same amount appears in your usage records. Failed synchronous requests are free; a request interrupted after output was generated is charged for what was generated and labelled as such in the export.

Why does another key not see the cost of my task?

The cost is returned only to the key that created the task. That keeps the "one key per cost centre" boundary intact and stops other keys on the same account from seeing spend that is not theirs.

Do chat requests have a per-request amount too?

Chat responses carry token counts but no amount. The per-request amount is in the console usage records, the CSV export lets you recompute each row, and filtering plus export gives you totals per key or department.