"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:
- Calculates true engagement rates across platforms
- Benchmarks against industry standards
- 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
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
Create .env:
SOCIAVAULT_API_KEY=your_sociavault_key
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;
}
}
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'
};
}
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}%`
};
}
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 };
}
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;
}
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();
Run with:
node index.js tiktok charlidamelio
node index.js instagram kyliejenner
node index.js twitter elonmusk
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%
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);
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
- Get your SociaVault API Key
- Pick a creator to analyze
- Get accurate engagement data in seconds
Stop guessing. Start measuring.
The difference between a good and great influencer partnership? Math.
Top comments (0)