A browser-console helper for cleaning up your own X profile by deleting posts, removing reposts, and removing likes in slow, human-paced batches.
This script is designed for personal account cleanup. It runs in your browser while you are logged in to X and interacts with the visible page, similar to how you would click through the UI manually.
- Delete posts from your profile timeline.
- Delete replies from your replies page.
- Undo reposts from your profile timeline.
- Remove likes from your likes page.
- Run in limited batches to reduce rate-limit and account-lock risk.
- Stop safely at any time with
XCleaner.stop().
Deleting posts is permanent. Make sure you have exported your X archive first if you want a backup.
X may rate-limit, challenge, or temporarily lock accounts that perform too many actions too quickly. This script intentionally uses delays and a per-run action limit, but there is still risk. Use small batches and take breaks.
Only use this on your own account.
- Open X in your browser and log in.
- Open the page you want to clean:
- Posts:
https://x.com/YOUR_USERNAME - Replies:
https://x.com/YOUR_USERNAME/with_replies - Likes:
https://x.com/YOUR_USERNAME/likes
- Posts:
- Open Developer Tools.
- Go to the Console tab.
- Paste the script below and press Enter.
- Run one of the cleanup commands.
Delete posts or replies from the current timeline:
XCleaner.deletePosts()Remove reposts from the current profile timeline:
XCleaner.removeReposts()Remove likes from the likes page:
XCleaner.removeLikes()Stop the current run:
XCleaner.stop()- Go to your profile page and run
XCleaner.deletePosts(). - Go to your replies page and run
XCleaner.deletePosts(). - Go to your profile page and run
XCleaner.removeReposts(). - Go to your likes page and run
XCleaner.removeLikes(). - Repeat in batches until your profile is clean.
By default, the script stops after 80 actions per run. This is intentional.
(() => {
window.XCleaner?.stop?.();
const CFG = {
delayMin: 1300,
delayMax: 2800,
afterActionDelay: 2500,
scrollDelay: 1800,
maxActionsPerRun: 80,
};
let stopped = false;
let actions = 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const rand = (min, max) => Math.floor(min + Math.random() * (max - min));
const log = (...a) =>
console.log("%c[XCleaner]", "color:#1d9bf0;font-weight:bold", ...a);
const txt = (el) => (el?.innerText || el?.textContent || "").trim();
function q(sel, root = document) {
return root.querySelector(sel);
}
function qa(sel, root = document) {
return [...root.querySelectorAll(sel)];
}
function visible(el) {
if (!el) return false;
const r = el.getBoundingClientRect();
const s = getComputedStyle(el);
return r.width > 0 && r.height > 0 && s.display !== "none" && s.visibility !== "hidden";
}
async function waitFor(selector, timeout = 5000) {
const start = Date.now();
while (!stopped && Date.now() - start < timeout) {
const el = q(selector);
if (visible(el)) return el;
await sleep(150);
}
return null;
}
async function click(el, label = "element") {
if (!el || stopped) return false;
el.scrollIntoView({ block: "center", inline: "center" });
await sleep(rand(250, 600));
el.click();
log("clicked", label);
await sleep(rand(CFG.delayMin, CFG.delayMax));
return true;
}
async function confirmSheet() {
const confirm =
q('[data-testid="confirmationSheetConfirm"]') ||
qa('[role="button"]').find((b) =>
/delete|unlike|undo repost|repost|remove/i.test(txt(b))
);
if (!confirm) return false;
return click(confirm, "confirm");
}
function getTweets() {
return qa('article[data-testid="tweet"]').filter(visible);
}
async function deleteOnePost(tweet) {
const menu =
q('[data-testid="caret"]', tweet) ||
qa('[role="button"]', tweet).find((b) => txt(b) === "" && visible(b));
if (!menu) return false;
await click(menu, "post menu");
const deleteItem = await waitFor('[role="menuitem"]', 3000);
const deleteButton = qa('[role="menuitem"], [role="button"]').find((b) =>
/^delete$/i.test(txt(b))
);
if (!deleteItem && !deleteButton) {
document.body.click();
return false;
}
await click(deleteButton || deleteItem, "delete menu item");
const ok = await confirmSheet();
if (ok) {
actions++;
log(`deleted post ${actions}/${CFG.maxActionsPerRun}`);
await sleep(CFG.afterActionDelay);
}
return ok;
}
async function undoOneRepost(tweet) {
const unRepost = q('[data-testid="unretweet"]', tweet);
if (!unRepost) return false;
await click(unRepost, "undo repost button");
const confirm =
await waitFor('[data-testid="unretweetConfirm"]', 3000) ||
qa('[role="menuitem"], [role="button"]').find((b) =>
/undo repost/i.test(txt(b))
);
if (!confirm) {
document.body.click();
return false;
}
await click(confirm, "confirm undo repost");
actions++;
log(`removed repost ${actions}/${CFG.maxActionsPerRun}`);
await sleep(CFG.afterActionDelay);
return true;
}
async function unlikeOnePost(tweet) {
const unlike = q('[data-testid="unlike"]', tweet);
if (!unlike) return false;
await click(unlike, "unlike");
actions++;
log(`removed like ${actions}/${CFG.maxActionsPerRun}`);
await sleep(CFG.afterActionDelay);
return true;
}
async function runMode(mode) {
stopped = false;
actions = 0;
log(`started mode: ${mode}`);
log("type XCleaner.stop() to stop");
while (!stopped && actions < CFG.maxActionsPerRun) {
let didAction = false;
const tweets = getTweets();
for (const tweet of tweets) {
if (stopped || actions >= CFG.maxActionsPerRun) break;
if (mode === "delete-posts") {
didAction = await deleteOnePost(tweet);
}
if (mode === "remove-reposts") {
didAction = await undoOneRepost(tweet);
}
if (mode === "remove-likes") {
didAction = await unlikeOnePost(tweet);
}
if (didAction) break;
}
if (!didAction) {
window.scrollBy(0, Math.floor(window.innerHeight * 0.85));
await sleep(CFG.scrollDelay);
}
}
log(`finished or paused. actions this run: ${actions}`);
}
window.XCleaner = {
deletePosts: () => runMode("delete-posts"),
removeReposts: () => runMode("remove-reposts"),
removeLikes: () => runMode("remove-likes"),
stop: () => {
stopped = true;
log("stopping...");
},
};
log("loaded.");
log("Use:");
log("XCleaner.deletePosts() // run on your profile / with_replies pages");
log("XCleaner.removeReposts() // run on your profile page");
log("XCleaner.removeLikes() // run on your /likes page");
log("XCleaner.stop() // stop anytime");
})();You can edit these values near the top of the script:
const CFG = {
delayMin: 1300,
delayMax: 2800,
afterActionDelay: 2500,
scrollDelay: 1800,
maxActionsPerRun: 80,
};Lower delays may be faster, but they increase the risk of rate limits or account challenges. Larger delays are safer for long cleanup sessions.
If nothing happens, make sure you are on the correct page and that the timeline has loaded.
If the script stops after scrolling, X may not have loaded more posts yet. Scroll manually, wait a few seconds, and run the command again.
If X changes its interface, selectors such as data-testid="tweet" or data-testid="unlike" may need updating.
This project is provided as-is for personal account management. You are responsible for any actions taken on your account, including permanent deletion of posts and possible X rate limits or account restrictions.