Your brand is being mentioned right now. On Twitter, Reddit, TikTok.
Are they praising you? Roasting you? Spreading misinformation?
You have no idea.
In this tutorial, we'll build a Social Listening Dashboard that:
- Monitors brand mentions across multiple platforms
- Analyzes sentiment in real-time
- Alerts you to potential PR crises or viral moments
Stop being the last to know what people say about your brand.
Why Brand Monitoring Matters
The numbers:
- 96% of people who discuss brands online don't follow the brand
- Negative posts spread 2x faster than positive ones
- Responding to complaints within 1 hour increases satisfaction by 80%
Real consequences:
- United Airlines lost $1.4B in stock value after a viral video
- Wendy's gained 300K followers from snarky Twitter replies
- Tesla gets free marketing from owner-generated content
If you're not listening, you're losing.
The Stack
- Node.js: Runtime
- SociaVault API: Multi-platform search
- OpenAI API: Sentiment analysis
- Express: Simple dashboard server
Step 1: Setup
mkdir brand-monitor
cd brand-monitor
npm init -y
npm install axios openai dotenv express
Create .env:
SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
PORT=3000
Step 2: Multi-Platform Search
Create monitor.js:
require('dotenv').config();
const axios = require('axios');
const OpenAI = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const SOCIAVAULT_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
async function searchTwitter(query) {
console.log(`π¦ Searching Twitter for "${query}"...`);
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/search`, {
params: { query, limit: 30 },
headers
});
const tweets = response.data.data || [];
return tweets.map(tweet => ({
platform: 'twitter',
id: tweet.id || tweet.rest_id,
text: tweet.full_text || tweet.text,
author: tweet.user?.screen_name || tweet.author?.username,
authorName: tweet.user?.name || tweet.author?.name,
authorFollowers: tweet.user?.followers_count || tweet.author?.followers_count || 0,
likes: tweet.favorite_count || tweet.likes || 0,
retweets: tweet.retweet_count || 0,
replies: tweet.reply_count || 0,
views: tweet.views_count || 0,
date: new Date(tweet.created_at),
url: `https://twitter.com/${tweet.user?.screen_name}/status/${tweet.id}`
}));
} catch (error) {
console.error('Twitter search error:', error.message);
return [];
}
}
async function searchReddit(query) {
console.log(`π΄ Searching Reddit for "${query}"...`);
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/reddit/search`, {
params: { query, limit: 30 },
headers
});
const posts = response.data.data || [];
return posts.map(post => ({
platform: 'reddit',
id: post.id,
text: `${post.title}\n\n${post.selftext || post.body || ''}`.trim(),
title: post.title,
author: post.author,
subreddit: post.subreddit,
upvotes: post.score || post.ups || 0,
comments: post.num_comments || 0,
date: new Date(post.created_utc * 1000),
url: `https://reddit.com${post.permalink}`
}));
} catch (error) {
console.error('Reddit search error:', error.message);
return [];
}
}
async function searchTikTok(query) {
console.log(`π± Searching TikTok for "${query}"...`);
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/search`, {
params: { query, limit: 20 },
headers
});
const videos = response.data.data || [];
return videos.map(video => ({
platform: 'tiktok',
id: video.id,
text: video.desc || video.description || '',
author: video.author?.uniqueId || video.authorMeta?.name,
authorName: video.author?.nickname || video.authorMeta?.nickname,
authorFollowers: video.author?.followerCount || video.authorStats?.followerCount || 0,
views: video.playCount || video.stats?.playCount || 0,
likes: video.diggCount || video.stats?.diggCount || 0,
comments: video.commentCount || video.stats?.commentCount || 0,
shares: video.shareCount || video.stats?.shareCount || 0,
date: new Date(video.createTime * 1000),
url: `https://tiktok.com/@${video.author?.uniqueId}/video/${video.id}`
}));
} catch (error) {
console.error('TikTok search error:', error.message);
return [];
}
}
async function searchInstagramHashtag(query) {
console.log(`πΈ Searching Instagram for "${query}"...`);
try {
// Clean query for hashtag search
const hashtag = query.toLowerCase().replace(/[^a-z0-9]/g, '');
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/hashtag`, {
params: { hashtag, limit: 20 },
headers
});
const posts = response.data.data?.posts || response.data.data || [];
return posts.map(post => ({
platform: 'instagram',
id: post.id || post.pk,
text: post.caption || '',
author: post.user?.username || post.owner?.username,
authorFollowers: post.user?.follower_count || 0,
likes: post.like_count || post.likes || 0,
comments: post.comment_count || post.comments || 0,
views: post.play_count || post.video_view_count || 0,
date: new Date(post.taken_at * 1000),
url: `https://instagram.com/p/${post.code || post.shortcode}`
}));
} catch (error) {
console.error('Instagram search error:', error.message);
return [];
}
}
module.exports = {
searchTwitter,
searchReddit,
searchTikTok,
searchInstagramHashtag
};
Step 3: Sentiment Analysis Engine
Create sentiment.js:
const OpenAI = require('openai');
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
async function analyzeSentiment(mentions) {
if (mentions.length === 0) return [];
// Batch analyze for efficiency
const batches = chunkArray(mentions, 10);
const results = [];
for (const batch of batches) {
const batchResults = await analyzeBatch(batch);
results.push(...batchResults);
}
return results;
}
async function analyzeBatch(mentions) {
const textsForAnalysis = mentions.map((m, i) => `[${i}] ${m.text.substring(0, 300)}`).join('\n\n');
const prompt = `Analyze the sentiment of each social media post about a brand. For each post, determine:
1. Sentiment: "positive", "negative", "neutral", or "mixed"
2. Sentiment score: -1 to 1 (negative to positive)
3. Category: "praise", "complaint", "question", "mention", "comparison", "review", "concern"
4. Urgency: "high" (needs response), "medium", "low"
5. Key topics: 1-3 main topics mentioned
Posts to analyze:
${textsForAnalysis}
Return JSON array with objects for each post index containing: sentiment, score, category, urgency, topics`;
try {
const response = await openai.chat.completions.create({
model: 'gpt-4o-mini',
messages: [{ role: 'user', content: prompt }],
response_format: { type: 'json_object' }
});
const analysis = JSON.parse(response.choices[0].message.content);
const results = analysis.posts || analysis.results || Object.values(analysis);
return mentions.map((mention, i) => ({
...mention,
sentiment: results[i]?.sentiment || 'neutral',
sentimentScore: results[i]?.score || 0,
category: results[i]?.category || 'mention',
urgency: results[i]?.urgency || 'low',
topics: results[i]?.topics || []
}));
} catch (error) {
console.error('Sentiment analysis error:', error.message);
return mentions.map(m => ({
...m,
sentiment: 'unknown',
sentimentScore: 0,
category: 'mention',
urgency: 'low',
topics: []
}));
}
}
function chunkArray(array, size) {
const chunks = [];
for (let i = 0; i < array.length; i += size) {
chunks.push(array.slice(i, i + size));
}
return chunks;
}
function calculateSentimentSummary(analyzedMentions) {
const total = analyzedMentions.length;
if (total === 0) return null;
const sentimentCounts = {
positive: 0,
negative: 0,
neutral: 0,
mixed: 0
};
const categoryCounts = {};
const topicCounts = {};
let totalScore = 0;
let urgentCount = 0;
analyzedMentions.forEach(m => {
sentimentCounts[m.sentiment] = (sentimentCounts[m.sentiment] || 0) + 1;
categoryCounts[m.category] = (categoryCounts[m.category] || 0) + 1;
totalScore += m.sentimentScore;
if (m.urgency === 'high') urgentCount++;
m.topics?.forEach(topic => {
topicCounts[topic] = (topicCounts[topic] || 0) + 1;
});
});
const avgScore = totalScore / total;
// Determine overall sentiment health
const positiveRatio = sentimentCounts.positive / total;
const negativeRatio = sentimentCounts.negative / total;
let health;
if (positiveRatio > 0.6) health = { status: 'Excellent', emoji: 'π', color: 'green' };
else if (positiveRatio > 0.4) health = { status: 'Good', emoji: 'π', color: 'blue' };
else if (negativeRatio > 0.4) health = { status: 'Concerning', emoji: 'π ', color: 'orange' };
else if (negativeRatio > 0.6) health = { status: 'Critical', emoji: 'π΄', color: 'red' };
else health = { status: 'Neutral', emoji: 'βͺ', color: 'gray' };
// Top topics
const topTopics = Object.entries(topicCounts)
.sort((a, b) => b[1] - a[1])
.slice(0, 5)
.map(([topic, count]) => ({ topic, count, percentage: Math.round((count / total) * 100) }));
return {
total,
sentimentCounts,
categoryCounts,
avgScore: avgScore.toFixed(2),
health,
urgentCount,
topTopics,
positiveRatio: Math.round(positiveRatio * 100),
negativeRatio: Math.round(negativeRatio * 100),
neutralRatio: Math.round((sentimentCounts.neutral / total) * 100)
};
}
module.exports = {
analyzeSentiment,
calculateSentimentSummary
};
Step 4: Alert System
Create alerts.js:
function checkAlerts(analyzedMentions, thresholds = {}) {
const alerts = [];
const defaults = {
viralThreshold: 10000, // Views/engagement for viral alert
negativeSpike: 3, // Number of negative mentions in batch
influencerThreshold: 50000, // Follower count for influencer alert
urgentLimit: 2 // Number of urgent mentions
};
const config = { ...defaults, ...thresholds };
// Check for viral mentions
const viralMentions = analyzedMentions.filter(m => {
const engagement = (m.views || 0) + (m.likes || 0) * 10 + (m.shares || 0) * 20;
return engagement > config.viralThreshold;
});
if (viralMentions.length > 0) {
alerts.push({
type: 'viral',
severity: 'high',
emoji: 'π',
title: 'Viral Mention Detected',
message: `${viralMentions.length} post(s) getting significant traction`,
mentions: viralMentions
});
}
// Check for negative sentiment spike
const negativeMentions = analyzedMentions.filter(m => m.sentiment === 'negative');
if (negativeMentions.length >= config.negativeSpike) {
alerts.push({
type: 'negative_spike',
severity: 'high',
emoji: 'β οΈ',
title: 'Negative Sentiment Spike',
message: `${negativeMentions.length} negative mentions detected`,
mentions: negativeMentions
});
}
// Check for influencer mentions
const influencerMentions = analyzedMentions.filter(m =>
m.authorFollowers >= config.influencerThreshold
);
if (influencerMentions.length > 0) {
alerts.push({
type: 'influencer',
severity: 'medium',
emoji: 'β',
title: 'Influencer Mention',
message: `${influencerMentions.length} mention(s) from high-follower accounts`,
mentions: influencerMentions
});
}
// Check for urgent responses needed
const urgentMentions = analyzedMentions.filter(m => m.urgency === 'high');
if (urgentMentions.length >= config.urgentLimit) {
alerts.push({
type: 'urgent',
severity: 'high',
emoji: 'π',
title: 'Urgent Response Needed',
message: `${urgentMentions.length} mention(s) require immediate attention`,
mentions: urgentMentions
});
}
// Check for competitor mentions
const comparisonMentions = analyzedMentions.filter(m => m.category === 'comparison');
if (comparisonMentions.length > 0) {
alerts.push({
type: 'comparison',
severity: 'low',
emoji: 'π',
title: 'Competitor Comparison',
message: `${comparisonMentions.length} mention(s) comparing to competitors`,
mentions: comparisonMentions
});
}
// Sort by severity
const severityOrder = { high: 0, medium: 1, low: 2 };
alerts.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
return alerts;
}
module.exports = { checkAlerts };
Step 5: Dashboard Server
Create server.js:
require('dotenv').config();
const express = require('express');
const { searchTwitter, searchReddit, searchTikTok, searchInstagramHashtag } = require('./monitor');
const { analyzeSentiment, calculateSentimentSummary } = require('./sentiment');
const { checkAlerts } = require('./alerts');
const app = express();
app.use(express.json());
// Store monitoring results
let monitoringData = {
lastUpdate: null,
brand: null,
mentions: [],
summary: null,
alerts: []
};
// Main monitoring endpoint
app.get('/api/monitor/:brand', async (req, res) => {
const brand = req.params.brand;
const platforms = req.query.platforms?.split(',') || ['twitter', 'reddit', 'tiktok'];
console.log(`\nπ Monitoring brand: ${brand}`);
console.log(`Platforms: ${platforms.join(', ')}\n`);
try {
// Collect mentions from all platforms
let allMentions = [];
if (platforms.includes('twitter')) {
const twitterMentions = await searchTwitter(brand);
allMentions.push(...twitterMentions);
}
if (platforms.includes('reddit')) {
const redditMentions = await searchReddit(brand);
allMentions.push(...redditMentions);
}
if (platforms.includes('tiktok')) {
const tiktokMentions = await searchTikTok(brand);
allMentions.push(...tiktokMentions);
}
if (platforms.includes('instagram')) {
const instagramMentions = await searchInstagramHashtag(brand);
allMentions.push(...instagramMentions);
}
console.log(`π Found ${allMentions.length} total mentions\n`);
// Analyze sentiment
console.log('π€ Analyzing sentiment...\n');
const analyzedMentions = await analyzeSentiment(allMentions);
// Calculate summary
const summary = calculateSentimentSummary(analyzedMentions);
// Check for alerts
const alerts = checkAlerts(analyzedMentions);
// Store results
monitoringData = {
lastUpdate: new Date().toISOString(),
brand,
mentions: analyzedMentions,
summary,
alerts
};
res.json(monitoringData);
} catch (error) {
console.error('Monitoring error:', error);
res.status(500).json({ error: error.message });
}
});
// Get latest data
app.get('/api/latest', (req, res) => {
res.json(monitoringData);
});
// Get alerts only
app.get('/api/alerts', (req, res) => {
res.json(monitoringData.alerts);
});
// Get mentions by sentiment
app.get('/api/mentions/:sentiment', (req, res) => {
const sentiment = req.params.sentiment;
const filtered = monitoringData.mentions.filter(m => m.sentiment === sentiment);
res.json(filtered);
});
// Dashboard HTML
app.get('/', (req, res) => {
res.send(`
<!DOCTYPE html>
<html>
<head>
<title>Brand Monitor Dashboard</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #0f172a;
color: #e2e8f0;
padding: 20px;
}
.container { max-width: 1400px; margin: 0 auto; }
h1 { font-size: 2rem; margin-bottom: 20px; }
.search-box {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
input {
flex: 1;
padding: 12px 16px;
font-size: 16px;
border: 1px solid #334155;
border-radius: 8px;
background: #1e293b;
color: #fff;
}
button {
padding: 12px 24px;
font-size: 16px;
background: #3b82f6;
color: white;
border: none;
border-radius: 8px;
cursor: pointer;
}
button:hover { background: #2563eb; }
.grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 20px; margin-bottom: 30px; }
.card {
background: #1e293b;
border-radius: 12px;
padding: 20px;
}
.card-title { font-size: 0.875rem; color: #94a3b8; margin-bottom: 8px; }
.card-value { font-size: 2rem; font-weight: bold; }
.card-subtitle { font-size: 0.75rem; color: #64748b; margin-top: 4px; }
.positive { color: #22c55e; }
.negative { color: #ef4444; }
.neutral { color: #94a3b8; }
.alerts { margin-bottom: 30px; }
.alert {
background: #1e293b;
border-left: 4px solid;
padding: 15px 20px;
margin-bottom: 10px;
border-radius: 0 8px 8px 0;
}
.alert.high { border-color: #ef4444; }
.alert.medium { border-color: #f59e0b; }
.alert.low { border-color: #3b82f6; }
.mentions-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 15px; }
.mention {
background: #1e293b;
border-radius: 8px;
padding: 15px;
}
.mention-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.platform-badge {
padding: 4px 8px;
border-radius: 4px;
font-size: 0.75rem;
background: #334155;
}
.mention-text { font-size: 0.875rem; line-height: 1.5; margin-bottom: 10px; }
.mention-meta { font-size: 0.75rem; color: #64748b; }
.sentiment-badge {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 0.75rem;
}
.sentiment-badge.positive { background: rgba(34, 197, 94, 0.2); color: #22c55e; }
.sentiment-badge.negative { background: rgba(239, 68, 68, 0.2); color: #ef4444; }
.sentiment-badge.neutral { background: rgba(148, 163, 184, 0.2); color: #94a3b8; }
.loading { text-align: center; padding: 40px; color: #64748b; }
</style>
</head>
<body>
<div class="container">
<h1>π― Brand Monitor</h1>
<div class="search-box">
<input type="text" id="brandInput" placeholder="Enter brand name to monitor..." value="">
<button onclick="monitor()">Monitor</button>
</div>
<div id="dashboard" class="loading">
Enter a brand name to start monitoring
</div>
</div>
<script>
async function monitor() {
const brand = document.getElementById('brandInput').value;
if (!brand) return;
document.getElementById('dashboard').innerHTML = '<div class="loading">π Searching across platforms...</div>';
try {
const res = await fetch('/api/monitor/' + encodeURIComponent(brand));
const data = await res.json();
renderDashboard(data);
} catch (err) {
document.getElementById('dashboard').innerHTML = '<div class="loading">Error: ' + err.message + '</div>';
}
}
function renderDashboard(data) {
const { summary, alerts, mentions, brand, lastUpdate } = data;
if (!summary) {
document.getElementById('dashboard').innerHTML = '<div class="loading">No mentions found</div>';
return;
}
let html = '<div class="grid">';
html += '<div class="card"><div class="card-title">Total Mentions</div><div class="card-value">' + summary.total + '</div><div class="card-subtitle">across all platforms</div></div>';
html += '<div class="card"><div class="card-title">Brand Health</div><div class="card-value">' + summary.health.emoji + ' ' + summary.health.status + '</div><div class="card-subtitle">Avg Score: ' + summary.avgScore + '</div></div>';
html += '<div class="card"><div class="card-title">Positive</div><div class="card-value positive">' + summary.positiveRatio + '%</div><div class="card-subtitle">' + summary.sentimentCounts.positive + ' mentions</div></div>';
html += '<div class="card"><div class="card-title">Negative</div><div class="card-value negative">' + summary.negativeRatio + '%</div><div class="card-subtitle">' + summary.sentimentCounts.negative + ' mentions</div></div>';
html += '</div>';
if (alerts.length > 0) {
html += '<div class="alerts"><h2 style="margin-bottom:15px">β οΈ Alerts</h2>';
alerts.forEach(a => {
html += '<div class="alert ' + a.severity + '"><strong>' + a.emoji + ' ' + a.title + '</strong><br>' + a.message + '</div>';
});
html += '</div>';
}
html += '<h2 style="margin-bottom:15px">π Recent Mentions</h2>';
html += '<div class="mentions-grid">';
mentions.slice(0, 20).forEach(m => {
html += '<div class="mention">';
html += '<div class="mention-header"><span class="platform-badge">' + m.platform + '</span><span class="sentiment-badge ' + m.sentiment + '">' + m.sentiment + '</span></div>';
html += '<div class="mention-text">' + (m.text || '').substring(0, 200) + '...</div>';
html += '<div class="mention-meta">@' + m.author + ' β’ ' + formatNumber(m.likes || 0) + ' likes β’ ' + m.category + '</div>';
html += '</div>';
});
html += '</div>';
document.getElementById('dashboard').innerHTML = html;
}
function formatNumber(n) {
if (n >= 1000000) return (n/1000000).toFixed(1) + 'M';
if (n >= 1000) return (n/1000).toFixed(1) + 'K';
return n;
}
</script>
</body>
</html>
`);
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`\nπ― Brand Monitor Dashboard running at http://localhost:${PORT}\n`);
});
Step 6: Run It
node server.js
Open http://localhost:3000 and enter a brand name.
Or use the API directly:
curl "http://localhost:3000/api/monitor/tesla?platforms=twitter,reddit,tiktok"
Sample Dashboard Output
π Monitoring brand: tesla
Platforms: twitter, reddit, tiktok
π¦ Searching Twitter for "tesla"...
π΄ Searching Reddit for "tesla"...
π± Searching TikTok for "tesla"...
π Found 87 total mentions
π€ Analyzing sentiment...
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π BRAND MONITORING REPORT: TESLA
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π SUMMARY
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Total Mentions: 87
Brand Health: π Good
Average Sentiment Score: 0.34
Sentiment Breakdown:
π Positive: 38 (44%)
βͺ Neutral: 31 (36%)
π΄ Negative: 18 (21%)
β οΈ ALERTS (3)
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
π HIGH: Viral Mention Detected
2 post(s) getting significant traction
β MEDIUM: Influencer Mention
3 mention(s) from high-follower accounts
π HIGH: Urgent Response Needed
4 mention(s) require immediate attention
π TOP TOPICS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. Cybertruck (23%)
2. FSD (18%)
3. Model Y (15%)
4. Stock (12%)
5. Charging (9%)
π΄ NEGATIVE MENTIONS REQUIRING ATTENTION
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. [Twitter] @angry_customer
"3 weeks waiting for Tesla service appointment..."
Urgency: HIGH | Category: complaint
2. [Reddit] r/teslamotors
"Phantom braking happened again on my MY..."
Urgency: HIGH | Category: concern
3. [Twitter] @tech_reviewer
"Compared Tesla FSD to Waymo today. Not even close..."
Urgency: MEDIUM | Category: comparison
π POSITIVE MENTIONS
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
1. [TikTok] @ev_enthusiast (125K followers)
"Tesla road trip across the country..."
Engagement: 234K views | Category: praise
2. [Twitter] @happy_owner
"Just hit 100K miles on my Model 3. Zero issues..."
Engagement: 1.2K likes | Category: review
What You Just Built
Enterprise social listening tools are expensive:
- Brandwatch: $800+/month
- Sprinklr: $1000+/month
- Mention: $41+/month
- Brand24: $99+/month
Your version monitors across platforms for cents per search.
Features You Can Add
- Scheduled monitoring: Run checks every hour
- Email/Slack alerts: Send notifications
- Competitor comparison: Track multiple brands
- Trend tracking: Monitor sentiment over time
- Response suggestions: AI-generated reply templates
Get Started
- Get your SociaVault API Key
- Run the server
- Monitor your brand
Stop being surprised by viral crises. Start listening in real-time.
Your brand has a reputation. Know what it is.
Top comments (0)