You spent $5,000 on influencer campaigns last month.
How much revenue did it generate? What's your cost per acquisition? Which platform performed best?
If you can't answer immediately, you're flying blind.
In this tutorial, we'll build a Social Media ROI Calculator that:
- Tracks campaign metrics across platforms
- Calculates real ROI, CAC, and ROAS
- Shows which creators and platforms deliver the best returns
Stop guessing if social media works. Start proving it with numbers.
Why ROI Tracking Matters
The harsh reality:
- 60% of marketers can't prove social media ROI
- Average influencer marketing ROAS is 5.2x (but varies wildly)
- Brands that track ROI are 3x more likely to increase budgets
What you need to measure:
- Cost per engagement (CPE)
- Cost per acquisition (CAC)
- Return on ad spend (ROAS)
- Conversion rates by platform
- Creator performance comparison
The Stack
- Node.js: Runtime
- SociaVault API: Fetch campaign content metrics
- Express: Simple dashboard
- SQLite: Store campaign data
Step 1: Setup
mkdir roi-calculator
cd roi-calculator
npm init -y
npm install axios dotenv express better-sqlite3
Create .env:
SOCIAVAULT_API_KEY=your_sociavault_key
PORT=3000
Step 2: Database Schema
Create database.js:
const Database = require('better-sqlite3');
const db = new Database('campaigns.db');
db.exec(`
CREATE TABLE IF NOT EXISTS campaigns (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
platform TEXT,
start_date TEXT,
end_date TEXT,
budget REAL DEFAULT 0,
goal TEXT,
status TEXT DEFAULT 'active',
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS campaign_posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER,
creator_handle TEXT,
platform TEXT,
post_url TEXT UNIQUE,
post_id TEXT,
cost REAL DEFAULT 0,
impressions INTEGER DEFAULT 0,
reach INTEGER DEFAULT 0,
views INTEGER DEFAULT 0,
likes INTEGER DEFAULT 0,
comments INTEGER DEFAULT 0,
shares INTEGER DEFAULT 0,
saves INTEGER DEFAULT 0,
clicks INTEGER DEFAULT 0,
conversions INTEGER DEFAULT 0,
revenue REAL DEFAULT 0,
tracked_at TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id)
);
CREATE TABLE IF NOT EXISTS tracking_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
campaign_id INTEGER,
post_id INTEGER,
original_url TEXT,
tracking_url TEXT UNIQUE,
clicks INTEGER DEFAULT 0,
conversions INTEGER DEFAULT 0,
revenue REAL DEFAULT 0,
FOREIGN KEY (campaign_id) REFERENCES campaigns(id),
FOREIGN KEY (post_id) REFERENCES campaign_posts(id)
);
CREATE INDEX IF NOT EXISTS idx_posts_campaign ON campaign_posts(campaign_id);
CREATE INDEX IF NOT EXISTS idx_posts_platform ON campaign_posts(platform);
`);
module.exports = db;
Step 3: Metrics Fetcher
Create metrics.js:
require('dotenv').config();
const axios = require('axios');
const SOCIAVAULT_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
async function fetchTikTokMetrics(postUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/video-info`, {
params: { url: postUrl },
headers
});
const data = response.data.data;
return {
platform: 'tiktok',
postId: data.id,
views: data.playCount || data.stats?.playCount || 0,
likes: data.diggCount || data.stats?.diggCount || 0,
comments: data.commentCount || data.stats?.commentCount || 0,
shares: data.shareCount || data.stats?.shareCount || 0,
saves: data.collectCount || data.stats?.collectCount || 0,
authorHandle: data.author?.uniqueId || data.authorMeta?.name,
authorFollowers: data.author?.followerCount || 0
};
} catch (error) {
console.error('TikTok metrics error:', error.message);
return null;
}
}
async function fetchInstagramMetrics(postUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/post`, {
params: { url: postUrl },
headers
});
const data = response.data.data;
return {
platform: 'instagram',
postId: data.id || data.pk,
views: data.play_count || data.video_view_count || 0,
likes: data.like_count || data.likes || 0,
comments: data.comment_count || data.comments || 0,
saves: data.save_count || 0,
authorHandle: data.user?.username || data.owner?.username,
authorFollowers: data.user?.follower_count || 0
};
} catch (error) {
console.error('Instagram metrics error:', error.message);
return null;
}
}
async function fetchTwitterMetrics(postUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/tweet`, {
params: { url: postUrl },
headers
});
const data = response.data.data;
return {
platform: 'twitter',
postId: data.id || data.rest_id,
views: data.views_count || data.views || 0,
likes: data.favorite_count || data.likes || 0,
retweets: data.retweet_count || 0,
replies: data.reply_count || 0,
quotes: data.quote_count || 0,
authorHandle: data.user?.screen_name,
authorFollowers: data.user?.followers_count || 0
};
} catch (error) {
console.error('Twitter metrics error:', error.message);
return null;
}
}
async function fetchYouTubeMetrics(videoUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/youtube/video`, {
params: { url: videoUrl },
headers
});
const data = response.data.data;
return {
platform: 'youtube',
postId: data.id,
views: data.viewCount || data.views || 0,
likes: data.likeCount || data.likes || 0,
comments: data.commentCount || data.comments || 0,
authorHandle: data.channelTitle || data.channel?.name,
authorSubscribers: data.channelSubscriberCount || 0
};
} catch (error) {
console.error('YouTube metrics error:', error.message);
return null;
}
}
function detectPlatform(url) {
if (url.includes('tiktok.com')) return 'tiktok';
if (url.includes('instagram.com')) return 'instagram';
if (url.includes('twitter.com') || url.includes('x.com')) return 'twitter';
if (url.includes('youtube.com') || url.includes('youtu.be')) return 'youtube';
return null;
}
async function fetchMetrics(postUrl) {
const platform = detectPlatform(postUrl);
switch (platform) {
case 'tiktok': return fetchTikTokMetrics(postUrl);
case 'instagram': return fetchInstagramMetrics(postUrl);
case 'twitter': return fetchTwitterMetrics(postUrl);
case 'youtube': return fetchYouTubeMetrics(postUrl);
default: return null;
}
}
module.exports = {
fetchTikTokMetrics,
fetchInstagramMetrics,
fetchTwitterMetrics,
fetchYouTubeMetrics,
fetchMetrics,
detectPlatform
};
Step 4: ROI Calculator Engine
Create calculator.js:
function calculateROI(campaign, posts) {
const totalCost = posts.reduce((sum, p) => sum + (p.cost || 0), 0) || campaign.budget || 0;
const totalRevenue = posts.reduce((sum, p) => sum + (p.revenue || 0), 0);
const totalConversions = posts.reduce((sum, p) => sum + (p.conversions || 0), 0);
const totalClicks = posts.reduce((sum, p) => sum + (p.clicks || 0), 0);
// Engagement metrics
const totalViews = posts.reduce((sum, p) => sum + (p.views || 0), 0);
const totalLikes = posts.reduce((sum, p) => sum + (p.likes || 0), 0);
const totalComments = posts.reduce((sum, p) => sum + (p.comments || 0), 0);
const totalShares = posts.reduce((sum, p) => sum + (p.shares || 0), 0);
const totalEngagement = totalLikes + totalComments + totalShares;
// Core ROI metrics
const roi = totalCost > 0 ? ((totalRevenue - totalCost) / totalCost) * 100 : 0;
const roas = totalCost > 0 ? totalRevenue / totalCost : 0;
// Cost metrics
const cpm = totalViews > 0 ? (totalCost / totalViews) * 1000 : 0;
const cpe = totalEngagement > 0 ? totalCost / totalEngagement : 0;
const cpc = totalClicks > 0 ? totalCost / totalClicks : 0;
const cpa = totalConversions > 0 ? totalCost / totalConversions : 0;
// Conversion metrics
const conversionRate = totalClicks > 0 ? (totalConversions / totalClicks) * 100 : 0;
const engagementRate = totalViews > 0 ? (totalEngagement / totalViews) * 100 : 0;
const clickThroughRate = totalViews > 0 ? (totalClicks / totalViews) * 100 : 0;
return {
overview: {
totalCost,
totalRevenue,
profit: totalRevenue - totalCost,
roi: roi.toFixed(2),
roas: roas.toFixed(2)
},
engagement: {
totalViews,
totalLikes,
totalComments,
totalShares,
totalEngagement,
engagementRate: engagementRate.toFixed(2)
},
conversions: {
totalClicks,
totalConversions,
clickThroughRate: clickThroughRate.toFixed(2),
conversionRate: conversionRate.toFixed(2)
},
costMetrics: {
cpm: cpm.toFixed(2),
cpe: cpe.toFixed(2),
cpc: cpc.toFixed(2),
cpa: cpa.toFixed(2)
}
};
}
function calculateCreatorPerformance(posts) {
const creatorMap = {};
posts.forEach(post => {
const creator = post.creator_handle || 'Unknown';
if (!creatorMap[creator]) {
creatorMap[creator] = {
handle: creator,
platform: post.platform,
posts: 0,
totalCost: 0,
totalRevenue: 0,
totalViews: 0,
totalEngagement: 0,
totalClicks: 0,
totalConversions: 0
};
}
creatorMap[creator].posts++;
creatorMap[creator].totalCost += post.cost || 0;
creatorMap[creator].totalRevenue += post.revenue || 0;
creatorMap[creator].totalViews += post.views || 0;
creatorMap[creator].totalEngagement += (post.likes || 0) + (post.comments || 0) + (post.shares || 0);
creatorMap[creator].totalClicks += post.clicks || 0;
creatorMap[creator].totalConversions += post.conversions || 0;
});
// Calculate performance metrics for each creator
return Object.values(creatorMap).map(creator => ({
...creator,
roas: creator.totalCost > 0 ? (creator.totalRevenue / creator.totalCost).toFixed(2) : 0,
cpe: creator.totalEngagement > 0 ? (creator.totalCost / creator.totalEngagement).toFixed(2) : 0,
cpa: creator.totalConversions > 0 ? (creator.totalCost / creator.totalConversions).toFixed(2) : 'N/A',
conversionRate: creator.totalClicks > 0 ? ((creator.totalConversions / creator.totalClicks) * 100).toFixed(2) : 0,
engagementRate: creator.totalViews > 0 ? ((creator.totalEngagement / creator.totalViews) * 100).toFixed(2) : 0
})).sort((a, b) => parseFloat(b.roas) - parseFloat(a.roas));
}
function calculatePlatformPerformance(posts) {
const platformMap = {};
posts.forEach(post => {
const platform = post.platform || 'Unknown';
if (!platformMap[platform]) {
platformMap[platform] = {
platform,
posts: 0,
totalCost: 0,
totalRevenue: 0,
totalViews: 0,
totalEngagement: 0,
totalClicks: 0,
totalConversions: 0
};
}
platformMap[platform].posts++;
platformMap[platform].totalCost += post.cost || 0;
platformMap[platform].totalRevenue += post.revenue || 0;
platformMap[platform].totalViews += post.views || 0;
platformMap[platform].totalEngagement += (post.likes || 0) + (post.comments || 0) + (post.shares || 0);
platformMap[platform].totalClicks += post.clicks || 0;
platformMap[platform].totalConversions += post.conversions || 0;
});
return Object.values(platformMap).map(p => ({
...p,
roas: p.totalCost > 0 ? (p.totalRevenue / p.totalCost).toFixed(2) : 0,
cpm: p.totalViews > 0 ? ((p.totalCost / p.totalViews) * 1000).toFixed(2) : 0,
cpa: p.totalConversions > 0 ? (p.totalCost / p.totalConversions).toFixed(2) : 'N/A'
})).sort((a, b) => parseFloat(b.roas) - parseFloat(a.roas));
}
function generateBenchmarks(metrics) {
// Industry benchmarks for comparison
const benchmarks = {
tiktok: { avgCPM: 10, avgCPE: 0.02, avgEngagement: 5, avgRoas: 5 },
instagram: { avgCPM: 8, avgCPE: 0.05, avgEngagement: 3, avgRoas: 4 },
youtube: { avgCPM: 20, avgCPE: 0.10, avgEngagement: 4, avgRoas: 6 },
twitter: { avgCPM: 6, avgCPE: 0.03, avgEngagement: 2, avgRoas: 3 }
};
const comparisons = {};
Object.keys(benchmarks).forEach(platform => {
const benchmark = benchmarks[platform];
const actual = metrics.platformPerformance?.find(p => p.platform === platform);
if (actual) {
comparisons[platform] = {
cpm: {
yours: parseFloat(actual.cpm),
benchmark: benchmark.avgCPM,
status: parseFloat(actual.cpm) < benchmark.avgCPM ? 'better' : 'worse'
},
roas: {
yours: parseFloat(actual.roas),
benchmark: benchmark.avgRoas,
status: parseFloat(actual.roas) > benchmark.avgRoas ? 'better' : 'worse'
},
engagement: {
yours: parseFloat(actual.totalEngagement / actual.totalViews * 100).toFixed(2),
benchmark: benchmark.avgEngagement,
status: (actual.totalEngagement / actual.totalViews * 100) > benchmark.avgEngagement ? 'better' : 'worse'
}
};
}
});
return comparisons;
}
module.exports = {
calculateROI,
calculateCreatorPerformance,
calculatePlatformPerformance,
generateBenchmarks
};
Step 5: API Server
Create server.js:
require('dotenv').config();
const express = require('express');
const db = require('./database');
const { fetchMetrics, detectPlatform } = require('./metrics');
const { calculateROI, calculateCreatorPerformance, calculatePlatformPerformance, generateBenchmarks } = require('./calculator');
const app = express();
app.use(express.json());
// Create campaign
app.post('/api/campaigns', (req, res) => {
const { name, platform, start_date, end_date, budget, goal } = req.body;
const stmt = db.prepare(`
INSERT INTO campaigns (name, platform, start_date, end_date, budget, goal)
VALUES (?, ?, ?, ?, ?, ?)
`);
const result = stmt.run(name, platform, start_date, end_date, budget, goal);
res.json({ success: true, id: result.lastInsertRowid });
});
// Get all campaigns
app.get('/api/campaigns', (req, res) => {
const campaigns = db.prepare('SELECT * FROM campaigns ORDER BY created_at DESC').all();
res.json(campaigns);
});
// Get campaign with full analytics
app.get('/api/campaigns/:id', (req, res) => {
const campaign = db.prepare('SELECT * FROM campaigns WHERE id = ?').get(req.params.id);
if (!campaign) {
return res.status(404).json({ error: 'Campaign not found' });
}
const posts = db.prepare('SELECT * FROM campaign_posts WHERE campaign_id = ?').all(req.params.id);
// Calculate all metrics
const roi = calculateROI(campaign, posts);
const creatorPerformance = calculateCreatorPerformance(posts);
const platformPerformance = calculatePlatformPerformance(posts);
const benchmarks = generateBenchmarks({ platformPerformance });
res.json({
campaign,
posts,
analytics: {
...roi,
creatorPerformance,
platformPerformance,
benchmarks
}
});
});
// Add post to campaign
app.post('/api/campaigns/:id/posts', async (req, res) => {
const { post_url, creator_handle, cost, clicks, conversions, revenue } = req.body;
// Fetch metrics from social platform
const metrics = await fetchMetrics(post_url);
if (!metrics) {
return res.status(400).json({ error: 'Could not fetch post metrics' });
}
const stmt = db.prepare(`
INSERT INTO campaign_posts (
campaign_id, creator_handle, platform, post_url, post_id,
cost, views, likes, comments, shares, saves,
clicks, conversions, revenue, tracked_at
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(post_url) DO UPDATE SET
views = excluded.views,
likes = excluded.likes,
comments = excluded.comments,
shares = excluded.shares,
saves = excluded.saves,
clicks = excluded.clicks,
conversions = excluded.conversions,
revenue = excluded.revenue,
tracked_at = excluded.tracked_at
`);
const result = stmt.run(
req.params.id,
creator_handle || metrics.authorHandle,
metrics.platform,
post_url,
metrics.postId,
cost || 0,
metrics.views || 0,
metrics.likes || 0,
metrics.comments || 0,
metrics.shares || 0,
metrics.saves || 0,
clicks || 0,
conversions || 0,
revenue || 0,
new Date().toISOString()
);
res.json({ success: true, id: result.lastInsertRowid, metrics });
});
// Update post conversion data
app.patch('/api/posts/:id', (req, res) => {
const { clicks, conversions, revenue, cost } = req.body;
const updates = [];
const params = [];
if (clicks !== undefined) {
updates.push('clicks = ?');
params.push(clicks);
}
if (conversions !== undefined) {
updates.push('conversions = ?');
params.push(conversions);
}
if (revenue !== undefined) {
updates.push('revenue = ?');
params.push(revenue);
}
if (cost !== undefined) {
updates.push('cost = ?');
params.push(cost);
}
if (updates.length === 0) {
return res.status(400).json({ error: 'No updates provided' });
}
params.push(req.params.id);
db.prepare(`UPDATE campaign_posts SET ${updates.join(', ')} WHERE id = ?`).run(...params);
res.json({ success: true });
});
// Refresh post metrics
app.post('/api/posts/:id/refresh', async (req, res) => {
const post = db.prepare('SELECT * FROM campaign_posts WHERE id = ?').get(req.params.id);
if (!post) {
return res.status(404).json({ error: 'Post not found' });
}
const metrics = await fetchMetrics(post.post_url);
if (!metrics) {
return res.status(400).json({ error: 'Could not refresh metrics' });
}
db.prepare(`
UPDATE campaign_posts SET
views = ?, likes = ?, comments = ?, shares = ?, saves = ?,
tracked_at = ?
WHERE id = ?
`).run(
metrics.views || 0,
metrics.likes || 0,
metrics.comments || 0,
metrics.shares || 0,
metrics.saves || 0,
new Date().toISOString(),
req.params.id
);
res.json({ success: true, metrics });
});
// Overall dashboard stats
app.get('/api/dashboard', (req, res) => {
const campaigns = db.prepare('SELECT * FROM campaigns WHERE status = "active"').all();
const allPosts = db.prepare('SELECT * FROM campaign_posts').all();
const totalSpend = allPosts.reduce((sum, p) => sum + (p.cost || 0), 0);
const totalRevenue = allPosts.reduce((sum, p) => sum + (p.revenue || 0), 0);
const totalConversions = allPosts.reduce((sum, p) => sum + (p.conversions || 0), 0);
const totalViews = allPosts.reduce((sum, p) => sum + (p.views || 0), 0);
const overallRoas = totalSpend > 0 ? totalRevenue / totalSpend : 0;
const overallRoi = totalSpend > 0 ? ((totalRevenue - totalSpend) / totalSpend) * 100 : 0;
res.json({
activeCampaigns: campaigns.length,
totalPosts: allPosts.length,
totalSpend,
totalRevenue,
profit: totalRevenue - totalSpend,
totalConversions,
totalViews,
overallRoas: overallRoas.toFixed(2),
overallRoi: overallRoi.toFixed(2),
platformBreakdown: calculatePlatformPerformance(allPosts),
topCreators: calculateCreatorPerformance(allPosts).slice(0, 5)
});
});
// Dashboard HTML
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Social Media ROI Calculator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, sans-serif; background: #0f172a; color: #e2e8f0; }
.container { max-width: 1400px; margin: 0 auto; padding: 20px; }
h1 { font-size: 1.5rem; margin-bottom: 20px; }
.grid { display: grid; grid-template-columns: repeat(5, 1fr); gap: 15px; margin-bottom: 30px; }
.card { background: #1e293b; padding: 20px; border-radius: 12px; }
.card-value { font-size: 2rem; font-weight: bold; }
.card-label { color: #94a3b8; font-size: 0.875rem; }
.positive { color: #22c55e; }
.negative { color: #ef4444; }
.section { margin-bottom: 30px; }
.section-title { font-size: 1.25rem; margin-bottom: 15px; }
table { width: 100%; border-collapse: collapse; background: #1e293b; border-radius: 8px; overflow: hidden; }
th, td { padding: 12px 16px; text-align: left; border-bottom: 1px solid #334155; }
th { background: #0f172a; font-weight: 600; }
.form-row { display: flex; gap: 10px; margin-bottom: 20px; }
input, select { padding: 10px; border: 1px solid #334155; border-radius: 6px; background: #1e293b; color: #fff; flex: 1; }
button { padding: 10px 20px; background: #3b82f6; color: white; border: none; border-radius: 6px; cursor: pointer; }
button:hover { background: #2563eb; }
</style>
</head>
<body>
<div class="container">
<h1>π Social Media ROI Calculator</h1>
<div class="grid" id="overviewCards"></div>
<div class="section">
<h2 class="section-title">β Track New Post</h2>
<div class="form-row">
<input type="text" id="postUrl" placeholder="Post URL (TikTok, Instagram, Twitter, YouTube)">
<input type="number" id="postCost" placeholder="Cost ($)">
<input type="number" id="postClicks" placeholder="Clicks">
<input type="number" id="postConversions" placeholder="Conversions">
<input type="number" id="postRevenue" placeholder="Revenue ($)">
<button onclick="addPost()">Track Post</button>
</div>
</div>
<div class="section">
<h2 class="section-title">π Top Performing Creators</h2>
<table id="creatorsTable"></table>
</div>
<div class="section">
<h2 class="section-title">π± Platform Performance</h2>
<table id="platformsTable"></table>
</div>
</div>
<script>
let currentCampaignId = 1;
async function loadDashboard() {
const res = await fetch('/api/dashboard');
const data = await res.json();
document.getElementById('overviewCards').innerHTML = \`
<div class="card"><div class="card-value">\${formatMoney(data.totalSpend)}</div><div class="card-label">Total Spend</div></div>
<div class="card"><div class="card-value">\${formatMoney(data.totalRevenue)}</div><div class="card-label">Total Revenue</div></div>
<div class="card"><div class="card-value \${data.profit >= 0 ? 'positive' : 'negative'}">\${formatMoney(data.profit)}</div><div class="card-label">Profit</div></div>
<div class="card"><div class="card-value">\${data.overallRoas}x</div><div class="card-label">ROAS</div></div>
<div class="card"><div class="card-value \${data.overallRoi >= 0 ? 'positive' : 'negative'}">\${data.overallRoi}%</div><div class="card-label">ROI</div></div>
\`;
document.getElementById('creatorsTable').innerHTML = \`
<tr><th>Creator</th><th>Platform</th><th>Posts</th><th>Cost</th><th>Revenue</th><th>ROAS</th><th>CPA</th></tr>
\${data.topCreators.map(c => \`
<tr>
<td>@\${c.handle}</td>
<td>\${c.platform}</td>
<td>\${c.posts}</td>
<td>\${formatMoney(c.totalCost)}</td>
<td>\${formatMoney(c.totalRevenue)}</td>
<td class="\${c.roas >= 1 ? 'positive' : 'negative'}">\${c.roas}x</td>
<td>\${c.cpa}</td>
</tr>
\`).join('')}
\`;
document.getElementById('platformsTable').innerHTML = \`
<tr><th>Platform</th><th>Posts</th><th>Views</th><th>Cost</th><th>Revenue</th><th>ROAS</th><th>CPM</th></tr>
\${data.platformBreakdown.map(p => \`
<tr>
<td>\${p.platform}</td>
<td>\${p.posts}</td>
<td>\${formatNumber(p.totalViews)}</td>
<td>\${formatMoney(p.totalCost)}</td>
<td>\${formatMoney(p.totalRevenue)}</td>
<td class="\${p.roas >= 1 ? 'positive' : 'negative'}">\${p.roas}x</td>
<td>$\${p.cpm}</td>
</tr>
\`).join('')}
\`;
}
async function addPost() {
const data = {
post_url: document.getElementById('postUrl').value,
cost: parseFloat(document.getElementById('postCost').value) || 0,
clicks: parseInt(document.getElementById('postClicks').value) || 0,
conversions: parseInt(document.getElementById('postConversions').value) || 0,
revenue: parseFloat(document.getElementById('postRevenue').value) || 0
};
await fetch('/api/campaigns/' + currentCampaignId + '/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
});
document.getElementById('postUrl').value = '';
document.getElementById('postCost').value = '';
document.getElementById('postClicks').value = '';
document.getElementById('postConversions').value = '';
document.getElementById('postRevenue').value = '';
loadDashboard();
}
function formatMoney(n) { return '$' + (n || 0).toLocaleString(undefined, {minimumFractionDigits: 2, maximumFractionDigits: 2}); }
function formatNumber(n) {
if (n >= 1000000) return (n/1000000).toFixed(1) + 'M';
if (n >= 1000) return (n/1000).toFixed(1) + 'K';
return n || 0;
}
// Create default campaign
fetch('/api/campaigns', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: 'Default Campaign', budget: 0 })
}).then(() => loadDashboard());
</script>
</body>
</html>
`);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`\nπ ROI Calculator running at http://localhost:${PORT}\n`);
});
Step 6: Run It
node server.js
Open http://localhost:3000 and start tracking your campaigns!
Sample Output
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π CAMPAIGN ROI REPORT: Q4 Influencer Campaign
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π° FINANCIAL OVERVIEW
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Spend: $15,000.00
Total Revenue: $67,500.00
Profit: $52,500.00
ROI: 350%
ROAS: 4.5x
π ENGAGEMENT METRICS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Views: 12,450,000
Total Likes: 892,000
Total Comments: 45,600
Total Shares: 23,400
Engagement Rate: 7.7%
π― CONVERSION METRICS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Clicks: 45,200
Conversions: 1,125
Click-Through Rate: 0.36%
Conversion Rate: 2.49%
π΅ COST METRICS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
CPM (per 1K views): $1.20
CPE (per engage): $0.016
CPC (per click): $0.33
CPA (per conv): $13.33
π TOP CREATORS BY ROAS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. @skincare_sarah ROAS: 8.2x CPA: $8.50 Revenue: $16,400
2. @fitness_mike ROAS: 6.1x CPA: $11.20 Revenue: $12,200
3. @lifestyle_jen ROAS: 4.8x CPA: $14.60 Revenue: $9,600
4. @beauty_alex ROAS: 3.5x CPA: $18.90 Revenue: $7,000
5. @fashion_kim ROAS: 2.9x CPA: $22.50 Revenue: $5,800
π± PLATFORM COMPARISON
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Platform | Spend | Revenue | ROAS | CPM | CPA
TikTok | $6,000 | $32,400 | 5.4x | $0.80 | $10.20
Instagram | $5,500 | $22,000 | 4.0x | $1.40 | $15.40
YouTube | $3,500 | $13,100 | 3.7x | $2.10 | $17.80
π BENCHMARK COMPARISON
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Yours Benchmark Status
TikTok ROAS 5.4x 5.0x β Above average
TikTok CPM $0.80 $10.00 β 12x better
Instagram ROAS 4.0x 4.0x β At benchmark
YouTube ROAS 3.7x 6.0x β Below average
What You Just Built
Marketing attribution tools cost a fortune:
- Triple Whale: $100+/month
- Rockerbox: $500+/month
- Northbeam: $1000+/month
Your version tracks ROI across platforms for free (just API costs).
Get Started
- Get your SociaVault API Key
- Run the calculator
- Track every influencer post
Stop guessing. Start measuring.
If you can't measure it, you can't improve it.
Top comments (0)