Last active
June 17, 2026 03:47
-
-
Save Tknott95/8731b5552d27478f709e3bc694dbf8a4 to your computer and use it in GitHub Desktop.
api bypass tweet deleter
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // ==UserScript== | |
| // @name TweetXer Evolved | |
| // @namespace https://github.com/lucahammer/tweetXer/ | |
| // @version 0.9.6-evolved-slow | |
| // @description Evolved fork of TweetXer: slow-delete your own X/Twitter posts without an export file, with real 429/420 network cooldown recovery. | |
| // @author Luca, dbort, pReya, Micolithe, STrRedWolf; evolved slow-delete fork by ASCII ALIEN/ChatGPT | |
| // @license NoHarm-draft | |
| // @match https://x.com/* | |
| // @match https://mobile.x.com/* | |
| // @match https://twitter.com/* | |
| // @match https://mobile.twitter.com/* | |
| // @icon https://www.google.com/s2/favicons?domain=twitter.com | |
| // @grant none | |
| // @run-at document-start | |
| // ==/UserScript== | |
| (function () { | |
| 'use strict' | |
| const TweetXerEvolved = { | |
| version: '0.9.6-evolved-slow', | |
| TweetCount: 1000000, | |
| dId: 'tweetxerEvolvedBar', | |
| dCount: 0, | |
| total: 1000000, | |
| running: false, | |
| stopped: false, | |
| perActionDelayMs: 1600, | |
| rateLimitCooldownMs: 10 * 60 * 1000, | |
| rateLimitCooldownUntil: 0, | |
| rateLimitReason: '', | |
| async init() { | |
| this.installNetworkRateLimitWatcher() | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', async () => { | |
| this.createTopBar() | |
| await this.getTweetCount() | |
| this.total = this.TweetCount | |
| this.updateInfo('Ready. Click OK at the top to slow-delete without an export file. Keep this tab open.') | |
| }) | |
| } else { | |
| this.createTopBar() | |
| await this.getTweetCount() | |
| this.total = this.TweetCount | |
| this.updateInfo('Ready. Click OK at the top to slow-delete without an export file. Keep this tab open.') | |
| } | |
| }, | |
| sleep(ms) { | |
| return new Promise(resolve => setTimeout(resolve, ms)) | |
| }, | |
| installNetworkRateLimitWatcher() { | |
| if (window.__tweetxerEvolvedRateWatcherInstalled) return | |
| window.__tweetxerEvolvedRateWatcherInstalled = true | |
| const app = this | |
| const originalFetch = window.fetch | |
| window.fetch = async function (...args) { | |
| const response = await originalFetch.apply(this, args) | |
| if (response && (response.status === 429 || response.status === 420)) { | |
| app.triggerRateLimitCooldown(`HTTP ${response.status} from X/Twitter`) | |
| } | |
| return response | |
| } | |
| const originalOpen = XMLHttpRequest.prototype.open | |
| const originalSend = XMLHttpRequest.prototype.send | |
| XMLHttpRequest.prototype.open = function (method, url, ...rest) { | |
| this.__tweetxerEvolvedUrl = url | |
| return originalOpen.call(this, method, url, ...rest) | |
| } | |
| XMLHttpRequest.prototype.send = function (...args) { | |
| this.addEventListener('loadend', function () { | |
| if (this.status === 429 || this.status === 420) { | |
| app.triggerRateLimitCooldown(`XHR HTTP ${this.status} from X/Twitter`) | |
| } | |
| }) | |
| return originalSend.apply(this, args) | |
| } | |
| console.log('TweetXer Evolved: network 429/420 watcher installed.') | |
| }, | |
| triggerRateLimitCooldown(reason) { | |
| const until = Date.now() + this.rateLimitCooldownMs | |
| if (until > this.rateLimitCooldownUntil) { | |
| this.rateLimitCooldownUntil = until | |
| this.rateLimitReason = reason | |
| console.warn(`TweetXer Evolved: ${reason}. Pausing for 10 minutes.`) | |
| this.updateInfo(`${reason}. Waiting 10 minutes before continuing. ${this.dCount} deleted.`) | |
| } | |
| }, | |
| async waitForRateLimitCooldown() { | |
| while (Date.now() < this.rateLimitCooldownUntil && !this.stopped) { | |
| const remaining = Math.ceil((this.rateLimitCooldownUntil - Date.now()) / 1000) | |
| const minutes = Math.floor(remaining / 60) | |
| const seconds = String(remaining % 60).padStart(2, '0') | |
| this.updateInfo(`${this.rateLimitReason || 'Rate limited'}. Waiting ${minutes}:${seconds}, then trying again. ${this.dCount} deleted.`) | |
| await this.sleep(1000) | |
| } | |
| }, | |
| createTopBar() { | |
| const old = document.getElementById(this.dId) | |
| if (old) old.remove() | |
| const div = document.createElement('div') | |
| div.id = this.dId | |
| div.innerHTML = ` | |
| <style> | |
| #${this.dId} { | |
| z-index: 2147483647; | |
| position: sticky; | |
| top: 0; | |
| left: 0; | |
| width: auto; | |
| margin: 0 auto; | |
| padding: 14px 10%; | |
| background: #87CEFA; | |
| opacity: 0.97; | |
| color: #000; | |
| font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; | |
| box-shadow: 0 2px 12px rgba(0,0,0,0.25); | |
| } | |
| #${this.dId} * { | |
| box-sizing: border-box; | |
| } | |
| #${this.dId} h2 { | |
| margin: 0 0 6px 0; | |
| font-size: 20px; | |
| font-weight: 800; | |
| color: #000; | |
| } | |
| #${this.dId} p { | |
| margin: 6px 0; | |
| color: #000; | |
| } | |
| #${this.dId} button { | |
| background: #eff3f4; | |
| border: 1px solid #333; | |
| border-radius: 999px; | |
| padding: 6px 14px; | |
| margin-right: 8px; | |
| cursor: pointer; | |
| font-weight: 700; | |
| color: #000; | |
| } | |
| #${this.dId} button:disabled { | |
| opacity: 0.6; | |
| cursor: not-allowed; | |
| } | |
| #${this.dId} progress { | |
| width: 100%; | |
| height: 16px; | |
| margin-top: 8px; | |
| } | |
| #${this.dId} small { | |
| color: #111; | |
| } | |
| </style> | |
| <h2 id="tweetsXer_title">TweetXer Evolved</h2> | |
| <p id="info">Loading profile count...</p> | |
| <p id="tweetxer_buttons"> | |
| <button id="tweetxerStartSlowDelete" type="button">OK — start slow delete without file</button> | |
| <button id="tweetxerStopSlowDelete" type="button">Stop after current action</button> | |
| <button id="tweetxerRemoveBar" type="button">Remove bar</button> | |
| </p> | |
| <p> | |
| <small>${this.version} · watches real 429/420 network errors · waits 10 minutes, then resumes.</small> | |
| </p> | |
| ` | |
| document.body.insertBefore(div, document.body.firstChild) | |
| document.getElementById('tweetxerStartSlowDelete').addEventListener('click', async () => { | |
| if (this.running) return | |
| const ok = window.confirm( | |
| 'Start TweetXer Evolved slow delete without a Twitter/X export file? Keep this tab open. This cannot be undone.' | |
| ) | |
| if (!ok) return | |
| this.stopped = false | |
| this.running = true | |
| document.getElementById('tweetxerStartSlowDelete').disabled = true | |
| this.createProgressBar() | |
| await this.slowDelete() | |
| }) | |
| document.getElementById('tweetxerStopSlowDelete').addEventListener('click', () => { | |
| this.stopped = true | |
| this.updateInfo(`Stopping after the current action. ${this.dCount} deleted so far.`) | |
| }) | |
| document.getElementById('tweetxerRemoveBar').addEventListener('click', () => { | |
| div.remove() | |
| }) | |
| }, | |
| updateTitle(text) { | |
| const title = document.getElementById('tweetsXer_title') | |
| if (title) title.textContent = text | |
| }, | |
| updateInfo(text) { | |
| const info = document.getElementById('info') | |
| if (info) info.textContent = text | |
| }, | |
| createProgressBar() { | |
| if (document.getElementById('progressbar')) return | |
| const progressbar = document.createElement('progress') | |
| progressbar.id = 'progressbar' | |
| progressbar.value = this.dCount | |
| progressbar.max = this.total || 1000000 | |
| document.getElementById(this.dId).appendChild(progressbar) | |
| }, | |
| updateProgressBar() { | |
| const progressbar = document.getElementById('progressbar') | |
| if (progressbar) { | |
| progressbar.max = this.total || 1000000 | |
| progressbar.value = this.dCount | |
| } | |
| this.updateInfo(`${this.dCount} deleted. TweetXer Evolved is still running.`) | |
| }, | |
| async countdown(ms, reason = 'Rate limited / too many requests') { | |
| const until = Date.now() + ms | |
| while (Date.now() < until && !this.stopped) { | |
| const remaining = Math.ceil((until - Date.now()) / 1000) | |
| const minutes = Math.floor(remaining / 60) | |
| const seconds = String(remaining % 60).padStart(2, '0') | |
| this.updateInfo(`${reason}. Waiting ${minutes}:${seconds}, then trying again. ${this.dCount} deleted.`) | |
| await this.sleep(1000) | |
| } | |
| }, | |
| async getTweetCount() { | |
| try { | |
| await waitForElemToExist('header', 20000) | |
| await this.sleep(1000) | |
| function extractTweetCount(selector) { | |
| const element = document.querySelector(selector) | |
| if (!element) return null | |
| const match = element.textContent.match(/((\d|,|\.|K)+) (\w+)$/) | |
| if (!match) return null | |
| return Number( | |
| match[1] | |
| .replace(/\.(\d+)K/, '$1'.padEnd(4, '0')) | |
| .replace('K', '000') | |
| .replace(',', '') | |
| .replace('.', '') | |
| ) | |
| } | |
| this.TweetCount = | |
| extractTweetCount('[data-testid="primaryColumn"]>div>div>div') || | |
| extractTweetCount('[data-testid="TopNavBar"]>div>div') || | |
| 1000000 | |
| } catch (error) { | |
| console.warn('Could not read post count. Using 1,000,000 as approximate progress max.', error) | |
| this.TweetCount = 1000000 | |
| } | |
| }, | |
| hideNonTweetJunk() { | |
| document | |
| .querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>div') | |
| .forEach(x => x.remove()) | |
| document | |
| .querySelectorAll('section [data-testid="cellInnerDiv"]>div>div>[role="link"]') | |
| .forEach(x => x.remove()) | |
| }, | |
| async goToRepliesTab() { | |
| const links = Array.from( | |
| document.querySelectorAll('[data-testid="ScrollSnap-List"] a, a[href*="/with_replies"]') | |
| ) | |
| const replies = | |
| links.find(a => a.href && a.href.includes('/with_replies')) || | |
| links[1] | |
| if (replies) { | |
| replies.click() | |
| await this.sleep(2500) | |
| } | |
| }, | |
| async slowDelete() { | |
| this.updateTitle('TweetXer Evolved: slow deleting') | |
| await this.goToRepliesTab() | |
| const moreSelector = '[data-testid="tweet"] [data-testid="caret"]' | |
| let emptyPasses = 0 | |
| while (!this.stopped) { | |
| await this.waitForRateLimitCooldown() | |
| if (this.stopped) break | |
| try { | |
| await this.sleep(this.perActionDelayMs + Math.floor(Math.random() * 700)) | |
| await this.waitForRateLimitCooldown() | |
| this.hideNonTweetJunk() | |
| let tweet = document.querySelector('[data-testid="tweet"]') | |
| let caret = document.querySelector(moreSelector) | |
| if (!tweet || !caret) { | |
| window.scrollBy(0, Math.floor(window.innerHeight * 0.85)) | |
| await this.sleep(3000) | |
| tweet = document.querySelector('[data-testid="tweet"]') | |
| caret = document.querySelector(moreSelector) | |
| emptyPasses++ | |
| if ((!tweet || !caret) && emptyPasses >= 4) { | |
| this.updateInfo(`No more visible posts found. Deleted ${this.dCount}. Reload X/Twitter to confirm.`) | |
| break | |
| } | |
| continue | |
| } | |
| emptyPasses = 0 | |
| tweet.scrollIntoView({ | |
| behavior: 'smooth', | |
| block: 'center' | |
| }) | |
| await this.sleep(500) | |
| await this.waitForRateLimitCooldown() | |
| const unretweet = tweet.querySelector('[data-testid="unretweet"]') | |
| if (unretweet) { | |
| unretweet.click() | |
| const confirmURT = await waitForElemToExist( | |
| '[data-testid="unretweetConfirm"]', | |
| 10000 | |
| ) | |
| await this.waitForRateLimitCooldown() | |
| confirmURT.click() | |
| } else { | |
| caret.click() | |
| await this.sleep(250) | |
| const menuItems = await waitForElemList('[role="menuitem"]', 10000) | |
| const deleteItem = menuItems.find(item => /delete/i.test(item.textContent || '')) | |
| const followItem = menuItems.find(item => /@/.test(item.textContent || '')) | |
| if (!deleteItem && followItem) { | |
| document.dispatchEvent( | |
| new KeyboardEvent('keydown', { | |
| key: 'Escape', | |
| bubbles: true | |
| }) | |
| ) | |
| tweet.remove() | |
| this.updateInfo(`Skipped another account's post. ${this.dCount} deleted.`) | |
| continue | |
| } | |
| if (!deleteItem) { | |
| throw new Error('Delete menu item not found; possible X/Twitter throttling or changed UI') | |
| } | |
| await this.waitForRateLimitCooldown() | |
| deleteItem.click() | |
| const confirmation = await waitForElemToExist( | |
| '[data-testid="confirmationSheetConfirm"]', | |
| 10000 | |
| ) | |
| await this.waitForRateLimitCooldown() | |
| confirmation.click() | |
| } | |
| await this.sleep(1500) | |
| await this.waitForRateLimitCooldown() | |
| this.dCount++ | |
| this.updateProgressBar() | |
| if (this.dCount % 25 === 0) { | |
| console.log(`${new Date().toUTCString()} Deleted ${this.dCount} posts`) | |
| } | |
| await this.sleep(800 + Math.floor(Math.random() * 700)) | |
| await this.waitForRateLimitCooldown() | |
| } catch (error) { | |
| console.error('TweetXer Evolved hit a DOM/UI error. Pausing 10 minutes:', error) | |
| await this.countdown( | |
| this.rateLimitCooldownMs, | |
| 'Hit a DOM/UI error / possible too many requests' | |
| ) | |
| } | |
| } | |
| this.running = false | |
| this.stopped = false | |
| const start = document.getElementById('tweetxerStartSlowDelete') | |
| if (start) start.disabled = false | |
| this.updateTitle('TweetXer Evolved') | |
| this.updateInfo(`Stopped/finished. Deleted ${this.dCount}. Reload X/Twitter to confirm.`) | |
| } | |
| } | |
| const waitForElemToExist = async (selector, timeoutMs = 15000) => { | |
| const elem = document.querySelector(selector) | |
| if (elem) return elem | |
| return new Promise((resolve, reject) => { | |
| const observer = new MutationObserver(() => { | |
| const elem = document.querySelector(selector) | |
| if (elem) { | |
| clearTimeout(timer) | |
| observer.disconnect() | |
| resolve(elem) | |
| } | |
| }) | |
| const timer = setTimeout(() => { | |
| observer.disconnect() | |
| reject(new Error(`Timeout waiting for ${selector}`)) | |
| }, timeoutMs) | |
| observer.observe(document.body, { | |
| subtree: true, | |
| childList: true | |
| }) | |
| }) | |
| } | |
| const waitForElemList = async (selector, timeoutMs = 15000) => { | |
| const elems = Array.from(document.querySelectorAll(selector)) | |
| if (elems.length > 0) return elems | |
| return new Promise((resolve, reject) => { | |
| const observer = new MutationObserver(() => { | |
| const elems = Array.from(document.querySelectorAll(selector)) | |
| if (elems.length > 0) { | |
| clearTimeout(timer) | |
| observer.disconnect() | |
| resolve(elems) | |
| } | |
| }) | |
| const timer = setTimeout(() => { | |
| observer.disconnect() | |
| reject(new Error(`Timeout waiting for ${selector}`)) | |
| }, timeoutMs) | |
| observer.observe(document.body, { | |
| subtree: true, | |
| childList: true | |
| }) | |
| }) | |
| } | |
| TweetXerEvolved.init() | |
| })() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment