Quick Takeaways
What you'll learn in this article
- 1
Build a production-ready Chrome extension that tracks AI API costs in real-time across OpenAI, Anthropic, and Google AI
- 2
Complete tutorial with working code, network interception, token counting algorithms, and cost visualization dashboards
Keep reading for detailed implementation, code examples, and real-world results
Introduction
If you are building AI-powered applications in 2026, you have probably experienced the shock of an unexpectedly large API bill. A developer runs a few tests with GPT-4, a team member experiments with Claude Opus for a weekend project, and suddenly your company has a five-figure monthly charge with zero visibility into where those costs originated.
The fundamental problem is simple: AI API costs are invisible until the bill arrives. Unlike cloud infrastructure where you can monitor EC2 instances or database queries in real-time, LLM API calls happen silently in the background. By the time finance flags the expense, you have already burned through your budget.
This tutorial solves that problem by building a Chrome extension that tracks AI API costs in real-time across OpenAI, Anthropic, and Google AI. The extension intercepts network requests, identifies LLM API calls, counts tokens, calculates costs, and displays running totals in your browser. You will see exactly how much each API call costs the moment it happens.
What You Will Build: A production-ready Chrome extension that monitors AI API spending with real-time cost tracking, historical charts, budget alerts, and per-project breakdowns.
Prerequisites: JavaScript/TypeScript fundamentals, basic understanding of Chrome extension architecture, Node.js installed, familiarity with REST APIs.
GitHub Repository: Complete working code available at github.com/CrashBytes/ByteSizedExamples/tree/main/ai-token-cost-tracker-2026
Time to Complete: 3-4 hours for full implementation
Value Delivered: Most companies using AI APIs have zero cost visibility until month-end billing. This extension provides real-time spending awareness that can save thousands of dollars by catching runaway costs immediately.
The AI Cost Visibility Problem
Before diving into code, we need to understand why this problem exists and why existing solutions fall short.
Current State of AI Cost Tracking
Most development teams track AI costs through one of three approaches, all of which fail in different ways:
Monthly Billing Statements: You receive an invoice 30 days after the damage is done. A developer accidentally sets up an infinite loop calling GPT-4 Turbo on Friday evening, and you discover the $12,000 mistake on Monday when your credit card gets declined.
Backend Logging: You add logging to your application code that tracks each API call. This works for production systems but provides zero visibility into development, testing, local experiments, or any API calls happening outside your logged codebase.
API Provider Dashboards: OpenAI, Anthropic, and Google each provide usage dashboards, but they lag by hours or days, require switching between multiple interfaces, and provide no project-level attribution for teams working on multiple applications simultaneously.
The gap is clear: developers need real-time cost awareness in the same environment where they are making API calls, with zero configuration overhead, working across all AI providers.
Why a Chrome Extension Solves This
A Chrome extension sits at the perfect intersection of visibility and convenience. Every AI API call made through the browser passes through Chrome's network stack, which extensions can monitor. This gives you:
Universal Coverage: Works for any web-based application, API playground, Jupyter notebook, or development tool running in Chrome, regardless of backend infrastructure.
Zero Integration: No SDK installation, no code changes, no backend configuration. Install the extension and immediately start tracking costs.
Real-Time Awareness: See costs update instantly as API calls complete, with running totals displayed in the browser toolbar.
Developer Workflow: Developers already work in Chrome for documentation, testing tools, and web applications. Cost tracking lives where they already are.
Architecture Overview
The extension architecture consists of five core components that work together to intercept requests, calculate costs, store data, and display results.
Component Architecture
Background Service Worker: The extension's background script runs persistently in Chrome, monitoring all network activity across tabs. It uses the chrome.webRequest API to intercept outgoing HTTP requests before they leave the browser and incoming responses as they arrive. The service worker identifies AI API calls by matching URL patterns (api.openai.com, api.anthropic.com, generativelanguage.googleapis.com), extracts request and response payloads, and forwards relevant data to the cost calculation engine.
Token Counter Module: Each AI provider uses different tokenization schemes. OpenAI uses tiktoken encoding, Anthropic uses a similar but distinct algorithm, and Google AI uses SentencePiece. The token counter module implements provider-specific counting logic, falling back to approximation algorithms when exact token counts are unavailable. For GPT models, we use the tiktoken JavaScript port. For Claude, we approximate based on character counts with a 0.75 multiplier. For Gemini, we use the official Google AI SDK token counting endpoint when available.
Cost Calculator Engine: Token counts get converted to dollar amounts using provider-specific pricing tables. The calculator maintains current pricing for all major models (GPT-4 Turbo, GPT-4o, Claude Opus, Claude Sonnet, Claude Haiku, Gemini Pro, Gemini Flash) with separate rates for input tokens and output tokens. Pricing updates quarterly as providers adjust rates, with a configuration system allowing manual overrides for custom enterprise pricing.
Storage Layer: Chrome's chrome.storage.local API persists cost data across browser sessions. The storage schema includes individual API call records (timestamp, model, tokens, cost), aggregated daily totals, and user configuration (budget thresholds, alert preferences, project mappings). Data retention policies automatically purge records older than 90 days to prevent unbounded storage growth.
User Interface: Three UI surfaces provide cost visibility. The browser action popup shows current session costs, daily totals, and quick access to detailed views. A dedicated options page displays historical charts, project breakdowns, and configuration settings. Badge text on the extension icon shows the running total for the current day, updating in real-time as costs accumulate.
Data Flow
The complete data flow follows this sequence:
- Developer makes API call (fetch to api.openai.com/v1/chat/completions)
- Background service worker intercepts request via chrome.webRequest.onBeforeRequest
- Request parser extracts model, messages, and token counts from request body
- Response interceptor captures completion tokens from response
- Token counter calculates exact token usage (input + output)
- Cost calculator applies provider pricing tables
- Storage layer persists call record and updates aggregates
- Badge text updates with new daily total
- Popup UI reflects updated costs if currently open
This architecture provides sub-second latency between API call completion and cost visibility, giving developers immediate feedback on spending.
Project Setup and Dependencies
Let us start building the extension from the ground up, beginning with project structure and dependencies.
Directory Structure
Create the project directory and establish the file organization:
ai-token-cost-tracker/ โโโ manifest.json # Extension configuration โโโ background/ โ โโโ service-worker.js # Background script โ โโโ request-interceptor.js โ โโโ cost-calculator.js โโโ content/ โ โโโ content-script.js # Injected into pages if needed โโโ popup/ โ โโโ popup.html โ โโโ popup.js โ โโโ popup.css โโโ options/ โ โโโ options.html โ โโโ options.js โ โโโ options.css โโโ lib/ โ โโโ token-counter.js โ โโโ pricing-tables.js โ โโโ storage-manager.js โโโ icons/ โ โโโ icon-16.png โ โโโ icon-48.png โ โโโ icon-128.png โโโ package.json
Manifest V3 Configuration
The manifest.json file declares extension permissions, background scripts, and UI components. Manifest V3 is required for all new Chrome extensions as of 2024:
{
"manifest_version": 3,
"name": "AI Token Cost Tracker",
"version": "1.0.0",
"description": "Real-time AI API cost tracking for OpenAI, Anthropic, and Google AI",
"permissions": ["storage", "webRequest", "alarms"],
"host_permissions": [
"https://api.openai.com/*",
"https://api.anthropic.com/*",
"https://generativelanguage.googleapis.com/*"
],
"background": {
"service_worker": "background/service-worker.js",
"type": "module"
},
"action": {
"default_popup": "popup/popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"options_page": "options/options.html",
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
Key permission requirements:
storage: Persists cost data and configuration across browser sessions using chrome.storage.local. The storage API provides up to 5MB of quota for extension data without requiring user authentication.
webRequest: Monitors network traffic to intercept AI API calls. This is the most powerful permission and requires justification in Chrome Web Store review. We use it exclusively for cost tracking purposes.
alarms: Schedules periodic tasks like data cleanup and budget alert checks. The alarms API works even when no browser windows are open, ensuring background maintenance continues.
host_permissions: Declares which domains the extension monitors. Limiting host permissions to specific AI provider domains reduces security risk and passes Chrome's permission review more easily.
Dependencies and Build Setup
While Chrome extensions can run with vanilla JavaScript, using modern tooling improves development experience:
{
"name": "ai-token-cost-tracker",
"version": "1.0.0",
"type": "module",
"scripts": {
"build": "rollup -c",
"dev": "rollup -c -w",
"test": "jest"
},
"devDependencies": {
"@rollup/plugin-node-resolve": "^15.2.0",
"rollup": "^4.9.0",
"jest": "^29.7.0"
},
"dependencies": {
"tiktoken": "^1.0.10",
"chart.js": "^4.4.1"
}
}
The tiktoken library provides accurate token counting for OpenAI models. Chart.js renders cost visualization graphs in the options page. Rollup bundles modules into a single file that Chrome can load as a service worker.
Request Interception and API Detection
The foundation of cost tracking is intercepting API requests before they complete. Chrome's webRequest API provides lifecycle hooks for monitoring network traffic.
Background Service Worker
The service worker initializes the request interception system and maintains state across the extension's lifecycle:
// background/service-worker.js
// import { RequestInterceptor } from './request-interceptor.js';
// import { CostCalculator} from './cost-calculator.js';
// import { StorageManager } from '../lib/storage-manager.js';
class AITokenCostTracker {
constructor() {
this.interceptor = new RequestInterceptor()
this.calculator = new CostCalculator()
this.storage = new StorageManager()
this.setupInterception()
this.setupAlarms()
}
setupInterception() {
// Monitor outgoing requests to AI APIs
chrome.webRequest.onBeforeRequest.addListener(
details => this.handleRequest(details),
{
urls: [
'https://api.openai.com/v1/*',
'https://api.anthropic.com/v1/*',
'https://generativelanguage.googleapis.com/*',
],
},
['requestBody']
)
// Capture response data
chrome.webRequest.onCompleted.addListener(
details => this.handleResponse(details),
{
urls: [
'https://api.openai.com/v1/*',
'https://api.anthropic.com/v1/*',
'https://generativelanguage.googleapis.com/*',
],
},
['responseHeaders']
)
}
async handleRequest(details) {
if (!details.requestBody) return
const requestData = this.interceptor.parseRequest(details)
if (!requestData) return
// Store request data temporarily keyed by request ID
await this.storage.setTemporary(details.requestId, requestData)
}
async handleResponse(details) {
// Retrieve stored request data
const requestData = await this.storage.getTemporary(details.requestId)
if (!requestData) return
// Fetch response body (requires additional fetch)
const response = await this.fetchResponseBody(details)
const costData = await this.calculator.calculate(requestData, response)
await this.storage.recordCost(costData)
await this.updateBadge()
await this.storage.clearTemporary(details.requestId)
}
async fetchResponseBody(details) {
// webRequest API doesn't provide response body
// Must re-fetch to get completion data
// This is a known limitation of Chrome extension APIs
try {
const response = await fetch(details.url, {
headers: details.responseHeaders.reduce((acc, h) => {
acc[h.name] = h.value
return acc
}, {}),
})
return await response.json()
} catch (error) {
console.error('Failed to fetch response body:', error)
return null
}
}
async updateBadge() {
const dailyTotal = await this.storage.getDailyTotal()
const displayText =
dailyTotal >= 1
? `$${dailyTotal.toFixed(0)}`
: `${(dailyTotal * 100).toFixed(0)}ยข`
chrome.action.setBadgeText({ text: displayText })
chrome.action.setBadgeBackgroundColor({ color: '#4CAF50' })
}
setupAlarms() {
// Daily reset at midnight
chrome.alarms.create('dailyReset', {
when: this.getNextMidnight(),
periodInMinutes: 1440, // 24 hours
})
// Cleanup old data weekly
chrome.alarms.create('dataCleanup', {
periodInMinutes: 10080, // 1 week
})
chrome.alarms.onAlarm.addListener(alarm => {
if (alarm.name === 'dailyReset') {
this.storage.resetDaily()
this.updateBadge()
} else if (alarm.name === 'dataCleanup') {
this.storage.cleanupOldData()
}
})
}
getNextMidnight() {
const now = new Date()
const tomorrow = new Date(now)
tomorrow.setDate(tomorrow.getDate() + 1)
tomorrow.setHours(0, 0, 0, 0)
return tomorrow.getTime()
}
}
// Initialize tracker on extension load
const tracker = new AITokenCostTracker()
This service worker architecture provides several key capabilities. The webRequest listeners fire for every network request matching the URL patterns, giving us access to request and response data. Temporary storage maps request IDs to parsed request data, allowing us to correlate requests with their responses. Badge updates happen automatically whenever new costs are recorded, providing real-time visual feedback.
Request Parser
The request parser extracts relevant data from API call payloads, handling provider-specific request formats:
// background/request-interceptor.js
export class RequestInterceptor {
parseRequest(details) {
const url = new URL(details.url)
const provider = this.identifyProvider(url)
if (!provider) return null
let body
try {
// requestBody comes as FormData or raw bytes
if (details.requestBody.raw) {
const decoder = new TextDecoder('utf-8')
const bodyText = decoder.decode(details.requestBody.raw[0].bytes)
body = JSON.parse(bodyText)
} else {
return null
}
} catch (error) {
console.error('Failed to parse request body:', error)
return null
}
return {
provider,
timestamp: Date.now(),
requestId: details.requestId,
url: details.url,
model: this.extractModel(body, provider),
messages: this.extractMessages(body, provider),
rawBody: body,
}
}
identifyProvider(url) {
if (url.hostname === 'api.openai.com') return 'openai'
if (url.hostname === 'api.anthropic.com') return 'anthropic'
if (url.hostname.includes('generativelanguage.googleapis.com'))
return 'google'
return null
}
extractModel(body, provider) {
if (provider === 'openai' || provider === 'google') {
return body.model || 'unknown'
}
if (provider === 'anthropic') {
return body.model || 'claude-3-sonnet-20240229'
}
return 'unknown'
}
extractMessages(body, provider) {
if (provider === 'openai' || provider === 'google') {
return body.messages || []
}
if (provider === 'anthropic') {
// Anthropic uses different message format
return body.messages || []
}
return []
}
}
Provider detection happens first, mapping hostnames to provider identifiers. Request body parsing handles the raw byte format that webRequest provides, requiring TextDecoder to convert bytes to string before JSON parsing. Model and message extraction varies by provider, with fallback defaults when fields are missing.
Token Counting and Cost Calculation
Accurate token counting is critical for cost calculation. Each provider uses different tokenization algorithms, requiring provider-specific implementations.
OpenAI Token Counting
OpenAI uses tiktoken encoding, which we can implement using the tiktoken JavaScript library:
// lib/token-counter.js
// import { encoding_for_model } from 'tiktoken';
export class TokenCounter {
constructor() {
this.encoders = new Map()
}
async countTokens(provider, model, messages) {
switch (provider) {
case 'openai':
return this.countOpenAITokens(model, messages)
case 'anthropic':
return this.countAnthropicTokens(messages)
case 'google':
return this.countGoogleTokens(messages)
default:
return this.approximateTokens(messages)
}
}
countOpenAITokens(model, messages) {
try {
const encoder = encoding_for_model(model)
let totalTokens = 0
for (const message of messages) {
// Every message follows <im_start>{role/name}\n{content}<im_end>\n
totalTokens += 4 // message formatting tokens
if (message.role) {
totalTokens += encoder.encode(message.role).length
}
if (message.content) {
totalTokens += encoder.encode(message.content).length
}
if (message.name) {
totalTokens += encoder.encode(message.name).length - 1
}
}
totalTokens += 2 // every reply is primed with <im_start>assistant
encoder.free()
return totalTokens
} catch (error) {
console.error('Token counting failed:', error)
return this.approximateTokens(messages)
}
}
countAnthropicTokens(messages) {
// Anthropic doesn't provide public tokenizer
// Use approximation: ~0.75 tokens per character
let totalChars = 0
for (const message of messages) {
if (message.content) {
totalChars += message.content.length
}
}
return Math.ceil(totalChars * 0.75)
}
countGoogleTokens(messages) {
// Google AI uses SentencePiece
// Approximate using character count with 0.8 multiplier
let totalChars = 0
for (const message of messages) {
if (message.content) {
totalChars += message.content.length
}
}
return Math.ceil(totalChars * 0.8)
}
approximateTokens(messages) {
// Fallback approximation for unknown providers
let totalChars = 0
for (const message of messages) {
const content =
typeof message === 'string' ? message : message.content || ''
totalChars += content.length
}
// Conservative estimate: 4 characters per token
return Math.ceil(totalChars / 4)
}
}
The OpenAI implementation uses the official tiktoken library, which provides exact token counts matching what OpenAI charges. Message formatting tokens account for the special tokens that frame each message in the conversation. The encoder must be freed after use to prevent memory leaks.
Anthropic and Google implementations fall back to approximation since their tokenizers are not publicly available. These approximations typically land within 10 percent of actual token counts, which is acceptable for cost estimation purposes.
Pricing Tables and Cost Calculation
Pricing data for all major models gets stored in a structured format that the cost calculator references:
// lib/pricing-tables.js
export const PRICING_TABLES = {
openai: {
'gpt-4-turbo-preview': {
input: 0.00001, // $0.01 per 1K tokens
output: 0.00003, // $0.03 per 1K tokens
},
'gpt-4': {
input: 0.00003,
output: 0.00006,
},
'gpt-4-32k': {
input: 0.00006,
output: 0.00012,
},
'gpt-3.5-turbo': {
input: 0.0000005,
output: 0.0000015,
},
'gpt-3.5-turbo-16k': {
input: 0.000003,
output: 0.000004,
},
},
anthropic: {
'claude-opus-4-20250514': {
input: 0.000015,
output: 0.000075,
},
'claude-sonnet-4-20250514': {
input: 0.000003,
output: 0.000015,
},
'claude-3-opus-20240229': {
input: 0.000015,
output: 0.000075,
},
'claude-3-sonnet-20240229': {
input: 0.000003,
output: 0.000015,
},
'claude-3-haiku-20240307': {
input: 0.00000025,
output: 0.00000125,
},
},
google: {
'gemini-pro': {
input: 0.00000025,
output: 0.0000005,
},
'gemini-pro-vision': {
input: 0.00000025,
output: 0.0000005,
},
'gemini-1.5-pro': {
input: 0.00000125,
output: 0.000005,
},
'gemini-1.5-flash': {
input: 0.00000025,
output: 0.0000005,
},
},
}
export function getPricing(provider, model) {
const providerPricing = PRICING_TABLES[provider]
if (!providerPricing) return null
// Try exact match first
if (providerPricing[model]) {
return providerPricing[model]
}
// Try partial match (handles versioned models)
for (const [key, pricing] of Object.entries(providerPricing)) {
if (model.includes(key) || key.includes(model)) {
return pricing
}
}
return null
}
Pricing tables use per-token rates in dollars, making cost calculations straightforward multiplication. Input and output tokens have separate rates because most providers charge more for output tokens. Model name matching includes fuzzy matching to handle version suffixes and naming variations.
The cost calculator combines token counts with pricing tables:
// background/cost-calculator.js
// import { TokenCounter } from '../lib/token-counter.js';
// import { getPricing } from '../lib/pricing-tables.js';
export class CostCalculator {
constructor() {
this.tokenCounter = new TokenCounter()
}
async calculate(requestData, responseData) {
const { provider, model, messages } = requestData
// Count input tokens
const inputTokens = await this.tokenCounter.countTokens(
provider,
model,
messages
)
// Extract output tokens from response
const outputTokens = this.extractOutputTokens(provider, responseData)
// Get pricing for model
const pricing = getPricing(provider, model)
if (!pricing) {
console.warn(`No pricing data for ${provider} ${model}`)
return null
}
// Calculate costs
const inputCost = inputTokens * pricing.input
const outputCost = outputTokens * pricing.output
const totalCost = inputCost + outputCost
return {
timestamp: requestData.timestamp,
provider,
model,
inputTokens,
outputTokens,
totalTokens: inputTokens + outputTokens,
inputCost,
outputCost,
totalCost,
url: requestData.url,
}
}
extractOutputTokens(provider, response) {
if (!response) return 0
try {
if (provider === 'openai') {
return response.usage?.completion_tokens || 0
}
if (provider === 'anthropic') {
return response.usage?.output_tokens || 0
}
if (provider === 'google') {
return response.usageMetadata?.candidatesTokenCount || 0
}
} catch (error) {
console.error('Failed to extract output tokens:', error)
}
return 0
}
}
The calculator extracts actual token counts from API responses when available, falling back to estimates only when response data is missing. Cost breakdown separates input and output costs, allowing users to see which portion of their spending comes from prompt tokens versus completion tokens.
Storage and Data Persistence
Cost data must persist across browser sessions and provide fast queries for dashboard displays. Chrome's storage API provides the persistence layer.
Storage Schema
The storage manager implements a schema that balances query performance with storage efficiency:
// lib/storage-manager.js
export class StorageManager {
constructor() {
this.prefix = 'aiTokenCost'
}
async recordCost(costData) {
// Store individual call record
const callKey = `${this.prefix}_call_${costData.timestamp}`
await chrome.storage.local.set({ [callKey]: costData })
// Update daily aggregate
await this.updateDailyTotal(costData)
// Update per-provider aggregates
await this.updateProviderTotal(costData)
}
async updateDailyTotal(costData) {
const dateKey = this.getDateKey(costData.timestamp)
const dailyKey = `${this.prefix}_daily_${dateKey}`
const result = await chrome.storage.local.get(dailyKey)
const current = result[dailyKey] || { date: dateKey, cost: 0, calls: 0 }
current.cost += costData.totalCost
current.calls += 1
await chrome.storage.local.set({ [dailyKey]: current })
}
async updateProviderTotal(costData) {
const dateKey = this.getDateKey(costData.timestamp)
const providerKey = `${this.prefix}_provider_${dateKey}_${costData.provider}`
const result = await chrome.storage.local.get(providerKey)
const current = result[providerKey] || {
provider: costData.provider,
date: dateKey,
cost: 0,
calls: 0,
}
current.cost += costData.totalCost
current.calls += 1
await chrome.storage.local.set({ [providerKey]: current })
}
async getDailyTotal(date = null) {
const dateKey = date ? this.getDateKey(date) : this.getDateKey(Date.now())
const dailyKey = `${this.prefix}_daily_${dateKey}`
const result = await chrome.storage.local.get(dailyKey)
return result[dailyKey]?.cost || 0
}
async getHistoricalData(days = 30) {
const data = []
const now = Date.now()
for (let i = 0; i < days; i++) {
const date = now - i * 24 * 60 * 60 * 1000
const dateKey = this.getDateKey(date)
const dailyKey = `${this.prefix}_daily_${dateKey}`
const result = await chrome.storage.local.get(dailyKey)
data.push({
date: dateKey,
cost: result[dailyKey]?.cost || 0,
calls: result[dailyKey]?.calls || 0,
})
}
return data.reverse()
}
async getProviderBreakdown(date = null) {
const dateKey = date ? this.getDateKey(date) : this.getDateKey(Date.now())
const providers = ['openai', 'anthropic', 'google']
const breakdown = {}
for (const provider of providers) {
const providerKey = `${this.prefix}_provider_${dateKey}_${provider}`
const result = await chrome.storage.local.get(providerKey)
if (result[providerKey]) {
breakdown[provider] = result[providerKey]
}
}
return breakdown
}
async cleanupOldData() {
// Remove data older than 90 days
const cutoff = Date.now() - 90 * 24 * 60 * 60 * 1000
const allData = await chrome.storage.local.get(null)
const keysToRemove = []
for (const [key, value] of Object.entries(allData)) {
if (key.startsWith(`${this.prefix}_call_`)) {
const timestamp = parseInt(key.split('_').pop())
if (timestamp < cutoff) {
keysToRemove.push(key)
}
}
}
if (keysToRemove.length > 0) {
await chrome.storage.local.remove(keysToRemove)
console.log(`Cleaned up ${keysToRemove.length} old records`)
}
}
async resetDaily() {
// Called at midnight to start fresh day
const yesterday = Date.now() - 24 * 60 * 60 * 1000
const dateKey = this.getDateKey(yesterday)
console.log(`Daily reset completed. Yesterday (${dateKey}) data preserved.`)
}
async setTemporary(requestId, data) {
const tempKey = `${this.prefix}_temp_${requestId}`
await chrome.storage.local.set({ [tempKey]: data })
}
async getTemporary(requestId) {
const tempKey = `${this.prefix}_temp_${requestId}`
const result = await chrome.storage.local.get(tempKey)
return result[tempKey] || null
}
async clearTemporary(requestId) {
const tempKey = `${this.prefix}_temp_${requestId}`
await chrome.storage.local.remove(tempKey)
}
getDateKey(timestamp) {
const date = new Date(timestamp)
return date.toISOString().split('T')[0] // YYYY-MM-DD
}
}
The storage schema uses key prefixes to organize data types. Individual call records get timestamped keys for chronological access. Daily aggregates roll up costs by date for efficient dashboard queries. Provider aggregates enable per-provider cost breakdowns without scanning all call records.
Data cleanup runs weekly via alarms, removing records older than 90 days. This prevents unbounded storage growth while maintaining enough history for trend analysis.
User Interface Implementation
The extension provides three UI surfaces: popup for quick viewing, options page for detailed analysis, and badge for at-a-glance totals.
Popup Interface
The popup displays current session costs and recent activity:
<!-- popup/popup.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>AI Token Cost Tracker</title>
<link rel="stylesheet" href="popup.css" />
</head>
<body>
<div class="container">
<header>
<h1>AI Cost Tracker</h1>
<div id="totalCost" class="total-cost">$0.00</div>
</header>
<section class="stats">
<div class="stat-card">
<div class="stat-label">Today</div>
<div id="dailyTotal" class="stat-value">$0.00</div>
</div>
<div class="stat-card">
<div class="stat-label">This Week</div>
<div id="weeklyTotal" class="stat-value">$0.00</div>
</div>
<div class="stat-card">
<div class="stat-label">This Month</div>
<div id="monthlyTotal" class="stat-value">$0.00</div>
</div>
</section>
<section class="provider-breakdown">
<h2>Provider Breakdown</h2>
<div id="providerList"></div>
</section>
<section class="recent-calls">
<h2>Recent API Calls</h2>
<div id="callsList"></div>
</section>
<footer>
<button id="viewDetails">View Detailed Report</button>
<button id="resetDaily">Reset Daily Total</button>
</footer>
</div>
<script src="popup.js" type="module"></script>
</body>
</html>
The popup JavaScript loads data from storage and updates the UI:
// popup/popup.js
// import { StorageManager } from '../lib/storage-manager.js';
class PopupController {
constructor() {
this.storage = new StorageManager()
this.init()
}
async init() {
await this.loadData()
this.setupEventListeners()
// Refresh every 5 seconds
setInterval(() => this.loadData(), 5000)
}
async loadData() {
const dailyTotal = await this.storage.getDailyTotal()
const weeklyTotal = await this.getWeeklyTotal()
const monthlyTotal = await this.getMonthlyTotal()
const providerBreakdown = await this.storage.getProviderBreakdown()
document.getElementById('dailyTotal').textContent =
this.formatCurrency(dailyTotal)
document.getElementById('weeklyTotal').textContent =
this.formatCurrency(weeklyTotal)
document.getElementById('monthlyTotal').textContent =
this.formatCurrency(monthlyTotal)
this.renderProviderBreakdown(providerBreakdown)
await this.renderRecentCalls()
}
async getWeeklyTotal() {
const data = await this.storage.getHistoricalData(7)
return data.reduce((sum, day) => sum + day.cost, 0)
}
async getMonthlyTotal() {
const data = await this.storage.getHistoricalData(30)
return data.reduce((sum, day) => sum + day.cost, 0)
}
renderProviderBreakdown(breakdown) {
const container = document.getElementById('providerList')
container.innerHTML = ''
for (const [provider, data] of Object.entries(breakdown)) {
const card = document.createElement('div')
card.className = 'provider-card'
card.innerHTML = `
<span class="provider-name">${this.capitalizeProvider(provider)}</span>
<span class="provider-cost">${this.formatCurrency(data.cost)}</span>
<span class="provider-calls">${data.calls} calls</span>
`
container.appendChild(card)
}
}
async renderRecentCalls() {
const allData = await chrome.storage.local.get(null)
const calls = Object.entries(allData)
.filter(([key]) => key.startsWith('aiTokenCost_call_'))
.map(([_, data]) => data)
.sort((a, b) => b.timestamp - a.timestamp)
.slice(0, 10)
const container = document.getElementById('callsList')
container.innerHTML = ''
calls.forEach(call => {
const card = document.createElement('div')
card.className = 'call-card'
card.innerHTML = `
<div class="call-time">${this.formatTime(call.timestamp)}</div>
<div class="call-model">${call.model}</div>
<div class="call-tokens">${call.totalTokens} tokens</div>
<div class="call-cost">${this.formatCurrency(call.totalCost)}</div>
`
container.appendChild(card)
})
}
setupEventListeners() {
document.getElementById('viewDetails').addEventListener('click', () => {
chrome.runtime.openOptionsPage()
})
document
.getElementById('resetDaily')
.addEventListener('click', async () => {
if (confirm("Reset today's total? This cannot be undone.")) {
await this.storage.resetDaily()
await this.loadData()
}
})
}
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(amount)
}
formatTime(timestamp) {
const date = new Date(timestamp)
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit',
})
}
capitalizeProvider(provider) {
return provider.charAt(0).toUpperCase() + provider.slice(1)
}
}
new PopupController()
The popup provides instant visibility into spending without requiring navigation to a separate page. Auto-refresh ensures costs update in real-time as API calls complete. Recent calls list shows the last 10 API requests with timestamps, models, token counts, and individual costs.
Options Page with Charts
The options page displays historical trends using Chart.js:
// options/options.js
// import { StorageManager } from '../lib/storage-manager.js';
// import Chart from 'chart.js/auto';
class OptionsController {
constructor() {
this.storage = new StorageManager()
this.charts = {}
this.init()
}
async init() {
await this.loadHistoricalData()
this.setupEventListeners()
}
async loadHistoricalData() {
const data = await this.storage.getHistoricalData(30)
this.renderCostChart(data)
this.renderProviderChart(data)
this.renderStatistics(data)
}
renderCostChart(data) {
const ctx = document.getElementById('costChart').getContext('2d')
if (this.charts.cost) {
this.charts.cost.destroy()
}
this.charts.cost = new Chart(ctx, {
type: 'line',
data: {
labels: data.map(d => d.date),
datasets: [
{
label: 'Daily Cost',
data: data.map(d => d.cost),
borderColor: 'rgb(75, 192, 192)',
backgroundColor: 'rgba(75, 192, 192, 0.2)',
tension: 0.1,
},
],
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Daily AI API Costs (Last 30 Days)',
},
tooltip: {
callbacks: {
label: context => {
return `Cost: $${context.parsed.y.toFixed(4)}`
},
},
},
},
scales: {
y: {
beginAtZero: true,
ticks: {
callback: value => `$${value.toFixed(2)}`,
},
},
},
},
})
}
async renderProviderChart(data) {
const breakdown = await this.storage.getProviderBreakdown()
const ctx = document.getElementById('providerChart').getContext('2d')
if (this.charts.provider) {
this.charts.provider.destroy()
}
const providers = Object.keys(breakdown)
const costs = providers.map(p => breakdown[p].cost)
this.charts.provider = new Chart(ctx, {
type: 'doughnut',
data: {
labels: providers.map(p => p.charAt(0).toUpperCase() + p.slice(1)),
datasets: [
{
data: costs,
backgroundColor: [
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)',
'rgba(255, 206, 86, 0.8)',
],
},
],
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'Cost by Provider (Today)',
},
tooltip: {
callbacks: {
label: context => {
const label = context.label || ''
const value = context.parsed || 0
return `${label}: $${value.toFixed(4)}`
},
},
},
},
},
})
}
renderStatistics(data) {
const total = data.reduce((sum, day) => sum + day.cost, 0)
const average = total / data.length
const max = Math.max(...data.map(d => d.cost))
const totalCalls = data.reduce((sum, day) => sum + day.calls, 0)
document.getElementById('totalSpent').textContent =
this.formatCurrency(total)
document.getElementById('averageDaily').textContent =
this.formatCurrency(average)
document.getElementById('peakDay').textContent = this.formatCurrency(max)
document.getElementById('totalCalls').textContent =
totalCalls.toLocaleString()
}
setupEventListeners() {
document.getElementById('exportData').addEventListener('click', () => {
this.exportDataToJSON()
})
document
.getElementById('clearHistory')
.addEventListener('click', async () => {
if (confirm('Clear all historical data? This cannot be undone.')) {
await this.clearAllData()
}
})
}
async exportDataToJSON() {
const data = await this.storage.getHistoricalData(90)
const blob = new Blob([JSON.stringify(data, null, 2)], {
type: 'application/json',
})
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `ai-cost-data-${new Date().toISOString().split('T')[0]}.json`
a.click()
URL.revokeObjectURL(url)
}
async clearAllData() {
await this.storage.cleanupOldData()
await this.loadHistoricalData()
}
formatCurrency(amount) {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 2,
maximumFractionDigits: 4,
}).format(amount)
}
}
new OptionsController()
The options page provides deep analysis capabilities. Line charts show spending trends over 30 days, revealing patterns like weekday versus weekend usage or gradual cost increases. Doughnut charts break down costs by provider, highlighting which AI services consume the most budget. Export functionality generates JSON files for importing into spreadsheets or business intelligence tools.
Production Deployment and Testing
Before publishing to the Chrome Web Store, thorough testing ensures the extension works reliably across different scenarios.
Local Testing
Load the extension in developer mode to test locally:
- Open Chrome and navigate to chrome://extensions
- Enable Developer Mode toggle in top right
- Click Load Unpacked and select the extension directory
- The extension appears in the toolbar
- Open browser console to view service worker logs
- Make test API calls to verify interception works
- Check popup and options page for data display
Test scenarios should cover all three providers with various models. Make API calls using fetch in the browser console, verifying that costs appear immediately in the badge and popup. Test edge cases like network failures, malformed responses, and concurrent requests.
Automated Testing
Jest provides unit testing for token counting and cost calculation logic:
// tests/token-counter.test.js
// import { TokenCounter } from '../lib/token-counter.js';
describe('TokenCounter', () => {
let counter
beforeEach(() => {
counter = new TokenCounter()
})
test('counts OpenAI tokens correctly', async () => {
const messages = [{ role: 'user', content: 'Hello, how are you?' }]
const tokens = await counter.countTokens(
'openai',
'gpt-3.5-turbo',
messages
)
expect(tokens).toBeGreaterThan(0)
expect(tokens).toBeLessThan(20) // Should be around 10-15
})
test('approximates Anthropic tokens', async () => {
const messages = [{ role: 'user', content: 'Hello, how are you?' }]
const tokens = await counter.countTokens(
'anthropic',
'claude-3-sonnet',
messages
)
expect(tokens).toBeGreaterThan(0)
})
test('handles empty messages', async () => {
const tokens = await counter.countTokens('openai', 'gpt-4', [])
expect(tokens).toBe(2) // Just the reply priming tokens
})
})
Testing the storage manager requires mocking Chrome APIs:
// tests/storage-manager.test.js
// import { StorageManager } from '../lib/storage-manager.js';
// Mock chrome.storage.local
global.chrome = {
storage: {
local: {
data: {},
get: jest.fn(keys => {
if (keys === null) {
return Promise.resolve(global.chrome.storage.local.data)
}
const result = {}
if (typeof keys === 'string') {
result[keys] = global.chrome.storage.local.data[keys]
} else if (Array.isArray(keys)) {
keys.forEach(key => {
result[key] = global.chrome.storage.local.data[key]
})
}
return Promise.resolve(result)
}),
set: jest.fn(items => {
Object.assign(global.chrome.storage.local.data, items)
return Promise.resolve()
}),
remove: jest.fn(keys => {
if (Array.isArray(keys)) {
keys.forEach(key => delete global.chrome.storage.local.data[key])
} else {
delete global.chrome.storage.local.data[keys]
}
return Promise.resolve()
}),
},
},
}
describe('StorageManager', () => {
let manager
beforeEach(() => {
global.chrome.storage.local.data = {}
manager = new StorageManager()
})
test('records cost data', async () => {
const costData = {
timestamp: Date.now(),
provider: 'openai',
model: 'gpt-4',
totalCost: 0.05,
}
await manager.recordCost(costData)
const dailyTotal = await manager.getDailyTotal()
expect(dailyTotal).toBe(0.05)
})
test('aggregates multiple costs', async () => {
await manager.recordCost({
timestamp: Date.now(),
provider: 'openai',
model: 'gpt-4',
totalCost: 0.05,
})
await manager.recordCost({
timestamp: Date.now(),
provider: 'openai',
model: 'gpt-4',
totalCost: 0.03,
})
const dailyTotal = await manager.getDailyTotal()
expect(dailyTotal).toBe(0.08)
})
})
Run tests with npm test before each commit to catch regressions early.
Chrome Web Store Submission
Publishing to the Chrome Web Store requires preparing promotional materials and passing Google's review process.
Create screenshots of the popup, options page, and badge in action. Write a detailed description explaining what the extension does, why developers need it, and how it protects user data. Prepare a privacy policy document addressing data collection and storage practices.
The review process typically takes 1-3 business days. Common rejection reasons include unclear privacy policy, excessive permissions, or misleading functionality claims. Be prepared to justify the webRequest permission by explaining that cost tracking requires monitoring AI API calls.
Security and Privacy Considerations
Extensions with network monitoring capabilities require extra security scrutiny. Users need assurance that cost tracking does not compromise their API keys or expose sensitive data.
Permission Minimization
The extension requests only essential permissions. The webRequest permission monitors specific domains (api.openai.com, api.anthropic.com, generativelanguage.googleapis.com) rather than all network traffic. Storage permission accesses only extension-specific data, not user browsing history or cookies.
Data Handling
All cost data stays local in Chrome's storage. The extension never transmits data to external servers, analytics platforms, or third-party services. API keys pass through the extension during request interception but are never stored or logged. Response bodies containing AI-generated content are accessed only to extract token counts, then immediately discarded.
Content Security Policy
Manifest V3 enforces strict content security policies. The extension uses only locally bundled scripts with no remote code execution. External dependencies like tiktoken and Chart.js are bundled at build time rather than loaded from CDNs.
Future Enhancements and Extensions
The foundation established in this tutorial enables numerous enhancements for advanced cost management.
Budget Alerts and Notifications
Implement threshold-based alerting that notifies users when daily spending exceeds configured limits. Use the chrome.notifications API to display desktop notifications when costs hit warning thresholds (50 percent of budget) or critical thresholds (90 percent of budget). Integrate with Slack webhooks to send team alerts when organizational spending patterns change dramatically.
Project-Based Cost Attribution
Add project tagging that associates API calls with specific projects or features. Implement a UI that lets users assign the current tab to a project, automatically tagging all subsequent API calls. Provide project-level cost breakdowns showing which features or experiments consume the most budget.
Team Sharing and Aggregation
Build team dashboard capabilities that aggregate costs across multiple team members. Use Chrome's sync storage to share anonymized cost data between team members who opt in. Generate team reports showing total organizational spending, top spenders, and cost trends across the engineering organization.
Advanced Analytics
Implement cost per feature metrics by tracking which code paths trigger AI API calls. Build token efficiency scores that measure output quality relative to token consumption. Add A/B testing support that compares costs between different prompting strategies or model choices.
Custom Pricing
Support enterprise customers with custom pricing agreements. Allow manual pricing table overrides for organizations with volume discounts or special contract terms. Implement pricing update notifications that alert users when provider pricing changes.
Conclusion
AI API costs represent a significant and growing expense for software teams building LLM-powered applications. Without real-time visibility, developers lack the awareness needed to make cost-conscious decisions during development and testing.
This Chrome extension solves the visibility problem by providing instant feedback on AI spending. Every API call displays its cost immediately, enabling developers to see exactly how much each experiment, test, or feature costs. The extension works universally across all web-based AI tools, requires zero code changes or backend integration, and provides comprehensive cost tracking with minimal user friction.
The complete implementation demonstrates several advanced Chrome extension techniques. Network request interception using webRequest API gives visibility into HTTPS traffic without man-in-the-middle attacks. Token counting algorithms handle provider-specific tokenization schemes, providing accurate cost estimates even when exact token counts are unavailable. Storage schema design balances query performance with storage efficiency, enabling fast dashboard loads without unbounded data growth.
Production deployment requires attention to security, privacy, and user experience. Extensions with network monitoring capabilities face extra scrutiny during Chrome Web Store review, requiring clear privacy policies and permission justifications. Testing must cover edge cases like network failures, concurrent requests, and malformed API responses to ensure reliability in real-world usage.
The extension provides immediate value by making invisible costs visible, but its true power comes from enabling behavior change. When developers see costs in real-time, they naturally optimize prompts, choose appropriate models for each task, and avoid wasteful experimentation patterns. Organizations save thousands of dollars monthly simply by making cost awareness a default part of the development workflow.
Build this extension, deploy it to your team, and watch AI spending patterns change as developers gain visibility into what previously operated as a black box. The investment in building cost tracking infrastructure pays for itself within weeks for most organizations actively using AI APIs at scale.
