Find what drove an AI API bill increase before changing models or adding a proxy. This guide compares two complete weeks and ranks cost changes by provider, model, processing mode and charge type.
You will prepare a small CSV and run a local Python script. It sends no API requests, needs no API key and does not collect prompts. It reviews past charges, not live spending limits.
What you need
- Python 3.10 or later. Check with
python --version,python3 --versionorpy -3 --version. - Read access to your billing reports and fourteen complete reporting days.
- A private working folder and a copy of the original export.
1. Pick two complete weeks
Choose an end date your billing source considers complete. This example compares 3–9 October with 10–16 October 2026, matching the weekdays.
Record the source, account filters, currency, timezone and export time. Keep subscription allowances, direct API charges and cloud-marketplace bills separate. If reporting-day boundaries differ, audit providers separately unless you can normalize timestamped records correctly.
2. Prepare the CSV
The script does not import provider exports automatically. Map a copy into costs.csv with exactly these columns:
date,provider,model,mode,charge_type,currency,cost_usdUse one row per day, provider, model, mode and charge type. Aggregate project or workspace subdivisions once. Never combine their totals with overlapping organization totals.
| Field | Required value |
|---|---|
| date | Reporting day as YYYY-MM-DD. |
| provider, model | Billed provider and model. Use not_applicable for charges without a model. |
| mode | Billed mode, or unknown. Do not assume Standard. |
| charge_type | A consistent category such as tokens, search or storage. |
| currency | USD only. Do not relabel another currency. |
| cost_usd | Non-negative dollar charges, not cents or token counts. Reconcile credits, refunds and tax separately. |
Check source units before mapping. Anthropic’s Cost API reports decimal strings in cents: 12.5 cents becomes USD 0.125. That endpoint excludes Priority Tier costs. These rules do not automatically apply to dashboard CSVs or other providers.
Exclude prompts, responses, keys and personal identifiers. Spend metadata can still be sensitive; use non-sensitive grouping labels.
3. Try the fictional sample
Save this as sample-costs.csv. These are invented charges, not model prices or measured savings.
date,provider,model,mode,charge_type,currency,cost_usd
2026-10-03,demo,demo-model,standard,tokens,USD,0.10
2026-10-04,demo,demo-model,standard,tokens,USD,0.10
2026-10-05,demo,demo-model,standard,tokens,USD,0.10
2026-10-06,demo,demo-model,standard,tokens,USD,0.10
2026-10-07,demo,demo-model,standard,tokens,USD,0.10
2026-10-08,demo,demo-model,standard,tokens,USD,0.10
2026-10-09,demo,demo-model,standard,tokens,USD,0.10
2026-10-10,demo,demo-model,standard,tokens,USD,0.20
2026-10-11,demo,demo-model,standard,tokens,USD,0.20
2026-10-12,demo,demo-model,standard,tokens,USD,0.20
2026-10-13,demo,demo-model,standard,tokens,USD,0.20
2026-10-14,demo,demo-model,standard,tokens,USD,0.20
2026-10-15,demo,demo-model,standard,tokens,USD,0.20
2026-10-16,demo,demo-model,standard,tokens,USD,0.804. Run the audit
Save this as audit_costs.py in the same folder. It validates the CSV, uses decimal arithmetic and prints the largest dollar increases first. It makes no network calls and writes no files.
import csv
import sys
from collections import defaultdict
from datetime import date, timedelta
from decimal import Decimal, InvalidOperation
FIELDS = ["date", "provider", "model", "mode", "charge_type", "currency", "cost_usd"]
def audit(path, end_text):
end = date.fromisoformat(end_text)
first = end - timedelta(days=13)
split = end - timedelta(days=6)
expected = {first + timedelta(days=i) for i in range(14)}
dates, keys = set(), set()
totals = defaultdict(lambda: [Decimal("0"), Decimal("0")])
with open(path, newline="", encoding="utf-8-sig") as handle:
reader = csv.DictReader(handle)
if reader.fieldnames != FIELDS:
raise ValueError("Use exactly the seven documented CSV columns, in order.")
for line, row in enumerate(reader, 2):
if None in row or any(value is None for value in row.values()):
raise ValueError(f"Line {line}: wrong number of columns.")
row = {key: value.strip() for key, value in row.items()}
if any(not value for value in row.values()):
raise ValueError(f"Line {line}: blank fields are not allowed.")
day = date.fromisoformat(row["date"])
if day.isoformat() != row["date"]:
raise ValueError(f"Line {line}: use YYYY-MM-DD dates.")
if day not in expected:
raise ValueError(f"Line {line}: date is outside the selected 14 days.")
if row["currency"] != "USD":
raise ValueError(f"Line {line}: this fixture accepts USD only.")
amount = Decimal(row["cost_usd"])
if not amount.is_finite() or amount < 0:
raise ValueError(f"Line {line}: cost must be finite and non-negative.")
group = tuple(row[k] for k in ["provider", "model", "mode", "charge_type"])
unique = (day,) + group
if unique in keys:
raise ValueError(f"Line {line}: duplicate daily cost bucket.")
keys.add(unique)
dates.add(day)
totals[group][int(day >= split)] += amount
if dates != expected:
raise ValueError("Missing dates: verify the export; add zero-cost rows only for confirmed zero days.")
print(f"Previous: {first} to {split - timedelta(days=1)}")
print(f"Current: {split} to {end}")
print("provider | model | mode | charge | previous USD | current USD | change USD")
for group, (old, new) in sorted(totals.items(), key=lambda item: item[1][1] - item[1][0], reverse=True):
print(" | ".join(group) + f" | {old:.6f} | {new:.6f} | {new-old:+.6f}")
old = sum((values[0] for values in totals.values()), Decimal("0"))
new = sum((values[1] for values in totals.values()), Decimal("0"))
print(f"TOTAL USD: {old:.6f} -> {new:.6f} ({new-old:+.6f})")
return old, new
if __name__ == "__main__":
try:
if len(sys.argv) != 3:
raise ValueError("Usage: python audit_costs.py costs.csv YYYY-MM-DD")
audit(sys.argv[1], sys.argv[2])
except (ValueError, InvalidOperation, OSError) as exc:
print(f"Audit stopped: {exc}", file=sys.stderr)
raise SystemExit(2)On Windows, run:
py -3 audit_costs.py sample-costs.csv 2026-10-16On macOS or Linux, use python3 instead of py -3, or substitute your installed interpreter consistently. The sample should end with:
TOTAL USD: 0.700000 -> 2.000000 (+1.300000)Then run the command against costs.csv, replacing the date with your chosen complete-period end date.
5. Reconcile, then investigate
Match each period’s total to the same filtered billing report. Explain differences such as excluded services, adjustments and rounding before changing application settings. If the source is estimated, label the audit estimated.
The script checks that all fourteen dates appear, not that every charge is present. Missing model or charge rows are treated as zero in the comparison. Confirm export completeness, and add zero-cost rows only for verified zero days.
| Largest increase | Check next |
|---|---|
| Model token costs | Request count, input length, cache writes/reads and output. Higher spend does not prove a price increase. |
| Fast or Priority usage | Actual billed mode and service-tier configuration. |
| Search, storage or execution | Tool counters, retention and service-specific prices. |
| More retry attempts | Bounded retry logs against actual charges. Do not assume every failure was billed or free. |
Check one representative request with the OpenAI, Claude or Gemini API cost calculator. Match the model, context band and mode; tool and other service charges may need separate calculations.
Avoid double-counting cache tokens. In the current OpenAI Responses format, cached and cache-write tokens are parts of total input. Claude reports uncached input, cache creation and reads separately. Follow the exact API schema, not similarly named fields.
Comparing with a local llama.cpp setup? Include hardware, electricity, administration and task quality. A smaller API bill alone does not prove lower overall cost.
Do you need a proxy?
Use this audit to identify which charge changed. Consider a gateway when you need shared routing, access controls or centrally managed limits across applications. Test its failure behavior and handling of prompts and credentials. A proxy estimate is not an invoice, and a configured budget is not necessarily an instantaneous hard cap.
Troubleshooting and cleanup
- Wrong columns: check the header, order and malformed rows. Extra columns are rejected.
- Duplicate bucket: check appended exports or omitted dimensions. Aggregate legitimate subdivisions once.
- Missing dates: check freshness, pagination and verified zero-day coverage.
- Currency or negative-value error: separate currencies and adjustments; never strip minus signs to pass validation.
- Totals disagree: check units, account filters, excluded services and partial days.
The workflow changes no API keys, application settings or billing controls. To stop, stop using the working copy. Retain original exports securely and remove disposable copies under your normal retention policy.
FAQ
Can this stop overspending?
No. It analyzes past charges. Use provider or application controls for live enforcement.
Why leave token counts out of the CSV?
Start with billed changes, then inspect the affected bucket’s usage. Adding token data requires matching dates, models, account scope, geography and mode.
Does a lower total prove savings?
No. Compare workload volume and task quality too. Fewer jobs can lower the bill without making the application more efficient.
Sources and scope
Reviewed 19 October 2026. The Hacker News proxy discussion inspired the reader question, not the billing claims. The CSV format and script are an original offline workflow.
- OpenAI pricing and cache usage definitions.
- Claude pricing, Cost API units and exclusions, and cache token definitions.
- Gemini Developer API pricing.
The fictional sample validates the workflow, not a real provider export, invoice or saving.