๐Ÿ‘ฅ Following Scraping

January 1, 2026 ยท View on GitHub

Scrape the complete list of accounts that any public X/Twitter user follows โ€” with mutual follow detection.

Author: nich (@nichxbt)


๐Ÿ“ฆ What You Get

When you scrape a user's following list, you get:

FieldDescription
usernameThe @handle of the followed account
nameDisplay name
bioProfile bio/description
verifiedBlue checkmark status
avatarProfile image URL
followsBackWhether this account follows the target user back

The followsBack indicator is powerful โ€” it lets you instantly identify:

  • โœ… Mutual follows (they follow each other)
  • โŒ One-way follows (target follows them, but they don't follow back)

๐Ÿ–ฅ๏ธ Example 1: Browser Console (Quick)

Best for: Scraping up to ~500 following quickly from your browser

Instructions

  1. Go to x.com/USERNAME/following (replace USERNAME with target)
  2. Open DevTools (F12 or Cmd+Opt+I)
  3. Go to Console tab
  4. Paste the code below and press Enter
// ============================================
// XActions - Following Scraper (Browser Console)
// Author: nich (@nichxbt)
// Go to: x.com/USERNAME/following
// Open console (F12), paste this entire script
// ============================================

(async () => {
  const TARGET_COUNT = 500; // Adjust as needed
  const SCROLL_DELAY = 1500; // ms between scrolls
  
  console.log('๐Ÿ” Starting following scrape...');
  console.log(`๐Ÿ“Š Target: ${TARGET_COUNT} accounts`);
  
  const following = new Map();
  let retries = 0;
  const maxRetries = 10;
  
  // Helper to extract user data from cells
  const extractUsers = () => {
    const cells = document.querySelectorAll('[data-testid="UserCell"]');
    const users = [];
    
    cells.forEach(cell => {
      try {
        // Get username from link
        const link = cell.querySelector('a[href^="/"]');
        const href = link?.getAttribute('href') || '';
        const username = href.split('/')[1];
        
        // Skip if invalid or query params
        if (!username || username.includes('?')) return;
        
        // Get display name
        const nameEl = cell.querySelector('[dir="ltr"] > span');
        const name = nameEl?.textContent?.trim() || null;
        
        // Get bio
        const bioEl = cell.querySelector('[data-testid="UserDescription"]');
        const bio = bioEl?.textContent?.trim() || null;
        
        // Check verified status
        const verified = !!cell.querySelector('svg[aria-label*="Verified"]');
        
        // Get avatar
        const avatarEl = cell.querySelector('img[src*="profile_images"]');
        const avatar = avatarEl?.src || null;
        
        // Check if they follow back (look for "Follows you" badge)
        const followsBackEl = cell.querySelector('[data-testid="userFollowIndicator"]');
        const followsBackText = cell.textContent?.includes('Follows you');
        const followsBack = !!(followsBackEl || followsBackText);
        
        users.push({
          username,
          name,
          bio,
          verified,
          avatar,
          followsBack,
        });
      } catch (e) {
        // Skip malformed cells
      }
    });
    
    return users;
  };
  
  // Sleep helper
  const sleep = (ms) => new Promise(r => setTimeout(r, ms));
  
  // Main scraping loop
  while (following.size < TARGET_COUNT && retries < maxRetries) {
    // Extract visible users
    const users = extractUsers();
    const prevSize = following.size;
    
    // Add to map (dedupes automatically)
    users.forEach(user => {
      if (!following.has(user.username)) {
        following.set(user.username, user);
      }
    });
    
    // Progress update
    const mutualCount = Array.from(following.values()).filter(u => u.followsBack).length;
    console.log(`๐Ÿ“ˆ Scraped: ${following.size} following (${mutualCount} mutual)`);
    
    // Check if we're stuck
    if (following.size === prevSize) {
      retries++;
      console.log(`โณ No new users found (retry ${retries}/${maxRetries})`);
    } else {
      retries = 0;
    }
    
    // Scroll to load more
    window.scrollTo(0, document.body.scrollHeight);
    await sleep(SCROLL_DELAY);
  }
  
  // Convert to array
  const result = Array.from(following.values());
  
  // Calculate stats
  const mutualFollows = result.filter(u => u.followsBack);
  const oneWayFollows = result.filter(u => !u.followsBack);
  const verifiedCount = result.filter(u => u.verified).length;
  const withBioCount = result.filter(u => u.bio).length;
  
  // Summary
  console.log('\nโœ… Scraping complete!');
  console.log('โ”'.repeat(40));
  console.log(`๐Ÿ“Š Total following scraped: ${result.length}`);
  console.log(`๐Ÿค Mutual follows: ${mutualFollows.length} (${Math.round(mutualFollows.length / result.length * 100)}%)`);
  console.log(`โžก๏ธ  One-way follows: ${oneWayFollows.length} (${Math.round(oneWayFollows.length / result.length * 100)}%)`);
  console.log(`โœ“ Verified: ${verifiedCount}`);
  console.log(`โœ“ With bio: ${withBioCount}`);
  console.log('โ”'.repeat(40));
  
  // Copy to clipboard
  const json = JSON.stringify(result, null, 2);
  await navigator.clipboard.writeText(json);
  console.log('\n๐Ÿ“‹ Full data copied to clipboard!');
  
  // Also expose data for inspection
  console.log('\n๐Ÿ’พ Data objects (right-click โ†’ Copy object):');
  console.log('All following:', result);
  console.log('Mutual follows:', mutualFollows);
  console.log('One-way follows:', oneWayFollows);
  
  // Create downloadable file
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.href = url;
  a.download = `following-${new Date().toISOString().split('T')[0]}.json`;
  a.click();
  console.log('\n๐Ÿ“ฅ Download started!');
  
  // Return for further use in console
  return {
    all: result,
    mutual: mutualFollows,
    oneWay: oneWayFollows,
    stats: {
      total: result.length,
      mutual: mutualFollows.length,
      oneWay: oneWayFollows.length,
      verified: verifiedCount,
      withBio: withBioCount,
    }
  };
})();

What Happens

  1. ๐Ÿ”„ Script scrolls through the following list automatically
  2. ๐Ÿ“Š Extracts data from each user card
  3. ๐Ÿ” Detects "Follows you" badge for each account
  4. ๐Ÿ” Deduplicates automatically
  5. ๐Ÿ“ˆ Shows live progress with mutual follow count
  6. ๐Ÿ“ฅ Downloads JSON file automatically
  7. ๐Ÿ“‹ Copies data to clipboard

Sample Output

๐Ÿ” Starting following scrape...
๐Ÿ“Š Target: 500 accounts
๐Ÿ“ˆ Scraped: 47 following (12 mutual)
๐Ÿ“ˆ Scraped: 89 following (31 mutual)
๐Ÿ“ˆ Scraped: 156 following (52 mutual)
...
๐Ÿ“ˆ Scraped: 500 following (187 mutual)

โœ… Scraping complete!
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”
๐Ÿ“Š Total following scraped: 500
๐Ÿค Mutual follows: 187 (37%)
โžก๏ธ  One-way follows: 313 (63%)
โœ“ Verified: 45
โœ“ With bio: 412
โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”โ”

๐Ÿ“‹ Full data copied to clipboard!

๐Ÿ“ฅ Download started!

๐Ÿš€ Example 2: Node.js with Puppeteer (Production-Ready)

Best for: Large following lists, automation, scheduled jobs, exporting to JSON/CSV

Installation

npm install puppeteer puppeteer-extra puppeteer-extra-plugin-stealth

Full Script

Save as scrape-following.js:

// ============================================
// XActions - Following Scraper (Node.js)
// Author: nich (@nichxbt)
// 
// Save as: scrape-following.js
// Run: node scrape-following.js <username> [limit]
// Example: node scrape-following.js elonmusk 1000
// ============================================

import puppeteer from 'puppeteer-extra';
import StealthPlugin from 'puppeteer-extra-plugin-stealth';
import fs from 'fs/promises';
import path from 'path';

// Apply stealth plugin to avoid detection
puppeteer.use(StealthPlugin());

/**
 * Scrape the accounts that a Twitter/X user follows
 * @param {string} username - Account to scrape following from
 * @param {Object} options - Configuration options
 * @returns {Promise<Array>} Array of following user objects
 */
async function scrapeFollowing(username, options = {}) {
  const {
    limit = 1000,
    headless = true,
    authToken = null,
    onProgress = null,
    scrollDelay = 1500,
    maxRetries = 10,
    timeout = 30000,
  } = options;

  console.log(`\n๐Ÿ” Scraping following for @${username}`);
  console.log(`๐Ÿ“Š Limit: ${limit}`);
  console.log(`๐Ÿ• Started: ${new Date().toLocaleTimeString()}\n`);

  const browser = await puppeteer.launch({
    headless: headless ? 'new' : false,
    args: [
      '--no-sandbox',
      '--disable-setuid-sandbox',
      '--disable-blink-features=AutomationControlled',
      '--disable-web-security',
      '--disable-features=IsolateOrigins,site-per-process',
    ],
  });

  try {
    const page = await browser.newPage();
    
    // Set realistic viewport with slight randomization
    await page.setViewport({ 
      width: 1280 + Math.floor(Math.random() * 100), 
      height: 800 + Math.floor(Math.random() * 100),
    });
    
    // Set realistic user agent
    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 if provided (for private accounts or rate limit bypass)
    if (authToken) {
      await page.setCookie({
        name: 'auth_token',
        value: authToken,
        domain: '.x.com',
        path: '/',
        httpOnly: true,
        secure: true,
      });
      console.log('๐Ÿ” Auth token set');
    }

    // Navigate to following page
    const url = `https://x.com/${username}/following`;
    console.log(`๐ŸŒ Navigating to ${url}`);
    
    await page.goto(url, {
      waitUntil: 'networkidle2',
      timeout,
    });

    // Wait for user cells to appear
    try {
      await page.waitForSelector('[data-testid="UserCell"]', { timeout: 10000 });
    } catch (e) {
      // Check if account exists or is private
      const pageContent = await page.content();
      if (pageContent.includes('This account doesn't exist')) {
        throw new Error(`Account @${username} does not exist`);
      }
      if (pageContent.includes('These posts are protected')) {
        throw new Error(`Account @${username} is private. Provide authToken to access.`);
      }
      throw new Error('Could not load following list. Page may have changed or be rate limited.');
    }
    
    // Small random delay to mimic human behavior
    await new Promise(r => setTimeout(r, 1000 + Math.random() * 1000));

    const following = new Map();
    let retries = 0;

    // Main scraping loop
    while (following.size < limit && retries < maxRetries) {
      // Extract users from page
      const users = await page.evaluate(() => {
        const cells = document.querySelectorAll('[data-testid="UserCell"]');
        return Array.from(cells).map(cell => {
          try {
            // Get username from profile link
            const link = cell.querySelector('a[href^="/"]');
            const href = link?.getAttribute('href') || '';
            const username = href.split('/')[1];
            
            // Skip if invalid username
            if (!username || username.includes('?') || username.includes('/')) return null;
            
            // Get display name
            const nameEl = cell.querySelector('[dir="ltr"] > span');
            const name = nameEl?.textContent?.trim() || null;
            
            // Get bio
            const bioEl = cell.querySelector('[data-testid="UserDescription"]');
            const bio = bioEl?.textContent?.trim() || null;
            
            // Get avatar URL
            const avatarEl = cell.querySelector('img[src*="profile_images"]');
            const avatar = avatarEl?.src || null;
            
            // Check verified status
            const verifiedEl = cell.querySelector('svg[aria-label*="Verified"]');
            const verified = !!verifiedEl;
            
            // Check if they follow back
            // Look for "Follows you" indicator
            const followsBackIndicator = cell.querySelector('[data-testid="userFollowIndicator"]');
            const cellText = cell.textContent || '';
            const followsBack = !!(followsBackIndicator || cellText.includes('Follows you'));
            
            return {
              username,
              name,
              bio,
              verified,
              avatar,
              followsBack,
              scrapedAt: new Date().toISOString(),
            };
          } catch {
            return null;
          }
        }).filter(Boolean);
      });

      const prevSize = following.size;
      
      // Add to map (automatically dedupes by username)
      users.forEach(user => {
        if (!following.has(user.username)) {
          following.set(user.username, user);
        }
      });

      // Fire progress callback if provided
      if (onProgress) {
        const current = Array.from(following.values());
        const mutualCount = current.filter(u => u.followsBack).length;
        onProgress({
          scraped: following.size,
          limit,
          percent: Math.round((following.size / limit) * 100),
          mutual: mutualCount,
        });
      }

      // Check if we're stuck (no new users found)
      if (following.size === prevSize) {
        retries++;
        // Longer wait when stuck
        await new Promise(r => setTimeout(r, scrollDelay + 1000));
      } else {
        retries = 0;
      }

      // Scroll down to trigger lazy loading
      await page.evaluate(() => {
        window.scrollTo(0, document.body.scrollHeight);
      });
      
      // Random delay between scrolls
      await new Promise(r => setTimeout(r, scrollDelay + Math.random() * 500));
    }

    // Convert to array and limit
    const result = Array.from(following.values()).slice(0, limit);
    
    // Calculate final stats
    const mutualCount = result.filter(u => u.followsBack).length;
    const oneWayCount = result.length - mutualCount;
    
    console.log(`\nโœ… Scraped ${result.length} following`);
    console.log(`๐Ÿค Mutual: ${mutualCount} | โžก๏ธ  One-way: ${oneWayCount}`);
    
    return result;

  } finally {
    await browser.close();
  }
}

/**
 * Export data to JSON file
 * @param {Array} data - Data to export
 * @param {string} filename - Output filename
 */
async function exportJSON(data, filename) {
  const output = {
    exportedAt: new Date().toISOString(),
    count: data.length,
    mutualCount: data.filter(u => u.followsBack).length,
    oneWayCount: data.filter(u => !u.followsBack).length,
    data,
  };
  
  await fs.writeFile(filename, JSON.stringify(output, null, 2));
  console.log(`๐Ÿ’พ Saved JSON to ${filename}`);
}

/**
 * Export data to CSV file
 * @param {Array} data - Data to export
 * @param {string} filename - Output filename
 */
async function exportCSV(data, filename) {
  const headers = ['username', 'name', 'bio', 'verified', 'followsBack', 'avatar', 'scrapedAt'];
  
  const escapeCSV = (val) => {
    if (val === null || val === undefined) return '';
    if (typeof val === 'boolean') return val ? 'true' : 'false';
    if (typeof val === 'string') {
      // Escape quotes and wrap in quotes if contains comma, quote, or newline
      const escaped = val.replace(/"/g, '""').replace(/\n/g, ' ');
      if (escaped.includes(',') || escaped.includes('"') || escaped.includes('\n')) {
        return `"${escaped}"`;
      }
      return escaped;
    }
    return String(val);
  };
  
  const rows = data.map(row => 
    headers.map(h => escapeCSV(row[h])).join(',')
  );
  
  const csv = [headers.join(','), ...rows].join('\n');
  await fs.writeFile(filename, csv);
  console.log(`๐Ÿ’พ Saved CSV to ${filename}`);
}

/**
 * Export only mutual follows
 */
async function exportMutualOnly(data, baseFilename) {
  const mutuals = data.filter(u => u.followsBack);
  const jsonFile = `${baseFilename}-mutuals.json`;
  const csvFile = `${baseFilename}-mutuals.csv`;
  
  await exportJSON(mutuals, jsonFile);
  await exportCSV(mutuals, csvFile);
  
  console.log(`\n๐Ÿค Exported ${mutuals.length} mutual follows separately`);
}

/**
 * Export only one-way follows (they don't follow back)
 */
async function exportOneWayOnly(data, baseFilename) {
  const oneWay = data.filter(u => !u.followsBack);
  const jsonFile = `${baseFilename}-oneway.json`;
  const csvFile = `${baseFilename}-oneway.csv`;
  
  await exportJSON(oneWay, jsonFile);
  await exportCSV(oneWay, csvFile);
  
  console.log(`\nโžก๏ธ  Exported ${oneWay.length} one-way follows separately`);
}

// ============================================
// CLI Execution
// ============================================

const args = process.argv.slice(2);
const username = args[0];
const limit = parseInt(args[1]) || 500;
const exportType = args[2] || 'all'; // 'all', 'mutual', 'oneway'

if (!username) {
  console.log(`
โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•—
โ•‘           XActions - Following Scraper                       โ•‘
โ•‘           Author: nich (@nichxbt)                            โ•‘
โ•šโ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

Usage: node scrape-following.js <username> [limit] [export-type]

Arguments:
  username     Twitter/X username (without @)
  limit        Maximum accounts to scrape (default: 500)
  export-type  What to export: 'all', 'mutual', 'oneway' (default: all)

Examples:
  node scrape-following.js elonmusk 1000
  node scrape-following.js naval 500 mutual
  node scrape-following.js pmarca 200 oneway
  node scrape-following.js vitalikbuterin

Output Files:
  {username}-following-{date}.json     Full data as JSON
  {username}-following-{date}.csv      Full data as CSV
  {username}-following-{date}-mutuals.json   Mutual follows only
  {username}-following-{date}-oneway.json    One-way follows only

Environment Variables:
  X_AUTH_TOKEN    Your X auth token for private accounts
`);
  process.exit(1);
}

// Get auth token from environment if available
const authToken = process.env.X_AUTH_TOKEN || null;

// Run the scraper with progress indicator
console.log('');
console.log('โ•'.repeat(60));

scrapeFollowing(username, {
  limit,
  authToken,
  onProgress: ({ scraped, limit, percent, mutual }) => {
    process.stdout.write(`\r๐Ÿ“ˆ Progress: ${scraped}/${limit} (${percent}%) | ๐Ÿค Mutual: ${mutual}`);
  },
})
  .then(async (following) => {
    console.log('\n');
    console.log('โ•'.repeat(60));
    
    // Calculate detailed stats
    const mutualFollows = following.filter(u => u.followsBack);
    const oneWayFollows = following.filter(u => !u.followsBack);
    const verified = following.filter(u => u.verified);
    const withBio = following.filter(u => u.bio);
    
    // Print summary
    console.log('\n๐Ÿ“Š Summary:');
    console.log('โ”€'.repeat(40));
    console.log(`   Total scraped:    ${following.length}`);
    console.log(`   ๐Ÿค Mutual follows: ${mutualFollows.length} (${Math.round(mutualFollows.length / following.length * 100)}%)`);
    console.log(`   โžก๏ธ  One-way:        ${oneWayFollows.length} (${Math.round(oneWayFollows.length / following.length * 100)}%)`);
    console.log(`   โœ“ Verified:       ${verified.length}`);
    console.log(`   โœ“ With bio:       ${withBio.length}`);
    console.log('โ”€'.repeat(40));
    
    // Generate base filename
    const date = new Date().toISOString().split('T')[0];
    const baseFilename = `${username}-following-${date}`;
    
    // Export based on type
    console.log('\n๐Ÿ“ Exporting files...\n');
    
    await exportJSON(following, `${baseFilename}.json`);
    await exportCSV(following, `${baseFilename}.csv`);
    
    if (exportType === 'all' || exportType === 'mutual') {
      await exportMutualOnly(following, baseFilename);
    }
    
    if (exportType === 'all' || exportType === 'oneway') {
      await exportOneWayOnly(following, baseFilename);
    }
    
    // Show sample data
    console.log('\n๐Ÿ“‹ Sample mutual follows (first 5):');
    mutualFollows.slice(0, 5).forEach(f => {
      console.log(`   @${f.username} ${f.verified ? 'โœ“' : ''} - ${f.name || 'No name'}`);
    });
    
    console.log('\n๐Ÿ“‹ Sample one-way follows (first 5):');
    oneWayFollows.slice(0, 5).forEach(f => {
      console.log(`   @${f.username} ${f.verified ? 'โœ“' : ''} - ${f.name || 'No name'}`);
    });
    
    console.log('\nโœจ Done!\n');
  })
  .catch((error) => {
    console.error('\nโŒ Error:', error.message);
    process.exit(1);
  });

Usage

# Basic usage - scrape 500 following
node scrape-following.js elonmusk

# Scrape 1000 following
node scrape-following.js elonmusk 1000

# Scrape and export only mutual follows
node scrape-following.js naval 500 mutual

# Scrape and export only one-way follows
node scrape-following.js pmarca 200 oneway

# Use with auth token for private accounts
X_AUTH_TOKEN="your_token_here" node scrape-following.js username 500

Output

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

๐Ÿ” Scraping following for @elonmusk
๐Ÿ“Š Limit: 1000
๐Ÿ• Started: 2:30:45 PM

๐ŸŒ Navigating to https://x.com/elonmusk/following
๐Ÿ“ˆ Progress: 1000/1000 (100%) | ๐Ÿค Mutual: 342

โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•

โœ… Scraped 1000 following
๐Ÿค Mutual: 342 | โžก๏ธ  One-way: 658

๐Ÿ“Š Summary:
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
   Total scraped:    1000
   ๐Ÿค Mutual follows: 342 (34%)
   โžก๏ธ  One-way:        658 (66%)
   โœ“ Verified:       156
   โœ“ With bio:       891
โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€

๐Ÿ“ Exporting files...

๐Ÿ’พ Saved JSON to elonmusk-following-2026-01-01.json
๐Ÿ’พ Saved CSV to elonmusk-following-2026-01-01.csv

๐Ÿค Exported 342 mutual follows separately
๐Ÿ’พ Saved JSON to elonmusk-following-2026-01-01-mutuals.json
๐Ÿ’พ Saved CSV to elonmusk-following-2026-01-01-mutuals.csv

โžก๏ธ  Exported 658 one-way follows separately
๐Ÿ’พ Saved JSON to elonmusk-following-2026-01-01-oneway.json
๐Ÿ’พ Saved CSV to elonmusk-following-2026-01-01-oneway.csv

๐Ÿ“‹ Sample mutual follows (first 5):
   @kloris โœ“ - Kloris Wong
   @jason โœ“ - Jason Calacanis
   @pmarca โœ“ - Marc Andreessen
   @naval - Naval Ravikant
   @balajis โœ“ - Balaji Srinivasan

๐Ÿ“‹ Sample one-way follows (first 5):
   @SpaceX โœ“ - SpaceX
   @Tesla โœ“ - Tesla
   @boring_company - The Boring Company
   @neuralink โœ“ - Neuralink
   @xAI โœ“ - xAI

โœจ Done!

๐Ÿ’ก Use Cases

1. Finding Mutual Follows

Identify who has a reciprocal relationship:

// After scraping, filter mutual follows
const mutuals = following.filter(u => u.followsBack);
console.log(`Found ${mutuals.length} mutual follows!`);

// Export just the usernames
const mutualUsernames = mutuals.map(u => `@${u.username}`);

2. Analyzing Someone's Network

Understand who an influencer or competitor follows:

// Analyze verified accounts they follow
const verifiedFollowing = following.filter(u => u.verified);
console.log(`They follow ${verifiedFollowing.length} verified accounts`);

// Find accounts with specific keywords in bio
const cryptoAccounts = following.filter(u => 
  u.bio?.toLowerCase().includes('crypto') ||
  u.bio?.toLowerCase().includes('bitcoin') ||
  u.bio?.toLowerCase().includes('web3')
);

3. Find Potential Unfollows

Identify accounts that don't follow back (candidates for cleanup):

// Get accounts that don't follow back
const notFollowingBack = following.filter(u => !u.followsBack);

console.log(`${notFollowingBack.length} accounts don't follow back`);
console.log('Consider unfollowing:');
notFollowingBack.forEach(u => console.log(`  @${u.username}`));

4. Compare Following Lists

Track changes over time:

// compare-following.js
import fs from 'fs/promises';

async function compareFollowing(oldFile, newFile) {
  const oldData = JSON.parse(await fs.readFile(oldFile, 'utf-8'));
  const newData = JSON.parse(await fs.readFile(newFile, 'utf-8'));
  
  const oldSet = new Set(oldData.data.map(u => u.username));
  const newSet = new Set(newData.data.map(u => u.username));
  
  const unfollowed = oldData.data.filter(u => !newSet.has(u.username));
  const newlyFollowed = newData.data.filter(u => !oldSet.has(u.username));
  
  console.log(`\n๐Ÿ”ด Unfollowed ${unfollowed.length} accounts:`);
  unfollowed.forEach(u => console.log(`   @${u.username}`));
  
  console.log(`\n๐ŸŸข Newly followed ${newlyFollowed.length} accounts:`);
  newlyFollowed.forEach(u => console.log(`   @${u.username}`));
  
  // Check for mutual status changes
  const bothHave = newData.data.filter(u => oldSet.has(u.username));
  const lostMutual = bothHave.filter(newU => {
    const oldU = oldData.data.find(o => o.username === newU.username);
    return oldU?.followsBack && !newU.followsBack;
  });
  
  if (lostMutual.length > 0) {
    console.log(`\nโš ๏ธ  ${lostMutual.length} accounts stopped following back:`);
    lostMutual.forEach(u => console.log(`   @${u.username}`));
  }
}

// Usage: node compare-following.js old.json new.json

5. Build a Follow Recommendation Engine

Find accounts followed by multiple people you track:

// Scrape following for multiple accounts, then find overlap
const followingLists = {
  'naval': navalFollowing,
  'balajis': balajiFollowing,
  'pmarca': pmarcaFollowing,
};

const followCounts = new Map();

Object.values(followingLists).forEach(list => {
  list.forEach(user => {
    const count = followCounts.get(user.username) || 0;
    followCounts.set(user.username, count + 1);
  });
});

// Find accounts followed by all three
const followedByAll = Array.from(followCounts.entries())
  .filter(([_, count]) => count === 3)
  .map(([username]) => username);

console.log('Accounts followed by all three:', followedByAll);

๐Ÿ“Š Performance Tips

Following CountEstimated Time
100~30 seconds
500~2-3 minutes
1,000~5-6 minutes
5,000~20-25 minutes
10,000+~45+ minutes

Optimizations

// For large accounts, increase scroll delay to avoid rate limits
const following = await scrapeFollowing('largAccount', {
  limit: 10000,
  scrollDelay: 2500, // Slower, safer
});

// For faster scraping of smaller accounts
const following = await scrapeFollowing('smallAccount', {
  limit: 200,
  scrollDelay: 1000, // Faster
});

Rate Limit Tips

  • โฑ๏ธ Don't scrape the same account more than once per hour
  • ๐Ÿ”„ Add 5-10 second delays between different account scrapes
  • ๐Ÿ” Use auth token for higher limits
  • ๐ŸŒ™ Run large scrapes during off-peak hours

๐ŸŒ Website Alternative

Don't want to code? Use xactions.app:

  1. ๐Ÿ” Login with your X account
  2. ๐Ÿ‘ค Enter any public username
  3. ๐Ÿ“‹ Click "Scrape Following"
  4. โณ Wait for completion
  5. ๐Ÿ“ฅ Download as CSV or JSON
  6. ๐Ÿค Filter by mutual/one-way directly in the UI

Features on xactions.app:

  • โœ… No coding required
  • โœ… Cloud-based processing
  • โœ… Automatic mutual detection
  • โœ… Export to multiple formats
  • โœ… Schedule recurring scrapes
  • โœ… Compare historical data


Built with โค๏ธ by @nichxbt
Part of the XActions toolkit