☰

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

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

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

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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