// ==UserScript==
// @name ChaturbateTokenStats
// @namespace http://tampermonkey.net/
// @version 2025.10.21
// @description Get Chaturbate token stats for individual rooms.
// @author nyoob/seraphine24
// @match https://chaturbate.com/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=chaturbate.com
// @grant none
// @require https://code.jquery.com/jquery-3.6.0.min.js
// ==/UserScript==
// BOC config
const groups = {
// add more groups like this:
// groupname: ["username", "username2"],
vtuber: ["emyliveshow", "kajira_kumiho", "kyrawildofficial", "tadakonyanko", "babydollstarlit", "Xstaceyliciousx", "Kittenlush", "Smartfeeling", "animecutie", "projectmelody", "skyeanette", "zonetron"],
}
// EOC config
const userToGroupMap = {};
for (const groupName in groups) {
groups[groupName].forEach(user => {
userToGroupMap[user] = groupName;
});
}
const div = `
<div style="background-color: crimson; height: auto; width: 100%; position: static; overflow: hidden; display: block; padding: 5px 0px; text-align: center; box-sizing: border-box; font-size: 14px; font-weight: 400; font-family: UbuntuMedium, Helvetica, Arial, sans-serif; color: rgb(73, 73, 73);" ts="p" class="siteNotice">
<div class="wrapper seratkstats" style="background-color: darksalmon; padding: 15px; border-width: 1px; border-style: solid;">
<h1 style="font-size: 150%">Stop spending money on porn! You can watch for free.</h1>
<div>Imagine what you could do with that money — an unforgettable trip, a gift that would make your mother smile, a home that finally feels complete, a car that’s truly yours.<br />
<b>Now imagine throwing all of that away for pixels that pretend to care..</b></div>
<div>Chaturbate isn’t harmless fun — <b>it’s engineered addiction</b>. It drains your wallet, your time, your confidence. Like a casino built out of loneliness.<br />
Every tip, every private show, every “hey baby” is a calculated hook, designed to keep you chasing validation that doesn’t exist.</div>
<div>The affection you think you’re buying? It’s scripted. The connection you feel? Manufactured.<br />
They’ve learned exactly how to make you feel special — just enough to keep you spending, never enough to make you whole.</div>
<div><b>This isn’t intimacy. It’s psychological exploitation disguised as attention.</b><br />
It’s dopamine on demand — <b>stronger than gambling, almost as binding as heroin.</b> Except this one doesn’t just empty your account — it empties you.</div>
<div>Check out: <a href="https://www.youtube.com/watch?v=Y0zePr-5ilE">Larry Wheels on camgirl addiction</a> | <a href="https://easypeasymethod.org">EasyPeasyMethod</a> |
<a href="https://www.nofap.com/wp-content/uploads/2016/12/Getting-Started-with-NoFap.pdf">Nofap</a></div>
<button class="seraBtn" onclick="getTkStats()">Load token stats</button>
<button class="seraBtn seraDlBtn" onclick="dlTkTx()" disabled>Download Tx JSON</button>
<button class="seraBtn seraDlBtn" onclick="dlTkTx(true)" disabled>Download Tx CSV</button>
</div>
</div>
`;
const styles = `
#site_notices .wrapper > div { margin-top: 8px; }
.loadStats { color: blue; }
.seratable { display: flex; justify-content: center; }
.seratable td, .seratable th { border-bottom: 1px solid white; }
.seraBtn { padding: 6px 12px; background-color: orange; border-radius: 8px; border: 1px solid pink; margin-top: 8px; }
.seraBtn:disabled { background-color: gray; }
`;
window.getTkStats = () => {
var jso = JSON.parse(localStorage.getItem("seraTkStats"))
var all = [];
if(jso) { all = [...jso]; }
function loadMore(last_tx_id) {
var params = "";
if(last_tx_id != null) {
params += "?max_transaction_id=" + last_tx_id + "&cashpage=0"
}
fetch("/api/ts/tipping/token-stats/" + params)
.then((r) => r.json())
.then((r) => {
all = [...all, ...Object.values(r.transactions)];
if(!r.txns_fully_loaded) {
loadMore(r.transactions[r.transactions.length - 1].id)
} else {
const unique = Array.from(new Map(all.map(i => [i.id, i])).values()); // make unique by id, in order not to get accidental duplicates, but still append new ones, even after switching accounts
localStorage.setItem("seraTkStats", JSON.stringify(unique));
alert("finished loading tk stats");
window.location.reload();
}
})
}
loadMore();
}
window.dlTkTx = (csv = false) => {
const stats = localStorage.getItem("seraTkStats");
var dataStr;
if(csv) {
dataStr = "data:text/csv;charset=utf-8," + encodeURIComponent(jsonToCsv(JSON.parse(stats)));
} else {
dataStr = "data:text/json;charset=utf-8," + encodeURIComponent(stats);
}
var downloadAnchorNode = document.createElement('a');
downloadAnchorNode.setAttribute("href", dataStr);
downloadAnchorNode.setAttribute("download", "transactions." + (csv ? "csv" : "json"));
document.body.appendChild(downloadAnchorNode); // required for firefox
downloadAnchorNode.click();
downloadAnchorNode.remove();
}
const calculateTotalStats = () => {
const data = JSON.parse(localStorage.getItem("seraTkStats"));
var totalSpent = 0;
var spentByUser = {};
var spentByGroup = {};
data.forEach((e) => {
if(e.description == "Tokens purchased") return;
totalSpent += Math.abs(e.tokens);
// by user
var username = e.username;
if(e.description == "Spy on private show" || e.description == "Private show") {
username = e.broadcaster_username;
}
if(!spentByUser[username]) {
spentByUser[username] = 0;
}
spentByUser[username] += Math.abs(e.tokens);
// by group
const group = userToGroupMap[username] ?? "ungrouped";
if(!spentByGroup[group]) {
spentByGroup[group] = 0;
}
spentByGroup[group] += Math.abs(e.tokens);
});
spentByUser = Object.entries(spentByUser).sort(([,a],[,b]) => b-a);
spentByGroup = Object.entries(spentByGroup).sort(([,a],[,b]) => b-a);
return {totalSpent, spentByUser, spentByGroup};
}
const tksToDollar = (tks) => {
const minPrice = 0.079
const maxPrice = 0.109
return {min: (tks * minPrice).toFixed(2), max: (tks * maxPrice).toFixed(2)};
}
function jsonToCsv(data) {
const headers = Object.keys(data[0]);
const replacer = (key, value) => value ?? ''; // handle null/undefined
const csvRows = data.map(row =>
headers.map(fieldName => JSON.stringify(row[fieldName], replacer)).join(',')
);
return [headers.join(','), ...csvRows].join('\r\n');
}
(function() {
// div
$("#site_notices").append(div);
// style
$('html > head').append(`<style>${styles}</style>`);
// stats
const totalStats = calculateTotalStats();
const totalSpentInDollar = tksToDollar(totalStats.totalSpent);
if(!totalStats) {
$("#site_notices .wrapper").append(`
<div class="seraAlert">Please load token stats. You have to be logged in to do that.<br/>Detailed stats will be shown after loading.</div>
`)
} else {
$(".seraDlBtn").prop('disabled', false);
$("#site_notices .wrapper").append(`
<div>Total spent: ${totalStats.totalSpent}tks (in dollars: ${totalSpentInDollar.min}-${totalSpentInDollar.max})</div>
<div>
<details>
<summary>Tks spent per user</summary>
<div class="seratable">
<table><thead>
<tr>
<td class="">Username</td>
<td class="">Tokens spent</td>
<td class="">Min Dollars</td>
<td class="">Max Dollars</td>
</tr></thead>
<tbody>
${totalStats.spentByUser.map(([user, spent]) => {
const totalSpentInDollar = tksToDollar(spent);
return `<tr>
<td><a href="https://chaturbate.com/${user}/">${user}</a></td><td>${spent}tks</td> <td>${totalSpentInDollar.min}</td><td>${totalSpentInDollar.max}</td>
</tr>`;
}).join("")}
</tbody>
</table>
</div>
</details>
` + (Object.keys(totalStats.spentByGroup).length > 0
? `<details>
<summary>Tks spent per group</summary>
<div class="seratable">
<table><thead>
<tr>
<td class="">Username</td>
<td class="">Tokens spent</td>
<td class="">Min Dollars</td>
<td class="">Max Dollars</td>
</tr></thead>
<tbody>
${totalStats.spentByGroup.map(([group, spent]) => {
const totalSpentInDollar = tksToDollar(spent);
return `<tr>
<td>${group}</td><td>${spent}tks</td> <td>${totalSpentInDollar.min}</td><td>${totalSpentInDollar.max}</td>
</tr>`;
}).join("")}
</tbody>
</table>
</div>
</details>`
: ``)
+ `</div>`)
}
})();