FetchLayer fetchlayer.dev Sign in

Tutorial

How to Track Competitor Tweets Automatically

Build an automated system to monitor your competitors on X/Twitter. Track their tweets, engagement, and new followers without manual checking.

Written by Alex P.

  • competitor monitoring
  • twitter tracking
  • X monitoring
  • competitive intelligence
  • twitter automation

You want to know what your competitors are tweeting about — new features, hiring announcements, partnership news, customer complaints. Manually checking their profiles isn’t sustainable.

Here’s how to build an automated competitor tracker for X/Twitter.


The Architecture

Cron (every 15 min) → Fetch competitor tweets → Compare against seen → Alert on notable tweets

Step 1: Fetch Competitor Tweets

Use the FetchLayer user-tweets endpoint to get recent tweets from any public account. No X developer account needed.

async function getUserTweets(handle, count = 20) {
  const res = await fetch('https://api.fetchlayer.dev/twitter/user-tweets', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FETCHLAYER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ handle, count })
  });
  const data = await res.json();
  return data.tweets || [];
}

Why not use X’s API? The official API costs $100/month minimum, rate-limits per 15-minute window, and doesn’t include follower network data on Basic tier. FetchLayer gives you user tweets, profile details, and follower graphs with one API key.


Step 2: Track and Compare

import { readFileSync, writeFileSync, existsSync } from 'fs';

const DB_FILE = './competitor-state.json';

function loadState() {
  if (!existsSync(DB_FILE)) return {};
  return JSON.parse(readFileSync(DB_FILE, 'utf-8'));
}

function saveState(state) {
  writeFileSync(DB_FILE, JSON.stringify(state, null, 2));
}

// Track: last seen tweet ID, follower count, tweet count per competitor
function getState(handle) {
  const state = loadState();
  return state[handle] || { lastTweetId: null, lastFollowerCount: 0, lastTweetCount: 0 };
}

function updateState(handle, data) {
  const state = loadState();
  state[handle] = data;
  saveState(state);
}

Step 3: Detect Notable Changes

function detectChanges(handle, tweets, profile) {
  const state = getState(handle);
  const alerts = [];

  // New tweets since last check
  const newTweets = tweets.filter(t => t.id !== state.lastTweetId);
  if (newTweets.length > 0) {
    alerts.push({
      type: 'new_tweets',
      handle,
      count: newTweets.length,
      tweets: newTweets,
    });

    // Update last seen tweet ID
    state.lastTweetId = tweets[0]?.id;
  }

  // Follower count change
  if (profile && profile.followersCount !== state.lastFollowerCount) {
    const diff = profile.followersCount - state.lastFollowerCount;
    alerts.push({
      type: 'follower_change',
      handle,
      diff,
      newCount: profile.followersCount,
    });
    state.lastFollowerCount = profile.followersCount;
  }

  updateState(handle, state);
  return alerts;
}

Step 4: Send Alerts

async function sendSlackAlert(alert) {
  if (alert.type === 'new_tweets') {
    const tweetList = alert.tweets
      .map(t => `• <${t.url}|${t.text.slice(0, 100)}...> (${t.likeCount} likes)`)
      .join('\n');

    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `🐦 @${alert.handle} posted ${alert.count} new tweet(s)`,
        blocks: [
          { type: 'section', text: { type: 'mrkdwn', text: `*@${alert.handle}* posted ${alert.count} new tweet${alert.count > 1 ? 's' : ''}:\n${tweetList}` } }
        ]
      })
    });
  }

  if (alert.type === 'follower_change') {
    const emoji = alert.diff > 0 ? '📈' : '📉';
    await fetch(process.env.SLACK_WEBHOOK_URL, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        text: `${emoji} @${alert.handle} follower change: ${alert.diff > 0 ? '+' : ''}${alert.diff}`,
        blocks: [
          { type: 'section', text: { type: 'mrkdwn', text: `${emoji} *@${alert.handle}* went from ${alert.newCount - alert.diff} → ${alert.newCount} followers (${alert.diff > 0 ? '+' : ''}${alert.diff})` } }
        ]
      })
    });
  }
}

Step 5: Full Script

// track-competitors.mjs
const COMPETITORS = ['competitor1', 'competitor2', 'competitor3'];

async function getProfile(handle) {
  const res = await fetch('https://api.fetchlayer.dev/twitter/user-profile-details', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FETCHLAYER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ handle })
  });
  return res.json();
}

async function run() {
  console.log(`[${new Date().toISOString()}] Checking ${COMPETITORS.length} competitors...`);

  for (const handle of COMPETITORS) {
    const [tweets, profile] = await Promise.all([
      getUserTweets(handle, 10),
      getProfile(handle).catch(() => null),
    ]);

    const alerts = detectChanges(handle, tweets, profile);
    for (const alert of alerts) {
      await sendSlackAlert(alert);
    }
  }
}

run().catch(console.error);

Step 6: Deploy

# Every 15 minutes
*/15 * * * * cd /app && node track-competitors.mjs >> competitors.log 2>&1

Advanced: Track Keyword + Competitor Mentions

Combine competitor tracking with keyword search to catch when people mention your competitors:

async function trackCompetitorMentions(competitorName) {
  const res = await fetch('https://api.fetchlayer.dev/twitter/search', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${process.env.FETCHLAYER_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify({ query: competitorName, product: 'Latest', count: 25 })
  });
  const data = await res.json();
  return data.results || [];
}

// Combine: own tweets + what others say about them
for (const competitor of COMPETITORS) {
  const [tweets, mentions] = await Promise.all([
    getUserTweets(competitor, 10),
    trackCompetitorMentions(competitor),
  ]);
  // Process both...
}

What’s Next