Your competitor is getting 3x your reach on TikTok. Same niche. Similar content. What are they doing differently?
Hashtags.
Not the obvious ones you both use. The ones they use that you don't. The gaps in your strategy that you don't even know exist.
I built a tool that compares hashtag strategies between any two creators and finds the exact gaps. Takes about 50 lines of actual logic. Here's the whole thing.
The Stack
- Node.js ā runtime
- SociaVault API ā fetch posts and captions across platforms
- Set operations ā difference, intersection, frequency analysis
The Concept
Your Hashtags: #fitness #gym #workout #gains #fitfam
Competitor's: #fitness #gym #gymtok #fitnesstips #homeworkout #formcheck
Gap (they use, you don't): #gymtok #fitnesstips #homeworkout #formcheck
Overlap (you both use): #fitness #gym
Wasted (you use, they don't): #workout #gains #fitfam
The gap is where the opportunity lives. Those are proven hashtags in your niche that you're ignoring.
Step 1: Fetch Post Data
const axios = require('axios');
const API_BASE = 'https://api.sociavault.com/v1';
const API_KEY = process.env.SOCIAVAULT_API_KEY;
const api = axios.create({
baseURL: API_BASE,
headers: { 'x-api-key': API_KEY },
});
async function getPosts(platform, username, limit = 50) {
const { data } = await api.get(`/${platform}/posts/${username}`, {
params: { limit },
});
return data.posts;
}
Step 2: Extract Hashtag Profiles
A hashtag "profile" isn't just which tags someone uses ā it's how often and how well they perform.
function buildHashtagProfile(posts, followerCount) {
const hashtags = {};
for (const post of posts) {
const tags = (post.caption || '').match(/#[\w\u00C0-\u024F]+/g) || [];
const engagement = (post.likeCount + post.commentCount) / Math.max(followerCount, 1);
for (const tag of tags) {
const normalized = tag.toLowerCase();
if (!hashtags[normalized]) {
hashtags[normalized] = {
tag: normalized,
count: 0,
totalEngagement: 0,
posts: [],
};
}
hashtags[normalized].count++;
hashtags[normalized].totalEngagement += engagement;
hashtags[normalized].posts.push({
id: post.id,
likes: post.likeCount,
comments: post.commentCount,
});
}
}
// Calculate average engagement per hashtag
for (const tag of Object.values(hashtags)) {
tag.avgEngagement = parseFloat(
((tag.totalEngagement / tag.count) * 100).toFixed(2)
);
}
return hashtags;
}
Step 3: Compare Hashtag Strategies
function analyzeHashtagGap(myProfile, competitorProfile) {
const myTags = new Set(Object.keys(myProfile));
const compTags = new Set(Object.keys(competitorProfile));
// Gap: competitor uses, I don't
const gap = [];
for (const tag of compTags) {
if (!myTags.has(tag)) {
gap.push({
...competitorProfile[tag],
source: 'competitor-only',
});
}
}
// Overlap: both use
const overlap = [];
for (const tag of myTags) {
if (compTags.has(tag)) {
overlap.push({
tag,
myUsage: myProfile[tag].count,
myEngagement: myProfile[tag].avgEngagement,
compUsage: competitorProfile[tag].count,
compEngagement: competitorProfile[tag].avgEngagement,
engagementDiff: parseFloat(
(competitorProfile[tag].avgEngagement - myProfile[tag].avgEngagement).toFixed(2)
),
});
}
}
// Wasted: I use, competitor doesn't
const wasted = [];
for (const tag of myTags) {
if (!compTags.has(tag)) {
wasted.push({
...myProfile[tag],
source: 'my-only',
});
}
}
return { gap, overlap, wasted };
}
Step 4: Score and Rank Opportunities
Not all gaps are equal. A hashtag the competitor uses once with low engagement isn't worth stealing. One they use in 30/50 posts with 2x engagement? Gold.
function scoreOpportunities(gap) {
return gap
.map(tag => ({
...tag,
opportunityScore: calculateOpportunityScore(tag),
}))
.sort((a, b) => b.opportunityScore - a.opportunityScore);
}
function calculateOpportunityScore(tag) {
let score = 0;
// Frequency: how consistently does the competitor use it?
if (tag.count >= 20) score += 40; // Staple hashtag
else if (tag.count >= 10) score += 30; // Regular
else if (tag.count >= 5) score += 20; // Occasional
else score += 5; // Rare
// Engagement: does it drive results?
if (tag.avgEngagement > 5) score += 40;
else if (tag.avgEngagement > 3) score += 30;
else if (tag.avgEngagement > 1.5) score += 20;
else score += 5;
// Consistency bonus: used frequently AND performs well
if (tag.count >= 10 && tag.avgEngagement > 3) score += 20;
return Math.min(100, score);
}
Step 5: Multi-Competitor Analysis
One competitor might be an anomaly. Three competitors using the same hashtag? That's a pattern.
async function multiCompetitorGapAnalysis(platform, myUsername, competitorUsernames) {
// Fetch my profile
const myPosts = await getPosts(platform, myUsername, 50);
const myFollowers = (await api.get(`/${platform}/profile/${myUsername}`)).data.followerCount;
const myHashtags = buildHashtagProfile(myPosts, myFollowers);
// Fetch all competitor profiles
const competitorHashtags = {};
for (const competitor of competitorUsernames) {
const posts = await getPosts(platform, competitor, 50);
const followers = (await api.get(`/${platform}/profile/${competitor}`)).data.followerCount;
competitorHashtags[competitor] = buildHashtagProfile(posts, followers);
}
// Find tags used by multiple competitors that I don't use
const tagCompetitorCount = {};
const tagBestEngagement = {};
for (const [competitor, profile] of Object.entries(competitorHashtags)) {
for (const [tag, data] of Object.entries(profile)) {
if (!myHashtags[tag]) {
tagCompetitorCount[tag] = (tagCompetitorCount[tag] || 0) + 1;
if (!tagBestEngagement[tag] || data.avgEngagement > tagBestEngagement[tag]) {
tagBestEngagement[tag] = data.avgEngagement;
}
}
}
}
// Rank by how many competitors use it
const opportunities = Object.entries(tagCompetitorCount)
.map(([tag, count]) => ({
tag,
usedByCompetitors: count,
totalCompetitors: competitorUsernames.length,
bestEngagement: tagBestEngagement[tag],
confidence: parseFloat((count / competitorUsernames.length * 100).toFixed(0)),
}))
.sort((a, b) => b.usedByCompetitors - a.usedByCompetitors || b.bestEngagement - a.bestEngagement);
return opportunities;
}
Step 6: Generate the Report
async function runGapAnalysis(platform, myUsername, competitors) {
console.log(`\n=== HASHTAG GAP ANALYSIS ===`);
console.log(`Your account: @${myUsername}`);
console.log(`Competitors: ${competitors.map(c => '@' + c).join(', ')}\n`);
const opportunities = await multiCompetitorGapAnalysis(platform, myUsername, competitors);
// High-confidence gaps (used by 2+ competitors)
const highConfidence = opportunities.filter(t => t.usedByCompetitors >= 2);
console.log(`šÆ HIGH-CONFIDENCE GAPS (used by 2+ competitors):`);
highConfidence.slice(0, 15).forEach((t, i) => {
console.log(
` ${i + 1}. ${t.tag} ā used by ${t.usedByCompetitors}/${t.totalCompetitors} competitors, ` +
`best engagement: ${t.bestEngagement}%, confidence: ${t.confidence}%`
);
});
// Single competitor gaps with high engagement
const singleHigh = opportunities
.filter(t => t.usedByCompetitors === 1 && t.bestEngagement > 3)
.slice(0, 10);
console.log(`\nš” WORTH TESTING (1 competitor, high engagement):`);
singleHigh.forEach((t, i) => {
console.log(
` ${i + 1}. ${t.tag} ā engagement: ${t.bestEngagement}%`
);
});
return { highConfidence, singleHigh, allOpportunities: opportunities };
}
// Run it
runGapAnalysis('tiktok', 'my_fitness_account', [
'competitor_1',
'competitor_2',
'competitor_3',
]);
Sample Output
=== HASHTAG GAP ANALYSIS ===
Your account: @my_fitness_account
Competitors: @competitor_1, @competitor_2, @competitor_3
šÆ HIGH-CONFIDENCE GAPS (used by 2+ competitors):
1. #gymtok ā used by 3/3 competitors, best engagement: 6.2%, confidence: 100%
2. #formcheck ā used by 3/3 competitors, best engagement: 5.8%, confidence: 100%
3. #fitnesstips ā used by 2/3 competitors, best engagement: 4.1%, confidence: 67%
4. #homeworkout ā used by 2/3 competitors, best engagement: 3.9%, confidence: 67%
5. #personaltrainer ā used by 2/3 competitors, best engagement: 3.5%, confidence: 67%
š” WORTH TESTING (1 competitor, high engagement):
1. #calisthenics ā engagement: 7.1%
2. #functionalfitness ā engagement: 4.8%
3. #mobilitywork ā engagement: 3.6%
All 3 competitors use #gymtok and #formcheck. You don't. That's not a coincidence ā those hashtags work in your niche.
Read the Full Guide
This is a condensed version. The full guide includes:
- Time-based analysis (which hashtags trend when)
- Hashtag clustering by theme
- Automatic caption rewriting with gap hashtags
- Tracking gap closure over time
Read the complete guide on SociaVault ā
Building social media analytics tools? SociaVault provides social media data APIs for TikTok, Instagram, YouTube, and 10+ platforms. Fetch posts, captions, hashtags, and engagement data through one unified API.
Discussion
What's your hashtag strategy? Manual research, tools, or just vibes? Have you ever found a hashtag gap that completely changed your reach? š
Top comments (0)