A console script that mass unfollows accounts on X (Twitter) with no run limit. Paste one JavaScript file into your browser console on your own Following page: it auto-scrolls the list, unfollows at a paced 10-20 seconds, verifies every unfollow actually registered, backs off when X throttles you, and remembers its progress across page reloads. No API, no OAuth, no extension.
X (formerly Twitter) provides no bulk unfollow. The only supported path is clicking Following -> Unfollow -> confirm on one account at a time, inside a list that lazy-loads a few hundred entries per page load and unmounts them again as you scroll. For an account that went through an automated or aggressive following phase, that means several thousand manual interactions across a UI that actively fights sequential work. The usual escape hatch is a third-party "unfollow tool", which solves the problem by taking OAuth write access to your account — handing an unknown server permission to act as you, permanently, for a one-off cleanup.
This project is a single JavaScript file you paste into your own browser console while sitting on your own Following page. It drives exactly the same DOM a human drives: it finds the Following button, clicks it, confirms the dialog, waits, and moves on. There is no API key, no OAuth grant, no browser extension, and no server — nothing leaves the tab, and revoking it means closing the page.
The design problem is not clicking buttons; it is surviving a long run. X virtualizes the follow list, so a snapshot taken at paste time is stale within seconds. X silently ignores unfollow clicks once you exceed an undocumented daily action budget, so a naive script reports thousands of successes while changing nothing. And no realistic list finishes in one sitting. This implementation addresses all three: it rescans the live DOM every round, verifies that each unfollow actually registered before counting it, and persists handled accounts to localStorage so a reload continues instead of restarting.
The run is a single loop with no fixed iteration count. Each round:
- Scan the live DOM for
[data-testid="UserCell"], [data-testid="cellInnerDiv"]and pick the first cell that has a profile link, has a Following-style button, and has not already been handled. The scan reads the DOM fresh every round because the previous round's element references are already stale. - Skip mutuals, if enabled — cells containing "Follows you" (or its Persian equivalent) are recorded as handled-this-session and passed over. This is a session-only mark, so turning the toggle off mid-run re-opens them.
- Act: scroll the button into view, click it, then wait up to 7 s for the confirmation sheet (
[data-testid="confirmationSheetConfirm"], polled every 300 ms) and click it. - Verify: poll the original button for up to 3 s. X flips its label from "Following" to "Follow" only when the unfollow is genuinely accepted. A button that still reads "Following" is a failed unfollow, not a successful one — this is what separates a real run from a script that cheerfully logs 3000 no-ops.
- Pace: sleep a random 10–20 s (configurable), counted down live in the panel, freezing while paused.
- Scroll when the scan finds nothing actionable, then wait for X to render the next slice. Four consecutive scrolls that grow neither the page height nor the cell count end the run.
+---------------------------+
| scan live DOM for the |
+-------->| next unhandled target |
| +-------------+-------------+
| |
| target found?
| |
| no +--------+--------+ yes
| | |
| v v
| +-----------------+ +------------------------+
| | scroll to load | | click Following |
| | more + settle | | confirm the dialog |
| +--------+--------+ +-----------+------------+
| | |
| grew the list? v
| | +------------------------+
| yes +---+---+ no | verify: did the button |
+----------+ | | flip to "Follow"? |
| | | +-----+------------+-----+
| | 4 strikes | |
| | | success failure
| | v | |
| | +-------+ v v
| | | done | wait 10-20 s backoff 1-15 min
| | +-------+ | |
+----------+-----------------------+------------+
Consecutive verification failures are the signal that X has started ignoring the clicks. The run backs off on an escalating ladder rather than hammering a rate-limited endpoint:
| Consecutive failures | Behaviour |
|---|---|
| 1–2 | Normal 10–20 s pacing, keep going |
| 3 | Cool down 1 minute |
| 4 | Cool down 2 minutes |
| 5 | Cool down 4 minutes |
| 6 | Cool down 8 minutes |
| 7 | Cool down 15 minutes (cap) |
| 8 | Stop the run |
A single success resets the counter to zero.
Every account the script attempts is written to localStorage under x-mass-unfollow:processed. On the next paste it reloads that set and skips those accounts, so clearing a large list becomes: run until throttled, reload the page, paste again, repeat over several days.
Mutuals that were merely skipped are deliberately not persisted — otherwise flipping "Skip mutuals" off later could never reach them.
| Item | Supported |
|---|---|
| Page | https://x.com/<you>/following (also twitter.com) |
| Browsers | Desktop Chrome, Edge, Firefox, Safari (any console with localStorage) |
| Mutual detection | English (follows you) and Persian (شما را دنبال میکند) |
| Button detection | following, unfollow, and the Persian دنبال میکنید |
| Run limit | None — runs until the list is exhausted or you stop it |
| Skipped automatically | Cells with no profile link (promoted content, separators) |
| Not supported | Mobile browsers, follower-side pruning, blocking, allow-lists |
- Open
https://x.com/<yourusername>/following. - Open the developer console.
- Paste the entire contents of
unfollow.js. - Press Enter, then leave the tab open and awake. The script scrolls and loads more profiles on its own.
Chrome / Edge
F12, orCtrl+Shift+J(Windows/Linux), orCmd+Option+J(macOS)- First paste into the console may require typing
allow pastingand pressing Enter
Firefox
F12, orCtrl+Shift+K(Windows/Linux), orCmd+Option+K(macOS)- Firefox asks you to type
allow pastingbefore the first paste
Safari
- Enable Settings -> Advanced -> Show features for web developers
- Open the console with
Cmd+Option+C
Building from the TypeScript source instead
npm install
npm run build # tsc -> dist/unfollow.js
npm test # jest, 35 tests
npm run lint # tsc --noEmitsrc/unfollow.ts is the typed, unit-tested source. It only auto-runs when location.hostname matches x.com or twitter.com, so importing it in tests or a bundler never starts a run. unfollow.js at the repo root is the standalone console build and runs immediately on paste.
A floating panel appears in the top-right corner. It is the whole interface — there is nothing to configure before pasting.
| Control | Effect |
|---|---|
| Min / Max Delay (sec) | Pacing window, applied to the next wait. Defaults to 10 and 20 |
| Skip mutuals | On by default: accounts that follow you back are left alone. Uncheck to clear everyone, including mutuals |
| Pause / Resume | Freezes the countdown where it stands; the remaining wait is not consumed while paused |
| Stop | Ends the run after the current account and prints the report |
| Close (x) | Stops the run and removes the panel |
Live statistics: Unfollowed, Skipped, Per Hour (measured, not projected), and elapsed time. The console prints a colour-coded line per account and a console.table report at the end.
The script does not fight X's daily limit — it detects it and stops wasting clicks. For a multi-thousand list the workflow is:
paste --> run until throttled --> reload page --> paste again --> repeat
Handled accounts are remembered between rounds, so each session starts where the last one ended.
| Following count | Tab time at default pacing | Realistic plan |
|---|---|---|
| 500 | ~2.5 hours | One sitting |
| 1,000 | ~5 hours | One or two sittings |
| 3,000 | ~14 hours | Several days |
| 5,000 | ~24 hours | About a week |
Throughput is roughly 200 unfollows per hour at the default 10–20 s window: a 15 s average wait plus 1–4 s of clicking, confirming and verifying per account, before any throttling. X's own daily action budget is undocumented, varies with account age and standing, and will usually bite well before those totals are reached in a single day.
To forget saved progress and re-walk the entire list:
localStorage.removeItem('x-mass-unfollow:processed')unfollow.js standalone console build, paste-ready, runs on load
src/unfollow.ts typed source, same logic, guarded auto-run
__tests__/unfollow.test.ts 35 jest tests (jsdom)
jest.config.js ts-jest + jsdom, coverage from src/
tsconfig.json ES2020, strict, outDir dist/
| Function | Purpose |
|---|---|
sleep(ms) |
Promise-based delay |
randomDelay(min?, max?) |
Random pacing value; tolerates swapped bounds |
txt(el) |
Normalised lowercase text of an element |
isMutual(cell) |
Detects "follows you" in either supported language |
getUsername(cell) |
Extracts the handle from the cell's profile link, ignoring status and intent links |
findUnfollowButton(cell) |
Locates the Following/Unfollow control |
formatElapsed(ms) |
HH:MM:SS for the panel |
claim(cell, username, persist?) |
Marks an account handled; persist: false keeps it session-only |
isClaimed(cell, username) |
Duplicate guard, by handle or by element identity |
loadMemory() / saveMemory() |
Read/write the localStorage progress set |
| Constant | Default | Meaning |
|---|---|---|
MIN_DELAY / MAX_DELAY |
10000 / 20000 ms | Pacing window, live-editable in the panel |
SCROLL_PAUSE |
2500 ms | Settle time after a scroll, twice per scroll attempt |
MAX_EMPTY_SCANS |
4 | Fruitless scroll attempts before declaring the list exhausted |
FAILURE_COOLDOWN_AFTER |
3 | Consecutive failures before the backoff ladder starts |
MAX_CONSECUTIVE_FAILURES |
8 | Consecutive failures before the run gives up |
STORE_KEY |
x-mass-unfollow:processed |
localStorage key holding a JSON array of handles |
Accounts are tracked by handle in a Set. Cells whose handle cannot be resolved fall back to a WeakSet keyed on the element itself, which avoids both re-processing and the memory leak of holding unmounted nodes. An account is claimed before the click is attempted, so a hang or an exception can never trap the loop on the same cell.
npm test35 jsdom tests cover the pure helpers: text normalisation, mutual detection, username extraction across status/intent/plain links, button discovery in multi-button cells, the pacing window including swapped bounds, elapsed formatting, and the claim/persistence rules — including the guarantee that a skipped mutual is not written to localStorage.
- Selector resilience: X ships DOM changes without notice, and
data-testidvalues are the only stable-ish anchor. A self-check that aborts loudly when the expected structure is missing would beat silent no-ops. - Allow-list support, so specific handles survive a full clear.
- Follower-side pruning (removing followers) using the same paced, verified loop.
- Optional export of the session log as CSV for record-keeping.
- Mobile-browser console support, which currently has no practical entry point.
This script automates the public web interface of a site you are logged into, on your own account, using your own session. It is not affiliated with X Corp. and it is not a supported integration.
Known limitations, stated plainly:
- The tab must stay open and awake. Backgrounded tabs get their timers throttled by the browser; a sleeping laptop stops the run.
- UI changes break selectors. When X restructures the follow list, detection fails and the run ends early rather than doing damage.
- Mutual detection is language-dependent. Only English and Persian strings are recognised. On any other interface language, "Skip mutuals" will not reliably protect mutuals.
- Unfollowing at volume is visible behaviour. Automation of this kind may violate X's terms of service, and aggressive use can attract rate limits or account action. The pacing and backoff exist to keep the run conservative, not to hide it.
Use it on your own account, at your own risk.
MIT. Original project and copyright: Shayan Taherkhani (shayantaherkhani.ir, @shayanthn) — see LICENSE.
This fork rewrites the execution model: the fixed 190-per-run cap is replaced by an unbounded auto-scrolling loop, pacing moves to 10–20 s, unfollows are verified rather than assumed, throttling triggers an escalating backoff, and progress persists across page reloads.