ChaturbateTokenStats

Get Chaturbate token stats for individual rooms.

K instalaci tototo skriptu si budete muset nainstalovat rozšíření jako Tampermonkey, Greasemonkey nebo Violentmonkey.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Violentmonkey.

K instalaci tohoto skriptu si budete muset nainstalovat rozšíření jako Tampermonkey nebo Userscripts.

You will need to install an extension such as Tampermonkey to install this script.

K instalaci tohoto skriptu si budete muset nainstalovat manažer uživatelských skriptů.

(Už mám manažer uživatelských skriptů, nechte mě ho nainstalovat!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(Už mám manažer uživatelských stylů, nechte mě ho nainstalovat!)

// ==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>`)
    }
})();