Cost attribution
Track LLM token spend, API call costs, and custom costs per agent, per tool, and per delegation chain.
Why cost attribution matters
When agents make LLM calls, every token has a price. In multi-agent systems where one agent can spawn or delegate to others, costs accumulate across multiple providers and chains. Without proper attribution you cannot answer basic questions: which agent is responsible for a $200 spike? Which tool is burning the most budget? Did the delegation chain for last night's job exceed its allocation?
Cost attribution gives you per-agent, per-tool, and per-delegation-chain cost records. It integrates with budget policies so you can fire alerts before spend becomes a problem, not after.
Setup
import { createKavach } from 'kavachos';
import { createCostAttributionModule } from 'kavachos/auth';
const kavach = await createKavach({
database: { provider: 'sqlite', url: 'kavach.db' },
});
const costs = createCostAttributionModule(kavach.db, {
currency: 'USD',
retentionDays: 90,
alertThresholds: { warn: 5.00, critical: 20.00 },
onAlert: async (alert) => {
console.warn(`[cost] ${alert.type}: agent ${alert.agentId} spent $${alert.currentCostUsd.toFixed(4)} (threshold: $${alert.threshold})`);
},
});Configuration options
Prop
Type
Recording costs
Call recordCost() after each LLM response or API call. Pass the raw token counts and the exact dollar amount from the provider's response.
// After an OpenAI completion
const completion = await openai.chat.completions.create({ model: 'gpt-4o', messages });
await costs.recordCost({
agentId: agent.id,
tool: 'openai:gpt-4o',
inputTokens: completion.usage?.prompt_tokens,
outputTokens: completion.usage?.completion_tokens,
costUsd: calculateOpenAiCost(completion.usage),
});RecordCostInput
Prop
Type
Costs are stored internally as integer microdollars (value × 1,000,000) to avoid floating-point drift across aggregations.
Provider helpers
function openAiCostUsd(usage: OpenAI.CompletionUsage, model: string): number {
const rates: Record<string, { input: number; output: number }> = {
'gpt-4o': { input: 2.50 / 1_000_000, output: 10.00 / 1_000_000 },
'gpt-4o-mini': { input: 0.15 / 1_000_000, output: 0.60 / 1_000_000 },
};
const rate = rates[model] ?? { input: 0, output: 0 };
return rate.input * usage.prompt_tokens + rate.output * usage.completion_tokens;
}
await costs.recordCost({
agentId,
tool: `openai:${completion.model}`,
inputTokens: completion.usage.prompt_tokens,
outputTokens: completion.usage.completion_tokens,
costUsd: openAiCostUsd(completion.usage, completion.model),
});function anthropicCostUsd(usage: Anthropic.Usage, model: string): number {
const rates: Record<string, { input: number; output: number }> = {
'claude-3-5-sonnet-20241022': { input: 3.00 / 1_000_000, output: 15.00 / 1_000_000 },
'claude-3-5-haiku-20241022': { input: 0.80 / 1_000_000, output: 4.00 / 1_000_000 },
};
const rate = rates[model] ?? { input: 0, output: 0 };
return rate.input * usage.input_tokens + rate.output * usage.output_tokens;
}
await costs.recordCost({
agentId,
tool: `anthropic:${message.model}`,
inputTokens: message.usage.input_tokens,
outputTokens: message.usage.output_tokens,
costUsd: anthropicCostUsd(message.usage, message.model),
});// For any provider with a flat per-call cost
await costs.recordCost({
agentId,
tool: 'mcp:github',
costUsd: 0.0001,
metadata: { operation: 'create_issue', repo: 'acme/app' },
});Generating cost reports
Per-agent report
const result = await costs.getAgentCost(agent.id);
if (result.success) {
console.log('Total:', result.data.totalCostUsd.toFixed(4));
console.log('By tool:', result.data.byTool);
console.log('By day:', result.data.byDay);
}Pass a custom period to narrow the query:
const result = await costs.getAgentCost(agent.id, {
start: new Date('2025-01-01'),
end: new Date('2025-01-31'),
});CostReport
Prop
Type
Owner report
Aggregate cost across all agents owned by a user:
const result = await costs.getOwnerCost(userId);
if (result.success) {
console.log(`Total spend for ${userId}: $${result.data.totalCostUsd.toFixed(2)}`);
}Top agents by cost
Find the most expensive agents in any period:
const result = await costs.getTopAgentsByCost(10, {
start: new Date('2025-01-01'),
end: new Date('2025-01-31'),
});
if (result.success) {
for (const { agentId, totalCostUsd } of result.data) {
console.log(`${agentId}: $${totalCostUsd.toFixed(4)}`);
}
}Delegation chain report
When agents delegate to sub-agents, you can attribute all costs back to the originating chain by passing delegationChainId in recordCost():
// When the parent agent creates a delegation
const chain = await kavach.delegate({
fromAgent: parentAgent.id,
toAgent: childAgent.id,
permissions: [{ resource: 'tool:summarize', actions: ['execute'] }],
expiresIn: '1h',
});
// In the child agent's handler
await costs.recordCost({
agentId: childAgent.id,
tool: 'openai:gpt-4o-mini',
costUsd: 0.02,
delegationChainId: chain.id,
});
// Later: total cost across all agents in that chain
const result = await costs.getDelegationChainCost(chain.id);Setting up alerts
Alerts fire automatically when recordCost() is called. There are three alert types:
| Type | When it fires |
|---|---|
warn | 24-hour rolling spend crosses the warn threshold |
critical | 24-hour rolling spend crosses the critical threshold |
budget_exceeded | Monthly spend crosses the maxTokensCostPerMonth limit from a budget policy |
const costs = createCostAttributionModule(kavach.db, {
alertThresholds: {
warn: 5.00, // $5 in 24 hours
critical: 20.00, // $20 in 24 hours
},
onAlert: async (alert) => {
if (alert.type === 'budget_exceeded') {
// Revoke or suspend the agent
await kavach.agent.revoke(alert.agentId);
}
// Send to Slack, PagerDuty, etc.
await notifyOpsChannel({
text: `[${alert.type.toUpperCase()}] Agent ${alert.agentId} spent $${alert.currentCostUsd.toFixed(2)} (limit: $${alert.threshold}) over ${alert.period}`,
});
},
});CostAlert
Prop
Type
Integration with budget policies
checkBudget() reads budget policies created via kavach.policies and compares them against actual spend from the cost events table. This gives you a real-time answer before authorizing an operation.
const result = await costs.checkBudget(agent.id);
if (result.success) {
const { withinBudget, spent, limit, remaining } = result.data;
if (!withinBudget) {
return new Response('Agent has exceeded its monthly cost budget', { status: 402 });
}
console.log(`$${spent.toFixed(4)} of $${limit?.toFixed(2) ?? '∞'} used`);
}To set a budget limit, create a policy with maxTokensCostPerMonth:
await kavach.policies.create({
agentId: agent.id,
limits: {
maxTokensCostPerMonth: 50, // $50/month
},
action: 'block',
});The budget_exceeded alert fires automatically on recordCost() when spend crosses this limit, so you do not need to poll.
Maintenance
Cost events accumulate. Run cleanup() periodically to remove events older than the retention window:
// In a cron job
const result = await costs.cleanup({ retentionDays: 90 });
if (result.success) {
console.log(`Deleted ${result.data.deleted} cost events`);
}If you configured retentionDays on the module, you can call cleanup() with no arguments and it uses that value.
Return types
All methods return a Result<T> union:
type Result<T> =
| { success: true; data: T }
| { success: false; error: KavachError };Check result.success before accessing result.data. Error codes:
| Code | Cause |
|---|---|
RECORD_COST_FAILED | Database insert failed |
GET_AGENT_COST_FAILED | Query failed for agent report |
GET_OWNER_COST_FAILED | Query failed for owner report |
GET_TOP_AGENTS_FAILED | Aggregation query failed |
GET_CHAIN_COST_FAILED | Chain attribution query failed |
CHECK_BUDGET_FAILED | Budget policy lookup failed |
CLEANUP_FAILED | Cleanup delete failed |