Skip to content

Instantly share code, notes, and snippets.

@Tknott95
Last active June 17, 2026 03:47
Show Gist options
  • Select an option

  • Save Tknott95/8731b5552d27478f709e3bc694dbf8a4 to your computer and use it in GitHub Desktop.

Select an option

Save Tknott95/8731b5552d27478f709e3bc694dbf8a4 to your computer and use it in GitHub Desktop.
api bypass tweet deleter
// ==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