☰

Auto Reload on Stake.com - CodeStats edition

Auto-claim your 10-minute Stake.com reloads and Stake.us daily bonus via the direct GraphQL API — no clicks, no navigation. Free companion to the CodeStats Stake auto-claimer — live Stake bonus codes, full auto-claiming at https://codestats.gg · NEW: Stake Bonus Drop Tracker (free open-source drop history & stats) https://github.com/brokechubb/stake-bonus-drop-tracker

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Greasemonkey lub Violentmonkey.

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

Aby zainstalować ten skrypt, wymagana jest instalacje jednego z następujących rozszerzeń: Tampermonkey, Violentmonkey.

Aby zainstalować ten skrypt, wymagana będzie instalacja rozszerzenia Tampermonkey lub Userscripts.

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

Aby zainstalować ten skrypt, musisz zainstalować rozszerzenie menedżera skryptów użytkownika.

(Mam już menedżera skryptów użytkownika, pozwól mi to zainstalować!)

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.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Będziesz musiał zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

Musisz zainstalować rozszerzenie menedżera stylów użytkownika, aby zainstalować ten styl.

(Mam już menedżera stylów użytkownika, pozwól mi to zainstalować!)

// ==UserScript==
// @name         Auto Reload on Stake.com - CodeStats edition
// @description  Auto-claim your 10-minute Stake.com reloads and Stake.us daily bonus via the direct GraphQL API — no clicks, no navigation. Free companion to the CodeStats Stake auto-claimer — live Stake bonus codes, full auto-claiming at https://codestats.gg · NEW: Stake Bonus Drop Tracker (free open-source drop history & stats) https://github.com/brokechubb/stake-bonus-drop-tracker
// @author       CHUBB
// @homepageURL  https://codestats.gg/
// @supportURL   https://greasyfork.org/en/scripts/546623/feedback
// @contributionURL https://codestats.gg/
// @license      MIT
// @namespace    https://codestats.gg/
// @version      4.1
// @match        https://stake.com/*
// @match        https://stake.us/*
// @match        https://stake.ac/*
// @match        https://stake.games/*
// @match        https://stake.bet/*
// @match        https://stake.pet/*
// @match        https://stake.mba/*
// @match        https://stake.jp/*
// @match        https://stake.bz/*
// @match        https://stake.ceo/*
// @match        https://stake.krd/*
// @match        https://staketr.com/*
// @match        https://stake1001.com/*
// @match        https://stake1002.com/*
// @match        https://stake1003.com/*
// @match        https://stake1004.com/*
// @match        https://stake1005.com/*
// @match        https://stake1021.com/*
// @match        https://stake1022.com/*
// @match        https://stake.br/*
// @exclude      https://stake.com/settings/*
// @exclude      https://stake.us/settings/*
// @exclude      https://stake.ac/settings/*
// @exclude      https://stake.games/settings/*
// @exclude      https://stake.bet/settings/*
// @exclude      https://stake.pet/settings/*
// @exclude      https://stake.mba/settings/*
// @exclude      https://stake.jp/settings/*
// @exclude      https://stake.bz/settings/*
// @exclude      https://stake.ceo/settings/*
// @exclude      https://stake.krd/settings/*
// @exclude      https://staketr.com/settings/*
// @exclude      https://stake1001.com/settings/*
// @exclude      https://stake1002.com/settings/*
// @exclude      https://stake1003.com/settings/*
// @exclude      https://stake1004.com/settings/*
// @exclude      https://stake1005.com/settings/*
// @exclude      https://stake1021.com/settings/*
// @exclude      https://stake1022.com/settings/*
// @exclude      https://stake.br/settings/*
// @grant        GM_getValue
// @grant        GM_setValue
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @connect      stake.com
// @connect      stake.us
// @connect      stake.ac
// @connect      stake.games
// @connect      stake.bet
// @connect      stake.pet
// @connect      stake.mba
// @connect      stake.jp
// @connect      stake.bz
// @connect      stake.ceo
// @connect      stake.krd
// @connect      staketr.com
// @connect      stake1001.com
// @connect      stake1002.com
// @connect      stake1003.com
// @connect      stake1004.com
// @connect      stake1005.com
// @connect      stake1021.com
// @connect      stake1022.com
// @connect      stake.br
// @connect      challenges.cloudflare.com
// @connect      api.coingecko.com
// @run-at       document-start
// @noframes
// ==/UserScript==

(function () {
    "use strict";

    // ===== CONSTANTS =====
    const VERSION = "4.0";
    const TURNSTILE_SITE_KEY = "0x4AAAAAAAGD4gMGOTFnvupz";
    const SESSION_RE = /(?:^|; )session=([^;]*)/;
    const GM_KEY = "csApiReload_v1";
    const isStakeUs = /stake\.us/.test(window.location.hostname);
    // claimInterval from the API is in milliseconds (confirmed: 600000 = 10 min)
    const RETRY_DELAY_MS = 2 * 60 * 1000; // retry when not available / error
    const POLL_BUFFER_MS = 30 * 1000; // extra buffer after claimInterval before claiming
    // stake.us requires GM_xmlhttpRequest due to CORS on mutations
    const GM_XHR_RE = /stake\.us/;
    const COINGECKO_IDS = {
        sol: "solana",
        usdt: "tether",
        eth: "ethereum",
        btc: "bitcoin",
        ltc: "litecoin",
        xrp: "ripple",
        eos: "eos",
        bch: "bitcoin-cash",
        trx: "tron",
        bnb: "binancecoin",
        doge: "dogecoin",
        usdc: "usd-coin",
        ape: "apecoin",
        busd: "binance-usd",
        cro: "crypto-com-chain",
        dai: "dai",
        link: "chainlink",
        sand: "the-sandbox",
        shib: "shiba-inu",
        uni: "uniswap",
        pol: "matic-network",
        trump: "official-trump",
    };
    const PRICE_TTL = 5 * 60 * 1000;
    const priceCache = {};

    function fetchPrice(currency) {
        const cached = priceCache[currency];
        if (cached && Date.now() - cached.ts < PRICE_TTL)
            return Promise.resolve(cached.price);
        const id = COINGECKO_IDS[currency];
        if (!id) return Promise.resolve(null);
        return new Promise((resolve) => {
            GM_xmlhttpRequest({
                method: "GET",
                url: `https://api.coingecko.com/api/v3/simple/price?ids=${id}&vs_currencies=usd`,
                onload(r) {
                    try {
                        const data = JSON.parse(r.responseText);
                        const price = data?.[id]?.usd ?? null;
                        if (price)
                            priceCache[currency] = { price, ts: Date.now() };
                        resolve(price);
                    } catch (_) {
                        resolve(null);
                    }
                },
                onerror: () => resolve(null),
                ontimeout: () => resolve(null),
            });
        });
    }

    const CURRENCIES = [
        "usdt",
        "btc",
        "eth",
        "ltc",
        "sol",
        "doge",
        "bch",
        "xrp",
        "trx",
        "eos",
        "bnb",
        "usdc",
        "ape",
        "busd",
        "cro",
        "dai",
        "link",
        "sand",
        "shib",
        "uni",
        "pol",
        "trump",
    ];

    // Status query — VipMeta is the known operation name on Stake's gateway
    // Fix: balances from API returns [{ available: { amount, currency } }]
    // We query flat shape; the response is nested. Handle in balanceFor.
    const STATUS_QUERY = `query VipMeta {
  user {
    id
    balances {
      available { amount currency }
    }
    reload: faucet {
      id
      active
      value
      claimInterval
      lastClaim
      expireAt
    }
  }
}`;

    // Claim mutation — confirmed schema from ClaimFaucet network trace
    const CLAIM_MUTATION = `mutation ClaimFaucet($currency: CurrencyEnum!, $turnstileToken: String!) {
  claimReload: claimFaucet(currency: $currency, turnstileToken: $turnstileToken) {
    reload: faucet {
      user {
        id
        balances {
          available { amount currency }
        }
        reload: faucet {
          id
          active
          value
          claimInterval
          lastClaim
          expireAt
        }
      }
    }
  }
}`;

    const VAULT_MUTATION = `mutation CreateVaultDeposit($currency: CurrencyEnum!, $amount: Float!) {
  createVaultDeposit(currency: $currency, amount: $amount) {
    id
    amount
    currency
  }
}`;

    const DAILY_BONUS_STATUS_QUERY = `query VipMeta {
  user {
    dailyBonus {
      id
      active
      lastClaim
      amounts { amount currency }
    }
  }
}`;

    const DAILY_BONUS_CLAIM_MUTATION = `mutation ClaimDailyBonus($turnstileToken: String!) {
  claimDailyBonus(turnstileToken: $turnstileToken) {
    amount
    createdAt
    currency
    id
  }
}`;

    // ===== PERSISTENT STATE =====
    function loadState() {
        const defaults = {
            currency: "sol",
            reloadEnabled: true,
            claimCount: 0,
            autoVault: false,
            dailyBonusEnabled: false,
            dailyBonusClaimCount: 0,
            dailyBonusLastClaim: null,
        };
        try {
            const raw = GM_getValue(GM_KEY, null);
            return raw ? Object.assign(defaults, raw) : defaults;
        } catch (_) {
            return defaults;
        }
    }

    function saveState() {
        try {
            GM_setValue(GM_KEY, state);
        } catch (_) {}
    }

    const state = loadState();

    // ===== LOGGING =====
    const tag = "[CodeStats API Reload Claimer]";
    const log = (...a) => console.log(tag, ...a);
    const warn = (...a) => console.warn(tag, ...a);

    function logHUD(msg) {
        const el = document.getElementById("csarc-log");
        if (!el) return;
        const line = document.createElement("div");
        line.textContent = `[${new Date().toLocaleTimeString()}] ${msg}`;
        line.style.padding = "1px 0";
        el.appendChild(line);
        while (el.children.length > 60) el.removeChild(el.firstChild);
        el.scrollTop = el.scrollHeight;
    }

    function setStatus(text) {
        const el = document.getElementById("csarc-status");
        if (el) el.textContent = text;
    }

    function setTimer(ms) {
        const el = document.getElementById("csarc-timer");
        if (!el) return;
        if (ms <= 0) {
            el.textContent = "Claiming soon…";
            return;
        }
        const s = Math.floor(ms / 1000);
        el.textContent = `Next claim in: ${Math.floor(s / 60)}m ${s % 60}s`;
    }

    function setCounter() {
        const el = document.getElementById("csarc-count");
        if (el) el.textContent = state.claimCount;
    }

    function setInfo(faucet, claimedAmount, estimatedAmount) {
        const el = document.getElementById("csarc-info");
        if (!el || !faucet) return;
        const fmt = (iso) => {
            if (!iso) return "—";
            const d = new Date(iso);
            return isNaN(d)
                ? iso
                : d.toLocaleString([], {
                      month: "short",
                      day: "numeric",
                      hour: "2-digit",
                      minute: "2-digit",
                  });
        };
        const activeColor = faucet.active ? "#0f0" : "#f66";
        const activeText = faucet.active ? "YES" : "NO";
        const intervalMin = faucet.claimInterval
            ? Math.round(faucet.claimInterval / 60000) + "m"
            : "—";
        const sep = '<span style="color:#333"> | </span>';
        el.innerHTML =
            [
                `Active: <strong style="color:${activeColor}">${activeText}</strong>`,
                `Value: <strong style="color:#0f0">${faucet.value != null ? "$" + faucet.value : "—"}${claimedAmount != null ? ` <span style="color:#aaa">(${claimedAmount} ${state.currency.toUpperCase()})</span>` : estimatedAmount != null ? ` <span style="color:#666">(~${estimatedAmount} ${state.currency.toUpperCase()})</span>` : ""}</strong>`,
                `Interval: <strong style="color:#ff0">${intervalMin}</strong>`,
            ].join(sep) +
            `<br>` +
            [
                `Last Claim: <strong style="color:#0ff">${fmt(faucet.lastClaim)}</strong>`,
                `Expires: <strong style="color:#f90">${fmt(faucet.expireAt)}</strong>`,
            ].join(sep);
    }

    // Fix: API returns balances as [{ available: { amount, currency } }]
    // Flatten to [{ amount, currency }] for easier lookup
    function flattenBalances(balances) {
        if (!Array.isArray(balances)) return [];
        return balances.map(b => b.available).filter(Boolean);
    }

    function balanceFor(flatBalances, currency) {
        if (!Array.isArray(flatBalances)) return null;
        const entry = flatBalances.find((b) => b.currency === currency);
        return entry ? entry.amount : null;
    }

    async function vaultDeposit(currency, amount) {
        const resp = await gql(
            VAULT_MUTATION,
            { currency, amount },
            "CreateVaultDeposit",
            "mutation",
        );
        if (resp?.errors?.length)
            throw new Error(resp.errors[0]?.message || "Vault error");
        return resp?.data?.createVaultDeposit;
    }

    // ===== HTTP =====
    function getSession() {
        const m = document.cookie.match(SESSION_RE);
        return m ? m[1] : "";
    }

    function gql(query, variables, operationName, opType = "query") {
        const token = getSession();
        const url = `${window.location.origin}/_api/graphql`;
        const body = JSON.stringify({
            operationName,
            query,
            variables: variables || {},
        });
        const headers = {
            accept: "*/*",
            "content-type": "application/json",
            "x-access-token": token,
            "x-language": "en",
            "x-operation-name": operationName,
            "x-operation-type": opType,
        };

        if (GM_XHR_RE.test(window.location.hostname)) {
            return new Promise((resolve, reject) => {
                GM_xmlhttpRequest({
                    method: "POST",
                    url,
                    headers,
                    data: body,
                    onload(r) {
                        try {
                            resolve(JSON.parse(r.responseText));
                        } catch (e) {
                            reject(
                                new Error(
                                    `Parse error: ${r.responseText.slice(0, 100)}`,
                                ),
                            );
                        }
                    },
                    onerror: () => reject(new Error("Network error")),
                    ontimeout: () => reject(new Error("Request timeout")),
                });
            });
        }

        return fetch(url, {
            method: "POST",
            headers,
            body,
            credentials: "include",
        }).then((r) => r.json());
    }

    // ===== TURNSTILE =====
    class TurnstileManager {
        constructor() {
            this.cache = [];
            this.maxCache = 3;
            this.tokenTTL = 4 * 60 * 1000;
            this.generatingCount = 0;
            this.maxConcurrent = 2;
            this._maintenance = null;
        }

        async init() {
            await this._loadScript();
            log("Turnstile ready");
            setTimeout(() => this._fill(), 1000);
            this._maintenance = setInterval(() => {
                this._cleanExpired();
                this._fill();
            }, 30000);
        }

        _loadScript() {
            return new Promise((resolve, reject) => {
                if (unsafeWindow.turnstile) {
                    resolve();
                    return;
                }
                const s = document.createElement("script");
                s.src = "https://challenges.cloudflare.com/turnstile/v0/api.js";
                s.onload = resolve;
                s.onerror = () =>
                    reject(new Error("Turnstile script failed to load"));
                document.head.appendChild(s);
            });
        }

        _fill() {
            const needed =
                this.maxCache - this.cache.length - this.generatingCount;
            const toGen = Math.min(
                needed,
                this.maxConcurrent - this.generatingCount,
            );
            for (let i = 0; i < toGen; i++)
                setTimeout(() => this._genCached(), i * 2000);
        }

        async _genCached() {
            try {
                const token = await this._createToken();
                this.cache.push({ token, ts: Date.now() });
                log(
                    `Token cached (pool: ${this.cache.length}/${this.maxCache})`,
                );
            } catch (e) {
                warn("Cache token failed:", e.message);
            }
        }

        _createToken() {
            this.generatingCount++;
            return new Promise((resolve, reject) => {
                try {
                    const id = `_ts_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`;
                    const el = document.createElement("div");
                    el.id = id;
                    el.style.cssText =
                        "visibility:hidden;position:absolute;left:-10000px;top:-10000px;width:300px;height:65px;z-index:-1;";
                    document.body.appendChild(el);

                    let widgetId,
                        cleaned = false;
                    const cleanup = (delay = 0) => {
                        if (cleaned) return;
                        cleaned = true;
                        this.generatingCount--;
                        if (widgetId !== undefined) {
                            try {
                                unsafeWindow.turnstile.remove(widgetId);
                            } catch (_) {}
                        }
                        setTimeout(() => {
                            try {
                                el.parentNode?.removeChild(el);
                            } catch (_) {}
                        }, delay);
                    };

                    widgetId = unsafeWindow.turnstile.render(el, {
                        sitekey: TURNSTILE_SITE_KEY,
                        theme: "dark",
                        callback: (token) => {
                            cleanup(100);
                            resolve(token);
                        },
                        "error-callback": (err) => {
                            cleanup(2000);
                            reject(new Error(`Turnstile error: ${err}`));
                        },
                        "timeout-callback": () => {
                            cleanup(500);
                            reject(new Error("Turnstile timeout"));
                        },
                    });
                } catch (e) {
                    this.generatingCount--;
                    reject(e);
                }
            });
        }

        _cleanExpired() {
            const now = Date.now();
            this.cache = this.cache.filter((e) => now - e.ts < this.tokenTTL);
        }

        async getToken() {
            this._cleanExpired();
            if (this.cache.length > 0) {
                const entry = this.cache.shift();
                this._fill();
                return entry.token;
            }
            for (let i = 1; i <= 3; i++) {
                try {
                    return await Promise.race([
                        this._createToken(),
                        new Promise((_, r) =>
                            setTimeout(
                                () => r(new Error("Token timeout")),
                                30000,
                            ),
                        ),
                    ]);
                } catch (e) {
                    const retryable = /300030|300010|timeout/i.test(e.message);
                    if (retryable && i < 3) {
                        warn(`Token attempt ${i}/3: ${e.message} — retrying`);
                        await new Promise((r) => setTimeout(r, 1000));
                        continue;
                    }
                    throw e;
                }
            }
        }
    }

    // ===== SCHEDULER =====
    let scheduleTimer = null;
    let countdownTimer = null;
    let claimInProgress = false;
    let turnstile = null;
    let lastFaucet = null;

    function schedule(delayMs) {
        clearTimeout(scheduleTimer);
        clearInterval(countdownTimer);
        if (!state.reloadEnabled) return;

        const delay = Math.max(0, delayMs);
        const endTime = Date.now() + delay;

        setTimer(delay);
        countdownTimer = setInterval(() => {
            const rem = endTime - Date.now();
            if (rem <= 0) {
                clearInterval(countdownTimer);
                setTimer(0);
                return;
            }
            setTimer(rem);
        }, 1000);

        scheduleTimer = setTimeout(() => {
            clearInterval(countdownTimer);
            checkAndClaim();
        }, delay);
    }

    // ===== MAIN LOGIC =====
    async function checkAndClaim() {
        if (claimInProgress) return;
        if (!state.reloadEnabled) return;

        claimInProgress = true;
        setStatus("Checking reload status…");
        logHUD("Checking reload status…");

        let faucet, prevBalanceFlat;
        try {
            const resp = await gql(STATUS_QUERY, {}, "VipMeta", "query");
            if (resp?.errors?.length)
                throw new Error(resp.errors[0]?.message || "GraphQL error");
            const user = resp?.data?.user;
            faucet = user?.reload;
            if (!faucet) throw new Error("No faucet data in response");
            // Fix: flatten the nested balance structure from API
            prevBalanceFlat = flattenBalances(user?.balances ?? []);
            lastFaucet = faucet;
            setInfo(faucet);
            if (faucet.value != null) {
                const cur = state.currency;
                fetchPrice(cur)
                    .then((price) => {
                        if (price && state.currency === cur) {
                            const est = parseFloat(
                                (faucet.value / price).toFixed(8),
                            );
                            setInfo(faucet, null, est);
                        }
                    })
                    .catch(() => {});
            }
        } catch (e) {
            warn("Status check failed:", e.message);
            logHUD(`Status check error: ${e.message}`);
            setStatus("Status error — retry in 2m");
            claimInProgress = false;
            schedule(RETRY_DELAY_MS);
            return;
        }

        log("Faucet status:", JSON.stringify(faucet));

        // Check expiry
        const expireAt = faucet.expireAt
            ? new Date(faucet.expireAt).getTime()
            : Infinity;

        // Calculate when next claim is available based on API
        const nextClaimMs = computeNextMs(faucet);

        // Check if reload has expired (no longer available)
        if (isFinite(expireAt) && expireAt < Date.now()) {
            const nextMs = Math.max(computeNextMs(faucet), 30 * 60 * 1000);
            schedule(nextMs);
            setStatus("Expired — next in " + Math.round(nextMs / 60000) + "m");
            logHUD("Reload expired — next check in " + Math.round(nextMs / 60000) + "m");
            claimInProgress = false;
            return;
        }

        if (!faucet.active) {
            // active:false = no reload assigned — use longer interval
            const nextMs = Math.max(computeNextMs(faucet), 30 * 60 * 1000);
            schedule(nextMs);
            setStatus("Not active — next in " + Math.round(nextMs / 60000) + "m");
            logHUD("Reload not active — next check in " + Math.round(nextMs / 60000) + "m");
            claimInProgress = false;
            return;
        }

        // Check if claimable (time-based)
        if (nextClaimMs > 0) {
            logHUD("Not yet claimable — retry in " + Math.round(nextClaimMs / 60000) + "m");
            setStatus("Wait " + Math.round(nextClaimMs / 60000) + "m");
            claimInProgress = false;
            schedule(nextClaimMs);
            return;
        }

        // Available — claim it
        await doClaim(faucet, prevBalanceFlat);
    }

    async function doClaim(prevFaucet, prevBalanceFlat) {
        setStatus("Getting Turnstile token…");
        logHUD("Getting Turnstile token…");

        let token;
        try {
            token = await turnstile.getToken();
        } catch (e) {
            warn("Turnstile failed:", e.message);
            logHUD(`Token error: ${e.message} — reloading…`);
            setStatus("Turnstile error — reloading…");
            claimInProgress = false;
            setTimeout(() => window.location.reload(), 3000);
            return;
        }

        setStatus("Sending claim…");
        logHUD("Sending claim request…");

        const claimCurrency = isStakeUs ? "sweeps" : state.currency;

        let resp;
        try {
            resp = await gql(
                CLAIM_MUTATION,
                { currency: claimCurrency, turnstileToken: token },
                "ClaimFaucet",
                "query",
            );
        } catch (e) {
            warn("Claim request failed:", e.message);
            logHUD(`Request error: ${e.message}`);
            setStatus("Network error — retry in 2m");
            claimInProgress = false;
            schedule(RETRY_DELAY_MS);
            return;
        }

        if (resp?.errors?.length) {
            const msg = resp.errors[0]?.message || "Unknown error";
            warn("Claim GraphQL error:", msg);
            logHUD(`Claim error: ${msg}`);
            const lower = msg.toLowerCase();
            if (
                lower.includes("already") ||
                lower.includes("not available") ||
                lower.includes("interval")
            ) {
                // Re-query status to get the real next claim time
                claimInProgress = false;
                try {
                    const stResp = await gql(STATUS_QUERY, {}, "VipMeta", "query");
                    const stFaucet = stResp?.data?.user?.reload;
                    if (stFaucet) {
                        lastFaucet = stFaucet;
                        setInfo(stFaucet);
                        const nextMs = computeNextMs(stFaucet);
                        const waitMs = nextMs > 0 ? nextMs : RETRY_DELAY_MS;
                        setStatus(`Claim rejected — next in ${Math.round(waitMs / 60000)}m`);
                        logHUD(`Claim rejected: ${msg} — next in ${Math.round(waitMs / 60000)}m`);
                        schedule(waitMs);
                    } else {
                        logHUD(`Claim rejected: ${msg} — retry in ${Math.round(RETRY_DELAY_MS / 60000)}m`);
                        schedule(RETRY_DELAY_MS);
                    }
                } catch {
                    schedule(RETRY_DELAY_MS);
                }
                return;
            } else {
                setStatus(`Error — retry in 2m`);
                claimInProgress = false;
                schedule(RETRY_DELAY_MS);
            }
            return;
        }

        const claimUser = resp?.data?.claimReload?.reload?.user;
        const faucetAfter = claimUser?.reload;
        log("Post-claim faucet data:", JSON.stringify(faucetAfter));

        // Fix: flatten nested balance structure from API
        const balAfterFlat = flattenBalances(claimUser?.balances ?? []);
        const before = balanceFor(prevBalanceFlat ?? [], claimCurrency) ?? 0;
        const after = balanceFor(balAfterFlat, claimCurrency) ?? 0;
        const delta = parseFloat((after - before).toFixed(8));

        if (faucetAfter) {
            lastFaucet = faucetAfter;
            setInfo(faucetAfter, delta > 0 ? delta : null);
        }

        state.claimCount++;
        saveState();
        setCounter();

        const valueStr =
            prevFaucet?.value != null ? ` ($${prevFaucet.value})` : "";
        logHUD(`Reload claimed!${valueStr} Total: ${state.claimCount}`);

        if (state.autoVault) {
            if (delta > 0) {
                try {
                    await vaultDeposit(claimCurrency, delta);
                    logHUD(`Vaulted ${delta} ${claimCurrency.toUpperCase()}`);
                } catch (e) {
                    warn("Vault failed:", e.message);
                    logHUD(`Vault failed: ${e.message}`);
                }
            } else {
                logHUD("Vault: no balance delta detected");
            }
        }

        const nextMs = faucetAfter
            ? computeNextMs(faucetAfter)
            : 600000 + POLL_BUFFER_MS;
        setStatus(
            `Claimed${valueStr} — next in ${Math.round(nextMs / 60000)}m`,
        );
        log(
            `Claim #${state.claimCount} success. Next in ${Math.round(nextMs / 1000)}s`,
        );

        claimInProgress = false;
        schedule(nextMs);
    }

    // claimInterval is in MILLISECONDS from the API (confirmed: 600000 = 10 min)
    // Guard: if API returns seconds (< 3600), convert to ms to handle either format
    function computeNextMs(faucet) {
        const { claimInterval, lastClaim } = faucet || {};
        if (!claimInterval || !lastClaim) return 600000 + POLL_BUFFER_MS;

        const lastMs = new Date(lastClaim).getTime();
        if (isNaN(lastMs)) {
            warn("Could not parse lastClaim:", lastClaim);
            return 600000 + POLL_BUFFER_MS;
        }

        // Normalize: if claimInterval looks like seconds (<100000), convert to ms
        const intervalMs = claimInterval < 100000 ? claimInterval * 1000 : claimInterval;

        const nextAt = lastMs + intervalMs;
        const fromNow = nextAt - Date.now();
        log(`computeNextMs: interval=${intervalMs}ms nextAt=${new Date(nextAt).toISOString()} fromNow=${fromNow}ms`);
        if (fromNow <= 0) return 0;
        return fromNow + POLL_BUFFER_MS;
    }

    function computeDailyNextMs(lastClaim) {
        if (!lastClaim) return 0;
        const lastMs = new Date(lastClaim).getTime();
        if (isNaN(lastMs)) return 0;
        const DAILY_INTERVAL_MS = 24 * 60 * 60 * 1000;
        const fromNow = lastMs + DAILY_INTERVAL_MS - Date.now();
        return fromNow <= 0 ? 0 : fromNow + POLL_BUFFER_MS;
    }

    // ===== DAILY BONUS =====
    let dailyBonusTimer = null;
    let dailyBonusClaiming = false;
    let dailyBonusNextCheck = null;

    function fmtMsDaily(ms) {
        if (ms <= 0) return "now";
        const h = Math.floor(ms / 3600000);
        const m = Math.floor((ms % 3600000) / 60000);
        const s = Math.floor((ms % 60000) / 1000);
        if (h > 0) return `${h}h ${m}m ${s}s`;
        if (m === 0) return `${s}s`;
        return `${m}m ${s}s`;
    }

    function scheduleDailyBonus(delayMs) {
        clearTimeout(dailyBonusTimer);
        if (!isStakeUs || !state.dailyBonusEnabled) return;
        dailyBonusNextCheck = Date.now() + delayMs;
        updateDailyBonusStatus();
        dailyBonusTimer = setTimeout(checkDailyBonus, delayMs);
    }

    function updateDailyBonusStatus() {
        const el = document.getElementById("csarc-daily-status");
        if (!el) return;
        if (!isStakeUs) { el.textContent = "Not on stake.us"; return; }
        if (!state.dailyBonusEnabled) { el.textContent = "Disabled"; return; }
        if (dailyBonusClaiming) { el.textContent = "Claiming…"; return; }
        if (dailyBonusNextCheck == null) { el.textContent = "Waiting…"; return; }
        const rem = dailyBonusNextCheck - Date.now();
        el.textContent = rem <= 0 ? "Checking…" : `Next: ${fmtMsDaily(rem)}`;
    }

    async function checkDailyBonus() {
        if (!isStakeUs || !state.dailyBonusEnabled || dailyBonusClaiming) return;
        dailyBonusClaiming = true;
        updateDailyBonusStatus();
        logHUD("💰 Checking Daily Bonus…");

        try {
            const resp = await gql(DAILY_BONUS_STATUS_QUERY, {}, "VipMeta", "query");
            if (resp?.errors?.length) {
                logHUD(`Daily Bonus error: ${resp.errors[0]?.message}`);
                dailyBonusClaiming = false;
                scheduleDailyBonus(15 * 60 * 1000);
                return;
            }

            const db = resp?.data?.user?.dailyBonus;
            if (!db || !db.active) {
                logHUD("💰 Daily Bonus not active");
                dailyBonusClaiming = false;
                scheduleDailyBonus(30 * 60 * 1000);
                return;
            }

            const nextMs = computeDailyNextMs(db.lastClaim);
            if (nextMs > 0) {
                dailyBonusClaiming = false;
                scheduleDailyBonus(nextMs);
                return;
            }

            // Time to claim
            setStatus("Daily Bonus — getting token…");
            logHUD("💰 Getting token for Daily Bonus…");

            let token;
            try {
                token = await turnstile.getToken();
            } catch (e) {
                logHUD(`Daily Bonus token error: ${e.message}`);
                dailyBonusClaiming = false;
                scheduleDailyBonus(5 * 60 * 1000);
                return;
            }

            setStatus("Daily Bonus — claiming…");
            logHUD("💰 Claiming Daily Bonus…");

            let claimResp;
            try {
                claimResp = await gql(
                    DAILY_BONUS_CLAIM_MUTATION,
                    { turnstileToken: token },
                    "ClaimDailyBonus",
                    "query",
                );
            } catch (e) {
                logHUD(`Daily Bonus request error: ${e.message}`);
                dailyBonusClaiming = false;
                scheduleDailyBonus(5 * 60 * 1000);
                return;
            }

            if (claimResp?.errors?.length) {
                const msg = claimResp.errors[0]?.message || "Unknown error";
                const errType = claimResp.errors[0]?.errorType || "";
                logHUD(`Daily Bonus error: ${msg}`);
                dailyBonusClaiming = false;
                if (errType === "dailyBonusClaimInterval" || msg.toLowerCase().includes("interval")) {
                    scheduleDailyBonus(30 * 60 * 1000);
                } else {
                    scheduleDailyBonus(5 * 60 * 1000);
                }
                return;
            }

            const bonusData = claimResp?.data?.claimDailyBonus;
            if (bonusData && bonusData.amount) {
                const amt = Number(bonusData.amount).toFixed(8).replace(/\.?0+$/, "");
                const cur = (bonusData.currency || "").toUpperCase();
                logHUD(`💰 Daily Bonus claimed! ${amt} ${cur}`);
                state.dailyBonusClaimCount++;
                state.dailyBonusLastClaim = new Date().toISOString();
                saveState();
                updateDailyCount();
            }

            dailyBonusClaiming = false;
            scheduleDailyBonus(25 * 60 * 60 * 1000); // 25h
        } catch (e) {
            logHUD(`Daily Bonus error: ${e.message}`);
            dailyBonusClaiming = false;
            scheduleDailyBonus(15 * 60 * 1000);
        } finally {
            updateDailyBonusStatus();
        }
    }

    function startDailyBonus() {
        if (!isStakeUs || !state.dailyBonusEnabled) return;
        scheduleDailyBonus(30 * 1000); // First check in 30s
    }

    function stopDailyBonus() {
        clearTimeout(dailyBonusTimer);
        dailyBonusTimer = null;
        dailyBonusNextCheck = null;
        dailyBonusClaiming = false;
        updateDailyBonusStatus();
    }

    function updateDailyCount() {
        const el = document.getElementById("csarc-daily-count");
        if (el) el.textContent = state.dailyBonusClaimCount || 0;
    }
    function buildHUD() {
        if (document.getElementById("csarc-hud")) return;

        const hud = document.createElement("div");
        hud.id = "csarc-hud";
        hud.style.cssText = [
            "position:fixed",
            "bottom:10px",
            "right:10px",
            "width:500px",
            "max-height:275px",
            "background:rgba(0,0,0,0.87)",
            "color:#0f0",
            "font:12px/1.4 monospace",
            "padding:10px",
            "border:1px solid #0f0",
            "border-radius:8px",
            "z-index:2147483647",
            "overflow:hidden",
        ].join(";");

        hud.innerHTML = `
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:7px;text-decoration:underline">
  <span style="font-size:15px;font-weight:550;letter-spacing:0.3px"><a href="https://codestats.gg" target="_blank" rel="noopener" style="color:#0f0;text-decoration:underline">CodeStats.gg</a> VIP Reload Claimer v${VERSION}</span>
  <button id="csarc-min" style="padding:2px 8px;background:#0f0;color:#000;border:none;border-radius:3px;cursor:pointer;font-weight:bold">−</button>
</div>
<div id="csarc-body">
  <div style="display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap">
    <button id="csarc-toggle" style="height:26px;padding:0 8px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;cursor:pointer;font-weight:bold;box-sizing:border-box">${state.reloadEnabled ? "RELOAD: ON" : "RELOAD: OFF"}</button>
    <button id="csarc-now" style="height:26px;padding:0 8px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;cursor:pointer;font-weight:bold;box-sizing:border-box">CHECK NOW</button>
    <label style="color:#aaa;display:flex;align-items:center;gap:4px">Currency:
      ${isStakeUs
        ? '<span style="height:26px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;padding:0 8px;font-weight:bold;display:inline-flex;align-items:center">SWEEPS</span>'
        : `<select id="csarc-cur" style="height:26px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;padding:0 8px;font-weight:bold;box-sizing:border-box">
        ${CURRENCIES.map((c) => `<option value="${c}"${c === state.currency ? " selected" : ""}>${c.toUpperCase()}</option>`).join("")}
      </select>`}
    </label>
    <label style="color:#aaa;display:flex;align-items:center;gap:4px">Auto-Vault:
      <select id="csarc-vault" style="height:26px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;padding:0 8px;font-weight:bold;box-sizing:border-box">
        <option value="0"${!state.autoVault ? " selected" : ""}>OFF</option>
        <option value="1"${state.autoVault ? " selected" : ""}>ON</option>
      </select>
    </label>
  </div>
  <div id="csarc-daily-row" style="display:flex;align-items:center;gap:6px;margin-bottom:6px;flex-wrap:wrap">
    <label style="color:#aaa;display:flex;align-items:center;gap:4px">
      ${!isStakeUs
        ? '<span style="height:26px;background:#000;color:#555;border:1px solid #333;border-radius:3px;padding:0 8px;font-weight:bold;display:inline-flex;align-items:center">N/A</span>'
        : `<button id="csarc-daily-toggle" style="height:26px;padding:0 8px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;cursor:pointer;font-weight:bold;box-sizing:border-box">DAILY $1: ${state.dailyBonusEnabled ? "ON" : "OFF"}</button>`}
    </label>
    ${!isStakeUs ? '' : '<button id="csarc-daily-check" style="height:26px;padding:0 8px;background:#000;color:#0f0;border:1px solid #0f0;border-radius:3px;cursor:pointer;font-weight:bold;box-sizing:border-box">CHECK NOW</button>'}
    <span id="csarc-daily-status" style="font-size:11px;color:#aaa">${!isStakeUs ? "Not on stake.us" : state.dailyBonusEnabled ? "Waiting…" : "Disabled"}</span>
    <span style="font-size:11px;color:#0f0">Claims: <strong id="csarc-daily-count">${state.dailyBonusClaimCount || 0}</strong></span>
  </div>
  <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:4px;font-size:11px;color:#aaa">
    <span>Status: <span id="csarc-status" style="color:#0f0">${state.reloadEnabled ? "Starting…" : "Paused"}</span></span>
    <span>Claims: <strong id="csarc-count" style="color:#0f0">${state.claimCount}</strong>
      <button id="csarc-reset" style="margin-left:5px;padding:1px 5px;background:#600;color:#f66;border:none;border-radius:3px;cursor:pointer;font-size:10px">RESET</button>
    </span>
  </div>
  <div id="csarc-info" style="margin-bottom:4px;font-size:11px;color:#aaa;line-height:1.6"></div>
  <div id="csarc-log" style="height:75px;overflow-y:auto;border-top:1px solid #030;padding-top:4px;margin-bottom:4px;font-size:11px"></div>
  <div id="csarc-timer" style="font-weight:bold;color:#ff0;font-size:11px"></div>
</div>`;

        document.body.appendChild(hud);

        document.getElementById("csarc-min").addEventListener("click", () => {
            const body = document.getElementById("csarc-body");
            const btn = document.getElementById("csarc-min");
            if (body.style.display === "none") {
                body.style.display = "";
                hud.style.maxHeight = "280px";
                btn.textContent = "−";
            } else {
                body.style.display = "none";
                hud.style.maxHeight = "40px";
                btn.textContent = "+";
            }
        });

        document
            .getElementById("csarc-toggle")
            .addEventListener("click", () => {
                state.reloadEnabled = !state.reloadEnabled;
                saveState();
                document.getElementById("csarc-toggle").textContent =
                    state.reloadEnabled ? "RELOAD: ON" : "RELOAD: OFF";
                if (state.reloadEnabled) {
                    setStatus("Resuming…");
                    logHUD("Reload claims enabled");
                    checkAndClaim();
                } else {
                    clearTimeout(scheduleTimer);
                    clearInterval(countdownTimer);
                    setStatus("Reloads off");
                    document.getElementById("csarc-timer").textContent = "—";
                    logHUD("Reload claims disabled");
                }
            });

        document.getElementById("csarc-now").addEventListener("click", () => {
            if (claimInProgress) {
                logHUD("Already in progress");
                return;
            }
            clearTimeout(scheduleTimer);
            clearInterval(countdownTimer);
            logHUD("Manual check triggered");
            checkAndClaim();
        });

        const curSelect = document.getElementById("csarc-cur");
        if (curSelect) {
            curSelect.addEventListener("change", (e) => {
                state.currency = e.target.value;
                saveState();
                logHUD(`Currency: ${state.currency.toUpperCase()}`);
                if (lastFaucet) {
                    setInfo(lastFaucet);
                    if (lastFaucet.value != null) {
                        const cur = state.currency;
                        fetchPrice(cur)
                            .then((price) => {
                                if (
                                    price &&
                                    lastFaucet.value != null &&
                                    state.currency === cur
                                ) {
                                    const est = parseFloat(
                                        (lastFaucet.value / price).toFixed(8),
                                    );
                                    setInfo(lastFaucet, null, est);
                                }
                            })
                            .catch(() => {});
                    }
                }
            });
        }

        document
            .getElementById("csarc-vault")
            .addEventListener("change", (e) => {
                state.autoVault = e.target.value === "1";
                saveState();
                logHUD(`Auto-Vault: ${state.autoVault ? "ON" : "OFF"}`);
            });

        document.getElementById("csarc-reset").addEventListener("click", () => {
            state.claimCount = 0;
            saveState();
            setCounter();
            logHUD("Counter reset");
        });

        const dailyToggle = document.getElementById("csarc-daily-toggle");
        if (dailyToggle) {
            dailyToggle.addEventListener("click", () => {
                state.dailyBonusEnabled = !state.dailyBonusEnabled;
                saveState();
                dailyToggle.textContent = `DAILY $1: ${state.dailyBonusEnabled ? "ON" : "OFF"}`;
                logHUD(`Daily Bonus: ${state.dailyBonusEnabled ? "ON" : "OFF"}`);
                if (state.dailyBonusEnabled) {
                    startDailyBonus();
                } else {
                    stopDailyBonus();
                }
            });
        }

        const dailyCheck = document.getElementById("csarc-daily-check");
        if (dailyCheck) {
            dailyCheck.addEventListener("click", () => {
                if (dailyBonusClaiming) { logHUD("Daily Bonus already in progress"); return; }
                clearTimeout(dailyBonusTimer);
                logHUD("💰 Manual Daily Bonus check triggered");
                checkDailyBonus();
            });
        }

        // Live countdown for daily bonus status
        setInterval(() => updateDailyBonusStatus(), 1000);
    }

    let hudGuard = null;
    function watchHUD() {
        if (hudGuard) return;
        hudGuard = setInterval(() => {
            if (!document.getElementById("csarc-hud") && document.body)
                buildHUD();
        }, 2000);
    }

    // ===== BOOTSTRAP =====
    async function start() {
        buildHUD();
        watchHUD();
        logHUD(`Loaded v${VERSION} successfully.`);

        turnstile = new TurnstileManager();
        try {
            await turnstile.init();
        } catch (e) {
            logHUD(`Turnstile failed: ${e.message} — reloading…`);
            warn("Turnstile init failed:", e);
            setStatus("Turnstile error — reloading…");
            setTimeout(() => window.location.reload(), 3000);
            return;
        }

        if (isStakeUs && state.dailyBonusEnabled) startDailyBonus();

        if (!state.reloadEnabled) {
            setStatus("Reloads off");
            logHUD("Reload claims disabled");
            return;
        }

        checkAndClaim();
    }

    function ready(fn) {
        if (
            document.readyState === "complete" ||
            document.readyState === "interactive"
        ) {
            setTimeout(fn, 1000);
        } else {
            document.addEventListener("DOMContentLoaded", () =>
                setTimeout(fn, 1000),
            );
        }
    }

    ready(start);
})();