DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build an Influencer Fake Follower Detector with Node.js

That influencer with 500K followers wants $5,000 for a sponsored post.

Sounds reasonable. Until you realize 60% of their followers are bots.

Fake followers are everywhere. One study found 45% of Instagram accounts show signs of fraudulent activity. Brands lose billions paying for audiences that don't exist.

In this tutorial, we'll build a Fake Follower Detector that:

  1. Analyzes any TikTok or Instagram creator
  2. Calculates engagement authenticity score
  3. Identifies red flags in audience demographics

Stop paying for ghost audiences.

The Math Behind Fake Detection

Detecting fake followers isn't magic. It's pattern recognition:

Red Flag #1: Engagement Rate

  • Real accounts: 1-5% engagement rate
  • Bought followers: 0.1-0.5% (followers don't engage)
  • Engagement pods: 10%+ (suspiciously high)

Red Flag #2: Follower/Following Ratio

  • Real influencers: High followers, low following
  • Fake accounts: Often follow thousands to get follow-backs

Red Flag #3: Growth Spikes

  • Real growth: Gradual, with viral spikes
  • Bought followers: Sudden jumps of exactly 10K, 50K, 100K

Red Flag #4: Comment Quality

  • Real: Specific, relevant comments
  • Fake: "Nice! πŸ”₯" "Love this ❀️" "Great content!"

The Stack

  • Node.js: Runtime
  • SociaVault API: To fetch creator data
  • OpenAI API: To analyze comment authenticity

Step 1: Setup

mkdir fake-follower-detector
cd fake-follower-detector
npm init -y
npm install axios openai dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
Enter fullscreen mode Exit fullscreen mode

Step 2: Fetch Creator Profile

Let's start with TikTok. The /v1/scrape/tiktok/profile endpoint gives us followers, likes, and engagement data.

Create index.js:

require('dotenv').config();
const axios = require('axios');
const OpenAI = require('openai');

const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const SOCIAVAULT_BASE = 'https://api.sociavault.com';

async function getTikTokProfile(handle) {
  console.log(`πŸ“₯ Fetching @${handle}'s profile...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/profile`, {
      params: { handle },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    const data = response.data.data;

    return {
      handle: data.uniqueId || data.handle,
      displayName: data.nickname || data.displayName,
      followers: data.followerCount || data.fans || 0,
      following: data.followingCount || data.following || 0,
      likes: data.heartCount || data.heart || data.likes || 0,
      videos: data.videoCount || data.video || 0,
      verified: data.verified || false,
      bio: data.signature || data.bio || ''
    };
  } catch (error) {
    console.error('Error fetching profile:', error.message);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Get Recent Videos with Engagement

We need actual engagement data from their content:

async function getRecentVideos(handle) {
  console.log(`πŸ“₯ Fetching recent videos...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/videos`, {
      params: { handle, amount: 30 },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    const videos = response.data.data || [];

    return videos.map(v => ({
      id: v.id,
      description: v.desc || v.description,
      likes: v.diggCount || v.stats?.diggCount || 0,
      comments: v.commentCount || v.stats?.commentCount || 0,
      shares: v.shareCount || v.stats?.shareCount || 0,
      views: v.playCount || v.stats?.playCount || 0,
      created: v.createTime
    }));
  } catch (error) {
    console.error('Error fetching videos:', error.message);
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Calculate Engagement Metrics

This is where the magic happens:

function calculateEngagementMetrics(profile, videos) {
  // Basic engagement rate (avg across videos)
  const engagementRates = videos.map(v => {
    if (v.views === 0) return 0;
    return ((v.likes + v.comments + v.shares) / v.views) * 100;
  });

  const avgEngagementRate = engagementRates.reduce((a, b) => a + b, 0) / engagementRates.length;

  // Follower to engagement ratio
  const totalEngagement = videos.reduce((sum, v) => sum + v.likes + v.comments, 0);
  const engagementPerFollower = totalEngagement / profile.followers;

  // Follower/Following ratio
  const followerRatio = profile.following > 0 
    ? profile.followers / profile.following 
    : profile.followers;

  // View consistency (standard deviation)
  const views = videos.map(v => v.views);
  const avgViews = views.reduce((a, b) => a + b, 0) / views.length;
  const variance = views.reduce((sum, v) => sum + Math.pow(v - avgViews, 2), 0) / views.length;
  const viewStdDev = Math.sqrt(variance);
  const viewConsistency = avgViews > 0 ? (viewStdDev / avgViews) : 0;

  // Comments to likes ratio (bots like but don't comment)
  const totalLikes = videos.reduce((sum, v) => sum + v.likes, 0);
  const totalComments = videos.reduce((sum, v) => sum + v.comments, 0);
  const commentToLikeRatio = totalLikes > 0 ? totalComments / totalLikes : 0;

  return {
    avgEngagementRate: avgEngagementRate.toFixed(2),
    engagementPerFollower: engagementPerFollower.toFixed(4),
    followerRatio: followerRatio.toFixed(1),
    viewConsistency: viewConsistency.toFixed(2),
    commentToLikeRatio: commentToLikeRatio.toFixed(3),
    avgViews: Math.round(avgViews),
    totalVideosAnalyzed: videos.length
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Fetch and Analyze Comments

Bot comments have patterns. Let's detect them:

async function getVideoComments(videoUrl) {
  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/comments`, {
      params: { url: videoUrl },
      headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
    });

    return response.data.data || [];
  } catch (error) {
    console.error('Error fetching comments:', error.message);
    return [];
  }
}

async function analyzeCommentAuthenticity(comments) {
  if (comments.length === 0) return { score: 0, analysis: 'No comments to analyze' };

  console.log(`πŸ€– Analyzing ${comments.length} comments...`);

  const sample = comments.slice(0, 50);

  const prompt = `
    Analyze these TikTok comments for signs of fake/bot engagement.

    Bot comment patterns:
    - Generic praise: "Nice!" "Love this" "Great content" "πŸ”₯πŸ”₯πŸ”₯"
    - Unrelated to video content
    - Repetitive emoji-only comments
    - Promotional spam with links
    - Very short, low-effort comments

    Real comment patterns:
    - Specific references to video content
    - Questions about the topic
    - Personal stories or opinions
    - Genuine reactions with context

    Return JSON:
    {
      "authenticityScore": 0-100 (100 = all real, 0 = all fake),
      "suspiciousComments": number of likely bot comments,
      "realComments": number of likely genuine comments,
      "redFlags": ["list of specific concerns"],
      "examples": {
        "suspicious": ["2 example bot-like comments"],
        "genuine": ["2 example real comments"]
      },
      "verdict": "one sentence assessment"
    }

    Comments to analyze:
    ${JSON.stringify(sample.map(c => c.text || c.comment))}
  `;

  const completion = await openai.chat.completions.create({
    model: 'gpt-4o-mini',
    messages: [{ role: 'user', content: prompt }],
    response_format: { type: 'json_object' }
  });

  return JSON.parse(completion.choices[0].message.content);
}
Enter fullscreen mode Exit fullscreen mode

Step 6: The Authenticity Score Algorithm

Let's combine everything into a final score:

function calculateAuthenticityScore(metrics, commentAnalysis) {
  let score = 100;
  const redFlags = [];

  // Engagement rate check
  const engRate = parseFloat(metrics.avgEngagementRate);
  if (engRate < 0.5) {
    score -= 25;
    redFlags.push(`Very low engagement rate (${engRate}%) - possible fake followers`);
  } else if (engRate > 15) {
    score -= 15;
    redFlags.push(`Unusually high engagement rate (${engRate}%) - possible engagement pods`);
  }

  // Follower ratio check
  const ratio = parseFloat(metrics.followerRatio);
  if (ratio < 2) {
    score -= 10;
    redFlags.push(`Low follower/following ratio (${ratio}x) - follow-for-follow pattern`);
  }

  // Comment to like ratio
  const ctr = parseFloat(metrics.commentToLikeRatio);
  if (ctr < 0.01) {
    score -= 20;
    redFlags.push(`Very few comments relative to likes (${(ctr * 100).toFixed(1)}%) - bots like but don't comment`);
  }

  // View consistency (suspicious if too consistent)
  const consistency = parseFloat(metrics.viewConsistency);
  if (consistency < 0.3) {
    score -= 10;
    redFlags.push(`Suspiciously consistent view counts - possible view botting`);
  }

  // Comment authenticity (from AI analysis)
  if (commentAnalysis.authenticityScore) {
    const commentScore = commentAnalysis.authenticityScore;
    if (commentScore < 50) {
      score -= 25;
      redFlags.push(`${100 - commentScore}% of comments appear to be bot-generated`);
    } else if (commentScore < 70) {
      score -= 10;
      redFlags.push(`${100 - commentScore}% of comments show bot-like patterns`);
    }
  }

  // Add red flags from AI
  if (commentAnalysis.redFlags) {
    redFlags.push(...commentAnalysis.redFlags);
  }

  return {
    score: Math.max(0, score),
    grade: getGrade(Math.max(0, score)),
    redFlags
  };
}

function getGrade(score) {
  if (score >= 90) return { letter: 'A', label: 'Highly Authentic', emoji: 'βœ…' };
  if (score >= 75) return { letter: 'B', label: 'Mostly Authentic', emoji: 'πŸ‘' };
  if (score >= 60) return { letter: 'C', label: 'Some Concerns', emoji: '⚠️' };
  if (score >= 40) return { letter: 'D', label: 'Likely Inflated', emoji: '🚨' };
  return { letter: 'F', label: 'Highly Suspicious', emoji: '❌' };
}
Enter fullscreen mode Exit fullscreen mode

Step 7: The Full Analysis

Tie it all together:

async function analyzeCreator(handle) {
  console.log('\nπŸ” FAKE FOLLOWER DETECTOR\n');
  console.log('═══════════════════════════════════════\n');

  // 1. Get profile
  const profile = await getTikTokProfile(handle);
  if (!profile) {
    console.log('Could not fetch profile.');
    return;
  }

  console.log(`πŸ‘€ @${profile.handle} ${profile.verified ? 'βœ“' : ''}`);
  console.log(`   ${profile.displayName}`);
  console.log(`   ${profile.followers.toLocaleString()} followers | ${profile.following.toLocaleString()} following`);
  console.log(`   ${profile.likes.toLocaleString()} total likes | ${profile.videos} videos\n`);

  // 2. Get videos
  const videos = await getRecentVideos(handle);
  if (videos.length === 0) {
    console.log('Could not fetch videos.');
    return;
  }

  // 3. Calculate metrics
  const metrics = calculateEngagementMetrics(profile, videos);

  console.log('πŸ“Š ENGAGEMENT METRICS');
  console.log('───────────────────────────────────────');
  console.log(`  Avg Engagement Rate: ${metrics.avgEngagementRate}%`);
  console.log(`  Avg Views per Video: ${metrics.avgViews.toLocaleString()}`);
  console.log(`  Follower Ratio: ${metrics.followerRatio}x`);
  console.log(`  Comment/Like Ratio: ${(parseFloat(metrics.commentToLikeRatio) * 100).toFixed(1)}%`);
  console.log(`  View Consistency: ${metrics.viewConsistency}`);
  console.log(`  Videos Analyzed: ${metrics.totalVideosAnalyzed}\n`);

  // 4. Analyze comments from top video
  const topVideo = videos.reduce((max, v) => v.views > max.views ? v : max, videos[0]);
  const videoUrl = `https://www.tiktok.com/@${handle}/video/${topVideo.id}`;
  const comments = await getVideoComments(videoUrl);
  const commentAnalysis = await analyzeCommentAuthenticity(comments);

  console.log('πŸ’¬ COMMENT ANALYSIS');
  console.log('───────────────────────────────────────');
  console.log(`  Authenticity Score: ${commentAnalysis.authenticityScore}/100`);
  console.log(`  Genuine Comments: ~${commentAnalysis.realComments}`);
  console.log(`  Suspicious Comments: ~${commentAnalysis.suspiciousComments}`);
  console.log(`  Verdict: ${commentAnalysis.verdict}\n`);

  // 5. Calculate final score
  const authenticity = calculateAuthenticityScore(metrics, commentAnalysis);

  console.log('═══════════════════════════════════════');
  console.log('🎯 AUTHENTICITY VERDICT');
  console.log('═══════════════════════════════════════\n');

  console.log(`  ${authenticity.grade.emoji} GRADE: ${authenticity.grade.letter} - ${authenticity.grade.label}`);
  console.log(`  πŸ“Š SCORE: ${authenticity.score}/100\n`);

  if (authenticity.redFlags.length > 0) {
    console.log('  🚩 RED FLAGS:');
    authenticity.redFlags.forEach((flag, i) => {
      console.log(`     ${i + 1}. ${flag}`);
    });
  } else {
    console.log('  βœ… No significant red flags detected');
  }

  // 6. Recommendation
  console.log('\nπŸ“ RECOMMENDATION:');
  if (authenticity.score >= 75) {
    console.log('   This creator appears legitimate. Safe to collaborate.');
  } else if (authenticity.score >= 50) {
    console.log('   Proceed with caution. Ask for media kit and past campaign results.');
  } else {
    console.log('   High risk of fake engagement. Consider other creators.');
  }

  return { profile, metrics, commentAnalysis, authenticity };
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Run It

async function main() {
  const handle = process.argv[2] || 'charlidamelio';
  await analyzeCreator(handle);
}

main();
Enter fullscreen mode Exit fullscreen mode

Run with:

node index.js khaby.lame
Enter fullscreen mode Exit fullscreen mode

Sample Output

πŸ” FAKE FOLLOWER DETECTOR
═══════════════════════════════════════

πŸ“₯ Fetching @khaby.lame's profile...
πŸ“₯ Fetching recent videos...

πŸ‘€ @khaby.lame βœ“
   Khabane lame
   162,500,000 followers | 78 following
   2,400,000,000 total likes | 1,200 videos

πŸ“Š ENGAGEMENT METRICS
───────────────────────────────────────
  Avg Engagement Rate: 3.2%
  Avg Views per Video: 45,000,000
  Follower Ratio: 2,083,333.3x
  Comment/Like Ratio: 2.8%
  View Consistency: 1.4
  Videos Analyzed: 30

πŸ€– Analyzing 50 comments...

πŸ’¬ COMMENT ANALYSIS
───────────────────────────────────────
  Authenticity Score: 78/100
  Genuine Comments: ~35
  Suspicious Comments: ~15
  Verdict: Mostly authentic engagement with some generic comments

═══════════════════════════════════════
🎯 AUTHENTICITY VERDICT
═══════════════════════════════════════

  βœ… GRADE: A - Highly Authentic
  πŸ“Š SCORE: 92/100

  βœ… No significant red flags detected

πŸ“ RECOMMENDATION:
   This creator appears legitimate. Safe to collaborate.
Enter fullscreen mode Exit fullscreen mode

Adding Instagram Support

The same logic works for Instagram. Just swap the endpoints:

async function getInstagramProfile(handle) {
  const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/profile`, {
    params: { handle },
    headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
  });

  const data = response.data.data;
  return {
    handle: data.username,
    displayName: data.full_name,
    followers: data.follower_count,
    following: data.following_count,
    posts: data.media_count,
    verified: data.is_verified,
    bio: data.biography
  };
}

async function getInstagramPosts(handle) {
  const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/posts`, {
    params: { handle },
    headers: { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` }
  });

  return response.data.data || [];
}
Enter fullscreen mode Exit fullscreen mode

Bulk Analysis for Agencies

If you're vetting multiple creators:

async function analyzeMultipleCreators(handles) {
  const results = [];

  for (const handle of handles) {
    console.log(`\n${'─'.repeat(50)}`);
    const result = await analyzeCreator(handle);
    results.push({ handle, ...result });

    // Be nice to the API
    await new Promise(r => setTimeout(r, 2000));
  }

  // Summary table
  console.log('\n\nπŸ“Š SUMMARY');
  console.log('═══════════════════════════════════════════════════');
  console.log('Handle'.padEnd(20) + 'Score'.padEnd(10) + 'Grade'.padEnd(10) + 'Followers');
  console.log('─'.repeat(55));

  results
    .sort((a, b) => b.authenticity.score - a.authenticity.score)
    .forEach(r => {
      console.log(
        `@${r.handle}`.padEnd(20) +
        `${r.authenticity.score}/100`.padEnd(10) +
        r.authenticity.grade.letter.padEnd(10) +
        r.profile.followers.toLocaleString()
      );
    });

  return results;
}

// Usage
analyzeMultipleCreators([
  'charlidamelio',
  'khaby.lame',
  'addisonre',
  'suspicious_account_123'
]);
Enter fullscreen mode Exit fullscreen mode

What This Tool Would Cost Elsewhere

Influencer authenticity tools charge:

  • HypeAuditor: $299/month (limited reports)
  • Social Blade Pro: $3.99/month (basic stats only)
  • Influencer Marketing Hub: $500+/month (full vetting)

Your version:

  • SociaVault credits: ~$0.05 per analysis
  • OpenAI API: ~$0.01 per analysis
  • Total: ~$0.06 per creator

At that price, you can afford to vet every influencer before paying them.

Get Started

  1. Get your SociaVault API Key
  2. Add your OpenAI key
  3. Start exposing fake influencers

Your marketing budget deserves real audiences.


The fake follower industry is a $1.3 billion problem. Be part of the solution.

Top comments (0)