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
- How to Monitor Twitter for Keywords — keyword monitoring
- How to Monitor Replies to a Tweet — reply tracking
- How to Get Twitter Followers as CSV — export follower data
- FetchLayer API Reference