When I started building TokenPulse, I assumed I would need an API key to read Claude's rate limit data. I was wrong. Claude exposes exact utilization percentages and reset timestamps through an internal endpoint that your browser session already has access to — because Claude's own interface uses it.
Here is exactly how it works and how I built the integration.
Discovering the endpoint
The first step was understanding what data Claude's interface actually requests. I opened DevTools → Network tab, filtered by XHR and Fetch, and started a conversation with Claude while watching the requests.
Within a few messages, I spotted a request to an endpoint returning usage data:
{
"raw_limits": {
"five_hour": {
"remaining_tokens": 18432,
"total_tokens": 90000,
"reset_at": "2026-07-15T14:14:00.000Z"
},
"seven_day": {
"remaining_tokens": 58900,
"total_tokens": 90000,
"reset_at": "2026-07-21T21:00:00.000Z"
}
}
}
The browser already had permission to read this because the session cookie was included automatically. No API key needed.
Intercepting the response in a content script
Content scripts run in an isolated world — they cannot directly intercept fetch calls made by the page's JavaScript. The solution is injecting a script into the page's actual JavaScript context:
// content-script.js
function injectPageScript() {
const script = document.createElement('script')
script.src = chrome.runtime.getURL('injected.js')
script.onload = () => script.remove()
;(document.head || document.documentElement).appendChild(script)
}
injectPageScript()
The injected script runs in page context and intercepts fetch calls:
// injected.js
const originalFetch = window.fetch
window.fetch = async function(...args) {
const response = await originalFetch.apply(this, args)
const url = typeof args[0] === 'string' ? args[0] : args[0].url
if (url.includes('/api/usage') || url.includes('/rate_limits')) {
const clone = response.clone()
clone.json().then(data => {
window.dispatchEvent(new CustomEvent('tp-usage-data', {
detail: data
}))
}).catch(() => {})
}
return response
}
The content script listens for the custom event:
window.addEventListener('tp-usage-data', (event) => {
processUsageData(event.detail)
})
Parsing and displaying the data
function parseRateLimits(raw) {
const now = Date.now()
function parseWindow(w) {
if (!w) return null
const total = w.total_tokens || 90000
const remaining = w.remaining_tokens || total
const used = total - remaining
const resetAt = w.reset_at ? new Date(w.reset_at).getTime() : null
const minutesUntilReset = resetAt
? Math.floor(Math.max(0, resetAt - now) / 60000)
: null
return {
utilization: Math.min(1, used / total),
minutesUntilReset,
resetAt,
}
}
return {
five_hour: parseWindow(raw?.raw_limits?.five_hour),
seven_day: parseWindow(raw?.raw_limits?.seven_day),
}
}
function formatCountdown(minutes) {
if (!minutes || minutes <= 0) return 'Resetting...'
const h = Math.floor(minutes / 60)
const m = minutes % 60
if (h === 0) return `${m}m`
if (m === 0) return `${h}h`
return `${h}h ${m}m`
}
The MV3 manifest requirement
For the injected script to load, declare it in web_accessible_resources:
{
"web_accessible_resources": [{
"resources": ["injected.js", "icons/icon48.png"],
"matches": ["https://claude.ai/*"]
}]
}
Without this the injection silently fails.
What does not work and why
Direct fetch from service worker — fails with CORS because session cookies are not sent cross-origin from the service worker context.
Reading cookies directly — Claude's session tokens are HttpOnly, so JavaScript cannot read them.
The page-context injection pattern is the only reliable approach for intercepting authenticated requests made by the page's own JavaScript.
Full implementation at github.com/anu-ship-it/TokenPulse.
TokenPulse is free — tracks Claude, ChatGPT, Gemini, DeepSeek and Grok.
Top comments (0)