Package Exports
- @spendkill/sdk
Readme
Spend Kill
A Per-user spend-capped keys for AI APIs to prevent surprise AI bills. Give every user or server their own AI API key with a hard dollar limit. When that limit is hit, Spend Kill automatically stops requests so you never get surprise AI bills.
Installation
npm install @spendkill/sdkQuick Start
import SpendKill from "@spendkill/sdk";
const sk = new SpendKill({
base: "https://your-spend-kill-server.com",
adminKey: process.env.SK_ADMIN_HMAC,
});
// Issue a $10 capped key for a user
const { api_key } = await sk.issue("user-123", { limitUsd: 10 });
// Use the key with Claude
const claude = await sk.client(api_key).anthropic({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }],
});
// Use the key with OpenAI
const openai = await sk.client(api_key).openai({
model: "gpt-4o",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }],
});
// Use the key with Grok
const grok = await sk.client(api_key).grok({
model: "grok-beta",
max_tokens: 1024,
messages: [{ role: "user", content: "Hello!" }],
});Features
- ✅ Spend Limits - Hard dollar caps per user/key
- ✅ Multiple Providers - Claude, OpenAI, and Grok support
- ✅ Usage Tracking - Real-time spending in response headers
- ✅ Auto-Block - Automatically stops requests when cap is reached
- ✅ Webhooks - 80% budget alerts
Response Headers
Every response includes spending information:
response.headers.get("x-sk-spent-usd"); // "2.45"
response.headers.get("x-sk-cap-usd"); // "10.00"
response.headers.get("x-sk-remaining-usd"); // "7.55"
response.headers.get("x-sk-request-id"); // Unique request IDAPI Methods
issue(userId: string, options)
Create a spend-capped key for a user.
const { api_key } = await sk.issue("user-123", {
limitUsd: 10,
notes: "Premium tier user"
});revoke(userId: string)
Revoke a user's key.
await sk.revoke("user-123");listKeys()
List all keys with usage stats.
const keys = await sk.listKeys();
keys.forEach(key => {
console.log(`${key.user_id}: $${key.spent}/$${key.cap}`);
});report(userId: string)
Get detailed usage report for a user.
const report = await sk.report("user-123");
console.log(report.usage_by_model); // Usage by modeltopup(userId: string, usd: number)
Increase a user's spending cap.
// Add $5 to user's cap (e.g., $10 → $15)
await sk.topup("user-123", 5);reset(userId: string)
Reset spending to $0 while keeping the cap.
// Reset monthly spending (cap stays the same)
await sk.reset("user-123");setStatus(userId: string, status: 'active' | 'revoked')
Change a key's status.
// Reactivate a revoked key
await sk.setStatus("user-123", "active");
// Or revoke (same as sk.revoke())
await sk.setStatus("user-123", "revoked");client(apiKey).anthropic(body)
Call Anthropic Claude API.
const response = await sk.client(api_key).anthropic({
model: "claude-sonnet-4-20250514",
max_tokens: 1024,
messages: [
{ role: "user", content: "Write a haiku" }
],
});client(apiKey).openai(body)
Call OpenAI API.
const response = await sk.client(api_key).openai({
model: "gpt-4o",
max_tokens: 1024,
messages: [
{ role: "user", content: "Write a haiku" }
],
});client(apiKey).grok(body)
Call xAI Grok API.
const response = await sk.client(api_key).grok({
model: "grok-beta",
max_tokens: 1024,
messages: [
{ role: "user", content: "Write a haiku" }
],
});Handling Limits
When a user hits their spending cap:
const response = await sk.client(api_key).anthropic({...});
if (response.status === 429) {
console.log("Budget limit reached!");
// Handle gracefully - upgrade prompt, notify user, etc.
}Programmatic Usage
Perfect for Cloudflare Workers, AWS Lambda, Durable Objects, or any multi-tenant architecture where you need to dynamically issue capped API keys.
Example: Cloudflare Durable Objects
Each Durable Object gets its own capped API key. Perfect for game servers, chatbots, or any isolated tenant:
export class MinecraftServerDO {
state: DurableObjectState;
apiKey?: string;
async fetch(req: Request): Promise<Response> {
const sk = new SpendKill({
base: env.SK_BASE,
adminKey: env.SK_ADMIN_HMAC
});
// Lazy-initialize: issue key on first request
if (!this.apiKey) {
this.apiKey = await this.state.storage.get<string>("apiKey");
if (!this.apiKey) {
const issued = await sk.issue(`server-${this.state.id}`, {
limitUsd: 10,
notes: "Minecraft server"
});
this.apiKey = issued.api_key;
await this.state.storage.put("apiKey", this.apiKey);
}
}
// Use the key for AI calls
const response = await sk.client(this.apiKey).openai({
model: 'gpt-4o',
messages: [{ role: 'user', content: 'Hello!' }]
});
return new Response(JSON.stringify(response.json));
}
}Admin Monitoring
Build dashboards or automated billing systems:
const sk = new SpendKill({
base: "https://your-spendkill.com",
adminKey: process.env.SK_ADMIN_HMAC
});
// List all users and their spending
const keys = await sk.listKeys();
keys.forEach(k => {
console.log(`${k.user_id}: $${k.spent_usd}/$${k.cap_usd} (${k.percent_used}%)`);
});
// Find users near their cap
const nearCap = keys.filter(k => parseFloat(k.percent_used) >= 80);
// Top up users programmatically
for (const user of nearCap) {
await sk.topup(user.user_id, 10); // Add $10
}Links
License
This project is licensed under the FSL-1.1-MIT License. See the LICENSE file for details.