🚫 Unfollow Non-Followers
Find and unfollow people who don't follow you back on X (Twitter).
📋 What It Does
This feature helps you clean up your following list by:
- Scanning your following list - Goes through everyone you follow
- Identifying non-followers - Checks who doesn't have the "Follows you" badge
- Unfollowing automatically - Removes them from your following list with confirmation
- Rate limit protection - Includes delays to keep your account safe
Use cases:
- Clean up your following/follower ratio
- Remove accounts that unfollowed you
- Maintain a more engaged network
- Free up your timeline from one-sided follows
🌐 Example 1: Browser Console (Quick)
Best for: Unfollowing up to ~50 non-followers quickly
Steps:
- Go to
x.com/YOUR_USERNAME/following - Open browser console (F12 → Console tab)
- Paste the script below and press Enter
// ============================================
// XActions - Unfollow Non-Followers (Browser Console)
// Author: nich (@nichxbt)
// Go to: x.com/YOUR_USERNAME/following
// Open console (F12), paste this
// ============================================
(async () => {
// Configuration
const MAX_UNFOLLOWS = 50; // Maximum people to unfollow per run
const SCROLL_DELAY = 2000; // Time between scrolls (ms)
const UNFOLLOW_DELAY = 3000; // Time between unfollows (ms)
const BATCH_PAUSE = 15000; // Pause every 10 unfollows (ms)
const MAX_SCROLL_RETRIES = 10; // Stop if no new users found
console.log('🔍 XActions - Unfollow Non-Followers');
console.log('====================================');
console.log(`⚙️ Max unfollows: ${MAX_UNFOLLOWS}`);
console.log(`⚙️ Unfollow delay: ${UNFOLLOW_DELAY}ms`);
console.log('');
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Step 1: Collect all users you're following
console.log('📜 Step 1: Scanning your following list...');
const users = new Map();
let scrollRetries = 0;
while (scrollRetries < MAX_SCROLL_RETRIES) {
// Find all user cells
const cells = document.querySelectorAll('[data-testid="UserCell"]');
const prevSize = users.size;
cells.forEach(cell => {
try {
// Get username from link
const link = cell.querySelector('a[href^="/"]');
const href = link?.getAttribute('href') || '';
const username = href.split('/')[1];
if (!username || username.includes('?')) return;
// Check for "Follows you" badge
const followsYou = cell.querySelector('[data-testid="userFollowIndicator"]') !== null;
// Get display name
const nameEl = cell.querySelector('[dir="ltr"] > span');
const name = nameEl?.textContent?.trim() || username;
// Get the unfollow button for this user
const unfollowBtn = cell.querySelector('[data-testid$="-unfollow"]');
if (!users.has(username)) {
users.set(username, {
username,
name,
followsYou,
element: unfollowBtn,
cell
});
}
} catch (e) {
// Skip malformed cells
}
});
// Progress update
const nonFollowers = Array.from(users.values()).filter(u => !u.followsYou);
console.log(` 📊 Scanned: ${users.size} users (${nonFollowers.length} don't follow back)`);
// Check if we found new users
if (users.size === prevSize) {
scrollRetries++;
} else {
scrollRetries = 0;
}
// Scroll to load more
window.scrollTo(0, document.body.scrollHeight);
await sleep(SCROLL_DELAY);
}
// Step 2: Filter non-followers
console.log('');
console.log('🔎 Step 2: Identifying non-followers...');
const allUsers = Array.from(users.values());
const nonFollowers = allUsers.filter(u => !u.followsYou);
const followers = allUsers.filter(u => u.followsYou);
console.log(` ✅ Total following: ${allUsers.length}`);
console.log(` ✅ Follow you back: ${followers.length}`);
console.log(` ❌ Don't follow back: ${nonFollowers.length}`);
if (nonFollowers.length === 0) {
console.log('');
console.log('🎉 Great news! Everyone you follow also follows you back!');
return { unfollowed: [], nonFollowers: [] };
}
// Show list of non-followers
console.log('');
console.log('📋 Non-followers found:');
nonFollowers.slice(0, 20).forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.name})`);
});
if (nonFollowers.length > 20) {
console.log(` ... and ${nonFollowers.length - 20} more`);
}
// Step 3: Export list before unfollowing
console.log('');
console.log('💾 Step 3: Exporting non-followers list...');
const exportData = nonFollowers.map(u => ({
username: u.username,
name: u.name,
followsYou: false
}));
const json = JSON.stringify(exportData, null, 2);
const blob = new Blob([json], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `non-followers-${new Date().toISOString().split('T')[0]}.json`;
a.click();
console.log(' 📥 List downloaded! Check your downloads folder.');
await navigator.clipboard.writeText(json);
console.log(' 📋 Also copied to clipboard!');
// Step 4: Unfollow non-followers
console.log('');
console.log(`🚫 Step 4: Unfollowing (max ${MAX_UNFOLLOWS})...`);
console.log(' ⚠️ Press Ctrl+C in console to stop at any time');
console.log('');
const toUnfollow = nonFollowers.slice(0, MAX_UNFOLLOWS);
const unfollowed = [];
const failed = [];
for (let i = 0; i < toUnfollow.length; i++) {
const user = toUnfollow[i];
try {
// Scroll the user into view
user.cell.scrollIntoView({ behavior: 'smooth', block: 'center' });
await sleep(500);
// Find and click the unfollow button
const unfollowBtn = user.cell.querySelector('[data-testid$="-unfollow"]');
if (!unfollowBtn) {
console.log(` ⏭️ @${user.username} - Button not found, skipping`);
failed.push({ username: user.username, reason: 'Button not found' });
continue;
}
// Click unfollow
unfollowBtn.click();
await sleep(500);
// Click confirmation dialog
const confirmBtn = document.querySelector('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
confirmBtn.click();
unfollowed.push(user.username);
console.log(` ✅ ${i + 1}/${toUnfollow.length} - Unfollowed @${user.username}`);
} else {
// Sometimes confirmation doesn't appear, try alternative
failed.push({ username: user.username, reason: 'Confirm dialog not found' });
console.log(` ⚠️ ${i + 1}/${toUnfollow.length} - @${user.username} - No confirm dialog`);
}
// Wait between unfollows (rate limit protection)
await sleep(UNFOLLOW_DELAY + Math.random() * 1000);
// Pause every 10 unfollows for safety
if ((i + 1) % 10 === 0 && i < toUnfollow.length - 1) {
console.log(` ⏸️ Pausing for ${BATCH_PAUSE / 1000}s (safety break)...`);
await sleep(BATCH_PAUSE);
}
} catch (error) {
console.log(` ❌ Error with @${user.username}: ${error.message}`);
failed.push({ username: user.username, reason: error.message });
}
}
// Summary
console.log('');
console.log('====================================');
console.log('✅ COMPLETE!');
console.log('====================================');
console.log(`📊 Total non-followers found: ${nonFollowers.length}`);
console.log(`✅ Successfully unfollowed: ${unfollowed.length}`);
console.log(`❌ Failed: ${failed.length}`);
console.log('');
if (nonFollowers.length > MAX_UNFOLLOWS) {
console.log(`💡 There are ${nonFollowers.length - MAX_UNFOLLOWS} more non-followers.`);
console.log(' Wait 15-30 minutes, then run the script again!');
}
return {
unfollowed,
failed,
remaining: nonFollowers.length - unfollowed.length
};
})();
What happens:
- Script scrolls through your following list
- Identifies accounts without "Follows you" badge
- Downloads the non-followers list as JSON
- Asks for confirmation before unfollowing
- Unfollows with safe delays between each action
- Shows progress and final summary
Safety features:
- ✅ 3 second delay between unfollows
- ✅ 15 second pause every 10 unfollows
- ✅ Exports list before any changes
- ✅ Maximum 50 unfollows per run
🤖 Example 2: Node.js with Puppeteer (Production-Ready)
Best for: Large following lists, automation, scheduling, batch operations
// ============================================
// XActions - Unfollow Non-Followers (Node.js)
// Author: nich (@nichxbt)
// Save as: unfollow-non-followers.js
// Run: node unfollow-non-followers.js
// ============================================
import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import fs from 'fs/promises';
import readline from 'readline';
puppeteer.use(StealthPlugin());
// ============================================
// Configuration
// ============================================
const CONFIG = {
// Your X username (without @)
username: 'YOUR_USERNAME',
// Your auth_token cookie from X
// Get this from: Browser DevTools → Application → Cookies → x.com → auth_token
authToken: 'YOUR_AUTH_TOKEN_HERE',
// Maximum users to scan in each list
maxScan: 2000,
// Maximum users to unfollow per run
maxUnfollows: 50,
// Delay between unfollows (ms)
unfollowDelay: 4000,
// Pause every N unfollows (ms)
batchPause: 20000,
batchSize: 10,
// Run in headless mode
headless: true,
// Auto-unfollow without asking (⚠️ be careful!)
autoUnfollow: false,
};
// ============================================
// Helper Functions
// ============================================
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
const randomDelay = (min, max) => sleep(min + Math.random() * (max - min));
async function askConfirmation(question) {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
return new Promise(resolve => {
rl.question(question, answer => {
rl.close();
resolve(answer.toLowerCase().trim());
});
});
}
// ============================================
// Scrape Users from a List Page
// ============================================
async function scrapeUserList(page, url, maxUsers) {
await page.goto(url, { waitUntil: 'networkidle2', timeout: 30000 });
try {
await page.waitForSelector('[data-testid="UserCell"]', { timeout: 10000 });
} catch {
console.log(' ⚠️ No users found or page didn\'t load');
return [];
}
await sleep(1500);
const users = new Map();
let retries = 0;
const maxRetries = 15;
while (users.size < maxUsers && retries < maxRetries) {
const extracted = await page.evaluate(() => {
const cells = document.querySelectorAll('[data-testid="UserCell"]');
return Array.from(cells).map(cell => {
try {
const link = cell.querySelector('a[href^="/"]');
const href = link?.getAttribute('href') || '';
const username = href.split('/')[1];
if (!username || username.includes('?')) return null;
const nameEl = cell.querySelector('[dir="ltr"] > span');
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const followsYouEl = cell.querySelector('[data-testid="userFollowIndicator"]');
return {
username,
name: nameEl?.textContent?.trim() || null,
bio: bioEl?.textContent?.trim() || null,
followsYou: followsYouEl !== null,
};
} catch {
return null;
}
}).filter(Boolean);
});
const prevSize = users.size;
extracted.forEach(user => {
if (!users.has(user.username)) {
users.set(user.username, user);
}
});
if (users.size === prevSize) {
retries++;
} else {
retries = 0;
}
process.stdout.write(`\r 📊 Scraped: ${users.size} users`);
await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight));
await randomDelay(1500, 2500);
}
console.log('');
return Array.from(users.values());
}
// ============================================
// Unfollow a Single User
// ============================================
async function unfollowUser(page, username) {
try {
await page.goto(`https://x.com/${username}`, {
waitUntil: 'networkidle2',
timeout: 20000
});
await sleep(1000 + Math.random() * 1000);
// Find the unfollow/following button
const followingBtn = await page.$('[data-testid$="-unfollow"]');
if (!followingBtn) {
return { success: false, error: 'Follow button not found' };
}
// Click the following button
await followingBtn.click();
await sleep(500);
// Click confirm in the dialog
const confirmBtn = await page.$('[data-testid="confirmationSheetConfirm"]');
if (confirmBtn) {
await confirmBtn.click();
await sleep(500);
return { success: true };
} else {
return { success: false, error: 'Confirm dialog not found' };
}
} catch (error) {
return { success: false, error: error.message };
}
}
// ============================================
// Main Function
// ============================================
async function unfollowNonFollowers() {
console.log('');
console.log('🔍 XActions - Unfollow Non-Followers');
console.log('====================================');
console.log(`👤 Username: @${CONFIG.username}`);
console.log(`📊 Max scan: ${CONFIG.maxScan}`);
console.log(`🚫 Max unfollows: ${CONFIG.maxUnfollows}`);
console.log('');
// Validate config
if (CONFIG.username === 'YOUR_USERNAME' || CONFIG.authToken === 'YOUR_AUTH_TOKEN_HERE') {
console.error('❌ Error: Please update CONFIG with your username and auth_token');
console.log('');
console.log('To get your auth_token:');
console.log('1. Go to x.com in your browser');
console.log('2. Press F12 → Application tab → Cookies → x.com');
console.log('3. Find "auth_token" and copy its value');
process.exit(1);
}
// Launch browser
console.log('🚀 Launching browser...');
const browser = await puppeteer.launch({
headless: CONFIG.headless ? 'new' : false,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-blink-features=AutomationControlled',
],
});
try {
const page = await browser.newPage();
// Set viewport and user agent
await page.setViewport({
width: 1280 + Math.floor(Math.random() * 100),
height: 900 + Math.floor(Math.random() * 100),
});
await page.setUserAgent(
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
);
// Set auth cookie
console.log('🔑 Setting authentication...');
await page.setCookie({
name: 'auth_token',
value: CONFIG.authToken,
domain: '.x.com',
path: '/',
httpOnly: true,
secure: true,
});
// Test authentication
await page.goto('https://x.com/home', { waitUntil: 'networkidle2' });
await sleep(2000);
const isLoggedIn = await page.evaluate(() => {
return document.querySelector('[data-testid="SideNav_AccountSwitcher_Button"]') !== null;
});
if (!isLoggedIn) {
throw new Error('Authentication failed. Please check your auth_token.');
}
console.log('✅ Logged in successfully!');
console.log('');
// Step 1: Scrape following list
console.log('📜 Step 1: Fetching your following list...');
const following = await scrapeUserList(
page,
`https://x.com/${CONFIG.username}/following`,
CONFIG.maxScan
);
console.log(` ✅ Found ${following.length} accounts you follow`);
console.log('');
// Step 2: Scrape followers list
console.log('📜 Step 2: Fetching your followers list...');
const followers = await scrapeUserList(
page,
`https://x.com/${CONFIG.username}/followers`,
CONFIG.maxScan
);
console.log(` ✅ Found ${followers.length} followers`);
console.log('');
// Step 3: Find non-followers
console.log('🔎 Step 3: Identifying non-followers...');
const followerUsernames = new Set(followers.map(f => f.username));
const nonFollowers = following.filter(f => !followerUsernames.has(f.username));
console.log(` 📊 Following: ${following.length}`);
console.log(` 📊 Followers: ${followers.length}`);
console.log(` ❌ Non-followers: ${nonFollowers.length}`);
console.log('');
if (nonFollowers.length === 0) {
console.log('🎉 Great news! Everyone you follow also follows you back!');
return;
}
// Step 4: Export non-followers list
console.log('💾 Step 4: Exporting non-followers list...');
const date = new Date().toISOString().split('T')[0];
const exportData = nonFollowers.map(u => ({
username: u.username,
name: u.name,
bio: u.bio,
}));
const jsonFilename = `non-followers-${CONFIG.username}-${date}.json`;
const csvFilename = `non-followers-${CONFIG.username}-${date}.csv`;
// Export JSON
await fs.writeFile(jsonFilename, JSON.stringify(exportData, null, 2));
console.log(` 📥 Saved: ${jsonFilename}`);
// Export CSV
const csvHeaders = 'username,name,bio';
const csvRows = exportData.map(u =>
`"${u.username}","${(u.name || '').replace(/"/g, '""')}","${(u.bio || '').replace(/"/g, '""').replace(/\n/g, ' ')}"`
);
await fs.writeFile(csvFilename, [csvHeaders, ...csvRows].join('\n'));
console.log(` 📥 Saved: ${csvFilename}`);
console.log('');
// Show preview
console.log('📋 Non-followers preview (first 15):');
nonFollowers.slice(0, 15).forEach((u, i) => {
console.log(` ${(i + 1).toString().padStart(2)}. @${u.username.padEnd(20)} ${u.name || ''}`);
});
if (nonFollowers.length > 15) {
console.log(` ... and ${nonFollowers.length - 15} more`);
}
console.log('');
// Step 5: Ask for confirmation
if (!CONFIG.autoUnfollow) {
const answer = await askConfirmation(
`❓ Unfollow up to ${Math.min(nonFollowers.length, CONFIG.maxUnfollows)} accounts? (yes/no): `
);
if (answer !== 'yes' && answer !== 'y') {
console.log('');
console.log('👋 Cancelled. No accounts were unfollowed.');
console.log(' Your non-followers list has been saved to the files above.');
return;
}
}
// Step 6: Unfollow non-followers
console.log('');
console.log(`🚫 Step 5: Unfollowing non-followers...`);
console.log(' ⚠️ Press Ctrl+C to stop at any time');
console.log('');
const toUnfollow = nonFollowers.slice(0, CONFIG.maxUnfollows);
const unfollowed = [];
const failed = [];
for (let i = 0; i < toUnfollow.length; i++) {
const user = toUnfollow[i];
const result = await unfollowUser(page, user.username);
if (result.success) {
unfollowed.push(user.username);
console.log(` ✅ ${(i + 1).toString().padStart(3)}/${toUnfollow.length} - Unfollowed @${user.username}`);
} else {
failed.push({ username: user.username, error: result.error });
console.log(` ❌ ${(i + 1).toString().padStart(3)}/${toUnfollow.length} - Failed @${user.username}: ${result.error}`);
}
// Delay between unfollows
await randomDelay(CONFIG.unfollowDelay, CONFIG.unfollowDelay + 2000);
// Batch pause
if ((i + 1) % CONFIG.batchSize === 0 && i < toUnfollow.length - 1) {
console.log(` ⏸️ Safety pause (${CONFIG.batchPause / 1000}s)...`);
await sleep(CONFIG.batchPause);
}
}
// Summary
console.log('');
console.log('====================================');
console.log('✅ COMPLETE!');
console.log('====================================');
console.log(`📊 Non-followers found: ${nonFollowers.length}`);
console.log(`✅ Successfully unfollowed: ${unfollowed.length}`);
console.log(`❌ Failed: ${failed.length}`);
if (nonFollowers.length > CONFIG.maxUnfollows) {
console.log('');
console.log(`💡 ${nonFollowers.length - CONFIG.maxUnfollows} non-followers remaining.`);
console.log(' Wait 30-60 minutes, then run the script again!');
}
// Save results
const resultsFilename = `unfollow-results-${CONFIG.username}-${date}.json`;
await fs.writeFile(resultsFilename, JSON.stringify({
date: new Date().toISOString(),
unfollowed,
failed,
remaining: nonFollowers.length - unfollowed.length,
}, null, 2));
console.log('');
console.log(`📥 Results saved to: ${resultsFilename}`);
} finally {
await browser.close();
}
}
// ============================================
// Run
// ============================================
unfollowNonFollowers().catch(error => {
console.error('');
console.error('❌ Error:', error.message);
process.exit(1);
});
Setup & Run:
# Install dependencies
npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth
# Edit the CONFIG section with your username and auth_token
# Then run:
node unfollow-non-followers.js
Getting your auth_token:
- Log into X/Twitter in your browser
- Press F12 → Application tab (Chrome) or Storage tab (Firefox)
- Click Cookies →
x.com - Find
auth_tokenand copy the value - Paste it in the CONFIG section
Output example:
🔍 XActions - Unfollow Non-Followers
====================================
👤 Username: @nichxbt
📊 Max scan: 2000
🚫 Max unfollows: 50
🚀 Launching browser...
🔑 Setting authentication...
✅ Logged in successfully!
📜 Step 1: Fetching your following list...
📊 Scraped: 847 users
✅ Found 847 accounts you follow
📜 Step 2: Fetching your followers list...
📊 Scraped: 1203 users
✅ Found 1203 followers
🔎 Step 3: Identifying non-followers...
📊 Following: 847
📊 Followers: 1203
❌ Non-followers: 156
💾 Step 4: Exporting non-followers list...
📥 Saved: non-followers-nichxbt-2026-01-01.json
📥 Saved: non-followers-nichxbt-2026-01-01.csv
📋 Non-followers preview (first 15):
1. @inactive_user1 Old Account
2. @brand_account Some Brand
...
❓ Unfollow up to 50 accounts? (yes/no): yes
🚫 Step 5: Unfollowing non-followers...
⚠️ Press Ctrl+C to stop at any time
✅ 1/50 - Unfollowed @inactive_user1
✅ 2/50 - Unfollowed @brand_account
...
====================================
✅ COMPLETE!
====================================
📊 Non-followers found: 156
✅ Successfully unfollowed: 50
❌ Failed: 0
💡 106 non-followers remaining.
Wait 30-60 minutes, then run the script again!
📥 Results saved to: unfollow-results-nichxbt-2026-01-01.json
⚠️ Safety Tips & Rate Limits
Rate Limit Guidelines
| Action | Recommended Limit | Wait Time |
|---|---|---|
| Unfollows per session | 50 max | - |
| Delay between unfollows | 3-5 seconds | - |
| Batch pause (every 10) | 15-20 seconds | - |
| Between sessions | 30-60 minutes | Before re-running |
| Per day maximum | 100-150 | Total unfollows |
Best Practices
- Start small - Begin with 20-30 unfollows to test
- Export first - Always save the list before unfollowing (scripts do this automatically)
- Don't rush - Spread unfollows across multiple sessions
- Monitor your account - Watch for any warnings from X
- Use during active hours - Unfollowing during normal browsing looks more natural
Warning Signs to Stop
- ⛔ "You're doing that too fast" message
- ⛔ Unfollow buttons stop working
- ⛔ Account temporarily restricted
- ⛔ Multiple failed unfollows in a row
Recovery Steps
If you hit rate limits:
- Stop immediately
- Wait at least 1 hour
- Reduce batch size when resuming
- Increase delays between actions
🌐 Website Alternative
Don't want to run scripts? Use xactions.app:
- Login - Connect your X account securely
- Scan - Click "Find Non-Followers"
- Review - See the list of accounts that don't follow back
- Export - Download CSV/JSON before making changes
- Unfollow - Select accounts and unfollow safely
Benefits:
- ✅ No coding required
- ✅ Visual interface
- ✅ Built-in rate limiting
- ✅ Progress tracking
- ✅ Automatic exports
📝 Notes
- Protected accounts: You can only check if public accounts follow you
- Recent unfollowers: Someone might have just unfollowed you - the script shows current state
- Verification: Always double-check a few accounts manually if unsure
- Whitelist coming soon: Future versions will let you protect specific accounts
Author: nich (@nichxbt)
Part of XActions - X/Twitter Automation Tools
⚡ Ready to try Unfollow Non-Followers?
XActions is 100% free and open-source. No API keys, no fees, no signup.
Browse All Scripts