📝 Smart Unfollow Non-Followers on X (Twitter) — With Full Logging
Unfollow accounts that don't follow you back on X/Twitter — with comprehensive logging, CSV + JSON export, whitelist protection, and dry-run preview. Free, no API key, no app install.
Works on: 🌐 Browser Console
Difficulty: 🟢 Beginner
Time: ⏱️ 5–60 minutes (depends on following count)
Requirements: A web browser logged into x.com
📖 For the quick-reference version, see smart-unfollow.md
🎯 Real-World Scenario
You followed 2,000+ accounts over the past year while growing your X presence — competitors, niche influencers, potential collaborators. Now your following/follower ratio is terrible (following 2,100, followers 800), which makes your profile look spammy. You need to clean up — but you want to know exactly who you unfollowed and why, because some of those accounts might actually be valuable contacts you don't want to lose.
XActions' Smart Unfollow scans your entire following list, identifies accounts that don't follow you back, and unfollows them — while logging every single action with username, display name, bio, follower count, and timestamp. At the end, it automatically downloads both JSON and CSV files so you have a complete audit trail. You can whitelist accounts you want to keep, and preview everything in dry-run mode first.
Before XActions:
┌──────────────────────────────────────────────────────────────┐
│ Cleaning Your Following List (Manual) │
├──────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Go to your following list │
│ Step 2: For each person, check if they follow you back │
│ Step 3: If not, click ••• → Unfollow │
│ Step 4: Confirm the unfollow dialog │
│ Step 5: Write down who you unfollowed (maybe?) │
│ Step 6: Repeat 1,300 more times... │
│ │
│ Time estimate: 6+ hours │
│ Will you actually finish? No. │
│ Will you track who you unfollowed? Definitely not. │
└──────────────────────────────────────────────────────────────┘
After XActions Smart Unfollow:
┌──────────────────────────────────────────────────────────────┐
│ Cleaning Your Following List (XActions) │
├──────────────────────────────────────────────────────────────┤
│ │
│ Step 1: Go to x.com/YOUR_USERNAME/following │
│ Step 2: Paste script with dryRun: true │
│ Step 3: Review the preview — see exactly who'd be removed │
│ Step 4: Re-run with dryRun: false to execute │
│ Step 5: JSON + CSV files auto-download with full details │
│ │
│ Time: ~15 min for 1,000 accounts │
│ Full audit trail: ✅ (username, bio, followers, timestamp) │
│ Whitelist protected: ✅ │
└──────────────────────────────────────────────────────────────┘
📋 What This Does (Step by Step)
- 📜 Scans your following list — reads each user cell on the page
- 🔍 Checks for "Follows you" badge — identifies who follows you back
- 🛡️ Checks whitelist — skips accounts you've protected
- 📊 Extracts rich data — captures username, display name, bio, follower count for the log
- 🔙 Unfollows non-followers — clicks the Unfollow button with confirmation
- ⏱️ Waits between actions — randomized 1.5–4s Gaussian delays (human-like)
- 📜 Scrolls for more — loads additional users by scrolling
- 🚨 Handles rate limits — auto-pauses for 60s if X blocks you
- 💾 Persists progress — saves to
localStorageso you can resume - 📥 Exports results — auto-downloads JSON and CSV when done
┌──────────────────────────────────────────────────────────────┐
│ │
│ [Navigate to x.com/YOU/following] │
│ │ │
│ ▼ │
│ [For each UserCell on page] │
│ │ │
│ ▼ │
│ [Already processed?] ──Yes──→ [Skip] │
│ │ No │
│ ▼ │
│ [Has "Follows you" badge?] ──Yes──→ [Skip (mutual)] │
│ │ No │
│ ▼ │
│ [In whitelist?] ──Yes──→ [Skip (protected)] │
│ │ No │
│ ▼ │
│ [Extract: username, name, bio, followers] │
│ │ │
│ ├── DRY RUN: Log "Would unfollow @user" ──→ next │
│ │ │
│ ├── LIVE: Click Unfollow → Confirm dialog │
│ │ Wait 1.5-4s (Gaussian random) │
│ │ Log action with full details │
│ │ │
│ ▼ │
│ [Rate limited?] ──Yes──→ [Pause 60s] ──→ [Resume] │
│ │ No │
│ ▼ │
│ [Scroll for more users] │
│ │ │
│ ▼ │
│ [All done] → [Print summary] → [Export JSON + CSV] │
│ │
└──────────────────────────────────────────────────────────────┘
🌐 Method 1: Browser Console (Copy-Paste)
Best for: Everyone — no installs, runs right in your browser.
Prerequisites
- Logged into your X/Twitter account in a web browser
- On a desktop/laptop (not mobile)
Step 1: Navigate to your following list
Go to
x.com/YOUR_USERNAME/following— replaceYOUR_USERNAMEwith your actual handle.
┌──────────────────────────────────────────────────────────────┐
│ 🔍 x.com/nichxbt/following │
├──────────────────────────────────────────────────────────────┤
│ │
│ 👤 @elonmusk Following ✓ Follows you ✗ │
│ 👤 @naval Following ✓ Follows you ✗ │
│ 👤 @vitalikbuterin Following ✓ Follows you ✗ │
│ 👤 @yourfriend Following ✓ Follows you ✓ │
│ 👤 @deadaccount Following ✓ Follows you ✗ │
│ 👤 @cryptodev Following ✓ Follows you ✓ │
│ │
│ ... 2,094 more accounts below │
│ │
└──────────────────────────────────────────────────────────────┘
Step 2: Open Developer Console
| OS | Shortcut |
|---|---|
| Windows / Linux | F12 then click Console tab, or Ctrl + Shift + J |
| Mac | Cmd + Option + J |
Step 3: Paste and Run
Copy the entire script below. Important: Start with dryRun: true to preview before executing.
// ============================================
// XActions - Smart Unfollow with Full Log
// by nichxbt — https://xactions.app
// Go to: x.com/YOUR_USERNAME/following
// Open console (F12 → Console), paste, Enter
// ============================================
(() => {
'use strict';
const CONFIG = {
maxUnfollows: Infinity, // Set a number to limit, or Infinity for all
whitelist: [ // Accounts to never unfollow (without @)
// 'elonmusk',
// 'naval',
],
dryRun: true, // ⚠️ Start with true! Set to false to execute
minDelay: 1500, // Min delay between unfollows (ms)
maxDelay: 4000, // Max delay between unfollows (ms)
scrollDelay: 2000,
maxConsecutiveErrors: 8,
maxEmptyScrolls: 6,
rateLimitCooldown: 60000, // 60s cooldown if rate limited
logLevel: 'verbose', // 'verbose' | 'normal' | 'quiet'
exportFormat: 'both', // 'json' | 'csv' | 'both'
};
const SEL = {
unfollowBtn: ['[data-testid$="-unfollow"]', 'button[aria-label*="Following @"]'],
confirmBtn: ['[data-testid="confirmationSheetConfirm"]'],
userCell: ['[data-testid="UserCell"]', '[data-testid="cellInnerDiv"]'],
followsYou: ['[data-testid="userFollowIndicator"]'],
toast: ['[data-testid="toast"]', '[role="alert"]'],
};
const $ = (sel, ctx = document) => {
for (const s of (Array.isArray(sel) ? sel : [sel])) { const e = ctx.querySelector(s); if (e) return e; }
return null;
};
const $$ = (sel, ctx = document) => {
for (const s of (Array.isArray(sel) ? sel : [sel])) { const e = ctx.querySelectorAll(s); if (e.length) return [...e]; }
return [];
};
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const gaussian = (min, max) => Math.floor(min + ((Math.random() + Math.random()) / 2) * (max - min));
const isRateLimited = () => { const t = $(SEL.toast); return t && /rate limit|try again|too many|slow down/i.test(t.textContent); };
// ── State ──
let paused = false, aborted = false;
let unfollowed = 0, scanned = 0, errors = 0, consecutiveErrors = 0;
const startTime = Date.now();
const log = [];
const processedUsers = new Set();
const whitelistSet = new Set(CONFIG.whitelist.map(u => u.toLowerCase().replace(/^@/, '')));
// Restore from localStorage
const STORAGE_KEY = 'xactions_unfollowWDFBLog';
try {
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');
if (saved.processed) saved.processed.forEach(u => processedUsers.add(u));
if (saved.log) saved.log.forEach(entry => log.push(entry));
} catch {}
const persist = () => {
try { localStorage.setItem(STORAGE_KEY, JSON.stringify({ processed: [...processedUsers], log, lastRun: new Date().toISOString() })); } catch {}
};
// ── Controls ──
const showStatus = () => {
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
const rate = unfollowed > 0 ? (unfollowed / (elapsed / 60)).toFixed(1) : '0';
console.log(`📊 Unfollowed: ${unfollowed} | Scanned: ${scanned} | Errors: ${errors} | ${rate}/min | ${elapsed}s`);
};
const exportNow = () => {
if (log.length === 0) { console.log('Nothing to export.'); return; }
const ts = new Date().toISOString().slice(0, 10);
const tag = CONFIG.dryRun ? 'preview' : 'results';
if (CONFIG.exportFormat === 'json' || CONFIG.exportFormat === 'both') {
const data = { summary: { scanned, unfollowed, errors, dryRun: CONFIG.dryRun, exportedAt: new Date().toISOString() }, accounts: log };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `xactions-unfollowlog-${tag}-${ts}.json`;
document.body.appendChild(a); a.click(); a.remove();
}
if (CONFIG.exportFormat === 'csv' || CONFIG.exportFormat === 'both') {
const header = 'username,displayName,bio,followers,following,unfollowedAt\n';
const rows = log.map(r =>
`"${r.username}","${(r.displayName || '').replace(/"/g, '""')}","${(r.bio || '').replace(/"/g, '""').replace(/\n/g, ' ')}",${r.followers || 0},${r.following || 0},"${r.timestamp}"`
).join('\n');
const blob = new Blob([header + rows], { type: 'text/csv' });
const a = document.createElement('a'); a.href = URL.createObjectURL(blob); a.download = `xactions-unfollowlog-${tag}-${ts}.csv`;
document.body.appendChild(a); a.click(); a.remove();
}
console.log('📥 Exported.');
};
window.XActions = {
pause() { paused = true; console.log('⏸️ Paused.'); },
resume() { paused = false; console.log('▶️ Resumed.'); },
abort() { aborted = true; console.log('🛑 Aborting...'); },
status: showStatus,
export: exportNow,
};
const shouldContinue = async () => { while (paused && !aborted) await sleep(500); return !aborted; };
// ── Extract Rich User Data ──
const extractUserData = (cell) => {
const data = { username: null, displayName: null, bio: null, followers: 0, following: 0 };
const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
if (link) {
const match = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
if (match && !['home','explore','notifications','messages','i'].includes(match[1])) data.username = match[1];
}
if (!data.username) {
const spans = cell.querySelectorAll('span');
for (const s of spans) { const m = s.textContent.match(/^@([A-Za-z0-9_]+)$/); if (m) { data.username = m[1]; break; } }
}
const nameSpans = cell.querySelectorAll('a[href^="/"] span');
if (nameSpans.length > 0) data.displayName = nameSpans[0].textContent.trim();
const bioEl = cell.querySelector('[dir="auto"]:not(a [dir="auto"])');
if (bioEl && bioEl.textContent.length > 5) data.bio = bioEl.textContent.trim().slice(0, 300);
return data;
};
// ── Main ──
const run = async () => {
const W = 60;
console.log('╔' + '═'.repeat(W) + '╗');
console.log('║ 📝 UNFOLLOW NON-FOLLOWERS WITH LOG' + ' '.repeat(W - 37) + '║');
console.log('║ by nichxbt — v2.0' + ' '.repeat(W - 21) + '║');
console.log('╚' + '═'.repeat(W) + '╝');
if (!window.location.href.includes('/following')) {
console.error('❌ Navigate to x.com/YOUR_USERNAME/following first!');
return;
}
console.log(`\n⚙️ Dry run: ${CONFIG.dryRun ? 'YES' : 'NO'} | Whitelist: ${whitelistSet.size} | Export: ${CONFIG.exportFormat}`);
if (CONFIG.dryRun) console.log(' ⚠️ Preview only. Set dryRun=false to execute.\n');
let emptyScrolls = 0;
while (unfollowed < CONFIG.maxUnfollows && emptyScrolls < CONFIG.maxEmptyScrolls) {
if (!(await shouldContinue())) break;
if (isRateLimited()) { console.warn(`🚨 Rate limit! Cooling down ${CONFIG.rateLimitCooldown / 1000}s...`); await sleep(CONFIG.rateLimitCooldown); continue; }
const cells = $$(SEL.userCell);
let foundNew = false;
for (const cell of cells) {
if (!(await shouldContinue())) break;
if (unfollowed >= CONFIG.maxUnfollows) break;
const userData = extractUserData(cell);
if (!userData.username) continue;
const uLower = userData.username.toLowerCase();
if (processedUsers.has(uLower)) continue;
processedUsers.add(uLower);
foundNew = true;
scanned++;
if ($(SEL.followsYou, cell)) continue;
if (whitelistSet.has(uLower)) { if (CONFIG.logLevel === 'verbose') console.log(` 🛡️ Whitelisted: @${userData.username}`); continue; }
const entry = { ...userData, timestamp: new Date().toISOString(), dryRun: CONFIG.dryRun };
if (CONFIG.dryRun) {
if (CONFIG.logLevel !== 'quiet') console.log(` 🔍 Would unfollow: @${userData.username}${userData.displayName ? ` (${userData.displayName})` : ''}${userData.bio ? ` — "${userData.bio.slice(0, 60)}..."` : ''}`);
log.push(entry);
unfollowed++;
continue;
}
const btn = $(SEL.unfollowBtn, cell);
if (!btn) { errors++; consecutiveErrors++; continue; }
try {
btn.click();
await sleep(gaussian(400, 800));
const confirm = $(SEL.confirmBtn);
if (confirm) { confirm.click(); await sleep(gaussian(300, 600)); }
unfollowed++;
consecutiveErrors = 0;
log.push(entry);
if (CONFIG.logLevel === 'verbose' || unfollowed % 5 === 0) console.log(` 🔙 #${unfollowed} @${userData.username}${userData.displayName ? ` (${userData.displayName})` : ''}`);
persist();
await sleep(gaussian(CONFIG.minDelay, CONFIG.maxDelay));
} catch (e) {
errors++;
consecutiveErrors++;
if (consecutiveErrors >= CONFIG.maxConsecutiveErrors) { console.error(`❌ ${CONFIG.maxConsecutiveErrors} consecutive errors — aborting.`); break; }
}
}
if (consecutiveErrors >= CONFIG.maxConsecutiveErrors) break;
if (!foundNew) emptyScrolls++; else emptyScrolls = 0;
window.scrollTo(0, document.body.scrollHeight);
await sleep(gaussian(CONFIG.scrollDelay, CONFIG.scrollDelay + 1000));
}
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
console.log('\n╔' + '═'.repeat(52) + '╗');
console.log('║ 📊 FINAL RESULTS' + ' '.repeat(34) + '║');
console.log('╠' + '═'.repeat(52) + '╣');
console.log(`║ Scanned: ${String(scanned).padEnd(33)}║`);
console.log(`║ Unfollowed: ${String(unfollowed).padEnd(33)}║`);
console.log(`║ Errors: ${String(errors).padEnd(33)}║`);
console.log(`║ Duration: ${(elapsed + 's').padEnd(33)}║`);
console.log(`║ Mode: ${(CONFIG.dryRun ? 'DRY RUN' : 'LIVE').padEnd(33)}║`);
console.log('╚' + '═'.repeat(52) + '╝');
persist();
if (log.length > 0) exportNow();
};
run();
})();
Step 4: Review the dry-run output
╔════════════════════════════════════════════════════════════════╗
║ 📝 UNFOLLOW NON-FOLLOWERS WITH LOG ║
║ by nichxbt — v2.0 ║
╚════════════════════════════════════════════════════════════════╝
⚙️ Dry run: YES | Whitelist: 0 | Export: both
⚠️ Preview only. Set dryRun=false to execute.
🔍 Would unfollow: @deadaccount123 (Dead Account) — "inactive since 2020..."
🔍 Would unfollow: @cryptospammer (Crypto Guy) — "follow for follow..."
🛡️ Whitelisted: @naval
🔍 Would unfollow: @randomuser (Random) — "just vibes..."
...
╔════════════════════════════════════════════════════════════════╗
║ 📊 FINAL RESULTS ║
╠════════════════════════════════════════════════════════════════╣
║ Scanned: 847 ║
║ Unfollowed: 612 ║
║ Errors: 0 ║
║ Duration: 45s ║
║ Mode: DRY RUN ║
╚════════════════════════════════════════════════════════════════╝
📥 Exported.
Step 5: Execute for real
Once you're satisfied with the preview, change dryRun: false and run again. The script will actually unfollow and download full logs.
⚙️ Configuration Reference
| Setting | Default | Description |
|---|---|---|
maxUnfollows |
Infinity |
Maximum accounts to unfollow (set a number to limit) |
whitelist |
[] |
Usernames to never unfollow (without @) |
dryRun |
true |
Preview mode — no actions taken until set to false |
minDelay |
1500 |
Minimum delay between unfollows (ms) |
maxDelay |
4000 |
Maximum delay between unfollows (ms) |
scrollDelay |
2000 |
Delay between scroll-to-bottom actions (ms) |
maxConsecutiveErrors |
8 |
Abort after this many errors in a row |
maxEmptyScrolls |
6 |
Stop if no new users found after this many scrolls |
rateLimitCooldown |
60000 |
Pause duration when rate limited (ms) |
logLevel |
'verbose' |
'verbose', 'normal', or 'quiet' |
exportFormat |
'both' |
'json', 'csv', or 'both' |
Runtime Controls
| Command | Action |
|---|---|
XActions.pause() |
Pause the script |
XActions.resume() |
Resume after pause |
XActions.abort() |
Stop immediately |
XActions.status() |
Show progress stats |
XActions.export() |
Force export now |
📥 Export Format
JSON Output
{
"summary": {
"scanned": 847,
"unfollowed": 612,
"errors": 0,
"dryRun": false,
"exportedAt": "2026-02-24T14:30:00.000Z"
},
"accounts": [
{
"username": "deadaccount123",
"displayName": "Dead Account",
"bio": "Haven't posted since 2020",
"followers": 12,
"following": 890,
"timestamp": "2026-02-24T14:15:00.000Z",
"dryRun": false
}
]
}
CSV Output
username,displayName,bio,followers,following,unfollowedAt
"deadaccount123","Dead Account","Haven't posted since 2020",12,890,"2026-02-24T14:15:00.000Z"
"cryptospammer","Crypto Guy","follow for follow DM me",3,9400,"2026-02-24T14:15:04.000Z"
💡 Pro Tips
- Always start with dry run.
dryRun: trueshows exactly who would be unfollowed. Review the CSV export, then run for real. - Whitelist important accounts. Add brand partners, friends, or key influencers to the
whitelistarray even if they don't follow back. - Use the CSV in a spreadsheet. Import the CSV into Google Sheets or Excel to sort by follower count — you might spot accounts you actually want to keep.
- Run in batches. Set
maxUnfollows: 200and run multiple sessions over several days instead of unfollowing 1,000+ at once. - Check the exported JSON for patterns. Look at the bios of unfollowed accounts — if you see valuable contacts, add them to the whitelist and re-follow manually.
- Pair with the Follow Target Followers script. Unfollow non-followers → follow new niche accounts → repeat weekly. This is the classic growth loop.
⚠️ Important Warnings
Rate limits: X limits unfollow actions. The script uses Gaussian-distributed delays (1.5–4s) and auto-pauses on rate limit detection, but unfollowing 500+ accounts in one session may trigger a temporary block.
Recovery: Progress is saved to
localStorage. If the page crashes, your previously processed users won't be re-processed. To clear saved state:localStorage.removeItem('xactions_unfollowWDFBLog')
"Follows you" detection: The script looks for the
[data-testid="userFollowIndicator"]badge. If X changes this selector, mutuals could be accidentally unfollowed. Always preview with dry run first.
🔗 Related Features
- Unfollow Everyone Tutorial — Nuclear option: unfollow ALL accounts
- Unfollow Non-Followers Tutorial — Simpler version without detailed logging
- Follow Target Followers Tutorial — Build your audience back up after cleaning
- Detect Unfollowers Tutorial — Find out who unfollowed you
❓ FAQ
What's the difference between this and "Unfollow Non-Followers"?
This script (unfollowWDFBLog.js) is the production-grade version with comprehensive logging (username, display name, bio, followers, timestamp), CSV + JSON export, whitelist protection, pause/resume controls, resumable state, and rate limit handling. The simpler version just unfollows without detailed logs.
Will this unfollow accounts I'm mutuals with?
No. The script checks for the "Follows you" indicator on each user cell. If someone follows you back, they're skipped automatically.
Can I undo an unfollow?
Not through the script, but you can use the exported CSV/JSON to see exactly who was unfollowed and manually re-follow them. This is why the detailed logging exists.
How long does it take to process 2,000 accounts?
With default delays (1.5–4s per unfollow), processing 2,000 accounts takes roughly 30–90 minutes depending on how many are non-followers. Scanning is fast — the delay is between actual unfollow clicks.
What happens if X rate-limits me?
The script detects rate limit toasts and automatically pauses for 60 seconds. It will resume automatically after the cooldown.
Can I resume if my browser crashes?
Yes. The script saves progress to localStorage. When you re-paste and run the script, it skips previously processed users automatically.
Built with ❤️ by @nichxbt
xactions.app · GitHub
⚡ Ready to try Smart Unfollow Non-Followers on X (Twitter) — With Full Logging?
XActions is 100% free and open-source. No API keys, no fees, no signup.
Browse All Scripts