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.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

})();