n4nAI

Setting spend alerts before your LLM bill surprises you

Set up LLM spend alerts with token metering and cron jobs to avoid surprise bills. Step-by-step guide with runnable Python code for OpenAI-compatible gateways.

n4n Team3 min read648 words

Audio narration

Coming soon — every post will get a voice note here.

A surprise five-figure invoice from an LLM provider is a rite of passage for teams that ship without guardrails. LLM spend alerts turn that mystery into a predictable metric you can watch daily. This guide walks through building a minimal but production-realistic alerting pipeline using token usage metering and a scheduled job, no enterprise tooling required.

Step 1: Capture token usage on every request

Every OpenAI-compatible chat completion response returns a usage object. If you are not logging it, you are flying blind. Instrument your client wrapper to extract and emit that object on each call.

import os
import requests

BASE_URL = os.environ.get("LLM_BASE_URL", "https://api.openai.com/v1")
API_KEY = os.environ["LLM_API_KEY"]

def chat(model: str, messages: list, **kwargs) -> dict:
    resp = requests.post(
        f"{BASE_URL}/chat/completions",
        headers={"Authorization": f"Bearer {API_KEY}"},
        json={"model": model, "messages": messages, **kwargs},
        timeout=30,
    )
    resp.raise_for_status()
    data = resp.json()
    # usage is the only billing truth
    usage = data["usage"]
    log_usage(model, usage)
    return data

The log_usage function can write to stdout, a file, or directly to a database. Do not rely on estimates from character counts—providers bill on tokenizers you do not control.

Step 2: Persist usage to a local ledger

A flat log rotates and gets lost. Use SQLite to keep a queryable ledger. It is enough for most single-service deployments and avoids standing up Postgres just to count tokens.

import sqlite3
from datetime import datetime

conn = sqlite3.connect("llm_usage.db")
conn.execute("""
CREATE TABLE IF NOT EXISTS usage (
    id INTEGER PRIMARY KEY,
    ts TEXT,
    model TEXT,
    prompt_tokens INTEGER,
    completion_tokens INTEGER,
    total_tokens INTEGER
)
""")

def log_usage(model: str, usage: dict):
    conn.execute(
        "INSERT INTO usage (ts, model, prompt_tokens, completion_tokens, total_tokens) VALUES (?,?,?,?,?)",
        (datetime.utcnow().isoformat(), model,
         usage["prompt_tokens"], usage["completion_tokens"], usage["total_tokens"]),
    )
    conn.commit()

If you route through a gateway like n4n.ai, its per-token usage metering exposes aggregated totals via an API, skipping the manual ledger. For everyone else, the table above is the source of truth.

Step 3: Map usage to cost without guessing

Token counts are not dollars. You need a pricing table that reflects your actual negotiated rates, not the public list price you saw in a blog post. Treat the numbers below as placeholders.

# Replace with your contracted rates per 1K tokens
PRICING = {
    "gpt-4o": {"prompt": 0.005, "completion": 0.015},
    "gpt-3.5-turbo": {"prompt": 0.0005, "completion": 0.0015},
}

def cost_for(model: str, prompt_tokens: int, completion_tokens: int) -> float:
    rate = PRICING.get(model)
    if not rate:
        return 0.0  # unknown model: exclude or alert separately
    return (prompt_tokens / 1000) * rate["prompt"] + (completion_tokens / 1000) * rate["completion"]

Run this over your ledger rows to get a daily spend figure. Keep the mapping in a config file, not hardcoded, so finance can update it without a deploy.

Step 4: Define threshold policy and alert routing

LLM spend alerts should fire before you hit the hard limit, not after. Set a soft warning at 70% of budget and a hard alert at 100%. Route to a Slack channel or PagerDuty severity that someone actually watches.

import requests

SLACK_WEBHOOK = os.environ["SLACK_WEBHOOK"]

def send_alert(message: str, severity: str = "warning"):
    # severity can drive channel or emoji prefix
    prefix = ":rotating_light:" if severity == "critical" else ":warning:"
    requests.post(SLACK_WEBHOOK, json={"text": f"{prefix} {message}"}, timeout=10)

A minimal policy config:

{
  "daily_limit_usd": 50.0,
  "monthly_limit_usd": 1000.0,
  "warn_ratio": 0.7
}

Step 5: Run the alert checker on a schedule

Write a standalone script that sums the ledger for the current day and month, computes cost, and compares against limits. Run it every 15 minutes via cron.

import sqlite3
from datetime import datetime, timezone

def check_spend():
    conn = sqlite3.connect("llm_usage.db")
    today = datetime.now(timezone.utc).strftime("%Y-%m-%d")
    rows = conn.execute(
        "SELECT model, prompt_tokens, completion_tokens FROM usage WHERE ts >= ?",
        (today,),
    ).fetchall()

    daily_cost = sum(cost_for(m, p, c) for m, p, c in rows)
    limit = 50.0  # from config
    if daily_cost >= limit * 0.7:
        send_alert(f"Daily LLM spend at ${daily_cost:.2f} (70% of ${limit:.2f})", "warning")
    if daily_cost >= limit:
        send_alert(f"Daily LLM spend exceeded ${limit:.2f}: ${daily_cost:.2f}", "critical")

if __name__ == "__main__":
    check_spend()

Cron entry:

*/15 * * * * /usr/bin/python3 /opt/llm_alerts/check_spend.py >> /var/log/llm_alerts.log 2>&1

For monthly aggregation, extend the query to filter by month prefix and load monthly_limit_usd from the same config. The pattern is identical.

Step 6: Verify the pipeline end-to-end

An alerting system you have not tested is a liability. Inject a synthetic row that pushes spend past the warning threshold, then run the checker manually.

# test_inject.py
conn = sqlite3.connect("llm_usage.db")
conn.execute(
    "INSERT INTO usage (ts, model, prompt_tokens, completion_tokens, total_tokens) VALUES (?,?,?,?,?)",
    (datetime.utcnow().isoformat(), "gpt-4o", 100000, 100000, 200000),
)
conn.commit()

Run python3 check_spend.py. You should receive a Slack message within seconds. Remove the test row afterward so it does not pollute real metrics.

sqlite3 llm_usage.db "DELETE FROM usage WHERE model='gpt-4o' AND total_tokens=200000;"

Once real traffic flows, confirm the ledger grows and the daily cost calculation tracks your provider dashboard within a small rounding delta. LLM spend alerts are only useful if they reflect reality.

Edge cases that will bite you

Model aliases. A gateway may rewrite model to a versioned string like gpt-4o-2024-05-13. Key your pricing table on a normalized prefix, or you will silently exclude spend from unknown models.

Cached tokens. Some providers return prompt_tokens_details.cached_tokens. If your gateway forwards provider cache-control hints, those tokens may be billed at a discount. Extend cost_for to apply the cached rate if you have one.

Concurrent writers. SQLite handles low concurrency fine, but if you run multiple workers, use a connection per process and consider BEGIN IMMEDIATE to avoid lock errors.

Time zones. Store UTC always. Compare against UTC day boundaries. A 2 a.m. local-time cron with UTC ledger will mismatch your provider’s billing cycle.

What good looks like

After a week, you should have a daily spend curve, a Slack alert that fired at least once in test, and a config file finance has edited without your help. The LLM spend alerts are now a background process, not a monthly panic. If you later move to a gateway with native metering, you can replace Step 2’s ledger with a single API pull and keep the same threshold logic.

Tagscost-monitoringalertsbudgetingtoken-usage

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All token usage & cost monitoring posts →