Skip to content

Instantly share code, notes, and snippets.

@lucahammer
Last active February 15, 2026 22:16
Show Gist options
  • Select an option

  • Save lucahammer/1aa16b4d3c1fb04035839da5ef699d65 to your computer and use it in GitHub Desktop.

Select an option

Save lucahammer/1aa16b4d3c1fb04035839da5ef699d65 to your computer and use it in GitHub Desktop.
Delete all your Tweets Javascript
/*
This may get your account banned. It runs in your regular browser with your regular login without needing the API.
The script does the same things that you would do yourself:
Click the three dots, select delete Tweet, confirm, scroll to next Tweet, repeat.
==========================
Usage
1. Open your Twitter profile in a browser
2. Open the console in the developer tools (F12)
3. Paste the script and press enter
4. ???
5. Tweets are gone. (At least from your profile. They may still exist in backups.)
==========================
Copyright 2023 Nobody
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
const waitForElemToExist = async (selector) => {
return new Promise(resolve => {
if (document.querySelector(selector)) {
return resolve(document.querySelector(selector));
}
const observer = new MutationObserver(() => {
if (document.querySelector(selector)) {
resolve(document.querySelector(selector));
observer.disconnect();
}
});
observer.observe(document.body, {
subtree: true,
childList: true,
});
});
}
const deleteTweets = async () => {
const more = '[data-testid="tweet"] [aria-label="More"][data-testid="caret"]'
while (document.querySelectorAll(more).length > 0) {
// give the Tweets a chance to load; increase/decrease if necessary
// afaik the limit is 50 requests per minute
await new Promise(r => setTimeout(r, 1200));
// hide recommended profiles and stuff
document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>div').forEach(x => x.remove())
document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>[role="link"]').forEach(x => x.remove())
document.querySelector('[aria-label="Profile timelines"]').scrollIntoView({ 'behavior': 'smooth' })
// if it is a Fav, unfav it (only works if script is executed on Likes tab)
unfav = document.querySelector('[data-testid="unlike"]')
if (unfav) {
unfav.click()
document.querySelector('[data-testid="tweet"]').remove()
}
// if it is a Retweet, unretweet it
unretweet = document.querySelector('[data-testid="unretweet"]')
if (unretweet) {
unretweet.click()
confirmURT = await waitForElemToExist('[data-testid="unretweetConfirm"]')
confirmURT.click()
}
// delete Tweet
else {
caret = await waitForElemToExist(more)
caret.click()
menu = await waitForElemToExist('[role="menuitem"]');
if (menu.textContent.includes('@')) {
// don't unfollow people (because their Tweet is the reply tab)
caret.click()
document.querySelector('[data-testid="tweet"]').remove()
}
else {
menu.click();
confirmation = document.querySelector('[data-testid="confirmationSheetConfirm"]')
if (confirmation) confirmation.click()
}
}
del_count++
// print to the console how many Tweets already got deleted
// Change the 10 to how often you want an update.
// 10 for every 10th Tweet, 1 for every Tweet, 100 for every 100th Tweet
if (del_count % 10 == 0) console.log(`${new Date().toUTCString()} Deleted ${del_count} Tweets`)
}
console.log('Switching to Replies.')
document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[1].click()
await new Promise(r => setTimeout(r, 2000));
if (document.querySelectorAll(more).length > 0) {
deleteTweets();
}
else {
console.log('Switching to Tweets.')
document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[0].click()
await new Promise(r => setTimeout(r, 2000));
if (document.querySelectorAll(more).length > 0) {
deleteTweets();
}
}
console.log('No Tweets left. Please reload to confirm.')
}
del_count = 0;
deleteTweets();
@bitbent
Copy link

bitbent commented Jan 11, 2025

Running atm. Seems to work perfectly fine. Make sure to set your language to English. Takes approx. 0,6 seconds per tweet so be patient. Thank you very much for the script. It is incredibly hard to remove your data from X/Twitter.

@halk
Copy link

halk commented Mar 7, 2025

Changing L77 to

confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')

made this work for me.

@HectorShaw
Copy link

Changing L77 to

confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')

made this work for me.

yes this worked +1

@arasovic
Copy link

arasovic commented May 5, 2025

  • switch language to english
  • do this change confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')
  • bum

its working guys ty

@notmydictionary
Copy link

/*
This may get your account banned. It runs in your regular browser with your regular login without needing the API.
The script does the same things that you would do yourself:
Click the three dots, select delete Tweet, confirm, scroll to next Tweet, repeat.

Usage

  1. Open your Twitter profile in a browser

  2. Open the console in the developer tools (F12)

  3. Paste the script and press enter

  4. ???

  5. Tweets are gone. (At least from your profile. They may still exist in backups.)
    ==========================
    Copyright 2023 Nobody
    Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
    THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
    */
    const waitForElemToExist = async (selector) => {
    return new Promise(resolve => {
    if (document.querySelector(selector)) {
    return resolve(document.querySelector(selector));
    }

     const observer = new MutationObserver(() => {
         if (document.querySelector(selector)) {
             resolve(document.querySelector(selector));
             observer.disconnect();
         }
     });
    
     observer.observe(document.body, {
         subtree: true,
         childList: true,
     });
    

    });
    }

const deleteTweets = async () => {
const more = '[data-testid="tweet"] [aria-label="More"][data-testid="caret"]'
while (document.querySelectorAll(more).length > 0) {

    // give the Tweets a chance to load; increase/decrease if necessary
    // afaik the limit is 50 requests per minute
    await new Promise(r => setTimeout(r, 600));

    // hide recommended profiles and stuff
    document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>div').forEach(x => x.remove())
    document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>[role="link"]').forEach(x => x.remove())
    document.querySelector('[aria-label="Profile timelines"]').scrollIntoView({ 'behavior': 'smooth' })

    // if it is a Fav, unfav it (only works if script is executed on Likes tab)
    unfav = document.querySelector('[data-testid="unlike"]')
    if (unfav) {
        unfav.click()
        document.querySelector('[data-testid="tweet"]').remove()
    }

    // if it is a Retweet, unretweet it
    unretweet = document.querySelector('[data-testid="unretweet"]')
    if (unretweet) {
        unretweet.click()
        confirmURT = await waitForElemToExist('[data-testid="unretweetConfirm"]')
        confirmURT.click()
    }

    // delete Tweet
    else {
        caret = await waitForElemToExist(more)
        caret.click()
        menu = await waitForElemToExist('[role="menuitem"]');
        if (menu.textContent.includes('@')) {
            // don't unfollow people (because their Tweet is the reply tab)
            caret.click()
            document.querySelector('[data-testid="tweet"]').remove()
        }
        else {
            menu.click();
            confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')
            if (confirmation) confirmation.click()
        }
    }

    del_count++

    // print to the console how many Tweets already got deleted
    // Change the 10 to how often you want an update. 
    // 10 for every 10th Tweet, 1 for every Tweet, 100 for every 100th Tweet
    if (del_count % 10 == 0) console.log(`${new Date().toUTCString()} Deleted ${del_count} Tweets`)

}

console.log('Switching to Replies.')
document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[1].click()
await new Promise(r => setTimeout(r, 2000));
if (document.querySelectorAll(more).length > 0) {
    deleteTweets();
}
else {
    console.log('Switching to Tweets.')
    document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[0].click()
    await new Promise(r => setTimeout(r, 2000));
    if (document.querySelectorAll(more).length > 0) {
        deleteTweets();
    }
}

console.log('No Tweets left. Please reload to confirm.')

}

del_count = 0;
deleteTweets();

@AetherUnbound
Copy link

If you'd like to delete only your tweets but preserve your retweets, I've modified the script for that to work:

// From https://x.com/search?q=from%3A YOUR-USERNAME &src=typed_query
const waitForElemToExist = async (selector) => {
    return new Promise(resolve => {
        if (document.querySelector(selector)) {
            return resolve(document.querySelector(selector));
        }

        const observer = new MutationObserver(() => {
            if (document.querySelector(selector)) {
                resolve(document.querySelector(selector));
                observer.disconnect();
            }
        });

        observer.observe(document.body, {
            subtree: true,
            childList: true,
        });
    });
}

const deleteTweets = async () => {
    const more = '[data-testid="tweet"] [aria-label="More"][data-testid="caret"]'
    while (document.querySelectorAll(more).length > 0) {

        // give the Tweets a chance to load; increase/decrease if necessary
        // afaik the limit is 50 requests per minute
        await new Promise(r => setTimeout(r, 1200));

        // hide recommended profiles and stuff
        document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>div').forEach(x => x.remove())
        document.querySelectorAll('[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>[role="link"]').forEach(x => x.remove())
        // document.querySelector('[aria-label="Profile timelines"]').scrollIntoView({ 'behavior': 'smooth' })

        // if it is a Fav, unfav it (only works if script is executed on Likes tab)
        unfav = document.querySelector('[data-testid="unlike"]')
        if (unfav) {
            unfav.click()
            document.querySelector('[data-testid="tweet"]').remove()
        }

        // if it is a Retweet, unretweet it
        unretweet = document.querySelector('[data-testid="unretweet"]')
        if (unretweet) {
            unretweet.click()
            confirmURT = await waitForElemToExist('[data-testid="unretweetConfirm"]')
            confirmURT.click()
        }

        // delete Tweet
        else {
            caret = await waitForElemToExist(more)
            caret.click()
            menu = await waitForElemToExist('[role="menuitem"]');
            if (menu.textContent.includes('@')) {
                // don't unfollow people (because their Tweet is the reply tab)
                caret.click()
                document.querySelector('[data-testid="tweet"]').remove()
            }
            else {
                menu.click();
                confirmation = await waitForElemToExist('[data-testid="confirmationSheetConfirm"]')

                if (confirmation) confirmation.click()
            }
        }

        del_count++

        // print to the console how many Tweets already got deleted
        // Change the 10 to how often you want an update. 
        // 10 for every 10th Tweet, 1 for every Tweet, 100 for every 100th Tweet
        if (del_count % 10 == 0) console.log(`${new Date().toUTCString()} Deleted ${del_count} Tweets`)

    }

    console.log('Switching to Replies.')
    document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[1].click()
    await new Promise(r => setTimeout(r, 2000));
    if (document.querySelectorAll(more).length > 0) {
        deleteTweets();
    }
    else {
        console.log('Switching to Tweets.')
        document.querySelectorAll('[aria-label="Profile timelines"]>div>div>div>div>a')[0].click()
        await new Promise(r => setTimeout(r, 2000));
        if (document.querySelectorAll(more).length > 0) {
            deleteTweets();
        }
    }

    console.log('No Tweets left. Please reload to confirm.')

}

del_count = 0;
deleteTweets();

@sudarevic-pixel
Copy link

// Configuration - Human-like timing
const CONFIG = {
MIN_ACTION_DELAY: 3000, // minimum 3 seconds between actions
MAX_ACTION_DELAY: 8000, // maximum 8 seconds between actions
MIN_CLICK_DELAY: 500, // minimum delay between clicks
MAX_CLICK_DELAY: 1500, // maximum delay between clicks
TAB_SWITCH_DELAY: 4000, // wait 4 seconds after switching tabs
PAUSE_EVERY: 15, // take a break every N deletions
PAUSE_MIN: 10000, // minimum pause duration (10 seconds)
PAUSE_MAX: 25000, // maximum pause duration (25 seconds)
LOG_FREQUENCY: 5, // log every 5 deletions
WAIT_TIMEOUT: 15000, // max wait time for elements
SCROLL_VARIANCE: 3 // random scroll count
};

const SELECTORS = {
MORE_BUTTON: '[data-testid="tweet"] [aria-label="More"][data-testid="caret"]',
UNLIKE_BUTTON: '[data-testid="unlike"]',
UNRETWEET_BUTTON: '[data-testid="unretweet"]',
UNRETWEET_CONFIRM: '[data-testid="unretweetConfirm"]',
DELETE_CONFIRM: '[data-testid="confirmationSheetConfirm"]',
TWEET: '[data-testid="tweet"]',
MENU_ITEM: '[role="menuitem"]',
PROFILE_TIMELINES: '[aria-label="Profile timelines"]',
TAB_LINKS: '[aria-label="Profile timelines"]>div>div>div>div>a'
};

// Generate random delay within range (more human-like)
const randomDelay = (min, max) => {
return Math.floor(Math.random() * (max - min + 1)) + min;
};

// Human-like delay with variance
const humanDelay = async (baseMin = CONFIG.MIN_ACTION_DELAY, baseMax = CONFIG.MAX_ACTION_DELAY) => {
const delay = randomDelay(baseMin, baseMax);
await new Promise(r => setTimeout(r, delay));
};

// Improved element waiter with timeout and cleanup
const waitForElemToExist = async (selector, timeout = CONFIG.WAIT_TIMEOUT) => {
return new Promise((resolve, reject) => {
const element = document.querySelector(selector);
if (element) {
return resolve(element);
}

    const observer = new MutationObserver(() => {
        const element = document.querySelector(selector);
        if (element) {
            clearTimeout(timeoutId);
            observer.disconnect();
            resolve(element);
        }
    });

    const timeoutId = setTimeout(() => {
        observer.disconnect();
        reject(new Error(`Timeout waiting for element: ${selector}`));
    }, timeout);

    observer.observe(document.body, {
        subtree: true,
        childList: true,
    });
});

};

// Human-like click with random delay
const humanClick = async (element) => {
if (element) {
// Random delay before click (simulating mouse movement time)
await new Promise(r => setTimeout(r, randomDelay(100, 400)));
element.click();
// Random delay after click (simulating human reaction time)
await new Promise(r => setTimeout(r, randomDelay(CONFIG.MIN_CLICK_DELAY, CONFIG.MAX_CLICK_DELAY)));
}
};

// Human-like scrolling behavior
const humanScroll = async () => {
const scrollCount = randomDelay(1, CONFIG.SCROLL_VARIANCE);
for (let i = 0; i < scrollCount; i++) {
const timeline = document.querySelector(SELECTORS.PROFILE_TIMELINES);
if (timeline) {
timeline.scrollIntoView({ behavior: 'smooth' });
await new Promise(r => setTimeout(r, randomDelay(500, 1200)));
}
}
};

// Helper to remove clutter with human-like behavior
const removeClutter = async () => {
const clutterSelectors = [
'[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>div',
'[aria-label="Profile timelines"]+section [data-testid="cellInnerDiv"]>div>div>[role="link"]'
];

clutterSelectors.forEach(selector => {
    document.querySelectorAll(selector).forEach(el => el.remove());
});

await humanScroll();

};

// Take a human-like break
const takeBreak = async (count) => {
if (count > 0 && count % CONFIG.PAUSE_EVERY === 0) {
const pauseDuration = randomDelay(CONFIG.PAUSE_MIN, CONFIG.PAUSE_MAX);
console.log(⏸️ Taking a ${Math.round(pauseDuration/1000)}s break (appears more human)...);
await new Promise(r => setTimeout(r, pauseDuration));
console.log('▶️ Resuming...');
}
};

// Handle unlike action
const handleUnlike = async () => {
const unfavButton = document.querySelector(SELECTORS.UNLIKE_BUTTON);
if (unfavButton) {
await humanClick(unfavButton);
await new Promise(r => setTimeout(r, randomDelay(800, 1500)));
const tweet = document.querySelector(SELECTORS.TWEET);
if (tweet) tweet.remove();
return true;
}
return false;
};

// Handle unretweet action
const handleUnretweet = async () => {
const unretweetButton = document.querySelector(SELECTORS.UNRETWEET_BUTTON);
if (unretweetButton) {
await humanClick(unretweetButton);
try {
const confirmButton = await waitForElemToExist(SELECTORS.UNRETWEET_CONFIRM, 5000);
await humanClick(confirmButton);
await new Promise(r => setTimeout(r, randomDelay(800, 1500)));
return true;
} catch (e) {
console.warn('⚠️ Failed to confirm unretweet:', e.message);
}
}
return false;
};

// Handle delete tweet action
const handleDelete = async () => {
try {
const caret = await waitForElemToExist(SELECTORS.MORE_BUTTON, 5000);
await humanClick(caret);

    const menu = await waitForElemToExist(SELECTORS.MENU_ITEM, 5000);
    
    // Check if this is an unfollow menu (skip these)
    if (menu.textContent.includes('@')) {
        await humanClick(caret); // close menu
        await new Promise(r => setTimeout(r, randomDelay(500, 1000)));
        const tweet = document.querySelector(SELECTORS.TWEET);
        if (tweet) tweet.remove();
        return false;
    }
    
    await humanClick(menu);
    
    // Wait a bit before confirming (human reads the confirmation)
    await new Promise(r => setTimeout(r, randomDelay(600, 1200)));
    
    const confirmation = document.querySelector(SELECTORS.DELETE_CONFIRM);
    if (confirmation) {
        await humanClick(confirmation);
    }
    
    // Wait for deletion to process
    await new Promise(r => setTimeout(r, randomDelay(800, 1500)));
    return true;
} catch (e) {
    console.warn('⚠️ Failed to delete tweet:', e.message);
    return false;
}

};

// Switch to a specific tab with human-like delay
const switchTab = async (tabIndex, tabName) => {
console.log(🔄 Switching to ${tabName}...);
const tabs = document.querySelectorAll(SELECTORS.TAB_LINKS);
if (tabs[tabIndex]) {
await humanClick(tabs[tabIndex]);
// Extra wait for tab to load content
await new Promise(r => setTimeout(r, CONFIG.TAB_SWITCH_DELAY + randomDelay(0, 2000)));
return true;
}
return false;
};

// Calculate and display estimated time
const displayEstimate = (count) => {
const avgTime = (CONFIG.MIN_ACTION_DELAY + CONFIG.MAX_ACTION_DELAY) / 2;
const estimatedSeconds = Math.round((count * avgTime) / 1000);
const hours = Math.floor(estimatedSeconds / 3600);
const minutes = Math.floor((estimatedSeconds % 3600) / 60);

if (hours > 0) {
    console.log(`⏱️ Estimated time: ~${hours}h ${minutes}m for ${count} items (running at human speed)`);
} else {
    console.log(`⏱️ Estimated time: ~${minutes}m for ${count} items (running at human speed)`);
}

};

// Main deletion logic
const deleteTweets = async () => {
let deleteCount = 0;
const startTime = Date.now();

// Allow stopping via window.stopDeletion = true
window.stopDeletion = false;

const processCurrentTab = async () => {
    let moreTweets = document.querySelectorAll(SELECTORS.MORE_BUTTON);
    
    if (moreTweets.length > 0) {
        displayEstimate(moreTweets.length);
    }
    
    while (moreTweets.length > 0 && !window.stopDeletion) {
        // Human-like delay between actions
        await humanDelay();
        
        // Take periodic breaks
        await takeBreak(deleteCount);
        
        await removeClutter();
        
        let deleted = false;
        
        // Try unlike first
        if (await handleUnlike()) {
            deleted = true;
        }
        // Try unretweet
        else if (await handleUnretweet()) {
            deleted = true;
        }
        // Try delete
        else if (await handleDelete()) {
            deleted = true;
        }
        
        if (deleted) {
            deleteCount++;
            if (deleteCount % CONFIG.LOG_FREQUENCY === 0) {
                const elapsed = Math.round((Date.now() - startTime) / 1000 / 60);
                console.log(`✅ ${new Date().toLocaleTimeString()} | Deleted ${deleteCount} items | Running for ${elapsed}m`);
            }
        }
        
        // Occasionally scroll a bit extra (more human-like)
        if (Math.random() < 0.3) {
            await new Promise(r => setTimeout(r, randomDelay(500, 1500)));
        }
        
        // Check if we should continue
        moreTweets = document.querySelectorAll(SELECTORS.MORE_BUTTON);
    }
    
    return document.querySelectorAll(SELECTORS.MORE_BUTTON).length > 0;
};

// Process Tweets tab
console.log('🚀 Starting human-speed deletion process...');
console.log('💡 This runs slowly to avoid spam detection');
console.log('⏹️ To stop at any time, run: window.stopDeletion = true');
console.log('─────────────────────────────────────');

await processCurrentTab();

// Process Replies tab
if (!window.stopDeletion) {
    if (await switchTab(1, 'Replies')) {
        await processCurrentTab();
    }
}

// Go back to Tweets tab for final check
if (!window.stopDeletion) {
    if (await switchTab(0, 'Tweets')) {
        await processCurrentTab();
    }
}

const totalTime = Math.round((Date.now() - startTime) / 1000 / 60);

if (window.stopDeletion) {
    console.log('─────────────────────────────────────');
    console.log(`⏸️ Stopped by user. Deleted ${deleteCount} items in ${totalTime}m.`);
} else {
    console.log('─────────────────────────────────────');
    console.log(`🎉 Complete! Deleted ${deleteCount} items in ${totalTime}m.`);
    console.log('🔄 Please reload the page to confirm all items are gone.');
}

};

// Start the process with a countdown
console.log('🤖 Human-speed tweet deletion script loaded.');
console.log('⚡ Settings: 3-8 second delays, breaks every 15 items');
console.log('⏳ Starting in 5 seconds...');
setTimeout(() => {
console.log('3...');
setTimeout(() => {
console.log('2...');
setTimeout(() => {
console.log('1...');
setTimeout(() => deleteTweets(), 1000);
}, 1000);
}, 1000);
}, 2000);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment