@tktideai/ai-api-cost-guard
v1.0.0
Published
Hard-stop budget limiter for AI API calls. Prevents runaway charges from OpenAI, Anthropic, Google Gemini and more.
Maintainers
Readme
api-cost-guard
Set a hard stop on your AI API spend — in one line of code.
Your cloud provider's billing limit doesn't stop mid-request spikes, infinite loops, or per-user abuse. This does.
Built with TypeScript and money-safe decimal arithmetic. Zero floating-point surprises.
The problem
Your app has a bug → infinite loop → 1,000 API calls in 60 seconds
Your cloud billing limit? It fires an email. After the fact.
Your credit card? Already charged.api-cost-guard intercepts every request before it reaches the provider. If the cost would exceed your limit — it never goes out.
Install
npm install @tktideai/ai-api-cost-guardQuick start
import { ApiCostGuard } from '@tktideai/ai-api-cost-guard';
const guard = new ApiCostGuard({
hardLimit: 10.00, // stop all requests at $10
softLimit: 8.00, // warn at $8
perRequestLimit: 0.50, // no single request over $0.50
});
// Before every API call:
const check = guard.preflightCheck('gpt-4o', 500, 500);
if (!check.allowed) return; // blocked — never reaches OpenAI
const response = await openai.chat.completions.create({ ... });
// After — record real usage so future checks stay accurate:
guard.recordUsage('gpt-4o',
response.usage.prompt_tokens,
response.usage.completion_tokens,
);Simpler: use wrap()
const response = await guard.wrap(
() => openai.chat.completions.create({ model: 'gpt-4o', messages }),
{
model: 'gpt-4o',
estimatedInputTokens: 500,
estimatedOutputTokens: 500,
extractUsage: (res) => ({
inputTokens: res.usage.prompt_tokens,
outputTokens: res.usage.completion_tokens,
}),
},
);
// returns null if blocked, result if allowedOptions
const guard = new ApiCostGuard({
// ── Limits ───────────────────────────────────────────────────────────
hardLimit: 10.00, // $ — block all requests at this cumulative spend
softLimit: 8.00, // $ — fire onWarning (does not block)
perRequestLimit: 0.50, // $ — block any single request above this cost
// ── Rolling window ───────────────────────────────────────────────────
windowMs: 3_600_000, // only count spend in the last 1 hour (optional)
// ── Callbacks ────────────────────────────────────────────────────────
onWarning: (details) => {
console.log(`⚠️ Approaching limit: $${details.projectedTotal}`);
},
onBlock: (error) => {
console.log(`🛑 Blocked: ${error.message}`);
},
// ── Behaviour ────────────────────────────────────────────────────────
throwOnBlock: true, // throw BudgetExceededError when blocked (default: true)
// ── Custom pricing ───────────────────────────────────────────────────
customPricing: {
'my-fine-tuned-model': { input: '0.008', output: '0.016', unit: 1000 },
},
});Supported models
Pricing is built-in and matched by substring — versioned names like gpt-4o-mini-2024-07-18 work automatically.
OpenAI
| Model | Input / 1k | Output / 1k | |---|---|---| | gpt-4o | $0.0025 | $0.0100 | | gpt-4o-mini | $0.00015 | $0.0006 | | gpt-4-turbo | $0.0100 | $0.0300 | | gpt-4 | $0.0300 | $0.0600 | | gpt-3.5-turbo | $0.0005 | $0.0015 | | o1 | $0.0150 | $0.0600 | | o3 | $0.0100 | $0.0400 | | o4-mini | $0.0011 | $0.0044 | | o1-pro | $0.1500 | $0.6000 |
Anthropic
| Model | Input / 1k | Output / 1k | |---|---|---| | claude-opus-4 | $0.0150 | $0.0750 | | claude-sonnet-4 | $0.0030 | $0.0150 | | claude-3-5-sonnet | $0.0030 | $0.0150 | | claude-3-5-haiku | $0.0008 | $0.0040 | | claude-3-opus | $0.0150 | $0.0750 | | claude-3-haiku | $0.00025 | $0.00125 |
Google Gemini
| Model | Input / 1k | Output / 1k | |---|---|---| | gemini-2.5-pro | $0.00125 | $0.0100 | | gemini-2.0-flash | $0.0001 | $0.0004 | | gemini-1.5-pro | $0.00125 | $0.0050 | | gemini-1.5-flash | $0.000075 | $0.0003 |
Prices may change. Always verify against provider docs and override via
customPricingif needed.
Check your spend
const stats = guard.getStats();
console.log(stats.totalSpend.toFixed(4)); // "3.4200"
console.log(stats.remainingBudget.toFixed(4)); // "6.5800"
console.log(stats.percentUsed); // "34.20"
console.log(stats.requestCount); // 47Express middleware
import { ApiCostGuard, createMiddleware } from '@tktideai/ai-api-cost-guard';
const guard = new ApiCostGuard({ hardLimit: 50 });
app.post('/api/chat', createMiddleware(guard), async (req, res) => {
// If we reach here, budget is OK
const response = await openai.chat.completions.create(req.body);
guard.recordUsage(
req.body.model,
response.usage.prompt_tokens,
response.usage.completion_tokens,
);
res.json(response);
});
// Returns 429 automatically if budget exceededPer-user limits (SaaS)
// One guard per user — isolated budgets
const userGuards = new Map<string, ApiCostGuard>();
function getGuard(userId: string): ApiCostGuard {
if (!userGuards.has(userId)) {
userGuards.set(userId, new ApiCostGuard({
hardLimit: 5.00, // $5 per user
windowMs: 30 * 24 * 60 * 60 * 1000, // per month
}));
}
return userGuards.get(userId)!;
}
// In your route:
const guard = getGuard(req.user.id);
const check = guard.preflightCheck('gpt-4o', 500, 500);Email alerts (optional)
Get real email notifications when limits are hit. Uses Gmail SMTP — no external API key needed.
Setup
1. Get a Gmail app password for your sender address:
Google Account → Security → 2-Step Verification → App Passwords → Generate2. Add to your .env:
[email protected]
GMAIL_PASS=xxxx xxxx xxxx xxxx3. Use in your app:
import { ApiCostGuard } from '@tktideai/ai-api-cost-guard';
import { createEmailNotifier } from '@tktideai/ai-api-cost-guard/notifiers';
const notifier = createEmailNotifier({
from: process.env.GMAIL_FROM!, // your Gmail — used as the sender
password: process.env.GMAIL_PASS!, // your Gmail app password
to: '[email protected]', // where alerts are delivered
message: 'Your AI budget needs attention.', // optional
});
const guard = new ApiCostGuard({
softLimit: 8,
hardLimit: 10,
onWarning: notifier,
onBlock: notifier,
});What the email looks like
Subject: ⚠️ API Cost Alert — Budget Warning | TKTIDE
┌─────────────────────────────────────┐
│ ⚠️ Approaching Budget Limit │
├─────────────────────────────────────┤
│ Your AI budget needs attention. │ ← your message
│ │
│ Trigger Soft Limit Warning │
│ Current Spend $7.9900 │
│ Projected $8.0075 │
│ Limit $8.0000 │
├─────────────────────────────────────┤
│ [TKTIDE logo] │
│ TKTIDE is here to help │
│ https://tktide.com │
└─────────────────────────────────────┘Email failures are non-fatal — they never crash or block your app.
Why TypeScript + Decimal.js?
Plain JavaScript has a well-known problem with money math:
// JavaScript — this is real:
0.1 + 0.2 === 0.30000000000000004 // true
// Your $0.30 hard limit never triggers
// You get charged $0.30000000000000004api-cost-guard uses Decimal.js for all arithmetic:
new Decimal('0.10').plus('0.20').equals('0.30') // always trueEvery cost, limit, and comparison is exact.
Error handling
import { ApiCostGuard, BudgetExceededError } from '@tktideai/ai-api-cost-guard';
try {
guard.preflightCheck('gpt-4o', 5000, 2000);
} catch (e) {
if (e instanceof BudgetExceededError) {
console.log(e.details.type); // 'hard_limit' | 'per_request_limit'
console.log(e.details.limit); // Decimal
console.log(e.details.projectedTotal); // Decimal
console.log(e.details.currentSpend); // Decimal
}
}Or opt out of throwing:
const guard = new ApiCostGuard({
hardLimit: 10,
throwOnBlock: false, // returns { allowed: false } instead of throwing
});
const check = guard.preflightCheck('gpt-4o', 5000, 2000);
if (!check.allowed) {
console.log(check.reason); // 'hard_limit' | 'per_request_limit'
}Full API reference
new ApiCostGuard(options)
Creates a new guard instance.
guard.preflightCheck(model, inputTokens, outputTokens?)
Check if a request is within budget before sending.
Returns PreflightResult — { allowed: true } or { allowed: false, reason }.
guard.recordUsage(model, inputTokens, outputTokens, metadata?)
Record actual token usage after a successful API response.
Returns the cost as a Decimal.
guard.wrap(apiFn, options)
Combines preflight + API call + usage recording in one call.
Returns the API result or null if blocked (when throwOnBlock: false).
guard.getStats()
Returns a snapshot of current spend, request count, remaining budget, and the full ledger.
guard.reset()
Clears all spend records. Useful between billing periods or test runs.
Scripts
npm test # run all 62 tests
npm run build # compile TypeScript to dist/
npm run test:watch # watch modeLicense
MIT — built with ❤️ by TKTIDE
