Diagnostic LLM APIs
no hard spend limit is set, so the bill has no ceiling
The console shows a budget chart, and everybody who has looked at it believes it is a brake. It is not; it is a chart. The hard limit is a separate object on a separate admin endpoint, it is off by default, and until somebody turns it on there is no amount of spend in a month that will cause a request to be refused.
Three read-only calls with an organization admin key. GET /v1/organization/spend_limit returns a threshold_amount in cents, an interval, and an enforcement.status that is either "enforcing" or "inactive". GET /v1/organization/spend_alerts returns the warnings, which are configured separately and are frequently absent. GET /v1/organization/costs gives you month-to-date spend to judge both against.
Four states are worth telling apart: no limit at all, a limit that exists but is not enforcing, a limit so high it can never fire, and a limit with no alerts underneath it — a brake with no warning light.
The problem in plain words
Post-paid billing with auto-recharge has no natural ceiling. The platform's job is to serve requests and it will keep serving them, funded by a card, for as long as something keeps asking. The things that ask are rarely people: an agent loop with a broken termination condition, a retry storm that has mistaken a wall for a queue, a key that got committed to a public repository on Friday evening.
None of those produce an error. They produce traffic, and traffic is what the platform is for. The first signal is the invoice, or a spend alert if somebody configured one, and the cost report itself lags real time — so even a person watching the dashboard is watching history. The limit is the only mechanism in the system that turns a runaway into a controlled outage, and it is opt-in.
Why it happens
Enforcement and display live in different places. The budget number most teams have seen is a visualisation of the cost report. The thing that refuses requests is organization.spend_limit, on /v1/organization/spend_limit, with its own enforcement.status. An object can exist with a threshold set and a status of "inactive", which reads as configured and behaves as absent.
Alerts and limits are separately opt-in. You can have alerts and no limit, which is a warning with no brake. You can have a limit and no alerts, which is a brake with no warning — the first anyone hears is production returning 429 organization_spend_limit_exceeded. Both configurations are common and neither is visible without asking two endpoints.
threshold_amount is in cents. A limit typed as 500 intending five hundred dollars is five dollars, and it will fire within the hour. The mistake is easy, the symptom is a total outage, and the value reads perfectly plausible in the response body.
Anthropic has no equivalent to read. The Claude Admin API exposes GET /v1/organizations/cost_report and nothing that sets or reports a spending ceiling. On that side the honest finding is that no API-visible brake exists at all, and the control has to be a workspace-level budget you manage by hand.
The fix, as a flow
The script reads the limit, the alerts and month to date spend as three separate things, because an org can hold any two of them and still have nothing that would stop a runaway.
How to fix it
Read the limit object, not the dashboard
GET /v1/organization/spend_limit with an admin key. You are looking at two fields: threshold_amount, in cents, and enforcement.status. A missing object and a present object with status "inactive" have exactly the same effect on your bill, and they should be reported as two different findings because they are two different mistakes.
Read the alerts separately
GET /v1/organization/spend_alerts returns organization.spend_alert objects with their own threshold_amount and a notification_channel carrying recipients[]. An empty list under a working limit is the quiet failure here. So is a recipient list full of people who have left, which you can check against GET /v1/organization/users.
Get month-to-date spend to judge the number against
GET /v1/organization/costs?start_time={month_start}&limit=31, summing results[].amount.value. Without this the limit is an abstract number. With it you can say whether the ceiling is five times the run rate, in which case it will never fire, or below it, in which case it already has.
Project the month before you judge the ceiling
Spend on the third of the month tells you almost nothing on its own. Pro-rate it: divide by the fraction of the month elapsed and you have a projected month-end figure to compare the threshold against. A limit at twice the projection is a real brake; a limit at fifty times it is decoration.
Repeat per project, and print the bodies rather than sending them
Projects have their own limits at GET /v1/organization/projects/{project_id}/spend_limit. Walk them, then print the exact POST bodies for a human: the limit at roughly twice the projected month-end, in cents, and alerts at 50%, 75% and 90% of it with real recipients. A script holding an admin key should not be the thing that changes what your organization is allowed to spend.
How to check it worked
Re-run after the limit and alerts are configured. Every scope should report guarded.
python3 openai_spend_limit_audit.py --projects
# guarded organization $412.80 MTD, projecting $427.03, limit $900.00, 3 alert(s)
# 1 scope(s) checked, 0 finding(s)
The full code
Read-only against /v1/organization/*, which means an organization admin key: OPENAI_ADMIN_KEY. A project key is rejected by every one of these endpoints, so there is no way to run this with the credential your application uses, and that is the correct outcome. The four pure functions carry all the judgement — the cents conversion, the month projection, the verdict, and the check that alert recipients still work here.
"""Report whether anything would stop a runaway OpenAI bill.
Read only. GET requests and nothing else: OPENAI_ADMIN_KEY must be an
organization admin key (sk-admin-...) with read scopes, because every
/v1/organization endpoint rejects a project key. The repair is printed, never
performed, because a script should not be the thing that changes what an
organization is allowed to spend.
"""
import argparse
import calendar
import datetime as dt
import logging
import os
import sys
import requests
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
log = logging.getLogger("openai_spend_limit_audit")
API = "https://api.openai.com/v1"
def threshold_dollars(limit):
"""Read threshold_amount as dollars, or None when no limit is configured.
The field is in CENTS. A limit typed as 500 meaning five hundred dollars is
five dollars and takes production down inside the hour, so the conversion
lives in one named place rather than inline at three call sites.
"""
if not isinstance(limit, dict):
return None
obj = limit.get("spend_limit") if isinstance(limit.get("spend_limit"), dict) else limit
raw = obj.get("threshold_amount")
if raw is None or raw == "":
return None
try:
return float(raw) / 100.0
except (TypeError, ValueError):
return None
def projected_month_end(spent, now):
"""Pro-rate month-to-date spend to a month-end figure. Pure, clock injected.
Spend on the third of the month says almost nothing about the month. The
fraction of the month elapsed is measured to the hour, so the first day
does not divide by zero and does not produce an absurd projection either.
"""
days_in_month = calendar.monthrange(now.year, now.month)[1]
elapsed_hours = (now.day - 1) * 24 + now.hour + now.minute / 60.0
total_hours = days_in_month * 24.0
fraction = max(elapsed_hours / total_hours, 1.0 / total_hours)
return spent / fraction
def unknown_recipients(alerts, known_emails):
"""Alert recipients who are not members of the organization any more.
An alert addressed to someone who left is not an alert. Returned sorted so
the output is stable between runs.
"""
known = {str(e).strip().lower() for e in known_emails}
missing = set()
for a in alerts:
channel = a.get("notification_channel") or {}
for r in channel.get("recipients") or []:
if str(r).strip().lower() not in known:
missing.add(str(r))
return sorted(missing)
def verdict(limit, alerts, spent, now):
"""Classify one scope's protection against a runaway. Pure.
Returns (state, detail). Ordered deliberately: an absent limit and an
inactive one have the same effect on the bill and different repairs, and a
ceiling that can never fire is a separate finding from one that already has.
"""
projected = projected_month_end(spent, now)
threshold = threshold_dollars(limit)
money = "$%.2f month-to-date, projecting $%.2f" % (spent, projected)
if threshold is None:
return ("no-limit",
"%s, and no spend limit is configured. Nothing in the platform "
"will refuse a request no matter how much a runaway spends."
% money)
status = ""
if isinstance(limit, dict):
obj = limit.get("spend_limit") if isinstance(limit.get("spend_limit"), dict) else limit
enforcement = obj.get("enforcement") or {}
status = str(enforcement.get("status") or "")
if status and status != "enforcing":
return ("not-enforcing",
"%s. A limit of $%.2f exists but enforcement.status is %r, so it "
"displays and does not brake." % (money, threshold, status))
if threshold * 100 <= projected:
return ("cents-mistake",
"%s, against a limit of $%.2f. threshold_amount is in cents: a "
"value this far below the run rate is almost always a figure "
"typed as dollars, which is 100x too low and will page you "
"immediately." % (money, threshold))
if threshold <= spent:
return ("breached",
"%s, against a limit of $%.2f. Requests are already being "
"refused with 429 organization_spend_limit_exceeded."
% (money, threshold))
if threshold <= projected:
return ("will-breach",
"%s, against a limit of $%.2f. At this run rate the brake "
"engages before the interval resets." % (money, threshold))
if threshold >= projected * 5:
return ("ceiling-too-high",
"%s, against a limit of $%.2f. A ceiling more than five times "
"the run rate cannot fire in time to be useful."
% (money, threshold))
if not alerts:
return ("no-alerts",
"%s, with a limit of $%.2f enforcing and no spend alerts. A "
"brake with no warning light: the first signal is production "
"returning 429." % (money, threshold))
return ("guarded",
"%s, limit $%.2f, %d alert(s)" % (money, threshold, len(alerts)))
def get(session, path, **params):
r = session.get(API + path, params=params, timeout=30)
if r.status_code in (401, 403):
raise SystemExit("%d from OpenAI: /v1/organization endpoints need an "
"organization admin key, not a project key"
% r.status_code)
if r.status_code == 404:
return {}
r.raise_for_status()
return r.json()
def month_to_date(session, now, project_id=None):
start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
params = {"start_time": int(start.timestamp()), "limit": 31}
if project_id:
params["project_ids"] = project_id
costs = get(session, "/organization/costs", **params)
total = 0.0
for b in costs.get("data", []):
for r in b.get("results", []) or []:
total += float((r.get("amount") or {}).get("value") or 0.0)
return total
def report(scope, limit, alerts, spent, now):
state, detail = verdict(limit, alerts, spent, now)
line = "%-16s %-24s %s" % (state, scope, detail)
if state == "guarded":
log.info(line)
return 0
log.warning(line)
projected = projected_month_end(spent, now)
suggested = int(round(projected * 2)) * 100
log.warning(" repair, to run yourself: POST %s/organization/spend_limit "
"with a body of {\"threshold_amount\": %d, \"currency\": "
"\"USD\", \"interval\": \"month\"} -- that is %d cents, "
"which is $%.2f.", API, suggested, suggested, suggested / 100.0)
log.warning(" then alerts at 50%%, 75%% and 90%% of it via "
"%s/organization/spend_alerts, with a real recipients list.", API)
return 1
def main():
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("--projects", action="store_true",
help="also read the per-project limit and alerts")
ap.add_argument("--max-projects", type=int, default=25,
help="stop after this many projects")
args = ap.parse_args()
admin = os.environ.get("OPENAI_ADMIN_KEY")
if not admin:
log.error("set OPENAI_ADMIN_KEY (an organization admin key with read "
"scopes; project keys are rejected by /v1/organization/*)")
return 2
now = dt.datetime.now(dt.timezone.utc)
s = requests.Session()
s.headers.update({"Authorization": "Bearer " + admin})
limit = get(s, "/organization/spend_limit")
alerts = get(s, "/organization/spend_alerts", limit=100).get("data", [])
spent = month_to_date(s, now)
scopes = 1
bad = report("organization", limit, alerts, spent, now)
users = get(s, "/organization/users", limit=100).get("data", [])
stale = unknown_recipients(alerts, [u.get("email") for u in users])
if stale:
bad += 1
log.warning("%-16s %-24s alert recipients not in the organization: %s",
"stale-recipient", "organization", ", ".join(stale))
if args.projects:
projects = get(s, "/organization/projects", limit=args.max_projects)
for p in projects.get("data", [])[:args.max_projects]:
pid = p.get("id")
if not pid or str(p.get("status") or "active") != "active":
continue
scopes += 1
plimit = get(s, "/organization/projects/%s/spend_limit" % pid)
palerts = get(s, "/organization/projects/%s/spend_alerts" % pid,
limit=100).get("data", [])
pspent = month_to_date(s, now, project_id=pid)
bad += report(p.get("name") or pid, plimit, palerts, pspent, now)
log.info("%d scope(s) checked, %d finding(s)", scopes, bad)
return 1 if bad else 0
if __name__ == "__main__":
sys.exit(main())
/**
* Report whether anything would stop a runaway OpenAI bill.
*
* Read only. GET requests and nothing else: OPENAI_ADMIN_KEY must be an
* organization admin key with read scopes, because every /v1/organization
* endpoint rejects a project key. The repair is printed, never performed.
*/
const API = 'https://api.openai.com/v1';
/**
* Read threshold_amount as dollars, or null when no limit is configured. The
* field is in CENTS; a value typed as dollars is 100x too low and takes
* production down inside the hour, so the conversion lives in one named place.
*/
export function thresholdDollars(limit) {
if (!limit || typeof limit !== 'object') return null;
const obj = (limit.spend_limit && typeof limit.spend_limit === 'object')
? limit.spend_limit : limit;
const raw = obj.threshold_amount;
if (raw === null || raw === undefined || raw === '') return null;
const n = Number(raw);
return Number.isFinite(n) ? n / 100 : null;
}
/** Pro-rate month-to-date spend to a month-end figure. Pure, clock injected. */
export function projectedMonthEnd(spent, now) {
const daysInMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 0))
.getUTCDate();
const elapsedHours = (now.getUTCDate() - 1) * 24 + now.getUTCHours()
+ now.getUTCMinutes() / 60;
const totalHours = daysInMonth * 24;
const fraction = Math.max(elapsedHours / totalHours, 1 / totalHours);
return spent / fraction;
}
/** Alert recipients who are not members of the organization any more. Sorted. */
export function unknownRecipients(alerts, knownEmails) {
const known = new Set(knownEmails.map((e) => String(e ?? '').trim().toLowerCase()));
const missing = new Set();
for (const a of alerts) {
for (const r of a.notification_channel?.recipients ?? []) {
if (!known.has(String(r).trim().toLowerCase())) missing.add(String(r));
}
}
return [...missing].sort();
}
/**
* Classify one scope's protection against a runaway. Pure. Returns
* [state, detail].
*/
export function verdict(limit, alerts, spent, now) {
const projected = projectedMonthEnd(spent, now);
const threshold = thresholdDollars(limit);
const money = `$${spent.toFixed(2)} month-to-date, projecting $${projected.toFixed(2)}`;
if (threshold === null) {
return ['no-limit',
`${money}, and no spend limit is configured. Nothing in the platform will ` +
'refuse a request no matter how much a runaway spends.'];
}
const obj = (limit.spend_limit && typeof limit.spend_limit === 'object')
? limit.spend_limit : limit;
const status = String(obj.enforcement?.status ?? '');
if (status && status !== 'enforcing') {
return ['not-enforcing',
`${money}. A limit of $${threshold.toFixed(2)} exists but ` +
`enforcement.status is "${status}", so it displays and does not brake.`];
}
if (threshold * 100 <= projected) {
return ['cents-mistake',
`${money}, against a limit of $${threshold.toFixed(2)}. threshold_amount ` +
'is in cents: a value this far below the run rate is almost always a ' +
'figure typed as dollars, which is 100x too low and will page you ' +
'immediately.'];
}
if (threshold <= spent) {
return ['breached',
`${money}, against a limit of $${threshold.toFixed(2)}. Requests are ` +
'already being refused with 429 organization_spend_limit_exceeded.'];
}
if (threshold <= projected) {
return ['will-breach',
`${money}, against a limit of $${threshold.toFixed(2)}. At this run rate ` +
'the brake engages before the interval resets.'];
}
if (threshold >= projected * 5) {
return ['ceiling-too-high',
`${money}, against a limit of $${threshold.toFixed(2)}. A ceiling more ` +
'than five times the run rate cannot fire in time to be useful.'];
}
if (!alerts || alerts.length === 0) {
return ['no-alerts',
`${money}, with a limit of $${threshold.toFixed(2)} enforcing and no ` +
'spend alerts. A brake with no warning light: the first signal is ' +
'production returning 429.'];
}
return ['guarded',
`${money}, limit $${threshold.toFixed(2)}, ${alerts.length} alert(s)`];
}
async function get(key, path, params = {}) {
const url = new URL(API + path);
for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v);
const res = await fetch(url, { headers: { Authorization: `Bearer ${key}` } });
if (res.status === 401 || res.status === 403) {
throw new Error(`${res.status} from OpenAI: /v1/organization endpoints need ` +
'an organization admin key, not a project key');
}
if (res.status === 404) return {};
if (!res.ok) throw new Error(`${res.status} from ${url.pathname}`);
return res.json();
}
async function monthToDate(key, now, projectId) {
const start = Math.floor(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1) / 1000);
const params = { start_time: start, limit: 31 };
if (projectId) params.project_ids = projectId;
const costs = await get(key, '/organization/costs', params);
let total = 0;
for (const b of costs.data ?? []) {
for (const r of b.results ?? []) total += Number(r.amount?.value ?? 0);
}
return total;
}
function report(scope, limit, alerts, spent, now) {
const [state, detail] = verdict(limit, alerts, spent, now);
const line = `${state.padEnd(16)} ${String(scope).padEnd(24)} ${detail}`;
if (state === 'guarded') { console.log(line); return 0; }
console.warn(line);
const suggested = Math.round(projectedMonthEnd(spent, now) * 2) * 100;
console.warn(` repair, to run yourself: POST ${API}/organization/spend_limit ` +
`with a body of {"threshold_amount": ${suggested}, "currency": ` +
`"USD", "interval": "month"} -- that is ${suggested} cents, which ` +
`is $${(suggested / 100).toFixed(2)}.`);
console.warn(` then alerts at 50%, 75% and 90% of it via ` +
`${API}/organization/spend_alerts, with a real recipients list.`);
return 1;
}
async function main() {
const admin = process.env.OPENAI_ADMIN_KEY;
if (!admin) {
console.error('set OPENAI_ADMIN_KEY (an organization admin key with read ' +
'scopes; project keys are rejected by /v1/organization/*)');
process.exitCode = 2;
return;
}
const now = new Date();
const limit = await get(admin, '/organization/spend_limit');
const { data: alerts = [] } = await get(admin, '/organization/spend_alerts', { limit: 100 });
const spent = await monthToDate(admin, now);
let scopes = 1;
let bad = report('organization', limit, alerts, spent, now);
const { data: users = [] } = await get(admin, '/organization/users', { limit: 100 });
const stale = unknownRecipients(alerts, users.map((u) => u.email));
if (stale.length > 0) {
bad += 1;
console.warn(`${'stale-recipient'.padEnd(16)} ${'organization'.padEnd(24)} ` +
`alert recipients not in the organization: ${stale.join(', ')}`);
}
if (process.argv.includes('--projects')) {
const { data: projects = [] } = await get(admin, '/organization/projects', { limit: 25 });
for (const p of projects) {
if (!p.id || (p.status ?? 'active') !== 'active') continue;
scopes += 1;
const plimit = await get(admin, `/organization/projects/${p.id}/spend_limit`);
const { data: palerts = [] } = await get(
admin, `/organization/projects/${p.id}/spend_alerts`, { limit: 100 });
const pspent = await monthToDate(admin, now, p.id);
bad += report(p.name ?? p.id, plimit, palerts, pspent, now);
}
}
console.log(`${scopes} scope(s) checked, ${bad} finding(s)`);
process.exitCode = bad ? 1 : 0;
}
// Only run when invoked directly. The test file imports this module, and without
// the guard main() would run there too, fail on the missing key, and set a
// non-zero exit code that fails the whole test file even as every test passes.
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => { console.error(err.message); process.exitCode = 2; });
}
Add a test
Two rules earn their tests here. The cents conversion, because a threshold read as dollars is the difference between a $900 ceiling and a $9 one, and the only place that shows up is arithmetic. And the ordering of the states: a limit that exists but is not enforcing has to be reported before any comparison against spend, because comparing an inactive limit to a run rate produces a confident sentence about a brake that is not connected to anything. The month projection is exercised at a fixed clock so the first of the month and the twenty-eighth are both pinned.
import datetime as dt
from openai_spend_limit_audit import (projected_month_end, threshold_dollars,
unknown_recipients, verdict)
# The 15th of a 31-day month, so a little under half of it has elapsed.
NOW = dt.datetime(2026, 8, 15, 12, 0, tzinfo=dt.timezone.utc)
def limit_of(cents, status="enforcing"):
return {"object": "organization.spend_limit", "threshold_amount": cents,
"currency": "USD", "interval": "month",
"enforcement": {"status": status}}
def alert(cents, recipients=("oncall@example.com",)):
return {"object": "organization.spend_alert", "threshold_amount": cents,
"notification_channel": {"type": "email",
"recipients": list(recipients)}}
def test_threshold_is_cents_not_dollars():
assert threshold_dollars(limit_of(90000)) == 900.0
assert threshold_dollars({"spend_limit": limit_of(50000)}) == 500.0
assert threshold_dollars({}) is None
assert threshold_dollars(None) is None
assert threshold_dollars(limit_of("not a number")) is None
def test_projection_pro_rates_against_the_clock_it_is_given():
# 14.5 days of a 31 day month have elapsed, so spend roughly doubles.
assert round(projected_month_end(1000.0, NOW)) == 2138
# The first hour of the month must not divide by zero or project infinity.
first = dt.datetime(2026, 8, 1, 0, 0, tzinfo=dt.timezone.utc)
assert round(projected_month_end(10.0, first)) == 10 * 31 * 24
def test_no_limit_at_all_is_the_headline_finding():
state, detail = verdict({}, [], 400.0, NOW)
assert state == "no-limit"
assert "no spend limit is configured" in detail
def test_a_limit_that_is_not_enforcing_is_reported_before_any_arithmetic():
# An inactive limit has the same effect on the bill as no limit, and
# comparing it against the run rate would describe a brake that is not
# connected to anything.
state, _ = verdict(limit_of(90000, status="inactive"), [alert(45000)], 400.0, NOW)
assert state == "not-enforcing"
def test_a_threshold_typed_as_dollars_is_named_as_the_cents_mistake():
# 500 meaning five hundred dollars is five dollars.
state, detail = verdict(limit_of(500), [alert(250)], 400.0, NOW)
assert state == "cents-mistake"
assert "in cents" in detail
def test_already_over_and_on_track_to_go_over_are_different_states():
assert verdict(limit_of(30000), [alert(15000)], 400.0, NOW)[0] == "breached"
assert verdict(limit_of(70000), [alert(35000)], 400.0, NOW)[0] == "will-breach"
def test_a_ceiling_far_above_the_run_rate_cannot_fire_in_time():
state, detail = verdict(limit_of(5000000), [alert(2500000)], 400.0, NOW)
assert state == "ceiling-too-high"
assert "five times" in detail
def test_a_brake_with_no_warning_light_is_its_own_finding():
state, detail = verdict(limit_of(200000), [], 400.0, NOW)
assert state == "no-alerts"
assert "429" in detail
def test_a_limit_and_alerts_together_is_guarded():
state, detail = verdict(limit_of(200000), [alert(100000), alert(150000)],
400.0, NOW)
assert state == "guarded"
assert "2 alert(s)" in detail
def test_recipients_who_left_are_not_an_alert():
alerts = [alert(1000, ("oncall@example.com", "Departed@Example.com")),
alert(2000, ("oncall@example.com",))]
assert unknown_recipients(alerts, ["OnCall@example.com"]) == ["Departed@Example.com"]
assert unknown_recipients(alerts, ["oncall@example.com", "departed@example.com"]) == []
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { projectedMonthEnd, thresholdDollars, unknownRecipients, verdict }
from './openai-spend-limit-audit.mjs';
// The 15th of a 31-day month, so a little under half of it has elapsed.
const NOW = new Date('2026-08-15T12:00:00Z');
const limitOf = (cents, status = 'enforcing') => ({
object: 'organization.spend_limit', threshold_amount: cents, currency: 'USD',
interval: 'month', enforcement: { status },
});
const alert = (cents, recipients = ['oncall@example.com']) => ({
object: 'organization.spend_alert', threshold_amount: cents,
notification_channel: { type: 'email', recipients },
});
test('threshold is cents, not dollars', () => {
assert.equal(thresholdDollars(limitOf(90000)), 900);
assert.equal(thresholdDollars({ spend_limit: limitOf(50000) }), 500);
assert.equal(thresholdDollars({}), null);
assert.equal(thresholdDollars(null), null);
assert.equal(thresholdDollars(limitOf('not a number')), null);
});
test('projection pro-rates against the clock it is given', () => {
assert.equal(Math.round(projectedMonthEnd(1000, NOW)), 2138);
const first = new Date('2026-08-01T00:00:00Z');
assert.equal(Math.round(projectedMonthEnd(10, first)), 10 * 31 * 24);
});
test('no limit at all is the headline finding', () => {
const [state, detail] = verdict({}, [], 400, NOW);
assert.equal(state, 'no-limit');
assert.match(detail, /no spend limit is configured/);
});
test('a limit that is not enforcing is reported before any arithmetic', () => {
const [state] = verdict(limitOf(90000, 'inactive'), [alert(45000)], 400, NOW);
assert.equal(state, 'not-enforcing');
});
test('a threshold typed as dollars is named as the cents mistake', () => {
const [state, detail] = verdict(limitOf(500), [alert(250)], 400, NOW);
assert.equal(state, 'cents-mistake');
assert.match(detail, /in cents/);
});
test('already over and on track to go over are different states', () => {
assert.equal(verdict(limitOf(30000), [alert(15000)], 400, NOW)[0], 'breached');
assert.equal(verdict(limitOf(70000), [alert(35000)], 400, NOW)[0], 'will-breach');
});
test('a ceiling far above the run rate cannot fire in time', () => {
const [state, detail] = verdict(limitOf(5000000), [alert(2500000)], 400, NOW);
assert.equal(state, 'ceiling-too-high');
assert.match(detail, /five times/);
});
test('a brake with no warning light is its own finding', () => {
const [state, detail] = verdict(limitOf(200000), [], 400, NOW);
assert.equal(state, 'no-alerts');
assert.match(detail, /429/);
});
test('a limit and alerts together is guarded', () => {
const [state, detail] = verdict(limitOf(200000), [alert(100000), alert(150000)],
400, NOW);
assert.equal(state, 'guarded');
assert.match(detail, /2 alert\(s\)/);
});
test('recipients who left are not an alert', () => {
const alerts = [alert(1000, ['oncall@example.com', 'Departed@Example.com']),
alert(2000, ['oncall@example.com'])];
assert.deepEqual(unknownRecipients(alerts, ['OnCall@example.com']),
['Departed@Example.com']);
assert.deepEqual(
unknownRecipients(alerts, ['oncall@example.com', 'departed@example.com']), []);
});
FAQ
Is the budget number in the OpenAI console the same as a spend limit?
No. The console shows the cost report, which is a chart of what you have already spent. The thing that refuses requests is the organization.spend_limit object, read at GET /v1/organization/spend_limit, and it has its own enforcement.status. A threshold can be set with that status inactive, which looks configured and behaves as absent.
Why does my limit read as a strange number?
Because threshold_amount is in cents. 90000 is $900. If somebody typed 500 meaning five hundred dollars, the limit is $5 and production will stop almost immediately, which is the single most common mistake with this endpoint.
Do spend alerts come with the limit?
No, they are separate objects on a separate endpoint and are configured independently. An org can have alerts and no limit, which warns but does not stop, or a limit and no alerts, which stops with no warning. Read both before concluding anything.
What happens to my application when the limit is reached?
Requests start returning 429 with code organization_spend_limit_exceeded, or project_spend_limit_exceeded for a project cap. That is a controlled outage you chose over an unbounded invoice, but only if your retry logic branches on the code rather than the status, or it will sit there retrying a wall.
Does Anthropic have an equivalent endpoint?
Not for reading or setting a ceiling. The Claude Admin API exposes GET /v1/organizations/cost_report and the messages usage report, so you can see spend, but there is no spend-limit object to audit. On that side the honest finding is that no API-visible brake exists.
Related field notes
- A 429 that is a wall, not a throttle
- Output tokens are what the bill is made of
- Reasoning tokens billed but never returned
Sources
Every figure in this note is traced to one of these. Prices are list rates and change — check them for your own region before acting.
- Administration APIs — OpenAI API
- Costs — OpenAI API reference
- Projects — OpenAI API reference
- Get cost report — Claude Admin API
If your setup is misbehaving in a way this note does not cover, message me on LinkedIn with what you are seeing.