X Dev Credits Bypass

Allows purchasing credits below the $5.00 minimum on the X Developer Console billing page. Opens a Stripe payment sheet entirely within your browser using X's own Stripe session.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

// ==UserScript==
// @name         X Dev Credits Bypass
// @namespace    https://spin.rip/
// @version      1.0.0
// @description  Allows purchasing credits below the $5.00 minimum on the X Developer Console billing page. Opens a Stripe payment sheet entirely within your browser using X's own Stripe session.
// @match        https://console.x.com/*
// @grant        none
// @run-at       document-idle
// @license      AGPL-3.0-or-later
// ==/UserScript==

(function () {
  'use strict';

  const CREDITS_PATH_RE    = /\/accounts\/(\d+)\/billing\/credits/;
  const ORIGINAL_BUTTON_TEXT = 'Continue to payment';
  const CUSTOM_BUTTON_TEXT   = 'Continue with spinified amount';

  // ─── SPA navigation shim ────────────────────────────────────────────────────
  // console.x.com is a React SPA – patch history so we can react to URL changes.
  let _currentPath = location.pathname;

  function patchHistory() {
    ['pushState', 'replaceState'].forEach(method => {
      const orig = history[method];
      history[method] = function (...args) {
        const result = orig.apply(this, args);
        window.dispatchEvent(new Event('x-spa-navigate'));
        return result;
      };
    });
    window.addEventListener('popstate', () =>
      window.dispatchEvent(new Event('x-spa-navigate'))
    );
  }

  function getAccountId() {
    return location.pathname.match(CREDITS_PATH_RE)?.[1] ?? null;
  }

  function isCreditsPage() {
    return CREDITS_PATH_RE.test(location.pathname);
  }

  // ─── Helpers ─────────────────────────────────────────────────────────────────
  function getAmountInput() {
    return document.querySelector('input[placeholder="5.00 – 5,000.00"]');
  }

  function getContinueButton() {
    return [...document.querySelectorAll('button')]
      .find(b => b.textContent.trim() === ORIGINAL_BUTTON_TEXT ||
                 b.textContent.trim() === CUSTOM_BUTTON_TEXT);
  }

  function getParsedAmount() {
    const input = getAmountInput();
    if (!input) return null;
    const val = parseFloat(input.value);
    return isNaN(val) ? null : val;
  }

  function getPublishableKey() {
    const found = document.documentElement.innerHTML.match(/pk_(live|test)_[A-Za-z0-9]+/);
    return found ? found[0] : null;
  }

  function getStripeJsUrl() {
    const s = Array.from(document.scripts).find(sc => sc.src.includes('js.stripe.com'));
    return s ? s.src : 'https://js.stripe.com/basil/stripe.js';
  }

  // ─── Button label sync ────────────────────────────────────────────────────────
  function syncButtonLabel() {
    const btn = getContinueButton();
    if (!btn) return;
    const amount    = getParsedAmount();
    const isBelowMin = amount !== null && amount < 5.00;
    btn.textContent = isBelowMin ? CUSTOM_BUTTON_TEXT : ORIGINAL_BUTTON_TEXT;
    if (isBelowMin) btn.removeAttribute('disabled');
  }

  // ─── "Minimum $5.00" → strikethrough + "WHAT MINIMUM??" ─────────────────────
  function transformMinimumText(el) {
    if (el._spinifiedMin) return;
    el._spinifiedMin = true;
    el.innerHTML =
      '<s style="opacity:0.55">Minimum $5.00</s>' +
      '<span style="color:#f87171;font-weight:600"> WHAT MINIMUM??</span>';
  }

  function observeMinimumText() {
    const scan = () => {
      document.querySelectorAll('p').forEach(p => {
        if (p.textContent.trim() === 'Minimum $5.00') transformMinimumText(p);
      });
    };
    scan();
    new MutationObserver(scan).observe(document.body, { childList: true, subtree: true });
  }

  // ─── Transparency popup ───────────────────────────────────────────────────────
  function showInfoPopup(amountDollars, onConfirm, onCancel) {
    document.getElementById('spinified-popup')?.remove();

    const overlay = document.createElement('div');
    overlay.id = 'spinified-popup';
    overlay.style.cssText = `
      position: fixed; inset: 0; z-index: 99999;
      background: rgba(0,0,0,0.7); display: flex;
      align-items: center; justify-content: center;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
    `;
    overlay.innerHTML = `
      <div style="
        background: #1a1a1a; border: 1px solid rgba(255,255,255,0.12);
        border-radius: 16px; padding: 28px 32px; max-width: 480px; width: 90%;
        color: #fff; box-shadow: 0 30px 80px rgba(0,0,0,0.6);
      ">
        <h2 style="margin: 0 0 8px; font-size: 18px; font-weight: 600;">
          Custom Invoice - Below Minimum
        </h2>
        <p style="margin: 0 0 16px; font-size: 14px; color: #aaa; line-height: 1.5;">
          <strong style="color:#fff">What this does:</strong>
          This will open an <strong style="color:#fff">invoice / payment sheet</strong>
          for <strong style="color:#fff">$${amountDollars.toFixed(2)}</strong>
          — below the normal $5.00 minimum.
        </p>
        <ul style="margin: 0 0 20px; padding-left: 20px; font-size: 13px; color: #bbb; line-height: 1.8;">
          <li>A Stripe checkout session is requested from <strong style="color:#ddd">console.x.com</strong> using your existing login.</li>
          <li>Stripe's payment UI loads in a new tab, served directly by Stripe.</li>
          <li><strong style="color:#ddd">Nothing is sent to any third-party server.</strong> All traffic is between your browser, X's API, and Stripe.</li>
          <li>Your session cookie is used (same as normal checkout, no credentials are stored or exposed by this script).</li>
        </ul>
        <p style="margin: 0 0 20px; font-size: 12px; color: #666; border-top: 1px solid rgba(255,255,255,0.08); padding-top: 14px;">
          This userscript is installed locally in your browser and runs entirely client-side. It does not communicate with any external service beyond X and Stripe.
        </p>
        <div style="display: flex; gap: 10px; justify-content: flex-end;">
          <button id="spinified-cancel" style="
            padding: 8px 18px; border-radius: 999px; border: 1px solid rgba(255,255,255,0.15);
            background: transparent; color: #fff; cursor: pointer; font-size: 14px;
          ">Cancel</button>
          <button id="spinified-confirm" style="
            padding: 8px 20px; border-radius: 999px; border: none;
            background: #fff; color: #000; cursor: pointer; font-size: 14px; font-weight: 500;
          ">Open Invoice →</button>
        </div>
      </div>
    `;
    document.body.appendChild(overlay);
    document.getElementById('spinified-cancel').onclick = () => { overlay.remove(); onCancel?.(); };
    document.getElementById('spinified-confirm').onclick = () => { overlay.remove(); onConfirm?.(); };
    overlay.addEventListener('click', e => { if (e.target === overlay) { overlay.remove(); onCancel?.(); } });
  }

  // ─── Core payment flow ────────────────────────────────────────────────────────
  async function launchPayment(amountDollars) {
    const ACCOUNT_ID  = getAccountId();
    if (!ACCOUNT_ID) { alert('Could not determine account ID from URL.'); return; }
    const CHECKOUT_URL = `https://console.x.com/api/accounts/${ACCOUNT_ID}/credits/embedded_checkout`;
    const amountUnits  = Math.round(amountDollars * 100);

    let sessionData;
    try {
      const res = await fetch(CHECKOUT_URL, {
        method: 'POST',
        credentials: 'include',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          amount: amountUnits,
          currency: 'USD',
          savePaymentMethod: true,
          paymentMethodTypes: ['card']
        })
      });
      if (!res.ok) {
        const errText = await res.text();
        throw new Error(`X API returned ${res.status}: ${errText}`);
      }
      sessionData = await res.json();
    } catch (err) {
      alert(`Failed to create checkout session:\n\n${err.message}`);
      return;
    }

    const { clientSecret, sessionId } = sessionData;
    if (!clientSecret) {
      alert('No clientSecret returned from X API. Response:\n\n' + JSON.stringify(sessionData, null, 2));
      return;
    }

    const pk = getPublishableKey();
    if (!pk) { alert('Could not find Stripe publishable key on this page.'); return; }

    const stripeJsUrl = getStripeJsUrl();

    const html = `<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>X Credits – $${amountDollars.toFixed(2)} Invoice</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body {
      background: #0f0f0f; color: #fff; min-height: 100vh;
      font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
      display: flex; flex-direction: column; align-items: center;
      padding: 40px 20px;
    }
    header { width: 100%; max-width: 520px; display: flex; align-items: center; gap: 12px; margin-bottom: 28px; }
    header svg { width: 28px; height: 28px; fill: #fff; flex-shrink: 0; }
    header h1  { font-size: 18px; font-weight: 600; }
    header span { font-size: 14px; color: #888; margin-left: auto; white-space: nowrap; }
    #checkout-container {
      width: 100%; max-width: 520px;
      background: #1a1a1a; border-radius: 16px;
      border: 1px solid rgba(255,255,255,0.08);
      padding: 24px; min-height: 200px;
    }
    #loading  { color: #888; text-align: center; padding-top: 60px; font-size: 14px; }
    #status   { margin-top: 16px; font-size: 13px; color: #4ade80; text-align: center; width: 100%; max-width: 520px; }
    #error-msg{ margin-top: 16px; font-size: 13px; color: #f87171; text-align: center; width: 100%; max-width: 520px; }
    footer    { margin-top: 32px; font-size: 11px; color: #444; text-align: center; max-width: 520px; line-height: 1.6; }
  </style>
</head>
<body>
  <header>
    <svg viewBox="0 0 24 24"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-4.714-6.231-5.401 6.231H2.746l7.73-8.835L1.254 2.25H8.08l4.253 5.622 5.911-5.622zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>
    <h1>Credits Invoice</h1>
    <span>$${amountDollars.toFixed(2)}</span>
  </header>
  <div id="checkout-container">
    <p id="loading">Loading payment form\u2026</p>
  </div>
  <div id="status"></div>
  <div id="error-msg"></div>
  <footer>
    Payment processed by Stripe via X\u2019s API.&nbsp;
    Session:&nbsp;<code style="color:#555">${sessionId ?? 'n/a'}</code><br>
    Opened by a locally installed userscript \u2014 communicates only with X (<code style="color:#555">console.x.com</code>) and Stripe.
  </footer>
  <script>
    (function bootstrap() {
      var script = document.createElement('script');
      script.src = ${JSON.stringify(stripeJsUrl)};
      script.onload = initCheckout;
      script.onerror = function() {
        document.getElementById('error-msg').textContent =
          '\u2716 Could not load Stripe.js. Check your network connection and try again.';
        document.getElementById('loading')?.remove();
      };
      document.head.appendChild(script);
    })();

    async function initCheckout() {
      var errEl    = document.getElementById('error-msg');
      var statusEl = document.getElementById('status');
      var loadEl   = document.getElementById('loading');
      function showErr(msg) { if (loadEl) loadEl.remove(); errEl.textContent = '\u2716 Stripe error: ' + msg; }
      if (typeof Stripe === 'undefined') { showErr('Stripe did not initialise. Please refresh and try again.'); return; }
      try {
        var stripe   = Stripe(${JSON.stringify(pk)});
        var checkout = await stripe.initEmbeddedCheckout({
          clientSecret: ${JSON.stringify(clientSecret)},
          onComplete: function() { statusEl.textContent = '\u2713 Payment complete! You can close this tab.'; }
        });
        if (loadEl) loadEl.remove();
        checkout.mount('#checkout-container');
      } catch (err) { showErr(err.message || String(err)); console.error(err); }
    }
  <\/script>
</body>
</html>`;

    const blob    = new Blob([html], { type: 'text/html' });
    const blobURL = URL.createObjectURL(blob);
    const newTab  = window.open(blobURL, '_blank');
    if (!newTab) alert('Popup was blocked. Please allow popups for console.x.com and try again.');
  }

  // ─── Event interception ───────────────────────────────────────────────────────
  function attachClickInterceptor() {
    document.addEventListener('click', async (e) => {
      const btn = e.target.closest('button');
      if (!btn) return;
      if (btn.textContent.trim() !== CUSTOM_BUTTON_TEXT) return;
      const amount = getParsedAmount();
      if (amount === null || amount >= 5.00) return;
      e.preventDefault();
      e.stopImmediatePropagation();
      showInfoPopup(amount, () => launchPayment(amount), null);
    }, true);
  }

  // ─── Per-page init (runs each time URL becomes the credits page) ──────────────
  let _initDone = false;
  let _labelInterval = null;
  let _bodyObserver  = null;

  function initForCreditsPage() {
    if (_initDone) return;
    _initDone = true;

    // Watch for the amount input appearing (dialog may open after a click)
    _bodyObserver = new MutationObserver(() => {
      const input = getAmountInput();
      if (!input || input._spinifiedWatched) return;
      input._spinifiedWatched = true;
      input.addEventListener('input',  syncButtonLabel);
      input.addEventListener('change', syncButtonLabel);
      syncButtonLabel();
    });
    _bodyObserver.observe(document.body, { childList: true, subtree: true });

    _labelInterval = setInterval(syncButtonLabel, 400);

    observeMinimumText();
  }

  function teardown() {
    _initDone = false;
    if (_labelInterval)  { clearInterval(_labelInterval);  _labelInterval = null; }
    if (_bodyObserver)   { _bodyObserver.disconnect();      _bodyObserver  = null; }
  }

  // ─── SPA route watcher ────────────────────────────────────────────────────────
  function onNavigate() {
    if (isCreditsPage()) {
      initForCreditsPage();
    } else {
      teardown();
    }
  }

  // ─── Bootstrap ───────────────────────────────────────────────────────────────
  patchHistory();
  attachClickInterceptor(); // single global listener, safe to attach once
  window.addEventListener('x-spa-navigate', onNavigate);

  // Run immediately in case the page loaded directly on the credits URL
  if (isCreditsPage()) initForCreditsPage();

})();