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.