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:
- Tracks any account's follower and engagement trends
- Finds top-performing content patterns
- Identifies the best conversations to join for visibility
- 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
Create .env:
SOCIAVAULT_API_KEY=your_key_here
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 };
}
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),
};
}
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: '😐' };
}
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;
}
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 };
}
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);
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
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();
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
- Grab your API key at sociavault.com
- Start tracking accounts that matter to you
- 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.
Top comments (0)