See who doesn’t follow you back on X (Twitter), free

Get the list of accounts you follow on X that don’t follow you back, as a spreadsheet you can review. Free, no sign-up, no app connected to your account, and it doesn’t unfollow anyone.

Sign in on x.com, open your profile, click Following, then run the script. It only reads the list: it doesn't unfollow anyone.

export-followers.js · 145 linespaste in the DevTools console on x.com
// Responsive Muse · X (Twitter) Followers & Following Exporter · v1.0 · 2026-10-31
// Saves the followers or the following list of any X account you can open (yours or a public
// one) to CSV or TXT: name, @handle, user ID, bio, verified, whether they follow you and the
// profile link. On your own Following list it can keep only the accounts that don't follow
// you back. X only keeps on screen the people you can see, so the script collects them while
// it scrolls. It makes no network requests of its own and never reads cookies, passwords or tokens.
(async () => {
  const TOOL = 'Export X followers';
  const FORMAT = 'csv';        // 'csv' (Excel, Sheets) or 'txt' (one @handle per line)
  const ONLY = 'all';          // 'all' or 'not-following-back' (only accounts that don't follow you)
  const LIMIT = 2000;          // stop after this many accounts (0 = no limit; big accounts take a while)

  const page = location.pathname.match(/^\/([A-Za-z0-9_]{1,15})\/(followers|following|verified_followers)\/?$/);
  if (!/^(x|twitter)\.com$/.test(location.hostname) || !page) {
    alert('Open a followers or following list on X first (signed in), for example:\nhttps://x.com/YourName/following');
    return;
  }
  // A small box on the page shows progress and asks before changing anything,
  // so you never need to read or edit the console.
  const many = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;   // 1 video, 2 videos
  const panel = (() => {
    const box = document.createElement('div');
    box.style.cssText = 'display:block!important;position:fixed;right:16px;bottom:16px;z-index:2147483647;width:340px;max-width:calc(100vw - 32px);' +
      'background:#111318;color:#e6e8ee;font:14px/1.5 system-ui,sans-serif;border-radius:12px;padding:16px;' +
      'box-shadow:0 8px 30px rgba(0,0,0,.4);text-align:left';
    const head = document.createElement('strong');
    head.textContent = TOOL;
    const msg = document.createElement('p');
    msg.style.cssText = 'margin:8px 0 12px;white-space:pre-line';
    const row = document.createElement('div');
    row.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end';
    box.append(head, msg, row);
    document.body.append(box);
    const button = (label, primary, onClick) => {
      const b = document.createElement('button');
      b.textContent = label;
      b.style.cssText = 'font:600 14px system-ui,sans-serif;padding:8px 14px;border-radius:8px;cursor:pointer;' +
        (primary ? 'background:#0f766e;color:#fff;border:1px solid #0f766e' : 'background:none;color:#e6e8ee;border:1px solid #3a3f4b');
      b.onclick = onClick;
      row.append(b);
    };
    return {
      text(t) { msg.textContent = t; },
      ask(t, yes) {
        msg.textContent = t;
        row.replaceChildren();
        return new Promise(r => {
          button('Cancel', false, () => { row.replaceChildren(); r(false); });
          button(yes, true, () => { row.replaceChildren(); r(true); });
        });
      },
      stoppable(onStop) { row.replaceChildren(); button('Stop', false, onStop); },
      actions(t, list) {
        msg.textContent = t;
        row.replaceChildren();
        list.forEach(([label, primary, run]) => button(label, primary, run));
      },
      finish(t) { this.actions(t, [['Close', true, () => box.remove()]]); },
      close() { box.remove(); },
    };
  })();
  // Offer the result as a downloaded file or as clipboard text (a real click, so it always works)
  const deliver = (summary, body, file) => {
    const save = () => {
      const a = document.createElement('a');
      a.href = URL.createObjectURL(new Blob([/\.csv$/.test(file) ? '\ufeff' + body : body], { type: 'text/plain;charset=utf-8' }));
      a.download = file;
      a.click();
      setTimeout(() => URL.revokeObjectURL(a.href), 5000);
      panel.text(`${summary}\nSaved as ${file} (check your Downloads).`);
    };
    const copy = () => navigator.clipboard.writeText(body)
      .then(() => panel.text(`${summary}\nCopied. Paste it into a spreadsheet or a note.`))
      .catch(() => panel.text(`${summary}\nThe browser blocked the clipboard: use Download instead.`));
    panel.actions(summary, [['Close', false, () => panel.close()], ['Copy', false, copy], ['Download', true, save]]);
  };
  const sleep = ms => new Promise(r => setTimeout(r, ms));
  const people = new Map();
  let total = 0;               // accounts seen, before the "not following back" filter

  // Read the accounts currently on screen
  const collect = () => {
    for (const cell of document.querySelectorAll('[data-testid="UserCell"]')) {
      // Skip the "Who to follow" suggestions in the sidebar
      if (cell.closest('aside') || !cell.closest('[data-testid="primaryColumn"]')) continue;
      // Profile links are plain "/handle" (links in the bio carry ?src= or point to /status)
      const prof = [...cell.querySelectorAll('a[href^="/"]')].find(a => /^\/[A-Za-z0-9_]{1,15}$/.test(a.getAttribute('href')) && a.textContent.trim());
      if (!prof) continue;
      const handle = prof.getAttribute('href').slice(1);
      if (people.has(handle)) continue;
      // The Follow / Following button carries the numeric user ID: "<id>-follow" or "<id>-unfollow"
      const btn = cell.querySelector('[data-testid$="-follow"], [data-testid$="-unfollow"]');
      // Bio: the visible text block outside the links (hidden ones with an id are screen-reader labels)
      const bio = [...cell.querySelectorAll('div[dir="auto"]')].find(d => !d.closest('a') && !d.id);
      const followsYou = !!cell.querySelector('[data-testid="userFollowIndicator"]');
      total++;
      people.set(handle, {
        name: prof.textContent.trim(),
        handle: '@' + handle,
        id: (btn?.dataset.testid.match(/^(\d+)-/) || [])[1] || '',
        bio: (bio?.innerText || '').replace(/\s+/g, ' ').trim(),
        verified: cell.querySelector('[data-testid="icon-verified"]') ? 'yes' : '',
        followsYou: followsYou ? 'yes' : '',
        youFollow: btn && /-unfollow$/.test(btn.dataset.testid) ? 'yes' : '',
        url: 'https://x.com/' + handle,
      });
    }
  };

  // 1. Scroll down step by step, collecting as we go. X loads the next accounts only when you reach
  //    the bottom and can take a few seconds, so we stop after ~10 s at the bottom with nothing new.
  let stop = false;
  panel.stoppable(() => { stop = true; });
  window.scrollTo(0, 0);
  await sleep(1000);
  let idle = 0;
  while (idle < 8 && !stop && !(LIMIT && people.size >= LIMIT)) {
    const before = people.size;
    collect();
    const atBottom = scrollY + innerHeight >= document.scrollingElement.scrollHeight - 50;
    window.scrollBy(0, innerHeight * 0.8);
    await sleep(1200);
    idle = people.size !== before ? 0 : atBottom ? idle + 1 : idle;
    panel.text(`Collecting accounts… ${people.size}${LIMIT ? ' of up to ' + LIMIT : ''}`);
  }
  let list = [...people.values()];
  if (LIMIT) list = list.slice(0, LIMIT);
  if (!list.length) {
    panel.finish('No accounts found. Is the list empty or private? If not, X may have changed its layout: please report it on responsive-muse.com.');
    return 0;
  }
  if (ONLY === 'not-following-back') list = list.filter(p => !p.followsYou);

  // 2. Build the file and offer it
  const cell = v => '"' + String(v).replace(/"/g, '""') + '"';
  const body = FORMAT === 'txt'
    ? list.map(p => p.handle).join('\n')
    : ['#,Name,Handle,User ID,Bio,Verified,Follows you,You follow,URL', ...list.map((p, i) =>
        [i + 1, p.name, p.handle, p.id, p.bio, p.verified, p.followsYou, p.youFollow, p.url].map(cell).join(','))].join('\n');
  const what = ONLY === 'not-following-back' ? `${many(list.length, 'account')} that don't follow you back (of ${total})` : many(list.length, 'account');
  const file = `x-${page[1]}-${ONLY === 'not-following-back' ? 'not-following-back' : page[2]}.${FORMAT}`;
  deliver(`${stop ? 'Stopped early. ' : ''}Found ${what}.`, body, file);
  return list.length;
})();

🛡️ Chrome says “Don't paste code…”? That's normal. Type allow pasting in the console, press Enter and paste again. You only need to do it once.

Why does Chrome warn me?

Chrome shows this the first time anyone pastes into the console, to protect people from scams that ask them to paste unknown code. It's right to warn you, and that's why this script is fully visible above: 145 commented lines that send nothing anywhere and never touch your passwords or cookies. Why it's safe.

Rather not use the console? Drag the Who doesn't follow you back button to your bookmarks bar and click it on x.com. No warning that way.

Bookmarklet: drag the middle button to your bookmarks bar, open x.com and click the bookmark. It runs the same script with the options above.

Why it's safe to runAutomated check passed Oct 26, 2026 · tested on a real account Oct 31, 2026
  • Sends nothing anywhere. No fetch, XHR or external scripts. The only network activity is x.com itself loading your list.
  • Never touches your login. It doesn't read cookies, passwords, tokens or browser storage.
  • Only runs on x.com. Anywhere else it stops before doing anything.
  • Short and readable. 145 commented lines, not minified or obfuscated.
  • Shows its progress on the page. A small box tells you what it's doing and offers Download or Copy at the end.
  • Same code for everyone. SHA-256 of the default version: ace2a1cd24863101…
Verify it yourself in 30 seconds
  1. Read it: every line is above, with comments.
  2. Search the code for fetch, XMLHttpRequest, cookie, localStorage or eval. None of them appear.
  3. Open the Network tab before running it: you'll only see requests to x.com.
Did it work for you?

How to use it

  1. Sign in on x.com, open your profile and click Following.
  2. Open the console: F12 or Ctrl+Shift+J (Cmd+Option+J on Mac). If Chrome asks, type allow pasting and press Enter.
  3. Copy the script, paste it in the console and press Enter. It scrolls through everyone you follow and keeps the ones without the “Follows you” label.

What you get

A list with name, @handle, user ID, bio and profile link of each account that doesn’t follow you back, plus how many accounts it checked. Open the links to decide who to unfollow: nothing changes on your account unless you do it.

FAQ

How does it know who follows me back?

On your own Following list, X shows a small "Follows you" label next to each account that follows you. The script keeps the accounts without it.

Does it unfollow anyone?

No. It only makes the list, so you can decide yourself. Nothing on your account changes.

Is it safe for my account?

It only scrolls the page you have open, like you would, and sends nothing anywhere. You don't give your password or connect any app.

More X tools

Clean up

Export & back up