🚪 Remove Follower - XActions

Safety & Privacy scripts/twitter
v1.0.0 Updated: 2026-07-20 309 lines by @nichxbt

Remove followers without blocking them: X's native "Remove this follower", or a soft-block fallback.

⚠️ This changes who follows you and cannot be auto-undone. It DEFAULTS TO A DRY

How to Use

  1. Go to x.com/<your-username>/followers
  2. Open the browser console (F12 or Cmd+Option+I -> Console)
  3. Set CONFIG.dryRun = false when you are ready to actually remove
  4. Paste this entire script and press Enter

Default Configuration

const CONFIG = {
    // Specific followers to remove (with or without @). Empty = remove from the
    // top of your followers list up to maxRemove.
    usernames: [
      // 'username1',
      // 'username2',
    ],

    // Never remove these, even if they appear in usernames or the top-N sweep.
    whitelist: [],

    // Cap on removals this run (also the top-N count when usernames is empty).
    maxRemove: 25,

    // SAFETY: preview only. Set to false to actually remove followers.
    dryRun: true,

    // Extra confirm() prompt before the first live removal.
    confirm: true,

    // Timing (randomized between min and max, per removal)
    minDelay: 2500,
    maxDelay: 5000,

    // Scrolling to load more followers (top-N mode)
    maxScrollRounds: 40,
    scrollDelay: 2000,
    maxEmptyScrolls: 6
  };

Full Script

Copy and paste this entire script into your browser DevTools console on x.com.

// Copyright (c) 2024-2026 nich (@nichxbt). Licensed under the Apache License, Version 2.0.
/**
 * ============================================
 * 👋 Remove Follower - XActions
 * ============================================
 *
 * @name         remove-follower
 * @description  Remove followers without blocking them: X's native "Remove this follower", or a soft-block fallback.
 * @author       nichxbt
 * @version      1.0.0
 * @date         2026-07-20
 * @website      https://xactions.app
 *
 * Usage:
 *   1. Go to x.com/<your-username>/followers
 *   2. Open the browser console (F12 or Cmd+Option+I -> Console)
 *   3. Set CONFIG.dryRun = false when you are ready to actually remove
 *   4. Paste this entire script and press Enter
 *
 * Example:
 *   usernames: ['spambot1', 'spambot2'], dryRun: false. The script finds those two
 *   rows and, per row, opens the "..." menu and clicks "Remove this follower"
 *   (native, no block). If that option is missing it soft-blocks: block, confirm,
 *   then immediately unblock, which drops them without a lasting block. Leave
 *   usernames empty and set maxRemove to remove the first N followers in the list.
 *
 * ⚠️ This changes who follows you and cannot be auto-undone. It DEFAULTS TO A DRY
 *    RUN (previews only). Nothing is removed until you set CONFIG.dryRun = false.
 *
 * ============================================
 */

(async function removeFollower() {
  'use strict';

  // ============================================
  // 📝 CONFIGURATION - Customize these options
  // ============================================
  const CONFIG = {
    // Specific followers to remove (with or without @). Empty = remove from the
    // top of your followers list up to maxRemove.
    usernames: [
      // 'username1',
      // 'username2',
    ],

    // Never remove these, even if they appear in usernames or the top-N sweep.
    whitelist: [],

    // Cap on removals this run (also the top-N count when usernames is empty).
    maxRemove: 25,

    // SAFETY: preview only. Set to false to actually remove followers.
    dryRun: true,

    // Extra confirm() prompt before the first live removal.
    confirm: true,

    // Timing (randomized between min and max, per removal)
    minDelay: 2500,
    maxDelay: 5000,

    // Scrolling to load more followers (top-N mode)
    maxScrollRounds: 40,
    scrollDelay: 2000,
    maxEmptyScrolls: 6
  };

  // ============================================
  // 🔧 SELECTORS
  // ============================================
  const SELECTORS = {
    userCell: '[data-testid="UserCell"]',
    userActions: '[data-testid="userActions"]',
    menuItem: '[role="menuitem"]',
    confirm: '[data-testid="confirmationSheetConfirm"]'
  };

  // ============================================
  // 🛠️ HELPERS
  // ============================================
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  const randomDelay = () => {
    const delay = Math.floor(Math.random() * (CONFIG.maxDelay - CONFIG.minDelay + 1)) + CONFIG.minDelay;
    return sleep(delay);
  };

  const cleanHandle = (u) => String(u || '').trim().replace(/^@/, '').toLowerCase();

  const getUsername = (cell) => {
    const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
    if (!link) return null;
    const m = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
    if (!m) return null;
    if (['home', 'explore', 'notifications', 'messages', 'i', 'search', 'settings'].includes(m[1])) return null;
    return m[1];
  };

  const download = (data, filename) => {
    const a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }));
    a.download = filename;
    document.body.appendChild(a);
    a.click();
    a.remove();
    console.log(`📥 Downloaded: ${filename}`);
  };

  const log = {
    info: (msg) => console.log(`ℹ️ ${msg}`),
    success: (msg) => console.log(`✅ ${msg}`),
    warning: (msg) => console.log(`⚠️ ${msg}`),
    error: (msg) => console.log(`❌ ${msg}`),
    progress: (current, total) => console.log(`📊 Progress: ${current}/${total} removed`)
  };

  // ============================================
  // 🎯 STATE
  // ============================================
  const stats = { removed: 0, scanned: 0, skipped: 0, notFound: 0, errors: 0 };
  const removedList = [];
  const processed = new Set();

  // Stop switch: run window.stopRemoveFollower() to abort after the current row.
  let stopped = false;
  window.stopRemoveFollower = () => {
    stopped = true;
    log.warning('Stop requested. Finishing the current follower, then exiting.');
  };

  console.log(`
╔══════════════════════════════════════════════════════════════╗
║  👋 REMOVE FOLLOWER - XActions                               ║
${CONFIG.dryRun ? '║  ⚠️  DRY RUN - No followers will be removed                 ║' : '║  🔴 LIVE MODE - Followers WILL be removed                   ║'}
╚══════════════════════════════════════════════════════════════╝
  `);
  log.info('To stop early: window.stopRemoveFollower()');

  // Page guard
  if (!/\/followers/.test(window.location.pathname)) {
    log.error('Not on a followers page. Go to https://x.com/<your-username>/followers and re-run.');
    return stats;
  }

  const targetSet = new Set(CONFIG.usernames.map(cleanHandle).filter(Boolean));
  const whiteSet = new Set(CONFIG.whitelist.map(cleanHandle).filter(Boolean));
  const targetMode = targetSet.size > 0;

  log.info(`Mode: ${targetMode ? `list (${targetSet.size} targets)` : `top ${CONFIG.maxRemove} followers`}`);
  log.info(`Max removals: ${CONFIG.maxRemove} | Dry run: ${CONFIG.dryRun}`);

  if (!CONFIG.dryRun && CONFIG.confirm) {
    const proceed = window.confirm(
      `Remove up to ${CONFIG.maxRemove} follower(s) for real? This cannot be auto-undone.`
    );
    if (!proceed) {
      log.warning('Cancelled at confirmation prompt. Nothing removed.');
      return stats;
    }
  }

  // ============================================
  // 🧹 REMOVE ONE (native remove, else soft-block)
  // ============================================
  const removeOne = async (cell, username) => {
    const moreBtn = cell.querySelector(SELECTORS.userActions) || cell.querySelector('button[aria-label="More"]');
    if (!moreBtn) {
      log.warning(`No actions (...) button for @${username}.`);
      stats.notFound++;
      return false;
    }
    moreBtn.click();
    await sleep(700);

    const items = Array.from(document.querySelectorAll(SELECTORS.menuItem));

    // 1) Preferred: native "Remove this follower" (no block).
    const removeItem = items.find(i => /remove\s+this\s+follower|remove\s+follower/i.test(i.textContent));
    if (removeItem) {
      removeItem.click();
      await sleep(600);
      const confirm = document.querySelector(SELECTORS.confirm);
      if (confirm) { confirm.click(); await sleep(700); }
      log.success(`Removed @${username} (native remove).`);
      return true;
    }

    // 2) Fallback: soft-block = block, confirm, then immediately unblock.
    const blockItem = items.find(i => /\bblock\b/i.test(i.textContent) && !/unblock/i.test(i.textContent));
    if (!blockItem) {
      document.body.click();
      await sleep(300);
      log.warning(`No remove or block option for @${username}.`);
      stats.notFound++;
      return false;
    }

    blockItem.click();
    await sleep(600);
    const blockConfirm = document.querySelector(SELECTORS.confirm);
    if (blockConfirm) { blockConfirm.click(); await sleep(800); }

    // Re-open the menu on the same row and unblock to complete the soft-block.
    const moreBtn2 = cell.querySelector(SELECTORS.userActions) || cell.querySelector('button[aria-label="More"]');
    if (moreBtn2) {
      moreBtn2.click();
      await sleep(700);
      const items2 = Array.from(document.querySelectorAll(SELECTORS.menuItem));
      const unblock = items2.find(i => /unblock/i.test(i.textContent));
      if (unblock) {
        unblock.click();
        await sleep(500);
        const unblockConfirm = document.querySelector(SELECTORS.confirm);
        if (unblockConfirm) { unblockConfirm.click(); await sleep(500); }
      } else {
        document.body.click();
        await sleep(300);
      }
    }
    log.success(`Removed @${username} (soft-block).`);
    return true;
  };

  // ============================================
  // 🔁 MAIN LOOP
  // ============================================
  let scrollRounds = 0;
  let emptyScrolls = 0;

  while (!stopped && stats.removed < CONFIG.maxRemove && scrollRounds < CONFIG.maxScrollRounds && emptyScrolls < CONFIG.maxEmptyScrolls) {
    const cells = document.querySelectorAll(SELECTORS.userCell);
    let foundNew = false;

    for (const cell of cells) {
      if (stopped || stats.removed >= CONFIG.maxRemove) break;

      const username = getUsername(cell);
      if (!username) continue;
      const key = username.toLowerCase();
      if (processed.has(key)) continue;
      processed.add(key);
      foundNew = true;
      stats.scanned++;

      try {
        if (whiteSet.has(key)) { stats.skipped++; continue; }
        if (targetMode && !targetSet.has(key)) { stats.skipped++; continue; }

        if (CONFIG.dryRun) {
          log.info(`Would remove: @${username}`);
          removedList.push({ username, dryRun: true, timestamp: new Date().toISOString() });
          stats.removed++;
          continue;
        }

        const ok = await removeOne(cell, username);
        if (ok) {
          stats.removed++;
          removedList.push({ username, timestamp: new Date().toISOString() });
          log.progress(stats.removed, CONFIG.maxRemove);
          await randomDelay();
        } else {
          stats.errors++;
        }
      } catch (error) {
        log.error(`Error on @${username}: ${error.message}`);
        stats.errors++;
        document.body.click();
        await sleep(300);
      }
    }

    if (!foundNew) emptyScrolls++;
    else emptyScrolls = 0;

    window.scrollTo(0, document.body.scrollHeight);
    scrollRounds++;
    await sleep(CONFIG.scrollDelay);
  }

  // ============================================
  // 📊 SUMMARY
  // ============================================
  console.log(`
╔══════════════════════════════════════════════════════════╗
║  📊 REMOVE FOLLOWER - COMPLETE                           ║
╠══════════════════════════════════════════════════════════╣
║  ✅ Removed:           ${String(stats.removed).padEnd(32)}║
║  🔍 Scanned:           ${String(stats.scanned).padEnd(32)}║
║  ⏭️  Skipped:           ${String(stats.skipped).padEnd(32)}║
║  ❓ Not found:         ${String(stats.notFound).padEnd(32)}║
║  ❌ Errors:            ${String(stats.errors).padEnd(32)}║
║  🧪 Dry run:           ${String(CONFIG.dryRun).padEnd(32)}║
╚══════════════════════════════════════════════════════════╝
  `);

  if (removedList.length > 0) {
    download(
      { stats, dryRun: CONFIG.dryRun, removed: removedList, exportedAt: new Date().toISOString() },
      `xactions-removed-followers-${CONFIG.dryRun ? 'preview' : 'results'}-${new Date().toISOString().slice(0, 10)}.json`
    );
  }

  if (CONFIG.dryRun) log.warning('This was a DRY RUN. Set CONFIG.dryRun = false to actually remove.');
  log.success('Script completed! by nichxbt');
  return stats;
})();

⚡ More XActions Scripts

Browse 300+ free browser scripts for X/Twitter automation. No API keys, no fees.

Browse All Scripts