Clean Up Your X For You Feed

- x twitter javascript automation

At some point the For You tab on X (formerly Twitter) decided I needed nonstop updates about topics I never cared about. The culprit lives under Settings → Privacy and safety → Interests, where the platform keeps silently re-checking categories based on past engagement. Manually unchecking hundreds of boxes is tedious, and in my experience the rate limits tend to be aggressive.

To reclaim a clean feed without playing whack-a-mole, I put together a conservative automation helper. It is intentionally slow and backs off when it sees any non-200 response. You can grab the latest copy as a gist: Twitter Interests Bulk Unchecker (Rate-Limit Safe Script).

How to use it

  1. Open https://x.com/settings/your_twitter_data/twitter_interests in your browser.
  2. Open the developer console.
  3. Paste the script below and press Enter.
  4. Leave the tab focused. The script will log progress and pause when it hits errors.

The script is also available at the gist link above. Here’s the full code:

/**
 * X (formerly Twitter) Interests Bulk Unchecking Tool
 *
 * Purpose: Automatically unchecks interests on X's settings page
 * URL: https://x.com/settings/your_twitter_data/twitter_interests
 *
 * Based on: https://gist.github.com/TheSethRose/63d4cd0fd3b9fe33c3e51e83f87da26f
 *
 * Usage: Run this script in the browser console while on the X interests page.
 * The script will automatically uncheck all selected interests with rate limiting protection.
 *
 * Note: Initial delay set to 15 seconds per uncheck (as of 1 November 2025) because
 * X's rate limiting was found to be extremely aggressive. This conservative
 * approach prioritizes reliability over speed.
 */

const section = document.querySelector('section:nth-child(2)');
const checkboxes = section
  ? section.querySelectorAll('input[type="checkbox"]')
  : [];

// Configuration
const CONFIG = {
  initialDelay: 15000, // 15 seconds - conservative delay due to aggressive rate limiting (Nov 2025)
  maxDelay: 600000,
  backoffMultiplier: 2,
  maxRetries: 5,
  errorCooldown: 120000,
  consecutiveErrorLimit: 2
};

let uncheckedCount = 0;
let skippedCount = 0;
let errorCount = 0;
let consecutiveErrors = 0;
let currentDelay = CONFIG.initialDelay;

if (!section) {
  console.error('[ERROR] Unable to locate the interests section. Nothing to uncheck.');
}

// Intercepts ALL fetch/XHR requests to detect rate limits and errors
// This allows the script to react to ANY non-200 response, not just explicit errors
const setupRequestMonitor = () => {
  const requestStatus = {
    lastError: null,
    errorTime: null,
    lastStatus: null
  };

  const originalFetch = window.fetch;
  window.fetch = async (...args) => {
    try {
      const response = await originalFetch(...args);

      if (response.status !== 200) {
        requestStatus.lastError = `HTTP_${response.status}`;
        requestStatus.lastStatus = response.status;
        requestStatus.errorTime = Date.now();

        if (response.status === 429) {
          console.warn('[WARN] Rate limit detected (HTTP 429)');
        } else if (response.status >= 500) {
          console.warn(`[WARN] Server error detected (HTTP ${response.status})`);
        } else if (response.status >= 400) {
          console.warn(`[WARN] Client error detected (HTTP ${response.status})`);
        } else {
          console.warn(`[WARN] Non-200 response detected (HTTP ${response.status})`);
        }
      } else {
        requestStatus.lastError = null;
        requestStatus.lastStatus = 200;
        consecutiveErrors = 0;
        // Gradually reduce delay on success (adaptive timing)
        currentDelay = Math.max(CONFIG.initialDelay, currentDelay * 0.9);
      }

      return response;
    } catch (error) {
      requestStatus.lastError = 'NETWORK_ERROR';
      requestStatus.errorTime = Date.now();
      console.error('[ERROR] Network error detected:', error.message);
      throw error;
    }
  };

  const originalXHROpen = XMLHttpRequest.prototype.open;
  const originalXHRSend = XMLHttpRequest.prototype.send;

  XMLHttpRequest.prototype.open = function (...args) {
    this._url = args[1];
    return originalXHROpen.apply(this, args);
  };

  XMLHttpRequest.prototype.send = function (...args) {
    this.addEventListener('load', function () {
      if (this.status !== 200) {
        requestStatus.lastError = `HTTP_${this.status}`;
        requestStatus.lastStatus = this.status;
        requestStatus.errorTime = Date.now();
        console.warn(`[WARN] XHR non-200 response: ${this.status}`);
      } else {
        requestStatus.lastError = null;
        requestStatus.lastStatus = 200;
        consecutiveErrors = 0;
        currentDelay = Math.max(CONFIG.initialDelay, currentDelay * 0.9);
      }
    });

    this.addEventListener('error', function () {
      requestStatus.lastError = 'NETWORK_ERROR';
      requestStatus.errorTime = Date.now();
      console.error('[ERROR] XHR network error');
    });

    return originalXHRSend.apply(this, args);
  };

  return requestStatus;
};

// Exponential backoff with jitter to avoid thundering herd
const calculateBackoff = (attempt) => {
  const exponentialDelay = Math.min(
    currentDelay * Math.pow(CONFIG.backoffMultiplier, attempt),
    CONFIG.maxDelay
  );
  const jitter = exponentialDelay * 0.15 * (Math.random() - 0.5);
  return Math.floor(exponentialDelay + jitter);
};

const formatTime = (ms) => {
  const minutes = Math.floor(ms / 60000);
  const seconds = Math.floor((ms % 60000) / 1000);
  if (minutes > 0) {
    return `${minutes}m ${seconds}s`;
  }
  return `${seconds}s`;
};

// Countdown timer for long waits to show progress
const smartDelay = async (ms, reason = '') => {
  const formatted = formatTime(ms);
  if (reason) {
    console.log(`[INFO] Waiting ${formatted} ${reason}...`);
  }

  if (ms > 30000) {
    const supportsStdout =
      typeof process !== 'undefined' &&
      process.stdout &&
      typeof process.stdout.write === 'function';

    const startTime = Date.now();
    const interval = setInterval(() => {
      const elapsed = Date.now() - startTime;
      const remaining = ms - elapsed;
      if (remaining <= 0) {
        return;
      }
      const message = `[INFO] Time remaining: ${formatTime(remaining)}   `;
      if (supportsStdout) {
        process.stdout.write(`\r${message}`);
      } else {
        console.log(message);
      }
    }, 5000);

    await new Promise(resolve => setTimeout(resolve, ms));
    clearInterval(interval);

    if (supportsStdout) {
      process.stdout.write('\r' + ' '.repeat(50) + '\r');
    }
  } else {
    await new Promise(resolve => setTimeout(resolve, ms));
  }
};

const uncheckWithRetry = async (checkbox, index, requestMonitor) => {
  const label = checkbox.closest('label');
  const nameElement = label?.querySelector('[dir="ltr"]');
  const name = nameElement?.textContent || `Interest ${index + 1}`;

  for (let attempt = 0; attempt < CONFIG.maxRetries; attempt++) {
    try {
      // Extended cooldown after multiple consecutive errors
      if (consecutiveErrors >= CONFIG.consecutiveErrorLimit) {
        console.warn(`[WARN] ${consecutiveErrors} consecutive errors detected. Extended cooldown period...`);
        await smartDelay(CONFIG.errorCooldown, '(error cooldown)');
        consecutiveErrors = 0;
      }

      // Check if a recent error occurred (within 3 seconds)
      if (
        requestMonitor.lastError &&
        Date.now() - requestMonitor.errorTime < 3000
      ) {
        consecutiveErrors++;
        const backoffTime = calculateBackoff(attempt + 1);
        currentDelay = backoffTime;

        console.error(`[ERROR] Non-200 response detected: ${requestMonitor.lastError}`);
        await smartDelay(backoffTime, `(backoff after ${requestMonitor.lastError})`);
      }

      checkbox.click();

      // Wait for request to complete
      await smartDelay(2000);

      // Verify no error occurred during the click operation
      if (
        requestMonitor.lastError &&
        Date.now() - requestMonitor.errorTime < 3000
      ) {
        throw new Error(`${requestMonitor.lastError} (Status: ${requestMonitor.lastStatus})`);
      }

      uncheckedCount++;
      console.log(`[SUCCESS] [${uncheckedCount}] ${name}`);

      return true;

    } catch (error) {
      errorCount++;
      consecutiveErrors++;
      console.error(`[ERROR] Error on "${name}" (attempt ${attempt + 1}/${CONFIG.maxRetries}): ${error.message}`);

      if (attempt < CONFIG.maxRetries - 1) {
        const retryDelay = calculateBackoff(attempt + 1);
        await smartDelay(retryDelay, `(retry ${attempt + 2}/${CONFIG.maxRetries})`);
      }
    }
  }

  skippedCount++;
  console.error(`[SKIP] Skipped "${name}" after ${CONFIG.maxRetries} failed attempts`);
  return false;
};

const uncheck = async () => {
  const checkedBoxes = Array.from(checkboxes).filter(cb => cb.checked);
  const totalChecked = checkedBoxes.length;

  if (!section) {
    console.log('[INFO] No section found. Aborting.');
    return;
  }

  if (totalChecked === 0) {
    console.log('[INFO] No checked interests found. Nothing to uncheck!');
    return;
  }

  console.log('[INFO] Starting conservative bulk uncheck process...');
  console.log(`[INFO] Total interests found: ${checkboxes.length}`);
  console.log(`[INFO] Currently checked: ${totalChecked}`);
  console.log('[INFO] Configuration:');
  console.log(`[INFO]    Base delay: ${formatTime(CONFIG.initialDelay)}`);
  console.log(`[INFO]    Max backoff: ${formatTime(CONFIG.maxDelay)}`);
  console.log(`[INFO]    Max retries per item: ${CONFIG.maxRetries}`);
  console.log('[INFO]    Strategy: ANY non-200 response triggers backoff\n');

  const requestMonitor = setupRequestMonitor();
  const startTime = Date.now();

  let processedCount = 0;

  for (let i = 0; i < checkboxes.length; i++) {
    const checkbox = checkboxes[i];

    if (!checkbox.checked) {
      continue;
    }

    processedCount++;
    console.log(`\n[INFO] Processing ${processedCount} of ${totalChecked}...`);
    await uncheckWithRetry(checkbox, i, requestMonitor);

    if (processedCount < totalChecked) {
      const nextDelay = Math.max(CONFIG.initialDelay, currentDelay);
      await smartDelay(nextDelay, '(standard delay)');
    }
  }

  const duration = ((Date.now() - startTime) / 1000 / 60).toFixed(1);
  console.log(`\n${'='.repeat(60)}`);
  console.log('[INFO] PROCESS COMPLETE');
  console.log(`${'='.repeat(60)}`);
  console.log(`[INFO] Total duration: ${duration} minutes`);
  console.log(`[INFO] Checked interests found: ${totalChecked}`);
  console.log(`[INFO] Successfully unchecked: ${uncheckedCount}`);
  console.log(`[INFO] Skipped (failed): ${skippedCount}`);
  console.log(`[INFO] Total errors encountered: ${errorCount}`);
  console.log(`[INFO] Final delay setting: ${formatTime(currentDelay)}`);
  console.log(`${'='.repeat(60)}`);
};

uncheck();