DEV Community

Cover image for Build a Threads Growth Tracker to Monitor the Twitter/X Alternative
Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Threads Growth Tracker to Monitor the Twitter/X Alternative

Everyone launched on Threads. Most people forgot about it.

But here's the thing — Threads hit 300 million monthly active users in late 2025. The people who stayed are engaged. And the platform is still growing.

The problem? There are zero analytics tools for Threads. Instagram has hundreds. TikTok has dozens. Threads has... your own eyeballs.

Let's fix that. We're building a Threads Growth Tracker that:

  1. Tracks any account's follower and engagement trends
  2. Finds top-performing content patterns
  3. Identifies the best conversations to join for visibility
  4. Monitors keyword searches for brand mentions

Why Threads Matters Now

Meta is pumping Threads hard. They're integrating it with ActivityPub (Fediverse), pushing it in Instagram, and adding features weekly. For developers and marketers, it's an underserved platform with real opportunity.

The accounts growing fastest on Threads share a few traits:

  • They're conversational, not broadcast-style
  • They reply a lot (Threads rewards conversation)
  • They cross-post from Twitter/X with context

We can detect all of this programmatically.

The Stack

  • Node.js: Runtime
  • SociaVault API: Threads data endpoints
  • Chart.js (optional): Visualizations
  • lowdb: Simple JSON file database for tracking

Step 1: Setup

mkdir threads-tracker
cd threads-tracker
npm init -y
npm install axios lowdb dotenv chalk
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_key_here
Enter fullscreen mode Exit fullscreen mode

Step 2: Track a Profile Over Time

The /v1/scrape/threads/profile endpoint returns follower counts, post counts, and bio info. We'll store snapshots daily.

Create tracker.js:

require('dotenv').config();
const axios = require('axios');
const { join } = require('path');
const fs = require('fs');

const API_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
const DATA_DIR = join(__dirname, 'data');

if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });

function getDB(handle) {
  const file = join(DATA_DIR, `${handle}.json`);
  if (!fs.existsSync(file)) {
    fs.writeFileSync(file, JSON.stringify({ handle, snapshots: [], posts: [] }));
  }
  return JSON.parse(fs.readFileSync(file, 'utf-8'));
}

function saveDB(handle, data) {
  fs.writeFileSync(join(DATA_DIR, `${handle}.json`), JSON.stringify(data, null, 2));
}

async function getThreadsProfile(handle) {
  console.log(`📥 Fetching @${handle}...`);

  const { data } = await axios.get(`${API_BASE}/v1/scrape/threads/profile`, {
    params: { handle },
    headers
  });

  const profile = data.data;

  return {
    handle: profile.username || profile.handle || handle,
    displayName: profile.full_name || profile.name || handle,
    followers: profile.follower_count || profile.followers || 0,
    following: profile.following_count || profile.following || 0,
    bio: profile.biography || profile.bio || '',
    verified: profile.is_verified || false,
    profilePic: profile.profile_pic_url || null,
  };
}

async function snapshotProfile(handle) {
  const profile = await getThreadsProfile(handle);
  const db = getDB(handle);

  const snapshot = {
    date: new Date().toISOString().split('T')[0],
    timestamp: new Date().toISOString(),
    followers: profile.followers,
    following: profile.following,
  };

  // Don't add duplicate snapshots for same day
  const today = snapshot.date;
  const existingToday = db.snapshots.find(s => s.date === today);

  if (existingToday) {
    Object.assign(existingToday, snapshot);
  } else {
    db.snapshots.push(snapshot);
  }

  saveDB(handle, db);

  return { profile, snapshot };
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Fetch and Analyze Posts

Let's pull their recent posts and figure out what's working:

async function getUserPosts(handle) {
  console.log(`📥 Fetching posts from @${handle}...`);

  const { data } = await axios.get(`${API_BASE}/v1/scrape/threads/user-posts`, {
    params: { handle },
    headers
  });

  const posts = data.data || [];

  return posts.map(post => ({
    id: post.id || post.pk,
    text: post.text || post.caption || '',
    likes: post.like_count || post.likes || 0,
    replies: post.reply_count || post.replies || post.text_post_app_info?.direct_reply_count || 0,
    reposts: post.repost_count || post.reposts || 0,
    quotes: post.quote_count || post.quotes || 0,
    timestamp: post.taken_at || post.created_at || post.timestamp,
    hasImage: !!(post.image_versions2 || post.carousel_media || post.image),
    hasVideo: !!(post.video_versions || post.video),
    isReply: !!(post.replied_to || post.parent_id),
  }));
}

function analyzePostPerformance(posts, followerCount) {
  if (posts.length === 0) return null;

  // Separate original posts from replies
  const originals = posts.filter(p => !p.isReply);
  const replies = posts.filter(p => p.isReply);

  // Engagement calculations
  const getEngagement = (post) => {
    return post.likes + post.replies + post.reposts + post.quotes;
  };

  const getEngagementRate = (post) => {
    return followerCount > 0 
      ? (getEngagement(post) / followerCount) * 100 
      : 0;
  };

  // Top posts
  const sortedByEngagement = [...originals].sort((a, b) => 
    getEngagement(b) - getEngagement(a)
  );

  // Average stats
  const avgLikes = originals.reduce((sum, p) => sum + p.likes, 0) / originals.length;
  const avgReplies = originals.reduce((sum, p) => sum + p.replies, 0) / originals.length;
  const avgEngagementRate = originals.reduce((sum, p) => sum + getEngagementRate(p), 0) / originals.length;

  // Content type analysis
  const textOnly = originals.filter(p => !p.hasImage && !p.hasVideo);
  const withMedia = originals.filter(p => p.hasImage || p.hasVideo);

  const textOnlyAvgEng = textOnly.length > 0
    ? textOnly.reduce((sum, p) => sum + getEngagement(p), 0) / textOnly.length
    : 0;
  const mediaAvgEng = withMedia.length > 0
    ? withMedia.reduce((sum, p) => sum + getEngagement(p), 0) / withMedia.length
    : 0;

  // Post length analysis
  const shortPosts = originals.filter(p => p.text.length < 100);
  const longPosts = originals.filter(p => p.text.length >= 100);

  const shortAvgEng = shortPosts.length > 0
    ? shortPosts.reduce((sum, p) => sum + getEngagement(p), 0) / shortPosts.length
    : 0;
  const longAvgEng = longPosts.length > 0
    ? longPosts.reduce((sum, p) => sum + getEngagement(p), 0) / longPosts.length
    : 0;

  return {
    totalPosts: posts.length,
    originalPosts: originals.length,
    replies: replies.length,
    replyRatio: (replies.length / posts.length * 100).toFixed(1),

    avgLikes: Math.round(avgLikes),
    avgReplies: Math.round(avgReplies),
    avgEngagementRate: avgEngagementRate.toFixed(2),

    topPosts: sortedByEngagement.slice(0, 5).map(p => ({
      text: p.text.substring(0, 100) + (p.text.length > 100 ? '...' : ''),
      engagement: getEngagement(p),
      likes: p.likes,
      replies: p.replies,
    })),

    contentTypeWinner: mediaAvgEng > textOnlyAvgEng ? 'media' : 'text-only',
    textOnlyAvgEngagement: Math.round(textOnlyAvgEng),
    mediaAvgEngagement: Math.round(mediaAvgEng),

    lengthWinner: longAvgEng > shortAvgEng ? 'long (100+ chars)' : 'short (<100 chars)',
    shortPostAvgEngagement: Math.round(shortAvgEng),
    longPostAvgEngagement: Math.round(longAvgEng),
  };
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Monitor Keywords and Brand Mentions

Threads has search. Let's use it:

async function searchThreads(query) {
  console.log(`🔍 Searching Threads for "${query}"...`);

  const { data } = await axios.get(`${API_BASE}/v1/scrape/threads/search`, {
    params: { query },
    headers
  });

  const posts = data.data || [];

  console.log(`Found ${posts.length} results\n`);

  return posts.map(post => ({
    id: post.id,
    author: post.user?.username || post.author || 'unknown',
    authorFollowers: post.user?.follower_count || 0,
    text: post.text || post.caption || '',
    likes: post.like_count || post.likes || 0,
    replies: post.reply_count || post.replies || 0,
    timestamp: post.taken_at || post.created_at,
  }));
}

async function monitorBrandMentions(brand, previousMentions = new Set()) {
  const results = await searchThreads(brand);

  const newMentions = results.filter(r => !previousMentions.has(r.id));

  if (newMentions.length > 0) {
    console.log(`\n🔔 ${newMentions.length} new mentions of "${brand}":`);
    console.log(''.repeat(50));

    newMentions.forEach(mention => {
      const sentiment = quickSentiment(mention.text);
      console.log(`\n  @${mention.author} (${mention.authorFollowers.toLocaleString()} followers)`);
      console.log(`  ${sentiment.emoji} ${mention.text.substring(0, 150)}`);
      console.log(`  ❤️ ${mention.likes}  💬 ${mention.replies}`);
    });
  }

  // Return all IDs for next check
  return new Set(results.map(r => r.id));
}

function quickSentiment(text) {
  const lower = text.toLowerCase();
  const positive = ['love', 'great', 'amazing', 'best', 'awesome', 'perfect', 'recommend', 'good'];
  const negative = ['hate', 'terrible', 'worst', 'awful', 'bad', 'broken', 'disappointed', 'scam'];

  const posCount = positive.filter(w => lower.includes(w)).length;
  const negCount = negative.filter(w => lower.includes(w)).length;

  if (posCount > negCount) return { sentiment: 'positive', emoji: '😀' };
  if (negCount > posCount) return { sentiment: 'negative', emoji: '😠' };
  return { sentiment: 'neutral', emoji: '😐' };
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Find Accounts to Engage With

Growing on Threads is about conversations. Let's find the right ones:

async function findEngagementOpportunities(query) {
  console.log(`\n🎯 Finding engagement opportunities for "${query}"...\n`);

  const posts = await searchThreads(query);

  // Sort by engagement potential (high engagement + recent)
  const opportunities = posts
    .filter(p => p.replies < 50)  // Not too crowded
    .filter(p => p.authorFollowers > 100)  // Author has some reach
    .sort((a, b) => {
      // Score: author followers * engagement, prefer recent
      const scoreA = (a.likes + a.replies * 3) * Math.log10(a.authorFollowers + 1);
      const scoreB = (b.likes + b.replies * 3) * Math.log10(b.authorFollowers + 1);
      return scoreB - scoreA;
    })
    .slice(0, 10);

  console.log('🏆 Top Threads to Reply To:');
  console.log(''.repeat(60));

  opportunities.forEach((post, i) => {
    console.log(`\n${i + 1}. @${post.author} (${post.authorFollowers.toLocaleString()} followers)`);
    console.log(`   "${post.text.substring(0, 120)}${post.text.length > 120 ? '...' : ''}"`);
    console.log(`   ❤️ ${post.likes} | 💬 ${post.replies} replies | Opportunity: ${post.replies < 10 ? 'HIGH' : 'MEDIUM'}`);
  });

  return opportunities;
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Growth Dashboard

Put it all together into a comprehensive analysis:

async function growthDashboard(handle) {
  console.log('\n📊 THREADS GROWTH DASHBOARD');
  console.log(''.repeat(60));

  // Get current profile
  const { profile, snapshot } = await snapshotProfile(handle);
  const db = getDB(handle);

  console.log(`\n👤 @${profile.handle} ${profile.verified ? '' : ''}`);
  console.log(`   ${profile.displayName}`);
  console.log(`   ${profile.followers.toLocaleString()} followers | ${profile.following.toLocaleString()} following`);

  // Growth trends
  if (db.snapshots.length >= 2) {
    const latest = db.snapshots[db.snapshots.length - 1];
    const previous = db.snapshots[db.snapshots.length - 2];
    const oldest = db.snapshots[0];

    const dailyChange = latest.followers - previous.followers;
    const totalChange = latest.followers - oldest.followers;
    const daysCovered = db.snapshots.length;
    const avgDailyGrowth = totalChange / daysCovered;

    console.log(`\n📈 GROWTH`);
    console.log(`   Today: ${dailyChange >= 0 ? '+' : ''}${dailyChange.toLocaleString()} followers`);
    console.log(`   Total tracked: ${totalChange >= 0 ? '+' : ''}${totalChange.toLocaleString()} over ${daysCovered} days`);
    console.log(`   Avg daily: ${avgDailyGrowth >= 0 ? '+' : ''}${avgDailyGrowth.toFixed(0)} followers/day`);
    console.log(`   Growth rate: ${((totalChange / oldest.followers) * 100).toFixed(2)}% total`);

    // Project future growth
    const projectedMonthly = avgDailyGrowth * 30;
    console.log(`   Projected 30-day: ${projectedMonthly >= 0 ? '+' : ''}${projectedMonthly.toFixed(0)} followers`);
  }

  // Post analysis
  const posts = await getUserPosts(handle);
  const analysis = analyzePostPerformance(posts, profile.followers);

  if (analysis) {
    console.log(`\n📝 CONTENT PERFORMANCE (${analysis.totalPosts} posts analyzed)`);
    console.log(`   Original posts: ${analysis.originalPosts}`);
    console.log(`   Replies: ${analysis.replies} (${analysis.replyRatio}% of activity)`);
    console.log(`   Avg likes: ${analysis.avgLikes}`);
    console.log(`   Avg replies: ${analysis.avgReplies}`);
    console.log(`   Avg engagement rate: ${analysis.avgEngagementRate}%`);

    console.log(`\n🏆 TOP PERFORMING POSTS:`);
    analysis.topPosts.forEach((post, i) => {
      console.log(`   ${i + 1}. [${post.engagement} eng] ${post.text}`);
    });

    console.log(`\n💡 INSIGHTS:`);
    console.log(`   Content type: ${analysis.contentTypeWinner} posts perform better`);
    console.log(`     Text-only avg: ${analysis.textOnlyAvgEngagement} engagement`);
    console.log(`     Media avg: ${analysis.mediaAvgEngagement} engagement`);
    console.log(`   Post length: ${analysis.lengthWinner} performs better`);
    console.log(`     Short avg: ${analysis.shortPostAvgEngagement} engagement`);
    console.log(`     Long avg: ${analysis.longPostAvgEngagement} engagement`);
  }

  return { profile, analysis };
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run the Tracker

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

  switch (command) {
    case 'track':
      await growthDashboard(target);
      break;
    case 'posts':
      const posts = await getUserPosts(target);
      const profile = await getThreadsProfile(target);
      const analysis = analyzePostPerformance(posts, profile.followers);
      console.log(JSON.stringify(analysis, null, 2));
      break;
    case 'search':
      await searchThreads(target);
      break;
    case 'monitor':
      await monitorBrandMentions(target);
      break;
    case 'engage':
      await findEngagementOpportunities(target);
      break;
    default:
      console.log('Usage:');
      console.log('  node tracker.js track <handle>     - Full growth dashboard');
      console.log('  node tracker.js posts <handle>     - Analyze post performance');
      console.log('  node tracker.js search "keyword"   - Search Threads');
      console.log('  node tracker.js monitor "brand"    - Monitor brand mentions');
      console.log('  node tracker.js engage "topic"     - Find engagement opportunities');
  }
}

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

Automate It

Add a daily cron job to track growth over time:

# Run every day at 9am
0 9 * * * cd /path/to/threads-tracker && node tracker.js track zuck >> logs/daily.log 2>&1
Enter fullscreen mode Exit fullscreen mode

Or set up multiple accounts:

// multi-track.js
const accounts = ['zuck', 'mosseri', 'your_brand'];

async function trackAll() {
  for (const handle of accounts) {
    await snapshotProfile(handle);
    console.log(`✅ Tracked @${handle}`);
    await new Promise(r => setTimeout(r, 2000));
  }
}

trackAll();
Enter fullscreen mode Exit fullscreen mode

What Tools Exist for Threads Analytics?

Basically nothing. That's the opportunity:

Tool Threads Support Price
Sprout Social Coming soon $249/mo
Hootsuite Limited $99/mo
Buffer Posting only $6/mo
Your tracker Full analytics ~$0.05/analysis

You're ahead of the enterprise tools. Enjoy it while it lasts.

Get Started

  1. Grab your API key at sociavault.com
  2. Start tracking accounts that matter to you
  3. Run it daily and watch the growth data accumulate

Threads is the one platform where building a custom analytics tool actually gives you an edge — because nobody else has one yet.


300 million users and zero analytics tools. That's not a gap — that's a canyon.

threads #javascript #analytics #socialmedia

Top comments (0)