Last active
March 24, 2026 04:13
-
-
Save benhook1013/9c2d27db1693ea5e5428e457a30f26b0 to your computer and use it in GitHub Desktop.
Tampermonkey script to show cost and burn-rate stats on ChatGPT Codex usage page
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name Codex Credits -> Cost + Stats (Visited Pages Accumulator) | |
| // @namespace ben.codex.credit.cost | |
| // @version 10.4 | |
| // @description Show converted cost, visible-page totals, and accumulated spend across visited Codex usage pages. | |
| // @match https://chatgpt.com/* | |
| // @match https://chat.openai.com/* | |
| // @run-at document-end | |
| // @grant none | |
| // ==/UserScript== | |
| (function () { | |
| "use strict"; | |
| if (!location.pathname.includes("/codex/settings/usage")) return; | |
| const CONFIG = { | |
| currencyCode: "NZD", | |
| locale: "en-NZ", | |
| creditCost: 0.07, // 1000 credits = 70 NZD | |
| }; | |
| const STORAGE_KEY = "codex-usage-visited-pages-v4"; | |
| const moneyFmt = new Intl.NumberFormat(CONFIG.locale, { | |
| style: "currency", | |
| currency: CONFIG.currencyCode, | |
| minimumFractionDigits: 2, | |
| }); | |
| const numFmt = new Intl.NumberFormat(CONFIG.locale, { | |
| maximumFractionDigits: 2, | |
| }); | |
| const money = (v) => moneyFmt.format(v); | |
| const num = (v) => numFmt.format(v); | |
| function parseCredits(text) { | |
| const m = text?.match(/([\d.,]+)\s*credits?/i); | |
| return m ? Number(m[1].replace(/,/g, "")) : null; | |
| } | |
| function normalizeText(text) { | |
| return (text || "").replace(/\s+/g, " ").trim(); | |
| } | |
| function getCleanCellText(td) { | |
| const clone = td.cloneNode(true); | |
| clone.querySelectorAll(".codex-row-cost").forEach((el) => el.remove()); | |
| return normalizeText(clone.textContent); | |
| } | |
| function getRowCells(row) { | |
| return Array.from(row.querySelectorAll("td")).map(getCleanCellText); | |
| } | |
| function getRowCredits(row) { | |
| const span = row.querySelector("td:last-child span"); | |
| return parseCredits(span?.textContent || row.lastElementChild?.textContent || ""); | |
| } | |
| function makeRowKey(row) { | |
| const cells = getRowCells(row); | |
| return cells.join(" | "); | |
| } | |
| function loadStore() { | |
| try { | |
| const raw = localStorage.getItem(STORAGE_KEY); | |
| if (!raw) { | |
| return { rows: {} }; | |
| } | |
| const parsed = JSON.parse(raw); | |
| return { | |
| rows: parsed.rows || {}, | |
| }; | |
| } catch { | |
| return { rows: {} }; | |
| } | |
| } | |
| function saveStore(store) { | |
| localStorage.setItem(STORAGE_KEY, JSON.stringify(store)); | |
| } | |
| function resetStore() { | |
| localStorage.removeItem(STORAGE_KEY); | |
| } | |
| function syncVisibleRowsIntoStore() { | |
| const store = loadStore(); | |
| document.querySelectorAll("table tbody tr").forEach((row) => { | |
| const credits = getRowCredits(row); | |
| if (credits == null || Number.isNaN(credits)) return; | |
| const key = makeRowKey(row); | |
| if (!key) return; | |
| const text = getRowCells(row).join(" | "); | |
| if (!store.rows[key]) { | |
| store.rows[key] = { | |
| credits, | |
| text, | |
| firstSeenAt: new Date().toISOString(), | |
| lastSeenAt: new Date().toISOString(), | |
| }; | |
| } else { | |
| store.rows[key].lastSeenAt = new Date().toISOString(); | |
| store.rows[key].credits = credits; | |
| store.rows[key].text = text; | |
| } | |
| }); | |
| saveStore(store); | |
| } | |
| function getVisiblePageStats() { | |
| const vals = []; | |
| document.querySelectorAll("table tbody tr").forEach((row) => { | |
| const credits = getRowCredits(row); | |
| if (credits != null && !Number.isNaN(credits)) { | |
| vals.push(credits); | |
| } | |
| }); | |
| if (!vals.length) return null; | |
| const total = vals.reduce((a, b) => a + b, 0); | |
| return { | |
| total, | |
| avg: total / vals.length, | |
| rows: vals.length, | |
| spend: total * CONFIG.creditCost, | |
| }; | |
| } | |
| function getAccumulatedStats() { | |
| const store = loadStore(); | |
| const rows = Object.values(store.rows); | |
| if (!rows.length) return null; | |
| const total = rows.reduce((sum, row) => sum + (Number(row.credits) || 0), 0); | |
| return { | |
| total, | |
| avg: total / rows.length, | |
| rows: rows.length, | |
| spend: total * CONFIG.creditCost, | |
| }; | |
| } | |
| function injectRowCosts() { | |
| document.querySelectorAll("table tbody tr").forEach((row) => { | |
| const span = row.querySelector("td:last-child span"); | |
| if (!span) return; | |
| const credits = getRowCredits(row); | |
| if (credits == null || Number.isNaN(credits)) return; | |
| let cost = span.querySelector(".codex-row-cost"); | |
| if (!cost) { | |
| cost = document.createElement("span"); | |
| cost.className = "codex-row-cost"; | |
| cost.style.marginLeft = "4px"; | |
| cost.style.opacity = "0.7"; | |
| cost.style.fontSize = "0.95em"; | |
| span.appendChild(cost); | |
| } | |
| cost.textContent = ` (${money(credits * CONFIG.creditCost)})`; | |
| }); | |
| } | |
| function injectTableHeaderStats() { | |
| document.querySelectorAll("div.flex.flex-col.items-start").forEach((col) => { | |
| const label = col.querySelector("p"); | |
| if (label?.textContent.trim() !== "Credits remaining") return; | |
| const numberP = col.querySelectorAll("p")[1]; | |
| if (!numberP) return; | |
| const raw = numberP.firstChild?.nodeValue; | |
| const remaining = Number(raw?.replace(/[^0-9.]/g, "")); | |
| if (!remaining || Number.isNaN(remaining)) return; | |
| let remainingCost = numberP.querySelector(".codex-cost"); | |
| if (!remainingCost) { | |
| remainingCost = document.createElement("span"); | |
| remainingCost.className = "codex-cost"; | |
| remainingCost.style.marginLeft = "6px"; | |
| remainingCost.style.opacity = "0.7"; | |
| numberP.appendChild(remainingCost); | |
| } | |
| remainingCost.textContent = ` (${money(remaining * CONFIG.creditCost)})`; | |
| const visible = getVisiblePageStats(); | |
| const accumulated = getAccumulatedStats(); | |
| if (!visible && !accumulated) return; | |
| const avgDaysRemaining = | |
| accumulated && accumulated.avg > 0 ? num(remaining / accumulated.avg) : "-"; | |
| let statsDiv = col.querySelector(".codex-stats"); | |
| if (!statsDiv) { | |
| statsDiv = document.createElement("div"); | |
| statsDiv.className = "codex-stats"; | |
| statsDiv.style.marginTop = "4px"; | |
| statsDiv.style.fontSize = "0.85rem"; | |
| statsDiv.style.opacity = "0.85"; | |
| col.appendChild(statsDiv); | |
| } | |
| statsDiv.innerHTML = [ | |
| `<div style="opacity:0.75;">Config: ${CONFIG.currencyCode} at ${money(CONFIG.creditCost)}/credit</div>`, | |
| visible ? `<div>Visible page credits: ${num(visible.total)}</div>` : "", | |
| visible ? `<div>Visible page spend: ${money(visible.spend)}</div>` : "", | |
| accumulated ? `<div>Accumulated visited-page credits: ${num(accumulated.total)}</div>` : "", | |
| accumulated ? `<div>Accumulated visited-page spend: ${money(accumulated.spend)}</div>` : "", | |
| accumulated ? `<div>Tracked unique rows: ${num(accumulated.rows)}</div>` : "", | |
| accumulated | |
| ? `<div>Avg credits per day: ${num(accumulated.avg)} (${money(accumulated.avg * CONFIG.creditCost)})</div>` | |
| : "", | |
| accumulated ? `<div>Approx days remaining: ${avgDaysRemaining}</div>` : "", | |
| `<div style="margin-top:6px;">> <button type="button" class="codex-reset-cache" style="font: inherit; font-weight: 600; padding: 2px 6px; border-radius: 6px; cursor: pointer;">Reset accumulated total</button> <</div>`, | |
| ].join(""); | |
| const resetBtn = statsDiv.querySelector(".codex-reset-cache"); | |
| if (resetBtn && !resetBtn.dataset.bound) { | |
| resetBtn.dataset.bound = "true"; | |
| resetBtn.addEventListener("click", () => { | |
| resetStore(); | |
| tick(); | |
| }); | |
| } | |
| }); | |
| } | |
| function injectTopCreditsCardCost() { | |
| document.querySelectorAll("article").forEach((article) => { | |
| const label = article.querySelector("p"); | |
| if (label?.textContent.trim() !== "Credits remaining") return; | |
| const span = article.querySelector("span.text-2xl.font-semibold"); | |
| if (!span) return; | |
| const raw = span.firstChild?.nodeValue; | |
| const remaining = Number(raw?.replace(/[^0-9.]/g, "")); | |
| if (!remaining || Number.isNaN(remaining)) return; | |
| let cost = span.querySelector(".codex-top-cost"); | |
| if (!cost) { | |
| cost = document.createElement("span"); | |
| cost.className = "codex-top-cost"; | |
| cost.style.marginLeft = "6px"; | |
| cost.style.opacity = "0.7"; | |
| cost.style.fontSize = "0.9em"; | |
| cost.style.fontWeight = "normal"; | |
| span.appendChild(cost); | |
| } | |
| cost.textContent = ` (${money(remaining * CONFIG.creditCost)})`; | |
| }); | |
| } | |
| function tick() { | |
| syncVisibleRowsIntoStore(); | |
| injectRowCosts(); | |
| injectTableHeaderStats(); | |
| injectTopCreditsCardCost(); | |
| } | |
| tick(); | |
| setInterval(tick, 2000); | |
| })(); |
Author
benhook1013
commented
Mar 18, 2026
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment