DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Social Media ROI Calculator for Your Campaigns

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:

  1. Tracks campaign metrics across platforms
  2. Calculates real ROI, CAC, and ROAS
  3. 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
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
PORT=3000
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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
};
Enter fullscreen mode Exit fullscreen mode

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`);
});
Enter fullscreen mode Exit fullscreen mode

Step 6: Run It

node server.js
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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

  1. Get your SociaVault API Key
  2. Run the calculator
  3. Track every influencer post

Stop guessing. Start measuring.


If you can't measure it, you can't improve it.

Top comments (0)