Track who unfollowed you over time on X (Twitter).
This feature helps you monitor your follower changes by:
- Taking snapshots - Saves your current followers list with timestamps
- Comparing over time - Detects who unfollowed since your last check
- Tracking new followers - Also shows who started following you
- Persistent storage - Data survives browser restarts (localStorage/JSON files)
Use cases:
- Find out who unfollowed you after a tweet
- Monitor follower health over time
- Identify fake followers who follow/unfollow
- Keep records of your audience changes
- Detect mass unfollows from bot accounts
Best for: Quick checks from your browser, no setup needed
Steps:
- Go to
x.com/YOUR_USERNAME/followers - Open browser console (F12 → Console tab)
- Paste the script below and press Enter
- Run again later to detect changes!
// ============================================
// XActions - Detect Unfollowers (Browser Console)
// Author: nich (@nichxbt)
// Go to: x.com/YOUR_USERNAME/followers
// Open console (F12), paste this
// ============================================
(async () => {
// Configuration
const STORAGE_KEY = 'xactions_followers_snapshot';
const SCROLL_DELAY = 1500; // Time between scrolls (ms)
const MAX_SCROLL_RETRIES = 10; // Stop if no new users found
console.log('🔍 XActions - Detect Unfollowers');
console.log('=================================');
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
// Verify we're on the followers page
const pathMatch = window.location.pathname.match(/^\/([^/]+)\/followers/);
if (!pathMatch) {
console.error('❌ Please navigate to your FOLLOWERS page first!');
console.log('👉 Go to: x.com/YOUR_USERNAME/followers');
return;
}
const username = pathMatch[1];
console.log(`📍 Monitoring: @${username}`);
console.log('');
// Step 1: Scrape current followers
console.log('📜 Step 1: Scanning your followers list...');
const followers = new Map();
let scrollRetries = 0;
while (scrollRetries < MAX_SCROLL_RETRIES) {
const cells = document.querySelectorAll('[data-testid="UserCell"]');
const prevSize = followers.size;
cells.forEach(cell => {
try {
// Get username from link
const link = cell.querySelector('a[href^="/"]');
const href = link?.getAttribute('href') || '';
const handle = href.split('/')[1]?.toLowerCase();
if (!handle || handle.includes('?') || handle.includes('/')) return;
// Get display name
const nameEl = cell.querySelector('[dir="ltr"] > span');
const displayName = nameEl?.textContent?.trim() || handle;
// Get bio
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = bioEl?.textContent?.trim() || null;
if (!followers.has(handle)) {
followers.set(handle, {
username: handle,
displayName,
bio,
scrapedAt: new Date().toISOString()
});
}
} catch (e) {
// Skip malformed cells
}
});
console.log(` 📊 Found ${followers.size} followers so far...`);
// Check if we're stuck
if (followers.size === prevSize) {
scrollRetries++;
} else {
scrollRetries = 0;
}
// Scroll to load more
window.scrollTo(0, document.body.scrollHeight);
await sleep(SCROLL_DELAY);
}
const currentFollowers = Array.from(followers.values());
console.log('');
console.log(`✅ Scan complete! Found ${currentFollowers.length} followers`);
// Step 2: Load previous snapshot
console.log('');
console.log('📂 Step 2: Checking for previous snapshot...');
let previousData = null;
try {
const stored = localStorage.getItem(STORAGE_KEY);
if (stored) {
previousData = JSON.parse(stored);
}
} catch (e) {
console.warn(' ⚠️ Could not load previous data:', e.message);
}
// Step 3: Compare and show results
if (previousData && previousData.username === username) {
console.log(` 📸 Found snapshot from ${new Date(previousData.timestamp).toLocaleString()}`);
console.log(` 📊 Previous count: ${previousData.count}`);
console.log(` 📊 Current count: ${currentFollowers.length}`);
console.log('');
console.log('🔎 Step 3: Comparing snapshots...');
// Create sets for comparison
const prevUsernames = new Set(previousData.followers.map(f => f.username.toLowerCase()));
const currUsernames = new Set(currentFollowers.map(f => f.username.toLowerCase()));
// Find unfollowers (were in previous, not in current)
const unfollowers = previousData.followers.filter(
f => !currUsernames.has(f.username.toLowerCase())
);
// Find new followers (in current, not in previous)
const newFollowers = currentFollowers.filter(
f => !prevUsernames.has(f.username.toLowerCase())
);
// Display unfollowers
console.log('');
if (unfollowers.length > 0) {
console.log(`🚨 ${unfollowers.length} PEOPLE UNFOLLOWED YOU:`);
console.log('─'.repeat(40));
unfollowers.forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.displayName})`);
console.log(` └─ https://x.com/${u.username}`);
});
// Download unfollowers list
const unfollowersList = unfollowers.map(u => `@${u.username}`).join('\n');
const blob = new Blob([unfollowersList], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `unfollowers-${new Date().toISOString().split('T')[0]}.txt`;
a.click();
console.log('');
console.log('📥 Downloaded unfollowers list!');
} else {
console.log('✨ No one unfollowed you since last check!');
}
// Display new followers
console.log('');
if (newFollowers.length > 0) {
console.log(`🎉 ${newFollowers.length} NEW FOLLOWERS:`);
console.log('─'.repeat(40));
newFollowers.slice(0, 20).forEach((u, i) => {
console.log(` ${i + 1}. @${u.username} (${u.displayName})`);
});
if (newFollowers.length > 20) {
console.log(` ... and ${newFollowers.length - 20} more!`);
}
} else {
console.log('📭 No new followers since last check.');
}
// Summary
console.log('');
console.log('📈 SUMMARY:');
console.log('─'.repeat(40));
const netChange = newFollowers.length - unfollowers.length;
const changeIcon = netChange > 0 ? '📈' : netChange < 0 ? '📉' : '➡️';
console.log(` ${changeIcon} Net change: ${netChange >= 0 ? '+' : ''}${netChange} followers`);
console.log(` ➕ Gained: ${newFollowers.length}`);
console.log(` ➖ Lost: ${unfollowers.length}`);
} else {
console.log(' 📸 No previous snapshot found (first run)');
console.log(' 💡 Run this script again later to detect changes!');
}
// Step 4: Save current snapshot
console.log('');
console.log('💾 Step 4: Saving current snapshot...');
const snapshot = {
username,
followers: currentFollowers,
count: currentFollowers.length,
timestamp: new Date().toISOString()
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(snapshot));
console.log(` ✅ Saved ${currentFollowers.length} followers`);
console.log(` 📅 Timestamp: ${new Date().toLocaleString()}`);
// Final message
console.log('');
console.log('═'.repeat(50));
console.log('🔄 Run this script again anytime to detect changes!');
console.log('💡 Tip: Bookmark this script for easy access');
console.log('═'.repeat(50));
// Return data for further use
return {
username,
currentCount: currentFollowers.length,
unfollowers: previousData ?
previousData.followers.filter(f =>
!new Set(currentFollowers.map(c => c.username)).has(f.username)
) : [],
newFollowers: previousData ?
currentFollowers.filter(f =>
!new Set(previousData.followers.map(p => p.username)).has(f.username)
) : [],
snapshot
};
})();What happens:
- Script scrolls through your followers list
- Collects all follower usernames and display names
- Saves snapshot to localStorage with timestamp
- On subsequent runs, compares with previous snapshot
- Shows who unfollowed and who's new
- Downloads a text file of unfollowers
Output example:
🔍 XActions - Detect Unfollowers
=================================
📍 Monitoring: @nichxbt
📜 Step 1: Scanning your followers list...
📊 Found 156 followers so far...
📊 Found 312 followers so far...
📊 Found 489 followers so far...
✅ Scan complete! Found 489 followers
📂 Step 2: Checking for previous snapshot...
📸 Found snapshot from 12/28/2025, 3:45:00 PM
📊 Previous count: 495
📊 Current count: 489
🔎 Step 3: Comparing snapshots...
🚨 8 PEOPLE UNFOLLOWED YOU:
────────────────────────────────────────
1. @spammer123 (Spam Bot)
└─ https://x.com/spammer123
2. @fakefollower (Fake Account)
└─ https://x.com/fakefollower
...
📥 Downloaded unfollowers list!
🎉 2 NEW FOLLOWERS:
────────────────────────────────────────
1. @genuine_user (Real Person)
2. @newbie2025 (New User)
📈 SUMMARY:
────────────────────────────────────────
📉 Net change: -6 followers
➕ Gained: 2
➖ Lost: 8
💾 Step 4: Saving current snapshot...
✅ Saved 489 followers
📅 Timestamp: 1/1/2026, 10:30:00 AM
══════════════════════════════════════════════════
🔄 Run this script again anytime to detect changes!
💡 Tip: Bookmark this script for easy access
══════════════════════════════════════════════════
Best for: Automated daily checks, large follower lists, historical tracking
Features:
- Saves snapshots to JSON files with dates
- Keeps historical records
- Compare any two snapshots
- Export detailed reports
- Perfect for scheduling with cron
# Create project folder
mkdir unfollower-tracker && cd unfollower-tracker
# Initialize and install dependencies
npm init -y
npm install puppeteer// ============================================
// XActions - Detect Unfollowers (Node.js + Puppeteer)
// Author: nich (@nichxbt)
//
// Usage:
// node detect-unfollowers.js scrape <username>
// node detect-unfollowers.js compare <file1> <file2>
// node detect-unfollowers.js check <username>
// ============================================
const puppeteer = require('puppeteer');
const fs = require('fs');
const path = require('path');
// Configuration
const CONFIG = {
headless: true, // Set to false to see browser
scrollDelay: 2000, // Time between scrolls (ms)
maxRetries: 15, // Max scroll retries before stopping
snapshotDir: './snapshots', // Where to save snapshot files
userDataDir: './browser-data', // Persistent browser session
viewport: { width: 1280, height: 800 }
};
// Ensure directories exist
if (!fs.existsSync(CONFIG.snapshotDir)) {
fs.mkdirSync(CONFIG.snapshotDir, { recursive: true });
}
if (!fs.existsSync(CONFIG.userDataDir)) {
fs.mkdirSync(CONFIG.userDataDir, { recursive: true });
}
/**
* Sleep helper
*/
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
/**
* Get today's date string for filenames
*/
const getDateString = () => {
const now = new Date();
return now.toISOString().split('T')[0]; // YYYY-MM-DD
};
/**
* Launch browser with persistent session
*/
async function launchBrowser() {
return await puppeteer.launch({
headless: CONFIG.headless,
userDataDir: CONFIG.userDataDir,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-accelerated-2d-canvas',
'--disable-gpu'
]
});
}
/**
* Check if user is logged in to X/Twitter
*/
async function checkLogin(page) {
await page.goto('https://x.com/home', {
waitUntil: 'networkidle2',
timeout: 30000
});
// Check for login indicators
const isLoggedIn = await page.evaluate(() => {
// Look for compose tweet button or profile link
return !!document.querySelector('[data-testid="SideNav_NewTweet_Button"]') ||
!!document.querySelector('[data-testid="AppTabBar_Profile_Link"]');
});
return isLoggedIn;
}
/**
* Prompt user to log in manually
*/
async function manualLogin(page) {
console.log('\n🔐 Login Required');
console.log('═'.repeat(50));
console.log('1. A browser window will open');
console.log('2. Log in to your X/Twitter account');
console.log('3. Press Enter here when done');
console.log('═'.repeat(50));
// Open login page in visible browser
const browser = await puppeteer.launch({
headless: false,
userDataDir: CONFIG.userDataDir,
args: ['--no-sandbox']
});
const loginPage = await browser.newPage();
await loginPage.goto('https://x.com/login', { waitUntil: 'networkidle2' });
// Wait for user to press Enter
await new Promise(resolve => {
process.stdin.once('data', resolve);
});
await browser.close();
console.log('✅ Login saved! You can now run in headless mode.\n');
}
/**
* Scrape followers list for a username
*/
async function scrapeFollowers(username) {
console.log(`\n🔍 Scraping followers for @${username}`);
console.log('─'.repeat(50));
const browser = await launchBrowser();
const page = await browser.newPage();
await page.setViewport(CONFIG.viewport);
try {
// Check login status
const isLoggedIn = await checkLogin(page);
if (!isLoggedIn) {
await browser.close();
console.log('❌ Not logged in to X/Twitter');
await manualLogin(page);
// Retry with new browser instance
return await scrapeFollowers(username);
}
console.log('✅ Logged in to X/Twitter');
// Navigate to followers page
console.log(`📍 Navigating to @${username}/followers...`);
await page.goto(`https://x.com/${username}/followers`, {
waitUntil: 'networkidle2',
timeout: 30000
});
// Wait for content to load
await sleep(3000);
// Check if page exists
const pageError = await page.evaluate(() => {
return document.body.innerText.includes("This account doesn't exist") ||
document.body.innerText.includes("Account suspended");
});
if (pageError) {
throw new Error(`Account @${username} not found or suspended`);
}
// Scrape followers with scrolling
console.log('📜 Scanning followers list...');
const followers = new Map();
let retries = 0;
let lastHeight = 0;
while (retries < CONFIG.maxRetries) {
// Extract visible followers
const newFollowers = await page.evaluate(() => {
const users = [];
const cells = document.querySelectorAll('[data-testid="UserCell"]');
cells.forEach(cell => {
try {
const link = cell.querySelector('a[href^="/"]');
const href = link?.getAttribute('href') || '';
const username = href.split('/')[1]?.toLowerCase();
if (!username || username.includes('?') || username.includes('/')) return;
const nameEl = cell.querySelector('[dir="ltr"] > span');
const displayName = nameEl?.textContent?.trim() || username;
const bioEl = cell.querySelector('[data-testid="UserDescription"]');
const bio = bioEl?.textContent?.trim() || null;
const verified = !!cell.querySelector('svg[aria-label*="Verified"]');
users.push({ username, displayName, bio, verified });
} catch (e) {
// Skip malformed cells
}
});
return users;
});
// Add to map (deduplicates)
const prevSize = followers.size;
newFollowers.forEach(f => {
if (!followers.has(f.username)) {
followers.set(f.username, f);
}
});
console.log(` 📊 Found ${followers.size} followers...`);
// Check if stuck
if (followers.size === prevSize) {
retries++;
} else {
retries = 0;
}
// Scroll down
const currentHeight = await page.evaluate(() => {
window.scrollTo(0, document.body.scrollHeight);
return document.body.scrollHeight;
});
if (currentHeight === lastHeight) {
retries++;
}
lastHeight = currentHeight;
await sleep(CONFIG.scrollDelay);
}
const followersList = Array.from(followers.values());
console.log(`\n✅ Scraping complete! Found ${followersList.length} followers`);
// Create snapshot
const snapshot = {
username,
followers: followersList,
count: followersList.length,
timestamp: new Date().toISOString(),
scrapedAt: getDateString()
};
// Save to file
const filename = `${username}-${getDateString()}.json`;
const filepath = path.join(CONFIG.snapshotDir, filename);
fs.writeFileSync(filepath, JSON.stringify(snapshot, null, 2));
console.log(`💾 Saved snapshot: ${filepath}`);
return snapshot;
} finally {
await browser.close();
}
}
/**
* Compare two snapshot files and show differences
*/
function compareSnapshots(file1, file2) {
console.log('\n🔎 Comparing Snapshots');
console.log('═'.repeat(50));
// Load snapshots
let snapshot1, snapshot2;
try {
const path1 = file1.includes('/') ? file1 : path.join(CONFIG.snapshotDir, file1);
const path2 = file2.includes('/') ? file2 : path.join(CONFIG.snapshotDir, file2);
snapshot1 = JSON.parse(fs.readFileSync(path1, 'utf-8'));
snapshot2 = JSON.parse(fs.readFileSync(path2, 'utf-8'));
} catch (e) {
console.error(`❌ Error loading snapshots: ${e.message}`);
return null;
}
console.log(`📅 Older: ${snapshot1.scrapedAt} (${snapshot1.count} followers)`);
console.log(`📅 Newer: ${snapshot2.scrapedAt} (${snapshot2.count} followers)`);
console.log('');
// Create username sets
const users1 = new Set(snapshot1.followers.map(f => f.username));
const users2 = new Set(snapshot2.followers.map(f => f.username));
// Find differences
const unfollowers = snapshot1.followers.filter(f => !users2.has(f.username));
const newFollowers = snapshot2.followers.filter(f => !users1.has(f.username));
// Display unfollowers
if (unfollowers.length > 0) {
console.log(`🚨 ${unfollowers.length} UNFOLLOWED YOU:`);
console.log('─'.repeat(40));
unfollowers.forEach((u, i) => {
const verified = u.verified ? ' ✓' : '';
console.log(` ${i + 1}. @${u.username}${verified} — ${u.displayName}`);
});
console.log('');
} else {
console.log('✨ No one unfollowed!\n');
}
// Display new followers
if (newFollowers.length > 0) {
console.log(`🎉 ${newFollowers.length} NEW FOLLOWERS:`);
console.log('─'.repeat(40));
newFollowers.forEach((u, i) => {
const verified = u.verified ? ' ✓' : '';
console.log(` ${i + 1}. @${u.username}${verified} — ${u.displayName}`);
});
console.log('');
} else {
console.log('📭 No new followers.\n');
}
// Summary
console.log('📈 SUMMARY:');
console.log('─'.repeat(40));
const netChange = newFollowers.length - unfollowers.length;
const changeIcon = netChange > 0 ? '📈' : netChange < 0 ? '📉' : '➡️';
console.log(` ${changeIcon} Net change: ${netChange >= 0 ? '+' : ''}${netChange}`);
console.log(` ➕ Gained: ${newFollowers.length}`);
console.log(` ➖ Lost: ${unfollowers.length}`);
console.log(` 📊 Previous: ${snapshot1.count} → Current: ${snapshot2.count}`);
// Save report
const report = {
comparedAt: new Date().toISOString(),
older: { file: file1, date: snapshot1.scrapedAt, count: snapshot1.count },
newer: { file: file2, date: snapshot2.scrapedAt, count: snapshot2.count },
unfollowers,
newFollowers,
netChange
};
const reportFile = path.join(CONFIG.snapshotDir, `report-${getDateString()}.json`);
fs.writeFileSync(reportFile, JSON.stringify(report, null, 2));
console.log(`\n💾 Report saved: ${reportFile}`);
return report;
}
/**
* Quick check - scrape and compare with most recent snapshot
*/
async function quickCheck(username) {
console.log(`\n🚀 Quick Check for @${username}`);
console.log('═'.repeat(50));
// Find most recent snapshot for this user
const files = fs.readdirSync(CONFIG.snapshotDir)
.filter(f => f.startsWith(`${username}-`) && f.endsWith('.json'))
.sort()
.reverse();
const previousFile = files[0];
if (previousFile) {
console.log(`📂 Found previous snapshot: ${previousFile}`);
} else {
console.log('📸 First time checking this account');
}
// Scrape current followers
const currentSnapshot = await scrapeFollowers(username);
// Compare if we have a previous snapshot
if (previousFile && previousFile !== `${username}-${getDateString()}.json`) {
console.log('\n');
compareSnapshots(previousFile, `${username}-${getDateString()}.json`);
} else if (previousFile) {
console.log('\n⚠️ Already scraped today. Compare with an older snapshot:');
files.slice(0, 5).forEach(f => console.log(` • ${f}`));
}
return currentSnapshot;
}
/**
* List all snapshots for a user
*/
function listSnapshots(username) {
console.log(`\n📁 Snapshots for @${username || 'all users'}`);
console.log('═'.repeat(50));
const files = fs.readdirSync(CONFIG.snapshotDir)
.filter(f => {
if (!f.endsWith('.json')) return false;
if (username) return f.startsWith(`${username}-`);
return !f.startsWith('report-');
})
.sort()
.reverse();
if (files.length === 0) {
console.log(' No snapshots found.');
return;
}
files.forEach(f => {
const data = JSON.parse(fs.readFileSync(path.join(CONFIG.snapshotDir, f), 'utf-8'));
console.log(` 📸 ${f}`);
console.log(` └─ ${data.count} followers @ ${data.timestamp}`);
});
}
// ============================================
// CLI Handler
// ============================================
const args = process.argv.slice(2);
const command = args[0];
async function main() {
console.log('\n🔍 XActions - Unfollower Detector');
console.log('═'.repeat(50));
switch (command) {
case 'scrape':
if (!args[1]) {
console.log('Usage: node detect-unfollowers.js scrape <username>');
process.exit(1);
}
await scrapeFollowers(args[1].replace('@', ''));
break;
case 'compare':
if (!args[1] || !args[2]) {
console.log('Usage: node detect-unfollowers.js compare <older.json> <newer.json>');
console.log('\nAvailable snapshots:');
listSnapshots();
process.exit(1);
}
compareSnapshots(args[1], args[2]);
break;
case 'check':
if (!args[1]) {
console.log('Usage: node detect-unfollowers.js check <username>');
process.exit(1);
}
await quickCheck(args[1].replace('@', ''));
break;
case 'list':
listSnapshots(args[1]?.replace('@', ''));
break;
default:
console.log('Commands:');
console.log(' scrape <username> Scrape and save followers');
console.log(' compare <file1> <file2> Compare two snapshots');
console.log(' check <username> Scrape and compare with latest');
console.log(' list [username] List saved snapshots');
console.log('');
console.log('Examples:');
console.log(' node detect-unfollowers.js scrape nichxbt');
console.log(' node detect-unfollowers.js check nichxbt');
console.log(' node detect-unfollowers.js compare nichxbt-2025-12-25.json nichxbt-2026-01-01.json');
}
}
main().catch(console.error);# First run - scrape and save your followers
node detect-unfollowers.js scrape nichxbt
# Output:
# 🔍 Scraping followers for @nichxbt
# ──────────────────────────────────────────────────
# ✅ Logged in to X/Twitter
# 📍 Navigating to @nichxbt/followers...
# 📜 Scanning followers list...
# 📊 Found 156 followers...
# 📊 Found 312 followers...
# 📊 Found 489 followers...
#
# ✅ Scraping complete! Found 489 followers
# 💾 Saved snapshot: ./snapshots/nichxbt-2026-01-01.json# Later - quick check (scrapes and compares automatically)
node detect-unfollowers.js check nichxbt
# Output includes unfollowers and new followers!# Compare two specific snapshots
node detect-unfollowers.js compare nichxbt-2025-12-25.json nichxbt-2026-01-01.json# List all saved snapshots
node detect-unfollowers.js list nichxbtRun the script automatically to track changes over time.
# Edit crontab
crontab -e
# Add daily check at 9am
0 9 * * * cd /path/to/unfollower-tracker && node detect-unfollowers.js check nichxbt >> /var/log/unfollowers.log 2>&1
# Add weekly check every Monday at 9am
0 9 * * 1 cd /path/to/unfollower-tracker && node detect-unfollowers.js check nichxbt >> /var/log/unfollowers.log 2>&1- Open Task Scheduler
- Create Basic Task → "Unfollower Check"
- Trigger: Daily or Weekly
- Action: Start a program
- Program:
node - Arguments:
detect-unfollowers.js check nichxbt - Start in:
C:\path\to\unfollower-tracker
- Program:
# Install PM2
npm install -g pm2
# Create ecosystem file
cat > ecosystem.config.js << 'EOF'
module.exports = {
apps: [{
name: 'unfollower-check',
script: 'detect-unfollowers.js',
args: 'check nichxbt',
cron_restart: '0 9 * * *', // Daily at 9am
autorestart: false
}]
};
EOF
# Start the scheduled job
pm2 start ecosystem.config.js
pm2 saveDon't want to run scripts? Use xactions.app instead!
- ✅ No coding required
- ✅ Automatic daily monitoring
- ✅ Email/push notifications for unfollowers
- ✅ Historical charts and analytics
- ✅ Works on mobile and desktop
- ✅ Secure OAuth login (no password needed)
- Visit xactions.app
- Connect your X/Twitter account
- Enable "Unfollower Detection"
- Get notified whenever someone unfollows!
- Run consistently - Same time daily/weekly for accurate tracking
- Keep snapshots - Don't delete old files, they're useful for long-term analysis
- Check after tweets - Controversial posts often trigger unfollows
- Look for patterns - Mass unfollows might indicate bot cleanups by Twitter
- Export to spreadsheet - JSON files can be imported into Excel/Google Sheets
- Browser console: Safe for up to ~2,000 followers per session
- Puppeteer: Can handle 10,000+ with proper delays
- Don't run too often: Once daily is plenty, hourly is excessive
- Account safety: These scripts only READ data, never write/post
- Followers Scraping - Export your complete followers list
- Following Scraping - Export who you follow
- Unfollow Non-Followers - Clean up your following list
Author: nich (@nichxbt)
Repository: github.com/nirholas/XActions