DEV Community

Cover image for Reverse-Engineer Any Competitor's Posting Schedule with Node.js
Olamide Olaniyan
Olamide Olaniyan

Posted on

Reverse-Engineer Any Competitor's Posting Schedule with Node.js

Your competitor posts 4 times a week. Their engagement is consistently 3x yours.

Is it the content? Maybe. But what if it's when they post?

Timing matters more than most developers think. The algorithm doesn't just evaluate content quality — it evaluates initial engagement velocity. Post when your audience is online, and you get that early boost. Post when they're asleep, and your content is buried before anyone sees it.

I built a tool that analyzes any creator's posting history and extracts their exact schedule — day of week, hour of day, and which time slots get the best results. Here's the whole thing.

The Stack

  • Node.js – runtime
  • SociaVault API – fetch post history with timestamps
  • Statistics – frequency analysis, heatmap generation

Step 1: Fetch Post History

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 getPostHistory(platform, username, limit = 100) {
  const { data } = await api.get(`/${platform}/posts/${username}`, {
    params: { limit },
  });
  return data.posts;
}

async function getProfile(platform, username) {
  const { data } = await api.get(`/${platform}/profile/${username}`);
  return data;
}
Enter fullscreen mode Exit fullscreen mode

Step 2: Extract the Schedule

Convert raw post timestamps into a day-of-week × hour-of-day frequency map.

function extractPostingSchedule(posts, timezone = 'America/New_York') {
  const dayHourMap = {};
  const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];

  // Initialize 7×24 grid
  for (let day = 0; day < 7; day++) {
    dayHourMap[dayNames[day]] = {};
    for (let hour = 0; hour < 24; hour++) {
      dayHourMap[dayNames[day]][hour] = { count: 0, totalLikes: 0, totalComments: 0, posts: [] };
    }
  }

  for (const post of posts) {
    const date = new Date(post.createdAt);

    // Convert to target timezone
    const localTime = new Date(date.toLocaleString('en-US', { timeZone: timezone }));
    const day = dayNames[localTime.getDay()];
    const hour = localTime.getHours();

    dayHourMap[day][hour].count++;
    dayHourMap[day][hour].totalLikes += post.likeCount || 0;
    dayHourMap[day][hour].totalComments += post.commentCount || 0;
    dayHourMap[day][hour].posts.push(post);
  }

  return dayHourMap;
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Find Peak Posting Times

Which time slots does the competitor use most? And which perform best?

function findPeakTimes(dayHourMap) {
  const slots = [];

  for (const [day, hours] of Object.entries(dayHourMap)) {
    for (const [hour, data] of Object.entries(hours)) {
      if (data.count === 0) continue;

      const avgLikes = Math.round(data.totalLikes / data.count);
      const avgComments = Math.round(data.totalComments / data.count);

      slots.push({
        day,
        hour: parseInt(hour),
        hourFormatted: formatHour(parseInt(hour)),
        postCount: data.count,
        avgLikes,
        avgComments,
      });
    }
  }

  return {
    byFrequency: [...slots].sort((a, b) => b.postCount - a.postCount),
    byPerformance: [...slots].sort((a, b) => b.avgLikes - a.avgLikes),
  };
}

function formatHour(hour) {
  if (hour === 0) return '12 AM';
  if (hour === 12) return '12 PM';
  if (hour < 12) return `${hour} AM`;
  return `${hour - 12} PM`;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Generate a Posting Heatmap

A visual heatmap shows the entire schedule at a glance.

function generateHeatmap(dayHourMap) {
  const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
  const hours = [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22]; // 6 AM - 10 PM

  // Find max count for normalization
  let maxCount = 0;
  for (const day of days) {
    for (const hour of hours) {
      if (dayHourMap[day][hour].count > maxCount) {
        maxCount = dayHourMap[day][hour].count;
      }
    }
  }

  console.log('\n📅 POSTING FREQUENCY HEATMAP');
  console.log('   (░ = never, ▒ = sometimes, ▓ = often, █ = peak)\n');

  // Header
  console.log('         ' + hours.map(h => formatHour(h).padStart(5)).join(' '));

  for (const day of days) {
    let row = day.padEnd(10);

    for (const hour of hours) {
      const count = dayHourMap[day][hour].count;
      const intensity = maxCount > 0 ? count / maxCount : 0;

      let block;
      if (intensity === 0) block = '';
      else if (intensity < 0.33) block = '';
      else if (intensity < 0.66) block = '';
      else block = '';

      row += block;
    }

    console.log(row);
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Compare Your Schedule vs Competitor's

function compareSchedules(mySchedule, compSchedule) {
  const myPeak = findPeakTimes(mySchedule);
  const compPeak = findPeakTimes(compSchedule);

  // Slots competitor uses heavily that you don't
  const gaps = [];
  const compTopSlots = compPeak.byPerformance.slice(0, 10);

  for (const slot of compTopSlots) {
    const mySlotData = mySchedule[slot.day]?.[slot.hour];
    const myCount = mySlotData?.count || 0;

    if (myCount === 0) {
      gaps.push({
        ...slot,
        type: 'untapped',
        message: `Competitor posts ${slot.day} at ${slot.hourFormatted} (avg ${slot.avgLikes.toLocaleString()} likes) — you never post here`,
      });
    } else if (myCount < slot.postCount * 0.5) {
      gaps.push({
        ...slot,
        type: 'underused',
        message: `Competitor posts ${slot.day} at ${slot.hourFormatted} ${slot.postCount}x — you only post ${myCount}x`,
      });
    }
  }

  return gaps;
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Find Optimal Posting Windows

Combine frequency + performance to find the optimal schedule.

function findOptimalSchedule(dayHourMap, slotsPerWeek = 5) {
  const scored = [];

  for (const [day, hours] of Object.entries(dayHourMap)) {
    for (const [hour, data] of Object.entries(hours)) {
      if (data.count === 0) continue;

      // Score = avgLikes * consistency bonus
      const avgLikes = data.totalLikes / data.count;
      const consistencyBonus = Math.min(data.count / 3, 2); // Bonus for slots used 3+ times

      scored.push({
        day,
        hour: parseInt(hour),
        hourFormatted: formatHour(parseInt(hour)),
        score: avgLikes * consistencyBonus,
        avgLikes: Math.round(avgLikes),
        timesUsed: data.count,
      });
    }
  }

  // Pick top N non-overlapping slots
  const optimal = scored
    .sort((a, b) => b.score - a.score)
    .slice(0, slotsPerWeek);

  return optimal;
}
Enter fullscreen mode Exit fullscreen mode

Putting It All Together

async function analyzeCompetitorSchedule(platform, competitorUsername, myUsername) {
  console.log(`\n=== POSTING SCHEDULE ANALYSIS ===`);
  console.log(`Competitor: @${competitorUsername}`);
  console.log(`Your account: @${myUsername}\n`);

  // Fetch post history for both
  const compPosts = await getPostHistory(platform, competitorUsername, 100);
  const myPosts = await getPostHistory(platform, myUsername, 100);

  const compProfile = await getProfile(platform, competitorUsername);
  const myProfile = await getProfile(platform, myUsername);

  console.log(`Competitor: ${compPosts.length} posts analyzed (${compProfile.followerCount.toLocaleString()} followers)`);
  console.log(`You: ${myPosts.length} posts analyzed (${myProfile.followerCount.toLocaleString()} followers)`);

  // Extract schedules
  const compSchedule = extractPostingSchedule(compPosts);
  const mySchedule = extractPostingSchedule(myPosts);

  // Competitor heatmap
  console.log(`\n@${competitorUsername}'s posting pattern:`);
  generateHeatmap(compSchedule);

  // Peak times
  const compPeaks = findPeakTimes(compSchedule);
  console.log('\n🏆 Competitor\'s Top 5 Time Slots (by performance):');
  compPeaks.byPerformance.slice(0, 5).forEach((slot, i) => {
    console.log(
      `  ${i + 1}. ${slot.day} ${slot.hourFormatted} — ` +
      `avg ${slot.avgLikes.toLocaleString()} likes, ` +
      `used ${slot.postCount}x`
    );
  });

  // Schedule gaps
  const gaps = compareSchedules(mySchedule, compSchedule);
  if (gaps.length > 0) {
    console.log('\n⚡ Schedule Gaps (competitor uses, you don\'t):');
    gaps.forEach((gap, i) => {
      console.log(`  ${i + 1}. ${gap.message}`);
    });
  }

  // Optimal schedule recommendation
  const optimal = findOptimalSchedule(compSchedule, 5);
  console.log('\n📋 Recommended Posting Schedule (based on competitor data):');
  optimal.forEach((slot, i) => {
    console.log(`  ${i + 1}. ${slot.day} at ${slot.hourFormatted} (expected ~${slot.avgLikes.toLocaleString()} likes)`);
  });
}

analyzeCompetitorSchedule('tiktok', 'competitor_handle', 'my_handle');
Enter fullscreen mode Exit fullscreen mode

Sample Output

=== POSTING SCHEDULE ANALYSIS ===
Competitor: @fitnessguru_mike
Your account: @my_fitness_page

Competitor: 94 posts analyzed (245,000 followers)
You: 78 posts analyzed (52,000 followers)

@fitnessguru_mike's posting pattern:

📅 POSTING FREQUENCY HEATMAP
   (░ = never, ▒ = sometimes, ▓ = often, █ = peak)

           6 AM   7 AM   8 AM   9 AM  10 AM  11 AM  12 PM ...
Monday       ░      ░      ▓      █      ▒      ░      ░
Tuesday      ░      ░      ▒      ▒      ░      ░      ▓
Wednesday    ░      ░      ░      █      ▓      ░      ░
Thursday     ░      ░      ▓      ▒      ░      ░      ▒
Friday       ░      ░      ░      ░      ░      ░      ░
Saturday     ░      ▒      ▓      ▓      ░      ░      ░
Sunday       ░      ░      ░      ▒      ░      ░      ░

🏆 Competitor's Top 5 Time Slots (by performance):
  1. Monday 9 AM — avg 18,400 likes, used 8x
  2. Wednesday 9 AM — avg 16,200 likes, used 7x
  3. Saturday 8 AM — avg 14,800 likes, used 5x
  4. Tuesday 12 PM — avg 13,100 likes, used 4x
  5. Thursday 8 AM — avg 11,900 likes, used 6x

⚡ Schedule Gaps (competitor uses, you don't):
  1. Competitor posts Saturday at 8 AM (avg 14,800 likes) — you never post here
  2. Competitor posts Wednesday at 10 AM (avg 12,400 likes) — you never post here

📋 Recommended Posting Schedule (based on competitor data):
  1. Monday at 9 AM (expected ~18,400 likes)
  2. Wednesday at 9 AM (expected ~16,200 likes)
  3. Saturday at 8 AM (expected ~14,800 likes)
  4. Tuesday at 12 PM (expected ~13,100 likes)
  5. Thursday at 8 AM (expected ~11,900 likes)
Enter fullscreen mode Exit fullscreen mode

They post at 9 AM on Monday and Wednesday — and those are their highest performing slots. You're posting at 3 PM on random days. That alone could explain the 3x engagement gap.

Read the Full Guide

This is a condensed version. The full guide includes:

  • Timezone detection from audience location data
  • Multi-competitor schedule overlap analysis
  • Automated scheduling integration (Buffer, Later API)
  • Seasonal and trend-adjusted timing

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 post history with timestamps, engagement data, and creator profiles through one unified API.

Discussion

When do you post? Is it strategic or "whenever I finish editing"? Have you ever tested different times and seen a real difference? 👇

webdev #api #nodejs #socialmedia #javascript

Top comments (0)