← back to Designer Wallcoverings
DW-Agents/dw-agents/agent-ai-expenses/ai-expenses-tracker.ts
742 lines
/**
* DW-Agents: AI Expenses Tracker
*
* Real-time monitoring of Claude and Gemini API usage and costs across all agents
* - Track API calls with full token details
* - Calculate costs in real-time using current pricing
* - Budget alerts and usage analytics
* - Cost optimization recommendations
* - Agent-by-agent breakdown
*
* Port: 9951
*/
import Anthropic from '@anthropic-ai/sdk';
import express, { Request, Response } from 'express';
import cookieParser from 'cookie-parser';
import session from 'express-session';
import { chatMiddleware } from '../shared-chat-integration';
import { AgentMemory } from '../shared-memory-system';
import { TaskReporter } from '../shared-task-reporter';
import fetch from 'node-fetch';
import * as fs from 'fs';
import * as path from 'path';
const { getUniversalHeader, getThemeStyles } = require('../shared-ui-components.js');
const app = express();
const PORT = process.env.PORT || 9951;
// No authentication required - this agent accepts API calls from other agents
app.use(cookieParser());
// Add inter-agent chat widget
app.use(chatMiddleware({
agentId: 'ai-expenses',
agentName: 'AI Expenses Tracker',
port: PORT,
category: 'agent'
}));
// Anthropic AI client
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY || '',
});
// Initialize Agent Memory
const agentMemory = new AgentMemory('ai-expenses');
console.log('📚 Agent memory initialized');
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Session configuration
declare module 'express-session' {
interface SessionData {
chatHistory: Array<{ role: 'user' | 'assistant', content: string }>;
}
}
app.use(
session({
secret: 'dw-ai-expenses-secret-2025',
resave: false,
saveUninitialized: false,
cookie: {
secure: false,
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000, // 24 hours
},
})
);
// Pricing (as of January 2025)
const PRICING = {
claude: {
'claude-3-5-sonnet-20241022': {
input: 3.00 / 1_000_000,
output: 15.00 / 1_000_000,
name: 'Claude 3.5 Sonnet'
},
'claude-3-5-haiku-20241022': {
input: 0.80 / 1_000_000,
output: 4.00 / 1_000_000,
name: 'Claude 3.5 Haiku'
},
'claude-3-opus-20240229': {
input: 15.00 / 1_000_000,
output: 75.00 / 1_000_000,
name: 'Claude 3 Opus'
}
},
gemini: {
'gemini-3.5-flash': {
input: 0.00,
output: 0.00,
name: 'Gemini 2.0 Flash (Preview)'
},
'gemini-1.5-pro': {
input: 1.25 / 1_000_000,
output: 5.00 / 1_000_000,
name: 'Gemini 1.5 Pro'
},
'gemini-1.5-flash': {
input: 0.075 / 1_000_000,
output: 0.30 / 1_000_000,
name: 'Gemini 1.5 Flash'
}
}
};
// Budget thresholds (load from memory)
let budgetThresholds = {
daily: 10.00,
weekly: 50.00,
monthly: 200.00
};
// Load saved thresholds from memory
const savedThresholds = agentMemory.search('budget threshold');
if (savedThresholds.length > 0) {
try {
budgetThresholds = JSON.parse(savedThresholds[0].content);
console.log('💰 Loaded budget thresholds from memory');
} catch (e) {
console.log('💰 Using default budget thresholds');
}
}
// Data structures
interface APICall {
timestamp: Date;
agent: string;
model: string;
provider: 'claude' | 'gemini';
inputTokens: number;
outputTokens: number;
cost: number;
endpoint: string;
}
interface DailySummary {
date: string;
totalCost: number;
totalCalls: number;
totalInputTokens: number;
totalOutputTokens: number;
byProvider: {
claude: { calls: number; cost: number; tokens: number };
gemini: { calls: number; cost: number; tokens: number };
};
byAgent: Record<string, { calls: number; cost: number }>;
}
// In-memory storage (persistent to file)
let apiCalls: APICall[] = [];
let dailySummaries: Record<string, DailySummary> = {};
let alertsSent: string[] = [];
// Activity log
const activityLog: Array<{
timestamp: Date;
action: string;
details: string;
}> = [];
function logActivity(action: string, details: string) {
activityLog.unshift({
timestamp: new Date(),
action,
details,
});
if (activityLog.length > 100) activityLog.pop();
}
// Data persistence
const DATA_FILE = path.join(__dirname, 'expenses-data.json');
function loadData() {
try {
if (fs.existsSync(DATA_FILE)) {
const data = JSON.parse(fs.readFileSync(DATA_FILE, 'utf8'));
apiCalls = data.apiCalls.map((call: any) => ({
...call,
timestamp: new Date(call.timestamp)
}));
dailySummaries = data.dailySummaries;
alertsSent = data.alertsSent || [];
console.log(`📊 Loaded ${apiCalls.length} API calls from storage`);
logActivity('System', `Loaded ${apiCalls.length} historical API calls`);
}
} catch (error) {
console.error('Error loading data:', error);
logActivity('Error', 'Failed to load historical data');
}
}
function saveData() {
try {
fs.writeFileSync(DATA_FILE, JSON.stringify({ apiCalls, dailySummaries, alertsSent }, null, 2));
} catch (error) {
console.error('Error saving data:', error);
}
}
// Calculate cost for an API call
function calculateCost(provider: 'claude' | 'gemini', model: string, inputTokens: number, outputTokens: number): number {
const pricing = PRICING[provider][model];
if (!pricing) return 0;
return (inputTokens * pricing.input) + (outputTokens * pricing.output);
}
// Check budget alerts
async function checkBudgetAlerts(stats: any) {
const today = new Date().toISOString().split('T')[0];
const alertKey = `alert-${today}`;
if (alertsSent.includes(alertKey)) return;
if (stats.today.cost >= budgetThresholds.daily) {
const task = new TaskReporter(
'Daily AI Budget Alert',
`Daily AI expenses have reached $${stats.today.cost.toFixed(2)} (threshold: $${budgetThresholds.daily})`,
'ai-expenses',
'high'
);
await task.complete(`Budget: $${stats.today.cost.toFixed(2)} | Calls: ${stats.today.calls}`);
alertsSent.push(alertKey);
logActivity('Alert', `Daily budget threshold exceeded: $${stats.today.cost.toFixed(2)}`);
}
}
// Track an API call
async function trackAPICall(agent: string, provider: 'claude' | 'gemini', model: string, inputTokens: number, outputTokens: number, endpoint: string = '/messages') {
const cost = calculateCost(provider, model, inputTokens, outputTokens);
const call: APICall = {
timestamp: new Date(),
agent,
model,
provider,
inputTokens,
outputTokens,
cost,
endpoint
};
apiCalls.push(call);
updateDailySummary(call);
saveData();
logActivity('API Call', `${agent}: ${provider} ${model} - $${cost.toFixed(6)}`);
// Check budget alerts
const stats = getStats();
await checkBudgetAlerts(stats);
return call;
}
// Update daily summary
function updateDailySummary(call: APICall) {
const dateKey = call.timestamp.toISOString().split('T')[0];
if (!dailySummaries[dateKey]) {
dailySummaries[dateKey] = {
date: dateKey,
totalCost: 0,
totalCalls: 0,
totalInputTokens: 0,
totalOutputTokens: 0,
byProvider: {
claude: { calls: 0, cost: 0, tokens: 0 },
gemini: { calls: 0, cost: 0, tokens: 0 }
},
byAgent: {}
};
}
const summary = dailySummaries[dateKey];
summary.totalCost += call.cost;
summary.totalCalls++;
summary.totalInputTokens += call.inputTokens;
summary.totalOutputTokens += call.outputTokens;
summary.byProvider[call.provider].calls++;
summary.byProvider[call.provider].cost += call.cost;
summary.byProvider[call.provider].tokens += call.inputTokens + call.outputTokens;
if (!summary.byAgent[call.agent]) {
summary.byAgent[call.agent] = { calls: 0, cost: 0 };
}
summary.byAgent[call.agent].calls++;
summary.byAgent[call.agent].cost += call.cost;
}
// Get stats
function getStats() {
const now = new Date();
const today = now.toISOString().split('T')[0];
const last7Days = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
const last30Days = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
const todayCalls = apiCalls.filter(c => c.timestamp.toISOString().split('T')[0] === today);
const last7DaysCalls = apiCalls.filter(c => c.timestamp >= last7Days);
const last30DaysCalls = apiCalls.filter(c => c.timestamp >= last30Days);
return {
today: {
calls: todayCalls.length,
cost: todayCalls.reduce((sum, c) => sum + c.cost, 0),
tokens: todayCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
},
last7Days: {
calls: last7DaysCalls.length,
cost: last7DaysCalls.reduce((sum, c) => sum + c.cost, 0),
tokens: last7DaysCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
},
last30Days: {
calls: last30DaysCalls.length,
cost: last30DaysCalls.reduce((sum, c) => sum + c.cost, 0),
tokens: last30DaysCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
},
allTime: {
calls: apiCalls.length,
cost: apiCalls.reduce((sum, c) => sum + c.cost, 0),
tokens: apiCalls.reduce((sum, c) => sum + c.inputTokens + c.outputTokens, 0)
}
};
}
// Initialize
loadData();
// Main dashboard
app.get('/', (req, res) => {
const stats = getStats();
const today = new Date().toISOString().split('T')[0];
const todaySummary = dailySummaries[today];
const memoryContext = agentMemory.getContextSummary();
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>AI Expenses Tracker - DW-Agents</title>
${getThemeStyles('professional-dark')}
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: var(--font-primary);
background: var(--color-background);
color: var(--color-text-primary);
padding: 20px;
min-height: 100vh;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 20px;
margin: 30px 0;
}
.stat-card {
background: var(--color-surface);
border: 1px solid var(--color-neutral200);
border-radius: var(--radius-lg);
padding: 25px;
box-shadow: var(--shadow-md);
}
.stat-card h3 {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
font-weight: var(--font-weight-semibold);
}
.stat-value {
font-size: var(--font-size-3xl);
font-weight: var(--font-weight-bold);
color: var(--color-primary);
margin-bottom: 5px;
}
.stat-value.cost {
color: var(--color-success);
}
.stat-value.warning {
color: var(--color-warning);
}
.stat-label {
color: var(--color-text-secondary);
font-size: var(--font-size-sm);
}
.budget-alert {
background: rgba(234, 179, 8, 0.1);
border: 2px solid var(--color-warning);
border-radius: var(--radius-lg);
padding: 20px;
margin: 20px 0;
}
.table-container {
background: var(--color-surface);
border: 1px solid var(--color-neutral200);
border-radius: var(--radius-lg);
padding: 25px;
margin: 30px 0;
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th, td {
padding: 12px;
text-align: left;
border-bottom: 1px solid var(--color-neutral200);
}
th {
background: var(--color-neutral100);
font-weight: var(--font-weight-semibold);
color: var(--color-text-primary);
position: sticky;
top: 0;
}
tr:hover {
background: var(--color-neutral100);
}
.provider-badge {
display: inline-block;
padding: 4px 12px;
border-radius: var(--radius-full);
font-size: var(--font-size-sm);
font-weight: var(--font-weight-medium);
}
.provider-claude {
background: rgba(138, 99, 210, 0.2);
color: #8a63d2;
}
.provider-gemini {
background: rgba(66, 133, 244, 0.2);
color: #4285f4;
}
.refresh-btn {
background: var(--color-primary);
color: var(--color-text-inverse);
border: none;
padding: 12px 24px;
border-radius: var(--radius-md);
font-weight: var(--font-weight-semibold);
cursor: pointer;
transition: all 0.2s;
}
.refresh-btn:hover {
background: var(--color-secondary);
transform: translateY(-2px);
}
</style>
</head>
<body>
${getUniversalHeader('AI Expenses Tracker', 9951)}
<div style="max-width: 1400px; margin: 0 auto;">
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
<div>
<h1 style="font-size: var(--font-size-2xl); margin-bottom: 8px;">AI API Expenses Dashboard</h1>
<p style="color: var(--color-text-secondary);">Real-time monitoring of Claude and Gemini API usage across all 30+ agents</p>
</div>
<button class="refresh-btn" onclick="location.reload()">🔄 Refresh</button>
</div>
${stats.today.cost >= budgetThresholds.daily ? `
<div class="budget-alert">
<h3 style="color: var(--color-warning); margin-bottom: 10px;">⚠️ Daily Budget Alert</h3>
<p>Today's expenses ($${stats.today.cost.toFixed(2)}) have exceeded the daily threshold of $${budgetThresholds.daily.toFixed(2)}</p>
</div>
` : ''}
<div class="stats-grid">
<div class="stat-card">
<h3>Today</h3>
<div class="stat-value cost ${stats.today.cost >= budgetThresholds.daily ? 'warning' : ''}">$${stats.today.cost.toFixed(4)}</div>
<div class="stat-label">${stats.today.calls} calls • ${stats.today.tokens.toLocaleString()} tokens</div>
<div class="stat-label" style="margin-top: 8px;">Budget: $${budgetThresholds.daily} daily</div>
</div>
<div class="stat-card">
<h3>Last 7 Days</h3>
<div class="stat-value cost">$${stats.last7Days.cost.toFixed(2)}</div>
<div class="stat-label">${stats.last7Days.calls} calls • ${stats.last7Days.tokens.toLocaleString()} tokens</div>
<div class="stat-label" style="margin-top: 8px;">Avg: $${(stats.last7Days.cost / 7).toFixed(2)}/day</div>
</div>
<div class="stat-card">
<h3>Last 30 Days</h3>
<div class="stat-value cost">$${stats.last30Days.cost.toFixed(2)}</div>
<div class="stat-label">${stats.last30Days.calls} calls • ${stats.last30Days.tokens.toLocaleString()} tokens</div>
<div class="stat-label" style="margin-top: 8px;">Budget: $${budgetThresholds.monthly} monthly</div>
</div>
<div class="stat-card">
<h3>All Time</h3>
<div class="stat-value cost">$${stats.allTime.cost.toFixed(2)}</div>
<div class="stat-label">${stats.allTime.calls} calls • ${stats.allTime.tokens.toLocaleString()} tokens</div>
<div class="stat-label" style="margin-top: 8px;">Tracking since deployment</div>
</div>
</div>
${todaySummary ? `
<div class="table-container">
<h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">📊 Today's Usage by Agent</h2>
<table>
<thead>
<tr>
<th>Agent</th>
<th>API Calls</th>
<th>Cost</th>
<th>% of Today</th>
</tr>
</thead>
<tbody>
${Object.entries(todaySummary.byAgent)
.sort((a, b) => b[1].cost - a[1].cost)
.map(([agent, data]) => `
<tr>
<td><strong>${agent}</strong></td>
<td>${data.calls}</td>
<td>$${data.cost.toFixed(4)}</td>
<td>${((data.cost / todaySummary.totalCost) * 100).toFixed(1)}%</td>
</tr>
`).join('')}
</tbody>
</table>
</div>
` : '<div class="table-container"><p style="color: var(--color-text-secondary);">No API calls recorded today yet.</p></div>'}
<div class="table-container">
<h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">📝 Recent API Calls</h2>
${apiCalls.length > 0 ? `
<table>
<thead>
<tr>
<th>Time</th>
<th>Agent</th>
<th>Provider</th>
<th>Model</th>
<th>Input</th>
<th>Output</th>
<th>Cost</th>
</tr>
</thead>
<tbody>
${apiCalls.slice(-50).reverse().map(call => `
<tr>
<td>${new Date(call.timestamp).toLocaleTimeString()}</td>
<td>${call.agent}</td>
<td><span class="provider-badge provider-${call.provider}">${call.provider.toUpperCase()}</span></td>
<td>${PRICING[call.provider][call.model]?.name || call.model}</td>
<td>${call.inputTokens.toLocaleString()}</td>
<td>${call.outputTokens.toLocaleString()}</td>
<td>$${call.cost.toFixed(6)}</td>
</tr>
`).join('')}
</tbody>
</table>
` : '<p style="color: var(--color-text-secondary);">No API calls recorded yet.</p>'}
</div>
<div class="table-container">
<h2 style="margin-bottom: 20px; font-size: var(--font-size-xl);">💡 Recent Activity</h2>
${activityLog.length > 0 ? `
<table>
<thead>
<tr>
<th>Time</th>
<th>Action</th>
<th>Details</th>
</tr>
</thead>
<tbody>
${activityLog.slice(0, 20).map(log => `
<tr>
<td>${new Date(log.timestamp).toLocaleTimeString()}</td>
<td><strong>${log.action}</strong></td>
<td>${log.details}</td>
</tr>
`).join('')}
</tbody>
</table>
` : '<p style="color: var(--color-text-secondary);">No activity logged yet.</p>'}
</div>
</div>
<script>
// Auto-refresh every 30 seconds
setTimeout(() => location.reload(), 30000);
</script>
</body>
</html>
`);
});
// API endpoint to track expenses (called by other agents)
app.post('/api/track', async (req, res) => {
const { agent, provider, model, inputTokens, outputTokens, endpoint } = req.body;
if (!agent || !provider || !model || inputTokens === undefined || outputTokens === undefined) {
return res.status(400).json({ error: 'Missing required fields' });
}
const call = await trackAPICall(agent, provider, model, inputTokens, outputTokens, endpoint);
res.json({
success: true,
cost: call.cost,
totalToday: getStats().today.cost
});
});
// API endpoints
app.get('/api/stats', (req, res) => {
res.json(getStats());
});
app.get('/api/pricing', (req, res) => {
res.json(PRICING);
});
app.get('/api/activity', (req, res) => {
res.json(activityLog.slice(0, 50));
});
// Chat endpoint with memory integration
app.post('/api/chat', async (req, res) => {
try {
const { message } = req.body;
if (!message) {
return res.status(400).json({ error: 'Message is required' });
}
// Handle memory commands
if (message.toLowerCase().match(/^(remember|note|preference|learning):/i)) {
const parts = message.split(':');
const type = parts[0].toLowerCase();
const content = parts.slice(1).join(':').trim();
const memType = type === 'remember' ? 'note' : type as any;
agentMemory.add(content, memType);
return res.json({ response: `✓ Saved to memory: ${content}` });
}
// Handle budget threshold updates
if (message.toLowerCase().includes('set budget') || message.toLowerCase().includes('set threshold')) {
const match = message.match(/daily\s+\$?(\d+(?:\.\d+)?)|weekly\s+\$?(\d+(?:\.\d+)?)|monthly\s+\$?(\d+(?:\.\d+)?)/i);
if (match) {
if (match[1]) budgetThresholds.daily = parseFloat(match[1]);
if (match[2]) budgetThresholds.weekly = parseFloat(match[2]);
if (match[3]) budgetThresholds.monthly = parseFloat(match[3]);
agentMemory.add(JSON.stringify(budgetThresholds), 'preference');
return res.json({
response: `✓ Updated budget thresholds:\nDaily: $${budgetThresholds.daily}\nWeekly: $${budgetThresholds.weekly}\nMonthly: $${budgetThresholds.monthly}`
});
}
}
const stats = getStats();
const memoryContext = agentMemory.getContextSummary();
const response = await anthropic.messages.create({
model: 'claude-3-5-sonnet-20241022',
max_tokens: 2000,
messages: [{
role: 'user',
content: `You are the AI Expenses Tracker agent for Designer Wallcoverings. You monitor and analyze API usage and costs across all 30+ agents.
Current Statistics:
- Today: $${stats.today.cost.toFixed(4)} (${stats.today.calls} calls, ${stats.today.tokens.toLocaleString()} tokens)
- Last 7 days: $${stats.last7Days.cost.toFixed(2)} (${stats.last7Days.calls} calls)
- Last 30 days: $${stats.last30Days.cost.toFixed(2)} (${stats.last30Days.calls} calls)
- All time: $${stats.allTime.cost.toFixed(2)} (${stats.allTime.calls} total calls)
Budget Thresholds:
- Daily: $${budgetThresholds.daily}
- Weekly: $${budgetThresholds.weekly}
- Monthly: $${budgetThresholds.monthly}
Memory Context:
${memoryContext}
User question: ${message}
Provide helpful analysis, recommendations, and cost optimization suggestions. If the user asks about specific agents or time periods, analyze the data and provide insights.`
}]
});
// Track this API call
const usage = response.usage;
await trackAPICall('ai-expenses', 'claude', 'claude-3-5-sonnet-20241022', usage.input_tokens, usage.output_tokens, '/messages');
res.json({
response: response.content[0].type === 'text' ? response.content[0].text : ''
});
} catch (error) {
console.error('Chat error:', error);
res.status(500).json({ error: 'Failed to process message' });
}
});
app.listen(PORT, '0.0.0.0', () => {
console.log('');
console.log('💰 AI Expenses Tracker');
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
console.log('🌍 External: http://45.61.58.125:' + PORT);
console.log('📊 Tracking: Claude & Gemini API usage');
console.log('💵 Real-time: Cost calculations');
console.log('🎯 Budget Alerts: Enabled');
console.log('💾 Memory System: Active');
console.log('');
console.log(`📈 Loaded ${apiCalls.length} historical API calls`);
console.log(`💰 Budget - Daily: $${budgetThresholds.daily} | Weekly: $${budgetThresholds.weekly} | Monthly: $${budgetThresholds.monthly}`);
console.log('✅ AI Expenses Tracker ready...');
});