Back to blog

Cost optimizationGuide

Track API spend per project and get budget alerts in Python

A short Python script turns your usage log into a cost-per-project report and warns at 50, 75, 95 and 100% of a monthly budget.

By CheapTokens TeamPublished 5 min read

Terminal with a per-project spend report and a budget warning
On this page
  1. What Anthropic shipped, and what you can copy
  2. The log: one line per request
  3. The script: cost per project and threshold alerts
  4. Run it and read the result
  5. What a month looks like on cheap models
  6. Where this goes wrong

Add one JSON line to a file after every API call, tag it with a project name, and let a 30-line Python script turn that file into a cost-per-project report and a warning at 50, 75, 95 and 100% of your monthly budget. It is the same view Anthropic just gave Claude Enterprise teams, built from your own log.

Say one balance feeds an agent, a blog-drafting script and a small support bot. Month end arrives, the balance is lower than you hoped, and nothing tells you which of the three did it. That is the gap this closes.

What Anthropic shipped, and what you can copy#

Anthropic's announcement (28 October 2026) covers smart reports, in-app surveys and pooled group budgets, generally available on Claude Enterprise plans only (source). Two parts matter here. A report groups a team's work into workstreams and shows each one's spend. A pooled budget is one shared monthly number, and billing admins get an email at 50%, 75%, 95% and 100% of it. Anthropic adds that the reports are for planning investment, not for judging individual people.

You don't need an enterprise plan for the same shape. A workstream is just a label you choose, and the four thresholds are one list in code. Group by project, not by person, for the reason Anthropic gives.

Quick check

Your prepaid balance already stops requests at zero. Why add warnings at 50% and 75%?

Show answer

Answer: Because zero is too late: you want to notice a runaway loop while there is still money to look into it

The balance is a hard stop, not an alarm. Early thresholds turn a surprise at 100% into a question on day nine.

The log: one line per request#

Every chat completion response carries a usage object with prompt_tokens and completion_tokens. That is all the report needs, plus a project label you add yourself. This wrapper sends the request and appends one line to a monthly file (the file name changes each month, so a new month starts from zero):

python
import json, os, time
from openai import OpenAI

client = OpenAI(base_url="https://api.cheaptokens.dev/v1",
                api_key=os.environ["CHEAPTOKENS_API_KEY"])

def ask(project, model, messages):
    r = client.chat.completions.create(model=model, messages=messages)
    with open(time.strftime("usage-%Y-%m.jsonl"), "a") as f:
        f.write(json.dumps({"project": project, "model": model,
                            "prompt_tokens": r.usage.prompt_tokens,
                            "completion_tokens": r.usage.completion_tokens}) + "\n")
    return r

ask("support-bot", "zai-org/GLM-5.3-Flash",
    [{"role": "user", "content": "Where is my order?"}])

Use the full model id from the CheapTokens docs (deepseek-ai/DeepSeek-V4-Flash-0731, zai-org/GLM-5.3-Flash); the short names from price tables are not accepted by the API. The key comes from an environment variable, never from the file.

Flow from each request to the usage log, the project report and the alerts
How a request becomes a line in the log, then a report and an alert

The script: cost per project and threshold alerts#

Save this as report.py. The prices are placeholders: copy the current ones from the pricing table (both models share one example price here only to keep the arithmetic readable).

python
import json, os, sys, time
from collections import defaultdict

LOG = time.strftime("usage-%Y-%m.jsonl")
BUDGET = float(os.environ.get("BUDGET_USD", "10"))  # example, substitute your own
STATE = LOG + ".alert"
LEVELS = [50, 75, 95, 100]
PRICES = {  # USD per 1M tokens (input, output): example, substitute current prices
    "deepseek-ai/DeepSeek-V4-Flash-0731": (1.0, 2.0),
    "zai-org/GLM-5.3-Flash": (1.0, 2.0),
}

spend = defaultdict(float)
with open(LOG) as f:
    for line in f:
        r = json.loads(line)
        p_in, p_out = PRICES[r["model"]]
        spend[r["project"]] += (r["prompt_tokens"] * p_in
                                + r["completion_tokens"] * p_out) / 1_000_000

total = sum(spend.values())
if total == 0:
    sys.exit("log is empty")

for name, usd in sorted(spend.items(), key=lambda kv: -kv[1]):
    print(f"{name:<8} ${usd:6.2f} {usd / total:5.0%}")
print(f"{'total':<8} ${total:6.2f} of ${BUDGET:.2f} ({total / BUDGET:.0%})")

last = int(open(STATE).read()) if os.path.exists(STATE) else 0
crossed = [l for l in LEVELS if total >= BUDGET * l / 100 and l > last]
if crossed:
    print(f"WARNING: budget passed {crossed[-1]}%")
    open(STATE, "w").write(str(crossed[-1]))

The last four lines are the part people forget. Without the small state file, the script would shout "passed 50%" on every run for the rest of the month. With it, each level fires once.

Run it and read the result#

Take a toy log of four lines (a real one has a line per request, so thousands):

text
{"project": "agent", "model": "deepseek-ai/DeepSeek-V4-Flash-0731", "prompt_tokens": 2400000, "completion_tokens": 300000}
{"project": "agent", "model": "deepseek-ai/DeepSeek-V4-Flash-0731", "prompt_tokens": 1000000, "completion_tokens": 100000}
{"project": "blog", "model": "zai-org/GLM-5.3-Flash", "prompt_tokens": 800000, "completion_tokens": 400000}
{"project": "bot", "model": "zai-org/GLM-5.3-Flash", "prompt_tokens": 500000, "completion_tokens": 100000}

With the example prices and a budget of 10, python report.py prints:

text
agent    $  4.20   65%
blog     $  1.60   25%
bot      $  0.70   11%
total    $  6.50 of $10.00 (65%)
WARNING: budget passed 50%

The agent is nearly two thirds of the bill, and the warning came at 50%, not at the end. Run it again and the warning stays quiet until the total reaches 75%.

A terminal showing the per-project report and the 50% warning
Report output on the toy log: shares per project and the first warning

To make this work every month, you need three steps:

Step by step

Wire it up in three steps

Log every call

Route all your calls through ask(project, model, messages) and pick a short, stable project label. Renaming labels mid-month splits one project into two rows.

Set prices and budget

Put the current prices from the pricing table into PRICES, and export your limit: export BUDGET_USD=25.

Run it on a schedule

An hourly cron line is enough: 0 * * * * cd ~/ai && python3 report.py. Send the output to a mail or chat command if you want the warning pushed to you.

What a month looks like on cheap models#

Numbers get useful once they are your numbers. Assume the agent from the toy log averages 30,000 input and 2,000 output tokens per step, 20 steps per task, two tasks a day. One step on DeepSeek-V4-Flash costs $0.00065; on GLM-5.3-Flash, $0.00068. A day is 40 steps, a month 1,200. Move the sliders to match your own agent:

Cost calculator

One day of the agent: steps per task times tasks per day

Input tokens per request: 30,000 · Output tokens per request: 2,000 · Requests per day: 40

ModelPer dayPer month
GLM-5.3-Flash$0.0272$0.816
DeepSeek-V4-Flash$0.026$0.78
MiniMax-M2.7$0.072$2.16

And the month at 1,200 steps, with input and output counted together:

Model Per request 1,200 requests OpenRouter, 1,200 requests
GLM-5.3-Flash $0.00068 $0.816 $1.41
DeepSeek-V4-Flash $0.00065 $0.78 $2.86
MiniMax-M2.7 $0.0018 $2.16 $25.92

30,000 input and 2,000 output tokens per request. Prices per 1M tokens are current and change together with our tariffs; the OpenRouter column shows reference prices for the same workload.

In total, with input and output counted together, both models come out cheaper than OpenRouter's reference prices in that table, with one price at any hour and no peak surcharge. DeepSeek-V4-Flash also allows up to 32,768 output tokens per answer and a 380K context; GLM-5.3-Flash has a 390K context. Once the report shows which project is the heavy one, switching only that project's model string is a one-line change, and one balance covers every model. For how many tokens a single request really uses, see how many tokens an API request uses; for stopping one agent run mid-way, see tracking AI agent token cost. Prices and models are on the CheapTokens models page.

Where this goes wrong#

  • A model is missing from PRICES. The script stops with a KeyError that names the model. Add it; don't default to zero, or the project looks free.
  • Streaming calls. The final chunk carries usage only if you ask for it (stream_options={"include_usage": True} in the OpenAI SDK). Otherwise your log has gaps.
  • Failed requests. Log only responses that have a usage object; a request that errored out has nothing to count.
  • Per-person reports. Keep the label to a project or agent name. A report about people invites the wrong conclusions.