📊 Audience Demographics Analyzer

Analytics src
v1.0.0 Updated: 2026-02-24 276 lines by @nichxbt

Analyze your followers' demographics by scraping

How to Use

  1. Go to: https://x.com/YOUR_USERNAME/followers
  2. Open DevTools Console (F12)
  3. Paste and run
  4. Auto-scrolls to collect follower data

Default Configuration

const CONFIG = {
    maxFollowers: 200,
    scrollRounds: 10,
    scrollDelay: 2000,
    exportResults: true,
  };

Full Script

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

/**
 * ============================================================
 * 👥 Audience Demographics Analyzer — Production Grade
 * ============================================================
 *
 * @name        audienceDemographics.js
 * @description Analyze your followers' demographics by scraping
 *              visible profile data: bio keywords, locations,
 *              account age, verified status, engagement level,
 *              and content niches. Builds a comprehensive
 *              audience profile without API access.
 * @author      nichxbt (https://x.com/nichxbt)
 * @version     1.0.0
 * @date        2026-02-24
 * @repository  https://github.com/nirholas/XActions
 *
 * ============================================================
 * 📋 USAGE:
 *
 * 1. Go to: https://x.com/YOUR_USERNAME/followers
 * 2. Open DevTools Console (F12)
 * 3. Paste and run
 * 4. Auto-scrolls to collect follower data
 * ============================================================
 */
(() => {
  'use strict';

  const CONFIG = {
    maxFollowers: 200,
    scrollRounds: 10,
    scrollDelay: 2000,
    exportResults: true,
  };

  const sleep = (ms) => new Promise(r => setTimeout(r, ms));

  // ── Niche Detection Keywords ───────────────────────────────
  const NICHES = {
    'Tech/Dev':      ['developer', 'engineer', 'coder', 'software', 'frontend', 'backend', 'fullstack', 'devops', 'ml', 'ai', 'data science', 'blockchain', 'web3', 'crypto', 'python', 'javascript', 'rust', 'golang', 'react', 'nextjs', 'saas', 'startup', 'coding'],
    'Design':        ['designer', 'ux', 'ui', 'figma', 'creative', 'graphic', 'brand', 'illustration', 'art director', 'product design', 'visual'],
    'Marketing':     ['marketing', 'growth', 'seo', 'content', 'copywriting', 'social media', 'brand', 'digital marketing', 'performance', 'ads', 'ppc', 'funnel'],
    'Finance':       ['investor', 'trading', 'finance', 'fintech', 'stocks', 'bitcoin', 'defi', 'yield', 'portfolio', 'hedge fund', 'vc', 'venture capital'],
    'Founder/CEO':   ['founder', 'ceo', 'co-founder', 'entrepreneur', 'bootstrapped', 'building', 'shipped', 'solopreneur', 'indie hacker', 'cto', 'coo'],
    'Creator':       ['creator', 'youtuber', 'streamer', 'podcaster', 'content creator', 'writer', 'blogger', 'newsletter', 'substack', 'author'],
    'Education':     ['teacher', 'professor', 'phd', 'student', 'university', 'learning', 'education', 'academic', 'researcher', 'school'],
    'Health':        ['fitness', 'health', 'wellness', 'doctor', 'nurse', 'nutrition', 'yoga', 'mental health', 'gym', 'coach'],
    'Music/Art':     ['musician', 'artist', 'producer', 'dj', 'rapper', 'singer', 'band', 'composer', 'painter', 'photography'],
    'Politics/News': ['journalist', 'reporter', 'political', 'activist', 'media', 'news', 'democracy', 'policy', 'government'],
    'Gaming':        ['gamer', 'gaming', 'esports', 'twitch', 'streamer', 'game dev', 'indie game', 'nft gaming'],
  };

  // ── Location normalization ─────────────────────────────────
  const REGION_MAP = {
    'US': ['united states', 'usa', 'us', 'new york', 'san francisco', 'sf', 'la', 'los angeles', 'chicago', 'miami', 'austin', 'seattle', 'boston', 'denver', 'portland', 'atlanta', 'houston', 'dallas', 'dc', 'washington', 'california', 'texas', 'florida', 'new jersey', 'bay area', 'silicon valley', 'nyc'],
    'UK': ['united kingdom', 'uk', 'london', 'manchester', 'birmingham', 'england', 'scotland', 'wales', 'bristol'],
    'Canada': ['canada', 'toronto', 'vancouver', 'montreal', 'ottawa', 'calgary'],
    'India': ['india', 'mumbai', 'bangalore', 'bengaluru', 'delhi', 'hyderabad', 'chennai', 'pune', 'kolkata'],
    'Europe': ['germany', 'france', 'spain', 'italy', 'netherlands', 'berlin', 'paris', 'amsterdam', 'barcelona', 'portugal', 'lisbon', 'dublin', 'ireland', 'sweden', 'stockholm', 'zurich', 'switzerland'],
    'LATAM': ['brazil', 'brazil', 'mexico', 'argentina', 'colombia', 'chile', 'são paulo', 'bogota', 'buenos aires'],
    'Asia-Pacific': ['japan', 'tokyo', 'singapore', 'australia', 'sydney', 'melbourne', 'korea', 'seoul', 'hong kong', 'philippines', 'indonesia', 'vietnam', 'thailand', 'bangkok'],
    'MENA': ['dubai', 'uae', 'saudi', 'egypt', 'israel', 'tel aviv', 'nigeria', 'lagos', 'south africa', 'kenya', 'nairobi'],
  };

  const detectRegion = (location) => {
    if (!location) return 'Unknown';
    const loc = location.toLowerCase();
    for (const [region, keywords] of Object.entries(REGION_MAP)) {
      if (keywords.some(kw => loc.includes(kw))) return region;
    }
    return 'Other';
  };

  const detectNiches = (bio) => {
    if (!bio) return [];
    const bioLower = bio.toLowerCase();
    const matches = [];
    for (const [niche, keywords] of Object.entries(NICHES)) {
      if (keywords.some(kw => bioLower.includes(kw))) matches.push(niche);
    }
    return matches;
  };

  // ── Collect followers ──────────────────────────────────────
  const collectFollowers = async () => {
    const followers = new Map();

    for (let round = 0; round < CONFIG.scrollRounds && followers.size < CONFIG.maxFollowers; round++) {
      const cells = document.querySelectorAll('[data-testid="UserCell"]');

      for (const cell of cells) {
        if (followers.size >= CONFIG.maxFollowers) break;

        const link = cell.querySelector('a[href^="/"][role="link"]') || cell.querySelector('a[href^="/"]');
        if (!link) continue;
        const match = (link.getAttribute('href') || '').match(/^\/([A-Za-z0-9_]+)/);
        if (!match || ['home', 'explore', 'notifications', 'messages', 'i'].includes(match[1])) continue;

        const username = match[1].toLowerCase();
        if (followers.has(username)) continue;

        // Display name
        const nameSpans = cell.querySelectorAll('a[href^="/"] span');
        const displayName = nameSpans.length > 0 ? nameSpans[0].textContent.trim() : match[1];

        // Bio
        const textEls = cell.querySelectorAll('[dir="auto"]');
        let bio = '';
        for (const el of textEls) {
          const text = el.textContent.trim();
          if (text.length > 20 && !text.startsWith('@')) { bio = text; break; }
        }

        // Verified badge
        const verified = !!cell.querySelector('[data-testid="icon-verified"]') || !!cell.querySelector('svg[aria-label="Verified"]');

        // Default avatar check
        const avatar = cell.querySelector('img[src*="profile_images"]');
        const hasCustomAvatar = !!avatar && !avatar.src.includes('default_profile');

        followers.set(username, {
          username: match[1],
          displayName,
          bio: bio.slice(0, 300),
          verified,
          hasCustomAvatar,
          location: '', // Not always visible in follower list
        });
      }

      console.log(`   📜 Round ${round + 1}: ${followers.size} followers`);
      window.scrollTo(0, document.body.scrollHeight);
      await sleep(CONFIG.scrollDelay);
    }

    return [...followers.values()];
  };

  const run = async () => {
    const W = 60;
    console.log('╔' + '═'.repeat(W) + '╗');
    console.log('║  👥 AUDIENCE DEMOGRAPHICS ANALYZER' + ' '.repeat(W - 36) + '║');
    console.log('║  by nichxbt — v1.0' + ' '.repeat(W - 21) + '║');
    console.log('╚' + '═'.repeat(W) + '╝');

    if (!window.location.href.includes('/followers')) {
      console.error('❌ Navigate to x.com/YOUR_USERNAME/followers first!');
      return;
    }

    console.log(`\n📊 Collecting up to ${CONFIG.maxFollowers} followers...\n`);
    const followers = await collectFollowers();

    if (followers.length < 5) {
      console.error('❌ Need at least 5 followers to analyze.');
      return;
    }

    // ── Niche Analysis ──────────────────────────────────────
    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  🎯 AUDIENCE NICHE BREAKDOWN');
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    const nicheCount = {};
    let nicheless = 0;

    for (const f of followers) {
      const niches = detectNiches(f.bio);
      f.niches = niches;
      if (niches.length === 0) nicheless++;
      for (const n of niches) nicheCount[n] = (nicheCount[n] || 0) + 1;
    }

    const sortedNiches = Object.entries(nicheCount).sort((a, b) => b[1] - a[1]);
    const total = followers.length;

    console.log('');
    for (const [niche, count] of sortedNiches) {
      const pct = ((count / total) * 100).toFixed(1);
      const bar = '█'.repeat(Math.round(count / total * 30));
      console.log(`  ${niche.padEnd(18)} ${String(count).padStart(3)} (${pct.padStart(5)}%) ${bar}`);
    }
    console.log(`  ${'Unidentified'.padEnd(18)} ${String(nicheless).padStart(3)} (${((nicheless / total) * 100).toFixed(1).padStart(5)}%)`);

    // ── Verified Status ─────────────────────────────────────
    const verified = followers.filter(f => f.verified).length;
    const verifiedPct = ((verified / total) * 100).toFixed(1);

    console.log('\n━━━ ✅ VERIFICATION STATUS ━━━');
    console.log(`  Verified:     ${verified} (${verifiedPct}%)`);
    console.log(`  Non-verified: ${total - verified} (${(100 - verifiedPct).toFixed(1)}%)`);

    // ── Avatar Analysis ─────────────────────────────────────
    const customAvatar = followers.filter(f => f.hasCustomAvatar).length;
    const defaultAvatar = total - customAvatar;
    console.log('\n━━━ 🖼️ PROFILE QUALITY ━━━');
    console.log(`  Custom avatar:  ${customAvatar} (${((customAvatar / total) * 100).toFixed(1)}%)`);
    console.log(`  Default avatar: ${defaultAvatar} (${((defaultAvatar / total) * 100).toFixed(1)}%)`);

    const hasBio = followers.filter(f => f.bio && f.bio.length > 10).length;
    console.log(`  Has bio:        ${hasBio} (${((hasBio / total) * 100).toFixed(1)}%)`);
    console.log(`  No bio:         ${total - hasBio} (${(((total - hasBio) / total) * 100).toFixed(1)}%)`);

    // ── Bot Likelihood ──────────────────────────────────────
    const suspiciousCount = followers.filter(f => !f.hasCustomAvatar && (!f.bio || f.bio.length < 10)).length;
    console.log('\n━━━ 🤖 BOT LIKELIHOOD ━━━');
    console.log(`  Suspicious (no avatar + no bio): ${suspiciousCount} (${((suspiciousCount / total) * 100).toFixed(1)}%)`);

    if (suspiciousCount > total * 0.3) {
      console.log('  ⚠️ High proportion of suspicious accounts. Consider using removeFollowers.js with smart mode.');
    }

    // ── Common Words in Bios ────────────────────────────────
    console.log('\n━━━ 📝 TOP BIO KEYWORDS ━━━');
    const wordFreq = {};
    const stopWords = new Set(['the', 'and', 'for', 'that', 'with', 'this', 'from', 'are', 'was', 'not', 'but', 'all', 'they', 'have', 'had', 'has', 'you', 'your', 'who', 'what', 'just', 'about', 'can', 'will', 'one', 'out', 'its', 'also', 'into', 'over']);

    for (const f of followers) {
      if (!f.bio) continue;
      const words = f.bio.toLowerCase().replace(/[^\w\s]/g, '').split(/\s+/);
      for (const w of words) {
        if (w.length > 3 && !stopWords.has(w)) wordFreq[w] = (wordFreq[w] || 0) + 1;
      }
    }

    const topWords = Object.entries(wordFreq).sort((a, b) => b[1] - a[1]).slice(0, 15);
    for (const [word, count] of topWords) {
      console.log(`  "${word}" — ${count}x`);
    }

    // ── Insights ────────────────────────────────────────────
    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
    console.log('  💡 INSIGHTS & RECOMMENDATIONS');
    console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');

    if (sortedNiches.length > 0) {
      const topNiche = sortedNiches[0];
      console.log(`\n  🎯 Your primary audience is "${topNiche[0]}" (${topNiche[1]} followers).`);
      if (sortedNiches.length > 1) {
        console.log(`     Secondary: "${sortedNiches[1][0]}" (${sortedNiches[1][1]})`);
      }
      console.log('     → Tailor content to these niches for better engagement.');
    }

    if (suspiciousCount > total * 0.2) {
      console.log(`\n  🧹 ${suspiciousCount} potentially inactive/bot followers detected.`);
      console.log('     → Use removeFollowers.js (smart mode) to clean up.');
    }

    if (verified / total < 0.05) {
      console.log('\n  📊 Low verified follower ratio. Consider engaging with verified accounts.');
    }

    console.log('\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n');

    if (CONFIG.exportResults) {
      const data = {
        summary: {
          total, verified, defaultAvatar, nicheless, suspicious: suspiciousCount,
          topNiches: sortedNiches.slice(0, 5),
          topKeywords: topWords.slice(0, 10),
        },
        followers,
        analyzedAt: new Date().toISOString(),
      };
      const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
      const a = document.createElement('a'); a.href = URL.createObjectURL(blob);
      a.download = `xactions-demographics-${new Date().toISOString().slice(0, 10)}.json`;
      document.body.appendChild(a); a.click(); a.remove();
      console.log('📥 Full demographics exported as JSON.');
    }
  };

  run();
})();

⚡ More XActions Scripts

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

Browse All Scripts