DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build an Engagement Rate Calculator That Actually Works

"What's your engagement rate?"

Every brand asks. Most creators answer wrong.

Some divide likes by followers. Others include comments. Few account for the platform differences that make comparing a 3% on TikTok to a 3% on Instagram meaningless.

In this tutorial, we'll build an Engagement Rate Calculator that:

  1. Calculates true engagement rates across platforms
  2. Benchmarks against industry standards
  3. Grades creators (A to F) based on real data

No more guessing if 2.5% is good or bad.

Why Engagement Rate Math Is Broken

The problem: there's no standard formula.

Common formulas (all different):

Method 1: (Likes + Comments) / Followers × 100
Method 2: (Likes + Comments + Shares) / Followers × 100
Method 3: (Likes + Comments) / (Followers × Posts) × 100
Method 4: (Total Engagements) / Reach × 100
Enter fullscreen mode Exit fullscreen mode

Worse: platforms have different baselines.

Average engagement rates (2024):

  • TikTok: 4-8%
  • Instagram Reels: 1.5-3%
  • Instagram Posts: 1-2%
  • Twitter/X: 0.5-1%
  • YouTube: 2-5% (comments + likes / views)
  • LinkedIn: 2-5%

A 2% engagement on TikTok is bad. On Instagram, it's excellent.

The Stack

  • Node.js: Runtime
  • SociaVault API: To fetch creator stats
  • No AI needed: This is pure math

Step 1: Setup

mkdir engagement-calculator
cd engagement-calculator
npm init -y
npm install axios dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
Enter fullscreen mode Exit fullscreen mode

Step 2: Platform-Specific Fetchers

Create index.js:

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

const SOCIAVAULT_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };

// Industry benchmarks (2024 data)
const BENCHMARKS = {
  tiktok: {
    excellent: 8,
    good: 5,
    average: 3,
    belowAverage: 1.5,
    poor: 0
  },
  instagram: {
    excellent: 4,
    good: 2.5,
    average: 1.5,
    belowAverage: 0.8,
    poor: 0
  },
  twitter: {
    excellent: 2,
    good: 1,
    average: 0.5,
    belowAverage: 0.2,
    poor: 0
  },
  youtube: {
    excellent: 6,
    good: 4,
    average: 2,
    belowAverage: 1,
    poor: 0
  },
  linkedin: {
    excellent: 5,
    good: 3,
    average: 2,
    belowAverage: 1,
    poor: 0
  }
};

// TikTok
async function getTikTokData(handle) {
  console.log(`📥 Fetching TikTok data for @${handle}...`);

  try {
    // Get profile
    const profileRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/profile`, {
      params: { handle },
      headers
    });

    const profile = profileRes.data.data;

    // Get recent videos
    const videosRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/videos`, {
      params: { handle, amount: 30 },
      headers
    });

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

    return {
      platform: 'tiktok',
      handle,
      followers: profile.followerCount || profile.fans || 0,
      totalLikes: profile.heartCount || profile.heart || 0,
      videos: videos.map(v => ({
        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
      }))
    };
  } catch (error) {
    console.error('TikTok error:', error.message);
    return null;
  }
}

// Instagram
async function getInstagramData(handle) {
  console.log(`📥 Fetching Instagram data for @${handle}...`);

  try {
    // Get profile
    const profileRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/profile`, {
      params: { handle },
      headers
    });

    const profile = profileRes.data.data;

    // Get recent posts
    const postsRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/posts`, {
      params: { handle },
      headers
    });

    const posts = postsRes.data.data || [];

    return {
      platform: 'instagram',
      handle,
      followers: profile.follower_count || profile.followers || 0,
      posts: posts.map(p => ({
        likes: p.like_count || p.likes || 0,
        comments: p.comment_count || p.comments || 0,
        type: p.media_type // 'IMAGE', 'VIDEO', 'CAROUSEL_ALBUM'
      }))
    };
  } catch (error) {
    console.error('Instagram error:', error.message);
    return null;
  }
}

// Twitter
async function getTwitterData(handle) {
  console.log(`📥 Fetching Twitter data for @${handle}...`);

  try {
    // Get profile
    const profileRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/profile`, {
      params: { handle },
      headers
    });

    const profile = profileRes.data.data;

    // Get recent tweets
    const tweetsRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/user-tweets`, {
      params: { handle },
      headers
    });

    const tweets = tweetsRes.data.data || [];

    return {
      platform: 'twitter',
      handle,
      followers: profile.followers_count || profile.followers || 0,
      tweets: tweets.map(t => ({
        likes: t.favorite_count || t.likes || 0,
        retweets: t.retweet_count || t.retweets || 0,
        replies: t.reply_count || t.replies || 0,
        views: t.views_count || t.views || 0
      }))
    };
  } catch (error) {
    console.error('Twitter error:', error.message);
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: The Engagement Calculator Engine

function calculateEngagementRate(data) {
  const { platform, followers, videos, posts, tweets } = data;

  if (followers === 0) {
    return { error: 'No followers' };
  }

  let result = {
    platform,
    handle: data.handle,
    followers,
    contentAnalyzed: 0,
    metrics: {}
  };

  // Platform-specific calculations
  switch (platform) {
    case 'tiktok':
      return calculateTikTokEngagement(data);
    case 'instagram':
      return calculateInstagramEngagement(data);
    case 'twitter':
      return calculateTwitterEngagement(data);
    default:
      return { error: 'Unsupported platform' };
  }
}

function calculateTikTokEngagement(data) {
  const { followers, videos, handle } = data;

  if (videos.length === 0) {
    return { error: 'No videos found' };
  }

  // TikTok uses views as the denominator (not followers)
  const totalViews = videos.reduce((sum, v) => sum + v.views, 0);
  const totalLikes = videos.reduce((sum, v) => sum + v.likes, 0);
  const totalComments = videos.reduce((sum, v) => sum + v.comments, 0);
  const totalShares = videos.reduce((sum, v) => sum + v.shares, 0);
  const totalEngagements = totalLikes + totalComments + totalShares;

  // Method 1: Engagement per view (most accurate for TikTok)
  const engagementPerView = totalViews > 0 
    ? (totalEngagements / totalViews) * 100 
    : 0;

  // Method 2: Engagement per follower (for comparison)
  const engagementPerFollower = (totalEngagements / videos.length / followers) * 100;

  // Method 3: Average likes per video / followers
  const avgLikesRate = (totalLikes / videos.length / followers) * 100;

  // View-to-follower ratio (indicates reach)
  const avgViews = totalViews / videos.length;
  const viewToFollowerRatio = (avgViews / followers) * 100;

  return {
    platform: 'tiktok',
    handle,
    followers,
    contentAnalyzed: videos.length,
    metrics: {
      engagementPerView: engagementPerView.toFixed(2),
      engagementPerFollower: engagementPerFollower.toFixed(2),
      avgLikesRate: avgLikesRate.toFixed(2),
      viewToFollowerRatio: viewToFollowerRatio.toFixed(1),
      avgViews: Math.round(avgViews),
      avgLikes: Math.round(totalLikes / videos.length),
      avgComments: Math.round(totalComments / videos.length),
      avgShares: Math.round(totalShares / videos.length)
    },
    primaryRate: engagementPerFollower.toFixed(2), // Use this for grading
    primaryMethod: 'engagement per follower'
  };
}

function calculateInstagramEngagement(data) {
  const { followers, posts, handle } = data;

  if (posts.length === 0) {
    return { error: 'No posts found' };
  }

  const totalLikes = posts.reduce((sum, p) => sum + p.likes, 0);
  const totalComments = posts.reduce((sum, p) => sum + p.comments, 0);
  const totalEngagements = totalLikes + totalComments;

  // Standard Instagram engagement rate
  const engagementRate = (totalEngagements / posts.length / followers) * 100;

  // Likes-only rate (some use this)
  const likesRate = (totalLikes / posts.length / followers) * 100;

  // Separate by content type if available
  const reels = posts.filter(p => p.type === 'VIDEO');
  const images = posts.filter(p => p.type === 'IMAGE');

  let reelsEngagement = null;
  let imagesEngagement = null;

  if (reels.length > 0) {
    const reelsTotal = reels.reduce((sum, p) => sum + p.likes + p.comments, 0);
    reelsEngagement = (reelsTotal / reels.length / followers) * 100;
  }

  if (images.length > 0) {
    const imagesTotal = images.reduce((sum, p) => sum + p.likes + p.comments, 0);
    imagesEngagement = (imagesTotal / images.length / followers) * 100;
  }

  return {
    platform: 'instagram',
    handle,
    followers,
    contentAnalyzed: posts.length,
    metrics: {
      engagementRate: engagementRate.toFixed(2),
      likesOnlyRate: likesRate.toFixed(2),
      reelsEngagement: reelsEngagement ? reelsEngagement.toFixed(2) : 'N/A',
      imagesEngagement: imagesEngagement ? imagesEngagement.toFixed(2) : 'N/A',
      avgLikes: Math.round(totalLikes / posts.length),
      avgComments: Math.round(totalComments / posts.length),
      reelsCount: reels.length,
      imagesCount: images.length
    },
    primaryRate: engagementRate.toFixed(2),
    primaryMethod: 'likes + comments / followers'
  };
}

function calculateTwitterEngagement(data) {
  const { followers, tweets, handle } = data;

  if (tweets.length === 0) {
    return { error: 'No tweets found' };
  }

  const totalLikes = tweets.reduce((sum, t) => sum + t.likes, 0);
  const totalRetweets = tweets.reduce((sum, t) => sum + t.retweets, 0);
  const totalReplies = tweets.reduce((sum, t) => sum + t.replies, 0);
  const totalViews = tweets.reduce((sum, t) => sum + (t.views || 0), 0);
  const totalEngagements = totalLikes + totalRetweets + totalReplies;

  // Engagement per follower
  const engagementRate = (totalEngagements / tweets.length / followers) * 100;

  // Engagement per impression (if views available)
  const engagementPerView = totalViews > 0 
    ? (totalEngagements / totalViews) * 100 
    : null;

  return {
    platform: 'twitter',
    handle,
    followers,
    contentAnalyzed: tweets.length,
    metrics: {
      engagementRate: engagementRate.toFixed(2),
      engagementPerView: engagementPerView ? engagementPerView.toFixed(2) : 'N/A',
      avgLikes: Math.round(totalLikes / tweets.length),
      avgRetweets: Math.round(totalRetweets / tweets.length),
      avgReplies: Math.round(totalReplies / tweets.length),
      avgViews: totalViews > 0 ? Math.round(totalViews / tweets.length) : 'N/A'
    },
    primaryRate: engagementRate.toFixed(2),
    primaryMethod: 'likes + retweets + replies / followers'
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 4: The Grading System

function gradeEngagement(platform, rate) {
  const benchmarks = BENCHMARKS[platform];
  if (!benchmarks) return { grade: '?', label: 'Unknown platform' };

  const numRate = parseFloat(rate);

  if (numRate >= benchmarks.excellent) {
    return { grade: 'A+', label: 'Excellent', emoji: '🌟', percentile: 'Top 5%' };
  }
  if (numRate >= benchmarks.good) {
    return { grade: 'A', label: 'Above Average', emoji: '', percentile: 'Top 20%' };
  }
  if (numRate >= benchmarks.average) {
    return { grade: 'B', label: 'Average', emoji: '👍', percentile: 'Top 50%' };
  }
  if (numRate >= benchmarks.belowAverage) {
    return { grade: 'C', label: 'Below Average', emoji: '⚠️', percentile: 'Bottom 50%' };
  }
  return { grade: 'D', label: 'Poor', emoji: '', percentile: 'Bottom 20%' };
}

function getBenchmarkContext(platform) {
  const benchmarks = BENCHMARKS[platform];
  return {
    platform,
    excellent: `${benchmarks.excellent}%+`,
    good: `${benchmarks.good}-${benchmarks.excellent}%`,
    average: `${benchmarks.average}-${benchmarks.good}%`,
    belowAverage: `${benchmarks.belowAverage}-${benchmarks.average}%`,
    poor: `<${benchmarks.belowAverage}%`
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Main Analysis Function

async function analyzeEngagement(platform, handle) {
  console.log('\n📊 ENGAGEMENT RATE CALCULATOR\n');
  console.log('═══════════════════════════════════════════\n');

  // Fetch data
  let data;
  switch (platform) {
    case 'tiktok':
      data = await getTikTokData(handle);
      break;
    case 'instagram':
      data = await getInstagramData(handle);
      break;
    case 'twitter':
      data = await getTwitterData(handle);
      break;
    default:
      console.log('Unsupported platform. Use: tiktok, instagram, twitter');
      return;
  }

  if (!data) {
    console.log('Could not fetch data.');
    return;
  }

  // Calculate engagement
  const engagement = calculateEngagementRate(data);

  if (engagement.error) {
    console.log('Error:', engagement.error);
    return;
  }

  // Grade it
  const grade = gradeEngagement(platform, engagement.primaryRate);
  const benchmarks = getBenchmarkContext(platform);

  // Display results
  console.log(`Platform: ${platform.toUpperCase()}`);
  console.log(`Account: @${handle}`);
  console.log(`Followers: ${engagement.followers.toLocaleString()}`);
  console.log(`Content Analyzed: ${engagement.contentAnalyzed} posts\n`);

  console.log('───────────────────────────────────────────');
  console.log('📈 ENGAGEMENT METRICS');
  console.log('───────────────────────────────────────────\n');

  Object.entries(engagement.metrics).forEach(([key, value]) => {
    const label = key.replace(/([A-Z])/g, ' $1').replace(/^./, s => s.toUpperCase());
    console.log(`  ${label}: ${value}${typeof value === 'string' && value.match(/^\d/) ? '%' : ''}`);
  });

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

  console.log(`  ${grade.emoji} GRADE: ${grade.grade} - ${grade.label}`);
  console.log(`  📊 ENGAGEMENT RATE: ${engagement.primaryRate}%`);
  console.log(`  📍 PERCENTILE: ${grade.percentile}`);
  console.log(`  📐 METHOD: ${engagement.primaryMethod}\n`);

  console.log('───────────────────────────────────────────');
  console.log(`📋 ${platform.toUpperCase()} BENCHMARKS (2024)`);
  console.log('───────────────────────────────────────────\n');

  console.log(`  🌟 Excellent: ${benchmarks.excellent}`);
  console.log(`  ✅ Good: ${benchmarks.good}`);
  console.log(`  👍 Average: ${benchmarks.average}`);
  console.log(`  ⚠️  Below Avg: ${benchmarks.belowAverage}`);
  console.log(`  ❌ Poor: ${benchmarks.poor}`);

  return { engagement, grade, benchmarks };
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Multi-Platform Comparison

async function compareAcrossPlatforms(handles) {
  console.log('\n🔄 CROSS-PLATFORM COMPARISON\n');
  console.log('═══════════════════════════════════════════\n');

  const results = [];

  for (const [platform, handle] of Object.entries(handles)) {
    if (!handle) continue;

    let data;
    switch (platform) {
      case 'tiktok':
        data = await getTikTokData(handle);
        break;
      case 'instagram':
        data = await getInstagramData(handle);
        break;
      case 'twitter':
        data = await getTwitterData(handle);
        break;
    }

    if (data) {
      const engagement = calculateEngagementRate(data);
      const grade = gradeEngagement(platform, engagement.primaryRate);
      results.push({ platform, handle, engagement, grade });
    }

    // Rate limiting
    await new Promise(r => setTimeout(r, 1000));
  }

  // Display comparison table
  console.log('Platform'.padEnd(12) + 'Handle'.padEnd(18) + 'Rate'.padEnd(10) + 'Grade'.padEnd(8) + 'Followers');
  console.log(''.repeat(70));

  results.forEach(r => {
    console.log(
      r.platform.padEnd(12) +
      `@${r.handle}`.padEnd(18) +
      `${r.engagement.primaryRate}%`.padEnd(10) +
      `${r.grade.grade} ${r.grade.emoji}`.padEnd(8) +
      r.engagement.followers.toLocaleString()
    );
  });

  // Best performer
  const best = results.reduce((max, r) => {
    const maxScore = gradeToScore(max.grade.grade);
    const rScore = gradeToScore(r.grade.grade);
    return rScore > maxScore ? r : max;
  }, results[0]);

  console.log(`\n🏆 Best Performing Platform: ${best.platform.toUpperCase()} (${best.grade.grade})`);

  return results;
}

function gradeToScore(grade) {
  const scores = { 'A+': 5, 'A': 4, 'B': 3, 'C': 2, 'D': 1 };
  return scores[grade] || 0;
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run It

async function main() {
  const platform = process.argv[2] || 'tiktok';
  const handle = process.argv[3] || 'charlidamelio';

  // Single analysis
  await analyzeEngagement(platform, handle);

  // Or compare across platforms:
  // await compareAcrossPlatforms({
  //   tiktok: 'garyvee',
  //   instagram: 'garyvee',
  //   twitter: 'garyvee'
  // });
}

main();
Enter fullscreen mode Exit fullscreen mode

Run with:

node index.js tiktok charlidamelio
node index.js instagram kyliejenner
node index.js twitter elonmusk
Enter fullscreen mode Exit fullscreen mode

Sample Output

📊 ENGAGEMENT RATE CALCULATOR
═══════════════════════════════════════════

📥 Fetching TikTok data for @charlidamelio...

Platform: TIKTOK
Account: @charlidamelio
Followers: 155,200,000
Content Analyzed: 30 posts

───────────────────────────────────────────
📈 ENGAGEMENT METRICS
───────────────────────────────────────────

  Engagement Per View: 8.45%
  Engagement Per Follower: 3.21%
  Avg Likes Rate: 2.89%
  View To Follower Ratio: 45.2%
  Avg Views: 70,240,000
  Avg Likes: 4,490,000
  Avg Comments: 28,500
  Avg Shares: 45,200

═══════════════════════════════════════════
🎯 VERDICT
═══════════════════════════════════════════

  👍 GRADE: B - Average
  📊 ENGAGEMENT RATE: 3.21%
  📍 PERCENTILE: Top 50%
  📐 METHOD: engagement per follower

───────────────────────────────────────────
📋 TIKTOK BENCHMARKS (2024)
───────────────────────────────────────────

  🌟 Excellent: 8%+
  ✅ Good: 5-8%
  👍 Average: 3-5%
  ⚠️  Below Avg: 1.5-3%
  ❌ Poor: <1.5%
Enter fullscreen mode Exit fullscreen mode

Adding API Endpoint

Turn it into a service:

const express = require('express');
const app = express();

app.get('/api/engagement/:platform/:handle', async (req, res) => {
  const { platform, handle } = req.params;

  try {
    let data;
    switch (platform) {
      case 'tiktok': data = await getTikTokData(handle); break;
      case 'instagram': data = await getInstagramData(handle); break;
      case 'twitter': data = await getTwitterData(handle); break;
      default: return res.status(400).json({ error: 'Invalid platform' });
    }

    if (!data) {
      return res.status(404).json({ error: 'Profile not found' });
    }

    const engagement = calculateEngagementRate(data);
    const grade = gradeEngagement(platform, engagement.primaryRate);

    res.json({ engagement, grade });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

Cost Comparison

Manual engagement analysis:

  • Time: 15-30 minutes per creator
  • Spreadsheet formulas that break
  • Outdated benchmarks from 2019

Engagement rate tools:

  • Phlanx: Free but limited
  • HypeAuditor: $299/month
  • Influencer Marketing Hub: Manual entry only

Your version:

  • SociaVault: ~$0.02 per analysis
  • Accurate, current benchmarks
  • Automated across platforms

What You Just Built

This is the core feature of every influencer marketing platform.

Brands pay thousands for reports that do exactly this:

  • Calculate engagement rates correctly
  • Compare to industry benchmarks
  • Grade creators for partnership decisions

Now you have it for free.

Get Started

  1. Get your SociaVault API Key
  2. Pick a creator to analyze
  3. Get accurate engagement data in seconds

Stop guessing. Start measuring.


The difference between a good and great influencer partnership? Math.

Top comments (0)