DEV Community

Cover image for Build a TikTok Sound Trend Analyzer to Find Viral Audio Before Everyone Else
Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a TikTok Sound Trend Analyzer to Find Viral Audio Before Everyone Else

The sound makes the video.

That's not an opinion — it's TikTok's algorithm. Videos using trending sounds get 2-5x more reach than original audio. The creators who blow up aren't necessarily the most talented. They're the ones who catch the right sound early.

By the time a sound hits your For You Page, it's too late. Peak virality has passed. You needed to use it 3 days ago.

We're building a tool that solves this. A TikTok Sound Trend Analyzer that:

  1. Tracks popular and rising sounds in real-time
  2. Identifies sounds in the "early viral" stage
  3. Shows which sounds perform best in your niche
  4. Alerts you when a new sound starts trending

The Science of TikTok Sound Virality

Every TikTok sound follows a lifecycle:

  1. Discovery (Day 1-2): A few creators use it, one pops off
  2. Early Adoption (Day 3-5): Smart creators jump on. Best engagement window.
  3. Mass Adoption (Day 6-14): Everyone's using it. Good engagement, getting saturated.
  4. Oversaturation (Day 15+): Algorithm deprioritizes. Too late.

Our tool needs to catch sounds in stage 2. That's where the magic is.

The Stack

  • Node.js: Runtime
  • SociaVault API: TikTok music endpoints
  • SQLite: Track sounds over time
  • node-cron: Automated polling

Step 1: Setup

mkdir tiktok-sound-tracker
cd tiktok-sound-tracker
npm init -y
npm install axios better-sqlite3 dotenv node-cron chalk
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

Step 2: Database for Tracking Sound Trends

Create db.js:

const Database = require('better-sqlite3');
const path = require('path');

const db = new Database(path.join(__dirname, 'sounds.db'));

db.exec(`
  CREATE TABLE IF NOT EXISTS sounds (
    clip_id TEXT PRIMARY KEY,
    title TEXT,
    author TEXT,
    duration INTEGER,
    first_seen TEXT DEFAULT (datetime('now')),
    last_seen TEXT DEFAULT (datetime('now'))
  );

  CREATE TABLE IF NOT EXISTS sound_snapshots (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    clip_id TEXT,
    video_count INTEGER,
    rank INTEGER,
    is_new_on_board BOOLEAN,
    snapshot_date TEXT DEFAULT (datetime('now')),
    FOREIGN KEY (clip_id) REFERENCES sounds(clip_id)
  );

  CREATE TABLE IF NOT EXISTS sound_videos (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    clip_id TEXT,
    video_id TEXT,
    author TEXT,
    views INTEGER DEFAULT 0,
    likes INTEGER DEFAULT 0,
    comments INTEGER DEFAULT 0,
    shares INTEGER DEFAULT 0,
    collected_at TEXT DEFAULT (datetime('now')),
    FOREIGN KEY (clip_id) REFERENCES sounds(clip_id)
  );
`);

module.exports = db;
Enter fullscreen mode Exit fullscreen mode

Step 3: Fetch Popular Sounds

The /v1/scrape/tiktok/music/popular endpoint is our main data source. It returns the hottest sounds with ranking data:

Create tracker.js:

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

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

async function getPopularSounds(options = {}) {
  console.log('🎵 Fetching popular TikTok sounds...');

  const allSounds = [];
  const maxPages = options.pages || 3;

  for (let page = 1; page <= maxPages; page++) {
    const { data } = await axios.get(`${API_BASE}/v1/scrape/tiktok/music/popular`, {
      params: {
        page,
        timePeriod: options.timePeriod || 7,  // Last 7 days
        rankType: options.rankType,
        newOnBoard: options.newOnly ? true : undefined,
        commercialMusic: options.commercial ? true : undefined,
        countryCode: options.country || 'US',
      },
      headers
    });

    const sounds = data.data?.musicList || data.data || [];
    allSounds.push(...sounds);

    if (sounds.length === 0) break;
    await new Promise(r => setTimeout(r, 500));
  }

  console.log(`  Found ${allSounds.length} sounds`);
  return allSounds;
}

async function getSoundDetails(clipId) {
  const { data } = await axios.get(`${API_BASE}/v1/scrape/tiktok/music/details`, {
    params: { clipId },
    headers
  });
  return data.data;
}

async function getSoundVideos(clipId) {
  const { data } = await axios.get(`${API_BASE}/v1/scrape/tiktok/music/videos`, {
    params: { clipId },
    headers
  });
  return data.data?.videos || data.data || [];
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Store and Track Sound Trends

function storeSoundSnapshot(sounds) {
  const insertSound = db.prepare(`
    INSERT INTO sounds (clip_id, title, author, duration)
    VALUES (?, ?, ?, ?)
    ON CONFLICT(clip_id) DO UPDATE SET
      last_seen = datetime('now')
  `);

  const insertSnapshot = db.prepare(`
    INSERT INTO sound_snapshots (clip_id, video_count, rank, is_new_on_board)
    VALUES (?, ?, ?, ?)
  `);

  const transaction = db.transaction((sounds) => {
    sounds.forEach((sound, index) => {
      const clipId = sound.clipId || sound.id || sound.musicId;
      const title = sound.title || sound.musicName || '';
      const author = sound.author || sound.authorName || '';
      const duration = sound.duration || 0;
      const videoCount = sound.videoCount || sound.usageCount || 0;
      const isNew = sound.isNewOnBoard || sound.newOnBoard || false;

      insertSound.run(clipId, title, author, duration);
      insertSnapshot.run(clipId, videoCount, index + 1, isNew ? 1 : 0);
    });
  });

  transaction(sounds);
  console.log(`  Stored ${sounds.length} sound snapshots`);
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Detect Early Viral Sounds

This is the money feature. We compare snapshots to find sounds with accelerating growth:

function detectEarlyViral() {
  // Get sounds that appeared in the last 3 days
  const recentSounds = db.prepare(`
    SELECT 
      s.clip_id,
      s.title,
      s.author,
      s.first_seen,
      COUNT(ss.id) as snapshot_count,
      MIN(ss.rank) as best_rank,
      MAX(ss.rank) as worst_rank
    FROM sounds s
    JOIN sound_snapshots ss ON s.clip_id = ss.clip_id
    WHERE s.first_seen >= datetime('now', '-3 days')
    GROUP BY s.clip_id
    ORDER BY best_rank ASC
  `).all();

  const earlyViral = [];

  for (const sound of recentSounds) {
    // Get video count growth
    const snapshots = db.prepare(`
      SELECT video_count, rank, snapshot_date
      FROM sound_snapshots
      WHERE clip_id = ?
      ORDER BY snapshot_date ASC
    `).all(sound.clip_id);

    if (snapshots.length < 2) continue;

    const firstCount = snapshots[0].video_count;
    const latestCount = snapshots[snapshots.length - 1].video_count;
    const growthRate = firstCount > 0 ? ((latestCount - firstCount) / firstCount) * 100 : 0;

    // Rank improvement
    const firstRank = snapshots[0].rank;
    const latestRank = snapshots[snapshots.length - 1].rank;
    const rankImprovement = firstRank - latestRank; // Positive = climbing

    // Acceleration: is growth speeding up?
    let acceleration = 0;
    if (snapshots.length >= 3) {
      const mid = Math.floor(snapshots.length / 2);
      const firstHalfGrowth = snapshots[mid].video_count - snapshots[0].video_count;
      const secondHalfGrowth = latestCount - snapshots[mid].video_count;
      acceleration = secondHalfGrowth - firstHalfGrowth;
    }

    // Score the sound
    const viralScore = calculateViralScore({
      growthRate,
      rankImprovement,
      acceleration,
      currentRank: latestRank,
      videoCount: latestCount,
      ageHours: (Date.now() - new Date(sound.first_seen).getTime()) / 3600000,
    });

    earlyViral.push({
      ...sound,
      videoCount: latestCount,
      growthRate: growthRate.toFixed(1),
      rankImprovement,
      acceleration,
      viralScore,
      stage: getViralStage(viralScore, latestRank),
    });
  }

  return earlyViral.sort((a, b) => b.viralScore - a.viralScore);
}

function calculateViralScore({ growthRate, rankImprovement, acceleration, currentRank, videoCount, ageHours }) {
  let score = 0;

  // Growth rate (0-30 points)
  score += Math.min(growthRate / 10, 30);

  // Rank improvement (0-25 points)
  score += Math.min(rankImprovement * 2, 25);

  // Acceleration (0-25 points) — key indicator
  score += Math.min(acceleration / 100, 25);

  // Current rank bonus (0-10 points)
  if (currentRank <= 10) score += 10;
  else if (currentRank <= 25) score += 7;
  else if (currentRank <= 50) score += 4;

  // Freshness bonus (0-10 points) — newer = better opportunity
  if (ageHours < 24) score += 10;
  else if (ageHours < 48) score += 7;
  else if (ageHours < 72) score += 4;

  return Math.round(Math.max(0, Math.min(100, score)));
}

function getViralStage(score, rank) {
  if (score >= 70 && rank <= 20) return '🔥 EARLY VIRAL — Use NOW';
  if (score >= 50) return '📈 Rising Fast — Great opportunity';
  if (score >= 30) return '🌱 Growing — Watch closely';
  return '👀 New — Too early to tell';
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Analyze Sound Performance by Niche

Not every sound works for every niche. Let's check what's actually performing:

async function analyzeSoundForNiche(clipId, niche) {
  console.log(`\n🔍 Analyzing sound for "${niche}" niche...`);

  // Get videos using this sound
  const videos = await getSoundVideos(clipId);

  if (videos.length === 0) {
    console.log('  No videos found for this sound');
    return null;
  }

  // Analyze video performance
  const videoStats = videos.map(v => ({
    author: v.author?.uniqueId || v.author || '',
    views: v.stats?.playCount || v.playCount || v.views || 0,
    likes: v.stats?.diggCount || v.diggCount || v.likes || 0,
    comments: v.stats?.commentCount || v.commentCount || v.comments || 0,
    shares: v.stats?.shareCount || v.shareCount || v.shares || 0,
    description: v.desc || v.description || '',
  }));

  // Check how many are in the target niche
  const nicheKeywords = getNicheKeywords(niche);
  const nicheVideos = videoStats.filter(v => {
    const desc = v.description.toLowerCase();
    return nicheKeywords.some(kw => desc.includes(kw));
  });

  const totalViews = videoStats.reduce((sum, v) => sum + v.views, 0);
  const avgViews = totalViews / videoStats.length;
  const topVideo = videoStats.reduce((max, v) => v.views > max.views ? v : max, videoStats[0]);

  return {
    totalVideos: videoStats.length,
    avgViews: Math.round(avgViews),
    topVideoViews: topVideo.views,
    nicheRelevance: nicheVideos.length / videoStats.length,
    nicheVideos: nicheVideos.length,
    topCreators: videoStats.slice(0, 5).map(v => ({
      author: v.author,
      views: v.views,
    })),
  };
}

function getNicheKeywords(niche) {
  const keywords = {
    fitness: ['gym', 'workout', 'fitness', 'gains', 'exercise', 'training', 'lift'],
    beauty: ['makeup', 'skincare', 'beauty', 'grwm', 'tutorial', 'glow', 'skin'],
    food: ['recipe', 'cooking', 'food', 'meal', 'chef', 'kitchen', 'eat'],
    fashion: ['outfit', 'fashion', 'style', 'ootd', 'wear', 'aesthetic'],
    tech: ['tech', 'coding', 'developer', 'programming', 'setup', 'gadget'],
    comedy: ['funny', 'comedy', 'joke', 'skit', 'prank', 'humor'],
    business: ['business', 'entrepreneur', 'money', 'side hustle', 'startup'],
    education: ['learn', 'tip', 'hack', 'how to', 'tutorial', 'study'],
  };

  return keywords[niche.toLowerCase()] || [niche.toLowerCase()];
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Alert System

Get notified when a sound matches your criteria:

function getAlerts(options = {}) {
  const minScore = options.minScore || 50;
  const earlyViral = detectEarlyViral();

  const alerts = earlyViral.filter(s => s.viralScore >= minScore);

  if (alerts.length === 0) {
    console.log('\n😴 No trending sounds match your criteria right now.');
    return [];
  }

  console.log(`\n🚨 ${alerts.length} SOUND ALERTS`);
  console.log(''.repeat(60));

  alerts.forEach((sound, i) => {
    console.log(`\n${i + 1}. ${sound.stage}`);
    console.log(`   🎵 "${sound.title}" by ${sound.author}`);
    console.log(`   📊 Viral Score: ${sound.viralScore}/100`);
    console.log(`   📈 Growth: ${sound.growthRate}% | Rank: #${sound.best_rank}`);
    console.log(`   🎬 ${sound.videoCount?.toLocaleString()} videos using it`);
    console.log(`   ⏱️  First seen: ${sound.first_seen}`);
  });

  return alerts;
}
Enter fullscreen mode Exit fullscreen mode

Step 8: Automated Polling with Cron

const cron = require('node-cron');

async function pollSounds() {
  console.log(`\n[${new Date().toISOString()}] Polling sounds...`);

  try {
    // Get popular sounds
    const sounds = await getPopularSounds({ pages: 3 });
    storeSoundSnapshot(sounds);

    // Also check "new on board" sounds
    const newSounds = await getPopularSounds({ pages: 2, newOnly: true });
    storeSoundSnapshot(newSounds);

    // Check for alerts
    getAlerts({ minScore: 50 });

  } catch (error) {
    console.error('Polling error:', error.message);
  }
}

function startAutoTracker() {
  console.log('🤖 Starting automated sound tracker...');
  console.log('   Polling every 4 hours\n');

  // Poll immediately on start
  pollSounds();

  // Then every 4 hours
  cron.schedule('0 */4 * * *', pollSounds);
}
Enter fullscreen mode Exit fullscreen mode

Step 9: The Full Dashboard

async function dashboard() {
  console.log('\n🎵 TIKTOK SOUND TREND DASHBOARD');
  console.log(''.repeat(60));
  console.log(`📅 ${new Date().toISOString().split('T')[0]}\n`);

  // Current popular sounds
  const sounds = await getPopularSounds({ pages: 2 });
  storeSoundSnapshot(sounds);

  console.log('📊 TOP 10 SOUNDS RIGHT NOW:');
  console.log(''.repeat(60));

  sounds.slice(0, 10).forEach((sound, i) => {
    const title = (sound.title || sound.musicName || 'Unknown').substring(0, 40);
    const author = sound.author || sound.authorName || 'Unknown';
    const videos = sound.videoCount || sound.usageCount || 0;
    const isNew = sound.isNewOnBoard ? ' 🆕' : '';

    console.log(`  ${String(i + 1).padStart(2)}. "${title}" — ${author}${isNew}`);
    console.log(`      ${videos.toLocaleString()} videos`);
  });

  // Early viral detection
  console.log('\n\n🔥 EARLY VIRAL DETECTION:');
  console.log(''.repeat(60));

  const earlyViral = detectEarlyViral();

  if (earlyViral.length > 0) {
    earlyViral.slice(0, 5).forEach((sound, i) => {
      console.log(`\n  ${i + 1}. [Score: ${sound.viralScore}] ${sound.stage}`);
      console.log(`     "${sound.title}" — ${sound.author}`);
      console.log(`     Growth: ${sound.growthRate}% | Rank moved: ${sound.rankImprovement > 0 ? '+' : ''}${sound.rankImprovement} positions`);
    });
  } else {
    console.log('  No early viral sounds detected. Run tracker for 24+ hours for data.');
  }

  // Stats
  const totalTracked = db.prepare('SELECT COUNT(*) as count FROM sounds').get();
  const totalSnapshots = db.prepare('SELECT COUNT(*) as count FROM sound_snapshots').get();

  console.log(`\n\n📈 TRACKER STATS:`);
  console.log(`   Sounds tracked: ${totalTracked.count}`);
  console.log(`   Data points: ${totalSnapshots.count}`);
}

async function main() {
  const command = process.argv[2];
  const target = process.argv[3];

  switch (command) {
    case 'dashboard':
      await dashboard();
      break;
    case 'poll':
      await pollSounds();
      break;
    case 'alerts':
      const sounds = await getPopularSounds({ pages: 3 });
      storeSoundSnapshot(sounds);
      getAlerts({ minScore: parseInt(target) || 50 });
      break;
    case 'details':
      const details = await getSoundDetails(target);
      console.log(JSON.stringify(details, null, 2));
      break;
    case 'videos':
      const analysis = await analyzeSoundForNiche(target, process.argv[4] || 'general');
      console.log(JSON.stringify(analysis, null, 2));
      break;
    case 'auto':
      startAutoTracker();
      break;
    default:
      console.log('Usage:');
      console.log('  node tracker.js dashboard              - Full sound dashboard');
      console.log('  node tracker.js poll                   - Poll once and store data');
      console.log('  node tracker.js alerts 60              - Show sounds scoring 60+');
      console.log('  node tracker.js details <clipId>       - Sound details');
      console.log('  node tracker.js videos <clipId> fitness - Analyze sound for niche');
      console.log('  node tracker.js auto                   - Start auto-tracker (every 4h)');
  }
}

main().catch(console.error);
Enter fullscreen mode Exit fullscreen mode

Sample Output

🎵 TIKTOK SOUND TREND DASHBOARD
════════════════════════════════════════════════════════
📅 2026-02-06

📊 TOP 10 SOUNDS RIGHT NOW:
────────────────────────────────────────────────────────
   1. "original sound - creativestudio" — creativestudio 🆕
      2,847,000 videos
   2. "Die With A Smile" — Lady Gaga & Bruno Mars
      18,400,000 videos
   3. "APT." — ROSÉ & Bruno Mars
      12,300,000 videos
  ...

🔥 EARLY VIRAL DETECTION:
────────────────────────────────────────────────────────
  1. [Score: 82] 🔥 EARLY VIRAL — Use NOW
     "original sound - creativestudio" — creativestudio
     Growth: 340% | Rank moved: +47 positions

  2. [Score: 67] 📈 Rising Fast — Great opportunity
     "Sweater Weather (Slowed)" — The Neighbourhood
     Growth: 180% | Rank moved: +23 positions

  3. [Score: 54] 📈 Rising Fast — Great opportunity
     "that one sound everyone uses" — remixer_king
     Growth: 95% | Rank moved: +12 positions
Enter fullscreen mode Exit fullscreen mode

The Business Case

Agencies charge $500-2,000/month for "trend reports" that are basically someone scrolling TikTok and writing a Google Doc. This tool gives you better data in real-time.

Approach Cost Freshness
Hire a trend spotter $2,000/mo Daily updates, subjective
Use Tokboard/TrendTok $49-99/mo Near real-time, limited data
Your tracker ~$0.10/poll cycle Every 4 hours, objective data

Get Started

  1. Get your API key at sociavault.com
  2. Run node tracker.js auto to start collecting data
  3. Check node tracker.js dashboard daily for opportunities
  4. Jump on sounds scoring 60+ before everyone else does

The creators who grow fastest aren't more talented. They're just faster.


By the time a sound is on your For You Page, you're already late. Don't be late.

tiktok #javascript #musictech #socialmedia

Top comments (0)