Greasy Fork is available in English.

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.

Bu betiği kurabilmeniz için Tampermonkey, Greasemonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği yüklemek için Tampermonkey gibi bir uzantı yüklemeniz gerekir.

Bu betiği kurabilmeniz için Tampermonkey ya da Violentmonkey gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği kurabilmeniz için Tampermonkey ya da Userscripts gibi bir kullanıcı betiği eklentisini kurmanız gerekmektedir.

Bu betiği indirebilmeniz için ayrıca Tampermonkey gibi bir eklenti kurmanız gerekmektedir.

Bu komut dosyasını yüklemek için bir kullanıcı komut dosyası yöneticisi uzantısı yüklemeniz gerekecek.

(Zaten bir kullanıcı komut dosyası yöneticim var, kurmama izin verin!)

Bu stili yüklemek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için Stylus gibi bir uzantı kurmanız gerekir.

Bu stili yükleyebilmek için Stylus gibi bir uzantı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

Bu stili yüklemek için bir kullanıcı stili yöneticisi uzantısı kurmanız gerekir.

Bu stili yükleyebilmek için bir kullanıcı stili yöneticisi uzantısı yüklemeniz gerekir.

(Zateb bir user-style yöneticim var, yükleyeyim!)

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

})();