cd /news/developer-tools/i-built-a-chrome-extension-to-track-… · home topics developer-tools article
[ARTICLE · art-105990] src=dev.to ↗ pub= topic=developer-tools verified=true sentiment=· neutral

I Built a Chrome Extension to Track AI Token Usage — Here's How It Works

A developer built TokenPulse, a Chrome extension that injects a live token usage bar above the input box on Claude, ChatGPT, Gemini, DeepSeek, and Grok. The extension tracks context window usage, rate limits, cost estimates, and daily history without requiring an API key, using content scripts, a background service worker, and Chrome's storage API. It intercepts Claude's internal usage endpoint for exact percentages and estimates tokens for other platforms with approximately ±8% accuracy.

read5 min views2 publishedAug 21, 2026

Six weeks ago I got cut off mid-debugging session by Claude's rate limit with no warning. Two hours of context gone. I started looking for a tool that would show me how close I was before it happened. Nothing existed that worked across more than one platform without requiring an API key.

So I built one.

TokenPulse is a Chrome extension (MV3) that injects a live token bar above the input box on Claude, ChatGPT, Gemini, DeepSeek and Grok. It tracks context window usage, rate limits, cost estimates, and daily history — all from your existing browser session, no API key required.

Here's how it works technically.

Content Scripts (per platform)
        ↓
Background Service Worker
        ↓
Chrome Storage API (local)
        ↓
Popup UI
        ↓
Desktop Notifications

The extension runs a content script on each supported domain. Each script is responsible for:

chrome.runtime.sendMessage

The service worker aggregates data, writes to chrome.storage.local

, checks notification thresholds, and serves data to the popup on demand.Claude is the only platform that exposes real rate limit data through its internal API. When you use claude.ai, the browser session makes requests to a usage endpoint that returns exact utilization percentages and reset timestamps.

The content script intercepts this data by hooking into the platform's network requests using a MutationObserver

to detect when Claude updates its state, then reading the cached response.

The response looks roughly like:

{
  five_hour: {
    utilization: 0.82,
    reset_at: "2026-07-15T14:14:00Z"
  },
  seven_day: {
    utilization: 0.34,
    reset_at: "2026-07-21T21:00:00Z"
  }
}

This gives exact percentages — not estimates. The popup shows these directly.

ChatGPT, Gemini, DeepSeek and Grok don't expose usage data the same way. For these, TokenPulse estimates token usage from the conversation DOM.

The estimation approach:

function estimateTokens(text) {
  // ~4 characters per token — standard approximation
  return Math.ceil(text.length / 4)
}

function getConversationTokens() {
  const messages = document.querySelectorAll('[data-message-author-role]')
  let total = 0
  messages.forEach(msg => {
    total += estimateTokens(msg.textContent || '')
  })
  return total
}

Accuracy is approximately ±8% — sufficient for knowing whether you're at 20% or 80% of your context window.

The bar injection uses a MutationObserver

to watch for the input box appearing in the DOM, then inserts a container element immediately above it.

const observer = new MutationObserver(() => {
  const inputBox = document.querySelector(PLATFORM_INPUT_SELECTOR)
  if (inputBox && !document.getElementById('tp-bar')) {
    injectBar(inputBox)
  }
})

observer.observe(document.body, {
  childList: true,
  subtree: true
})

The bar is a thin div with a gradient fill that updates via CSS transition whenever new token data arrives:

function updateBar(pct) {
  const fill = document.getElementById('tp-bar-fill')
  if (fill) fill.style.width = `${pct}%`
}

The CSS transition handles the smooth animation — no JavaScript animation loops needed.

Cost is estimated by multiplying token count by current model pricing:

const PRICING = {
  'claude-sonnet-4': { input: 3.00, output: 15.00 },  // per 1M tokens
  'claude-opus-4':   { input: 15.00, output: 75.00 },
  'gpt-4o':          { input: 2.50, output: 10.00 },
  'gemini-2.0-flash':{ input: 0.10, output: 0.40 },
  'deepseek-v3':     { input: 0.27, output: 1.10 },
}

function estimateCost(tokens, model) {
  const price = PRICING[model]
  if (!price) return 0
  return (tokens / 1_000_000) * price.input
}

Model detection reads the platform's model selector from the DOM.

Notifications fire at configurable thresholds (default: 75%, 90%, 100%) using Chrome's notifications API. The threshold system is designed to fire once per crossing and reset when usage drops:

async function shouldNotify(key, currentPct, settings) {
  const thresholds = [75, 90, 100].filter(t => settings[`notify_${t}`])
  const crossed = thresholds.filter(t => currentPct >= t).pop() || 0
  const lastNotified = await getLastNotified()
  const last = lastNotified[key] || 0

  if (crossed <= 0 || crossed <= last) return null

  lastNotified[key] = crossed
  await saveLastNotified(lastNotified)
  return crossed
}

This prevents notification spam — you get one notification when you cross 75%, another when you cross 90%, and they reset when your usage drops back below the threshold.

Chrome's Manifest V3 requirement means the background script is a service worker — not a persistent background page. Service workers can be killed by Chrome at any time, which creates two constraints:

No persistent state in memory. Everything goes through chrome.storage.local

. The service worker reads from storage on every message handler invocation rather than keeping state in variables.

Async message handling. The chrome.runtime.onMessage

listener must handle the async/sync distinction carefully. Fire-and-forget messages (like saving usage data) return false

immediately. Messages that need a response return true

and call sendResponse

after the async work completes.

chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
  if (msg.type === 'SAVE_USAGE') {
    // Fire and forget — no response needed
    Storage.saveUsage(msg.usage)
    return false
  }

  if (msg.type === 'GET_ALL_DATA') {
    // Async response needed
    Promise.all([
      Storage.getUsage(),
      Storage.getHistory(),
      Storage.getSettings(),
    ]).then(([usage, history, settings]) => {
      sendResponse({ usage, history, settings })
    })
    return true // keep channel open
  }
})

MV3 CSP is strict. No inline scripts, no eval

, no remote code execution. Every event handler must be attached via addEventListener

— no onclick

attributes. This caught me early and took a full debugging session to unpack.

Platform DOM structures change without notice. Claude, ChatGPT and Gemini update their frontends regularly. Selectors that work today break in a week. The solution is multiple fallback selectors and defensive ?.

access throughout.

Service worker timing is unpredictable. Chrome can kill and restart the service worker between messages. Code that assumes the worker is alive from a previous message will fail intermittently and be very hard to debug.

Local storage is fast enough. I was worried chrome.storage.local

would be too slow for real-time updates. In practice, reads complete in under 5ms and the popup feels instant.

The extension is open source at github.com/anu-ship-it/TokenPulse. If you're building a Chrome extension that reads from AI platforms, feel free to look at how the content scripts are structured.

Install TokenPulse free — works on Claude, ChatGPT, Gemini, DeepSeek and Grok with no API key.

── more in #developer-tools 4 stories · sorted by recency
── more on @tokenpulse 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/i-built-a-chrome-ext…] indexed:0 read:5min 2026-08-21 ·