Greasy Fork is available in English.
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.
// ==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.
Session: <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();
})();