Every brand has the same problem: someone mentions you on TikTok, tags you in an Instagram comment, or drops your name in a Reddit thread — and you find out 3 days later.
By then the viral complaint already has 50K views and your PR team is scrambling.
I fixed this with a Slack bot that monitors social mentions across platforms and drops them into the right channel within minutes. Here's how to build it yourself.
What We're Building
A lightweight service that:
- Searches for your brand name (and common misspellings) across TikTok, Instagram, Twitter, and Reddit
- Classifies mentions as positive, negative, or neutral
- Routes them to the right Slack channel (#praise for positive, #urgent for negative)
- Runs every 15 minutes on a cron job
No Slack app store submission required. We're using incoming webhooks — the simplest approach.
The Stack
- Node.js – runtime
- SociaVault API – search across platforms
- Slack Incoming Webhooks – send formatted messages
- OpenAI API (optional) – sentiment classification
- node-cron – scheduling
Setup
mkdir social-monitor && cd social-monitor
npm init -y
npm install axios node-cron dotenv
# .env
SOCIAVAULT_API_KEY=your_key
SLACK_WEBHOOK_GENERAL=https://hooks.slack.com/services/xxx/yyy/zzz
SLACK_WEBHOOK_URGENT=https://hooks.slack.com/services/xxx/yyy/aaa
OPENAI_API_KEY=your_openai_key # optional, for sentiment
Setting Up Slack Webhooks
- Go to api.slack.com/apps → Create New App
- Incoming Webhooks → Activate
- Add New Webhook → Pick channel → Copy URL
- Repeat for each channel you want to route to
Step 1: Search for Mentions
// search.js
const axios = require('axios');
const api = axios.create({
baseURL: 'https://api.sociavault.com/v1/scrape',
headers: { 'x-api-key': process.env.SOCIAVAULT_API_KEY },
});
// Your brand terms — include common misspellings and variations
const BRAND_TERMS = [
'yourbrand',
'your_brand',
'your brand',
'@yourbrand',
];
async function searchTikTok(query) {
try {
const { data } = await api.get(`/tiktok/search?query=${encodeURIComponent(query)}&limit=10`);
return (data.data || []).map(item => ({
platform: 'TikTok',
text: item.desc || item.title || '',
author: item.author?.uniqueId || 'unknown',
url: item.webVideoUrl || '',
likes: item.diggCount || 0,
timestamp: item.createTime ? new Date(item.createTime * 1000) : new Date(),
}));
} catch (err) {
console.error(`TikTok search failed: ${err.message}`);
return [];
}
}
async function searchAllPlatforms() {
const allMentions = [];
for (const term of BRAND_TERMS) {
const results = await searchTikTok(term);
allMentions.push(...results);
// Add a delay between searches
await new Promise(r => setTimeout(r, 500));
}
// Deduplicate by URL
const seen = new Set();
return allMentions.filter(m => {
if (seen.has(m.url)) return false;
seen.add(m.url);
return true;
});
}
module.exports = { searchAllPlatforms };
Step 2: Classify Sentiment (Optional but Powerful)
You can do this with simple keyword matching or with OpenAI. Here's both:
// sentiment.js
const axios = require('axios');
// Option A: Simple keyword matching (free, no API needed)
const NEGATIVE_WORDS = ['scam', 'trash', 'worst', 'terrible', 'broken', 'hate', 'avoid', 'refund', 'lawsuit', 'fake'];
const POSITIVE_WORDS = ['love', 'amazing', 'best', 'incredible', 'recommend', 'awesome', 'great', 'perfect'];
function classifySimple(text) {
const lower = text.toLowerCase();
const negScore = NEGATIVE_WORDS.filter(w => lower.includes(w)).length;
const posScore = POSITIVE_WORDS.filter(w => lower.includes(w)).length;
if (negScore > posScore) return 'negative';
if (posScore > negScore) return 'positive';
return 'neutral';
}
// Option B: OpenAI (more accurate, costs ~$0.001 per classification)
async function classifyWithAI(text) {
if (!process.env.OPENAI_API_KEY) return classifySimple(text);
try {
const { data } = await axios.post(
'https://api.openai.com/v1/chat/completions',
{
model: 'gpt-4o-mini',
messages: [
{
role: 'system',
content: 'Classify the sentiment of this social media post about a brand. Respond with exactly one word: positive, negative, or neutral.',
},
{ role: 'user', content: text.slice(0, 500) },
],
max_tokens: 10,
},
{ headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}` } }
);
const result = data.choices[0].message.content.trim().toLowerCase();
return ['positive', 'negative', 'neutral'].includes(result) ? result : 'neutral';
} catch {
return classifySimple(text);
}
}
module.exports = { classify: classifyWithAI };
Step 3: Send to Slack
// slack.js
const axios = require('axios');
const WEBHOOKS = {
general: process.env.SLACK_WEBHOOK_GENERAL,
urgent: process.env.SLACK_WEBHOOK_URGENT,
};
function getEmoji(sentiment) {
if (sentiment === 'positive') return '💚';
if (sentiment === 'negative') return '🔴';
return '⚪';
}
async function sendToSlack(mention, sentiment) {
const webhook = sentiment === 'negative' ? WEBHOOKS.urgent : WEBHOOKS.general;
const payload = {
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `${getEmoji(sentiment)} *New ${mention.platform} mention* (${sentiment})\n\n>${mention.text.slice(0, 280)}`,
},
},
{
type: 'context',
elements: [
{
type: 'mrkdwn',
text: `👤 *${mention.author}* • ❤️ ${mention.likes.toLocaleString()} likes • <${mention.url}|View Post>`,
},
],
},
],
};
try {
await axios.post(webhook, payload);
} catch (err) {
console.error(`Slack send failed: ${err.message}`);
}
}
module.exports = { sendToSlack };
Step 4: The Main Loop
// index.js
require('dotenv').config();
const cron = require('node-cron');
const { searchAllPlatforms } = require('./search');
const { classify } = require('./sentiment');
const { sendToSlack } = require('./slack');
// Track what we've already sent (in-memory, resets on restart)
// For production, use SQLite like in the Discord bot article
const sentMentions = new Set();
async function monitor() {
console.log(`[${new Date().toISOString()}] Scanning for mentions...`);
const mentions = await searchAllPlatforms();
let newCount = 0;
for (const mention of mentions) {
// Skip if we've already sent this one
if (sentMentions.has(mention.url)) continue;
const sentiment = await classify(mention.text);
await sendToSlack(mention, sentiment);
sentMentions.add(mention.url);
newCount++;
// Small delay between Slack messages to avoid rate limits
await new Promise(r => setTimeout(r, 300));
}
console.log(`Found ${mentions.length} mentions, ${newCount} new`);
}
// Run immediately, then every 15 minutes
monitor();
cron.schedule('*/15 * * * *', monitor);
console.log('Social mention monitor started. Checking every 15 minutes.');
The Result
Your Slack now looks like this:
#social-mentions
💚 New TikTok mention (positive)
> "just discovered @yourbrand and honestly this is incredible, been looking for this for months"
👤 sarah_creates • ❤️ 12,400 likes • View Post
⚪ New TikTok mention (neutral)
> "comparing @yourbrand vs competitor for my workflow, will report back"
👤 tech_reviewer • ❤️ 890 likes • View Post
#social-urgent
🔴 New TikTok mention (negative)
> "@yourbrand charged me twice and support won't respond, this is basically a scam at this point"
👤 angry_customer_42 • ❤️ 5,200 likes • View Post ← your team sees this FAST
The negative mention routes to #social-urgent where someone can respond before it snowballs.
Cost
- 4 search queries × 96 runs/day = 384 credits/day (~11,500/month)
- With deduplication, actual unique queries are lower
- Fits SociaVault growth plan
Compare that to Brandwatch ($800/month) or Mention ($83/month) doing the same thing.
Read the Full Guide
Build a Slack Social Monitoring Bot → SociaVault Blog
Monitor social media mentions programmatically with SociaVault — one API for TikTok, Instagram, YouTube, Twitter, Reddit, and more.
Discussion
What's your current social monitoring setup? Are you still checking manually, using an expensive tool, or have you built something custom?
Top comments (0)