Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Tracks LLM token consumption and usage metrics for billing, monitoring, and optimization. Use this to log token usage, calculate costs, generate invoices, and understand which agents or users consume the most resources.
.claude/skills/microck-convex-agents-usage-tracking/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 83% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 91% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 114% | 0% |
| case-12 | ✗→✓ | ▲ Improved | 173% | 0% |
| case-13 | ✗→✓ | ▲ Improved | 162% | 0% |
Usage tracking records how many tokens each agent uses, enabling accurate billing, cost monitoring, and performance optimization. Essential for understanding LLM costs and user impact.
Create a handler to log usage:
typescript// convex/agents/myAgent.ts import { Agent } from "@convex-dev/agent"; import { components } from "../_generated/api"; import { openai } from "@ai-sdk/openai"; import { internal } from "../_generated/api"; const myAgent = new Agent(components.agent, { name: "My Agent", languageModel: openai.chat("gpt-4o-mini"), usageHandler: async (ctx, args) => { const { userId, threadId, agentName, model, provider, usage, // { inputTokens, outputTokens, totalTokens } providerMetadata, } = args; // Save usage to database await ctx.runMutation(internal.usage.recordUsage, { userId, threadId, agentName, model, provider, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, totalTokens: usage.totalTokens, timestamp: Date.now(), }); }, });
Save usage data for later analysis:
typescript// convex/usage.ts import { internalMutation } from "./_generated/server"; import { v } from "convex/values"; import { defineTable, defineSchema } from "convex/server"; export const schema = defineSchema({ usage: defineTable({ userId: v.string(), threadId: v.optional(v.string()), agentName: v.optional(v.string()), model: v.string(), provider: v.string(), inputTokens: v.number(), outputTokens: v.number(), totalTokens: v.number(), cost: v.number(), // Cost in dollars date: v.string(), // ISO date for daily rollups billingPeriod: v.string(), // YYYY-MM for monthly }) .index("billingPeriod_userId", ["billingPeriod", "userId"]) .index("date_userId", ["date", "userId"]) .index("userId", ["userId"]), invoices: defineTable({ userId: v.string(), billingPeriod: v.string(), amount: v.number(), status: v.union( v.literal("pending"), v.literal("paid"), v.literal("failed") ), generatedAt: v.number(), }) .index("billingPeriod_userId", ["billingPeriod", "userId"]) .index("userId", ["userId"]), }); export const recordUsage = internalMutation({ args: { userId: v.string(), threadId: v.optional(v.string()), agentName: v.optional(v.string()), model: v.string(), provider: v.string(), inputTokens: v.number(), outputTokens: v.number(), totalTokens: v.number(), }, handler: async ( ctx, { userId, threadId, agentName, model, provider, inputTokens, outputTokens, totalTokens, } ) => { const today = new Date().toISOString().split("T")[0]; const billingPeriod = today.substring(0, 7); // YYYY-MM // Calculate cost (example: $0.015 per 1M input tokens, $0.060 per 1M output) const cost = (inputTokens / 1_000_000) * 0.015 + (outputTokens / 1_000_000) * 0.06; await ctx.db.insert("usage", { userId, threadId, agentName, model, provider, inputTokens, outputTokens, totalTokens, cost, date: today, billingPeriod, }); }, });
Get total usage for a specific user:
typescript// convex/usage.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const getUserUsage = query({ args: { userId: v.string() }, handler: async (ctx, { userId }) => { const records = await ctx.db .query("usage") .withIndex("userId", (q) => q.eq("userId", userId)) .collect(); const totals = records.reduce( (acc, record) => ({ inputTokens: acc.inputTokens + record.inputTokens, outputTokens: acc.outputTokens + record.outputTokens, totalTokens: acc.totalTokens + record.totalTokens, cost: acc.cost + record.cost, }), { inputTokens: 0, outputTokens: 0, totalTokens: 0, cost: 0 } ); return totals; }, }); export const getMonthlyUsageByUser = query({ args: { billingPeriod: v.string() }, handler: async (ctx, { billingPeriod }) => { const records = await ctx.db .query("usage") .withIndex("billingPeriod_userId", (q) => q.eq("billingPeriod", billingPeriod) ) .collect(); // Group by userId const byUser: Record<string, any> = {}; for (const record of records) { if (!byUser[record.userId]) { byUser[record.userId] = { userId: record.userId, totalTokens: 0, cost: 0, records: 0, }; } byUser[record.userId].totalTokens += record.totalTokens; byUser[record.userId].cost += record.cost; byUser[record.userId].records += 1; } return Object.values(byUser); }, });
Create invoices from usage data:
typescript// convex/usage.ts import { action } from "./_generated/server"; import { v } from "convex/values"; export const generateInvoices = action({ args: { billingPeriod: v.string() }, handler: async (ctx, { billingPeriod }) => { // Get all usage for the period const records = await ctx.db .query("usage") .withIndex("billingPeriod_userId", (q) => q.eq("billingPeriod", billingPeriod) ) .collect(); // Group by user const byUser: Record<string, number> = {}; for (const record of records) { byUser[record.userId] = (byUser[record.userId] || 0) + record.cost; } // Create invoices for (const [userId, amount] of Object.entries(byUser)) { const existingInvoice = await ctx.db .query("invoices") .filter( (inv) => inv.billingPeriod === billingPeriod && inv.userId === userId ) .first(); if (!existingInvoice) { await ctx.db.insert("invoices", { userId, billingPeriod, amount, status: "pending", generatedAt: Date.now(), }); } } return { invoicesCreated: Object.keys(byUser).length }; }, });
Compare efficiency across agents:
typescript// convex/usage.ts import { query } from "./_generated/server"; import { v } from "convex/values"; export const getUsageByAgent = query({ args: { userId: v.string(), billingPeriod: v.string() }, handler: async (ctx, { userId, billingPeriod }) => { const records = await ctx.db .query("usage") .withIndex("billingPeriod_userId", (q) => q.eq("billingPeriod", billingPeriod) ) .filter((r) => r.userId === userId) .collect(); // Group by agent const byAgent: Record<string, any> = {}; for (const record of records) { const agent = record.agentName || "unknown"; if (!byAgent[agent]) { byAgent[agent] = { agent, totalTokens: 0, inputTokens: 0, outputTokens: 0, cost: 0, calls: 0, }; } byAgent[agent].totalTokens += record.totalTokens; byAgent[agent].inputTokens += record.inputTokens; byAgent[agent].outputTokens += record.outputTokens; byAgent[agent].cost += record.cost; byAgent[agent].calls += 1; } return Object.values(byAgent).sort((a, b) => b.cost - a.cost); }, });
Generate invoices automatically monthly:
typescript// convex/crons.ts import { cronJobs } from "convex/server"; import { internal } from "./_generated/api"; const crons = cronJobs(); // Generate invoices on the 2nd day of each month at midnight UTC crons.monthly( "generateMonthlyInvoices", { day: 2, hourUTC: 0, minuteUTC: 0 }, internal.usage.generateInvoices, { billingPeriod: calculatePreviousMonth() } ); export default crons; function calculatePreviousMonth(): string { const now = new Date(); const month = now.getMonth() === 0 ? 11 : now.getMonth() - 1; const year = now.getMonth() === 0 ? now.getFullYear() - 1 : now.getFullYear(); return `${year}-${String(month + 1).padStart(2, "0")}`; }
Alert on high usage:
typescript// convex/usage.ts import { action } from "./_generated/server"; import { v } from "convex/values"; export const checkUsageAlerts = action({ args: {}, handler: async (ctx, {}) => { const today = new Date().toISOString().split("T")[0]; // Get today's usage by user const records = await ctx.db .query("usage") .filter((r) => r.date === today) .collect(); const byUser: Record<string, number> = {}; for (const record of records) { byUser[record.userId] = (byUser[record.userId] || 0) + record.cost; } // Alert on users exceeding $100/day const alerts = []; for (const [userId, cost] of Object.entries(byUser)) { if (cost > 100) { alerts.push({ userId, cost, reason: "Daily spend exceeded $100" }); } } // Send alerts (email, Slack, etc.) for (const alert of alerts) { await ctx.runMutation(internal.notifications.sendAlert, alert); } return alerts; }, });
Query usage for display:
typescript// convex/usage.ts import { query } from "./_generated/server"; export const getDashboardStats = query({ args: { userId: v.string() }, handler: async (ctx, { userId }) => { // This month's usage const today = new Date(); const billingPeriod = `${today.getFullYear()}-${String( today.getMonth() + 1 ).padStart(2, "0")}`; const monthlyRecords = await ctx.db .query("usage") .withIndex("billingPeriod_userId", (q) => q.eq("billingPeriod", billingPeriod) ) .filter((r) => r.userId === userId) .collect(); const monthlyStats = monthlyRecords.reduce( (acc, r) => ({ totalTokens: acc.totalTokens + r.totalTokens, cost: acc.cost + r.cost, }), { totalTokens: 0, cost: 0 } ); // All time const allRecords = await ctx.db .query("usage") .withIndex("userId", (q) => q.eq("userId", userId)) .collect(); const allTimeStats = allRecords.reduce( (acc, r) => ({ totalTokens: acc.totalTokens + r.totalTokens, cost: acc.cost + r.cost, }), { totalTokens: 0, cost: 0 } ); return { monthly: monthlyStats, allTime: allTimeStats, averageDailyCost: monthlyStats.cost / Math.min(today.getDate(), 30), }; }, });
typescript// convex/billing/complete.ts import { Agent } from "@convex-dev/agent"; import { components } from "../_generated/api"; import { openai } from "@ai-sdk/openai"; import { internal } from "../_generated/api"; // Cost per million tokens const PRICING = { "gpt-4o-mini": { input: 0.015, output: 0.06, }, }; export const billingAgent = new Agent(components.agent, { name: "Billing Agent", languageModel: openai.chat("gpt-4o-mini"), usageHandler: async (ctx, { usage, userId, model }) => { if (!userId) return; const pricing = PRICING[model as keyof typeof PRICING] || { input: 0.001, output: 0.002, }; const cost = (usage.inputTokens / 1_000_000) * pricing.input + (usage.outputTokens / 1_000_000) * pricing.output; await ctx.runMutation(internal.billing.recordUsage, { userId, model, inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, totalTokens: usage.totalTokens, cost, }); }, });
| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 16,364 | 12,898 | -21% | 1 | 1 | 0% | 3,090 | 6,295 | +104% | 0 | 0 | — |
case-02 | pass→pass | 16,602 | 11,325 | -32% | 1 | 1 | 0% | 3,352 | 6,197 | +85% | 0 | 0 | — |
case-09 | pass→pass | 17,443 | 9,670 | -45% | 1 | 1 | 0% | 3,614 | 5,780 | +60% | 0 | 0 | — |
case-03 | pass→pass | 16,855 | 16,457 | -2% | 1 | 1 | 0% | 3,525 | 7,441 | +111% | 0 | 0 | — |
case-04 | fail→pass | 17,457 | 13,485 | -23% | 1 | 1 | 0% | 3,848 | 7,049 | +83% | 0 | 0 | — |
case-05 | fail→fail | 16,299 | 13,950 | -14% | 1 | 1 | 0% | 3,436 | 6,923 | +101% | 0 | 0 | — |
case-06 | fail→fail | 17,488 | 23,971 | +37% | 1 | 1 | 0% | 3,708 | 9,477 | +156% | 0 | 0 | — |
case-07 | fail→pass | 17,473 | 12,520 | -28% | 1 | 1 | 0% | 3,254 | 6,228 | +91% | 0 | 0 | — |
case-08 | fail→pass | 15,567 | 9,653 | -38% | 1 | 1 | 0% | 2,704 | 5,792 | +114% | 0 | 0 | — |
case-10 | fail→fail | 13,209 | 7,573 | -43% | 1 | 1 | 0% | 1,961 | 5,152 | +163% | 0 | 0 | — |
case-11 | pass→pass | 13,754 | 7,891 | -43% | 1 | 1 | 0% | 2,456 | 5,271 | +115% | 0 | 0 | — |
case-12 | fail→pass | 11,297 | 7,118 | -37% | 1 | 1 | 0% | 1,926 | 5,258 | +173% | 0 | 0 | — |
case-13 | fail→pass | 13,036 | 10,144 | -22% | 1 | 1 | 0% | 2,250 | 5,898 | +162% | 0 | 0 | — |
case-14 | pass→pass | 13,319 | 6,300 | -53% | 1 | 1 | 0% | 2,463 | 5,094 | +107% | 0 | 0 | — |
case-15 | pass→pass | 12,165 | 3,412 | -72% | 1 | 1 | 0% | 1,988 | 4,607 | +132% | 0 | 0 | — |
case-16 | fail→pass | 12,442 | 8,007 | -36% | 1 | 1 | 0% | 2,412 | 5,521 | +129% | 0 | 0 | — |
case-17 | fail→fail | 15,271 | 11,999 | -21% | 1 | 1 | 0% | 2,825 | 5,936 | +110% | 0 | 0 | — |
case-18 | fail→pass | 15,182 | 5,166 | -66% | 1 | 1 | 0% | 2,575 | 4,831 | +88% | 0 | 0 | — |
case-19 | pass→pass | 11,141 | 3,776 | -66% | 1 | 1 | 0% | 1,745 | 4,469 | +156% | 0 | 0 | — |
case-20 | fail→pass | 9,635 | 5,739 | -40% | 1 | 1 | 0% | 1,729 | 4,977 | +188% | 0 | 0 | — |
case-21 | pass→pass | 11,199 | 6,823 | -39% | 1 | 1 | 0% | 1,996 | 5,037 | +152% | 0 | 0 | — |
case-22 | fail→pass | 5,971 | 3,836 | -36% | 1 | 1 | 0% | 1,123 | 4,615 | +311% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +41 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.