Skip to content

Instantly share code, notes, and snippets.

@benhook1013
Last active March 24, 2026 04:13
Show Gist options
  • Select an option

  • Save benhook1013/9c2d27db1693ea5e5428e457a30f26b0 to your computer and use it in GitHub Desktop.

Select an option

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
// ==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;">&gt; <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> &lt;</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);
})();
@benhook1013
Copy link
Copy Markdown
Author

image image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment