DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a Comment Response Prioritizer with AI

You have 500 comments on your last post.

Most are spam. Some are genuine questions. A few are from potential customers. One is from an influencer who could 10x your reach.

Which do you answer first?

In this tutorial, we'll build a Comment Response Prioritizer that:

  1. Fetches all comments from your posts
  2. Uses AI to categorize and prioritize them
  3. Identifies high-value comments that need immediate attention

Stop treating all comments equally. Start responding strategically.

The Comment Priority Matrix

Not all comments deserve the same energy:

Priority Type Example Action
πŸ”΄ URGENT Purchase intent "Where can I buy this?" Respond in < 1 hour
πŸ”΄ URGENT Influencer Big account commenting Respond + follow up
🟑 HIGH Genuine question "How does this work?" Respond same day
🟑 HIGH Negative but fixable "This broke after 2 days" Respond + DM
🟒 MEDIUM Positive engagement "Love this! πŸ”₯" Like + maybe respond
βšͺ LOW Generic "Nice!" "πŸ”₯πŸ”₯πŸ”₯" Like only
β›” SKIP Spam/Bot "DM me for collab πŸ’―" Ignore

The Stack

  • Node.js: Runtime
  • SociaVault API: Fetch comments
  • OpenAI API: Prioritize and categorize

Step 1: Setup

mkdir comment-prioritizer
cd comment-prioritizer
npm init -y
npm install axios openai dotenv
Enter fullscreen mode Exit fullscreen mode

Create .env:

SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
Enter fullscreen mode Exit fullscreen mode

Step 2: Fetch Comments

Create index.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 getTikTokComments(videoUrl) {
  console.log('πŸ“± Fetching TikTok comments...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/comments`, {
      params: { url: videoUrl, limit: 100 },
      headers
    });

    return (response.data.data || []).map(comment => ({
      platform: 'tiktok',
      id: comment.cid || comment.id,
      text: comment.text || comment.comment,
      author: comment.user?.uniqueId || comment.uniqueId || comment.username,
      authorName: comment.user?.nickname || comment.nickname,
      authorFollowers: comment.user?.followerCount || 0,
      authorVerified: comment.user?.verified || false,
      likes: comment.diggCount || comment.likes || 0,
      replies: comment.replyCommentTotal || comment.replyCount || 0,
      timestamp: comment.createTime ? new Date(comment.createTime * 1000) : new Date()
    }));
  } catch (error) {
    console.error('TikTok comments error:', error.message);
    return [];
  }
}

async function getInstagramComments(postUrl) {
  console.log('πŸ“Έ Fetching Instagram comments...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/comments`, {
      params: { url: postUrl, limit: 100 },
      headers
    });

    return (response.data.data || []).map(comment => ({
      platform: 'instagram',
      id: comment.pk || comment.id,
      text: comment.text,
      author: comment.user?.username || comment.username,
      authorName: comment.user?.full_name,
      authorFollowers: comment.user?.follower_count || 0,
      authorVerified: comment.user?.is_verified || false,
      likes: comment.comment_like_count || comment.likes || 0,
      timestamp: comment.created_at ? new Date(comment.created_at * 1000) : new Date()
    }));
  } catch (error) {
    console.error('Instagram comments error:', error.message);
    return [];
  }
}

async function getYouTubeComments(videoUrl) {
  console.log('🎬 Fetching YouTube comments...');

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/youtube/video/comments`, {
      params: { url: videoUrl, limit: 100 },
      headers
    });

    return (response.data.data || []).map(comment => ({
      platform: 'youtube',
      id: comment.id,
      text: comment.text || comment.content,
      author: comment.author?.name || comment.authorDisplayName,
      authorChannel: comment.author?.channelId || comment.authorChannelUrl,
      authorFollowers: comment.author?.subscriberCount || 0,
      likes: comment.likes || comment.likeCount || 0,
      replies: comment.replyCount || 0,
      timestamp: comment.publishedAt ? new Date(comment.publishedAt) : new Date()
    }));
  } catch (error) {
    console.error('YouTube comments error:', error.message);
    return [];
  }
}

async function getTwitterReplies(tweetUrl) {
  console.log('🐦 Fetching Twitter replies...');

  try {
    // Get the tweet first to find replies
    const tweetRes = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/tweet`, {
      params: { url: tweetUrl },
      headers
    });

    const tweet = tweetRes.data.data;

    // Note: Twitter reply fetching may need specific endpoint
    // This is a simplified version
    return (tweet.replies || []).map(reply => ({
      platform: 'twitter',
      id: reply.id || reply.rest_id,
      text: reply.full_text || reply.text,
      author: reply.user?.screen_name || reply.author?.username,
      authorName: reply.user?.name,
      authorFollowers: reply.user?.followers_count || 0,
      authorVerified: reply.user?.verified || false,
      likes: reply.favorite_count || reply.likes || 0,
      retweets: reply.retweet_count || 0,
      timestamp: new Date(reply.created_at)
    }));
  } catch (error) {
    console.error('Twitter replies error:', error.message);
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: AI Prioritization Engine

async function prioritizeComments(comments, context = {}) {
  if (comments.length === 0) return [];

  // Split into batches for API limits
  const batchSize = 20;
  const batches = [];
  for (let i = 0; i < comments.length; i += batchSize) {
    batches.push(comments.slice(i, i + batchSize));
  }

  const results = [];

  for (const batch of batches) {
    const prioritized = await prioritizeBatch(batch, context);
    results.push(...prioritized);
  }

  // Sort by priority
  const priorityOrder = { 'urgent': 0, 'high': 1, 'medium': 2, 'low': 3, 'skip': 4 };
  results.sort((a, b) => {
    const priorityDiff = priorityOrder[a.priority] - priorityOrder[b.priority];
    if (priorityDiff !== 0) return priorityDiff;
    // Within same priority, sort by author influence
    return (b.authorFollowers || 0) - (a.authorFollowers || 0);
  });

  return results;
}

async function prioritizeBatch(comments, context) {
  const commentsForAnalysis = comments.map((c, i) => ({
    index: i,
    text: c.text?.substring(0, 200) || '',
    author: c.author,
    authorFollowers: c.authorFollowers || 0,
    authorVerified: c.authorVerified || false,
    likes: c.likes || 0
  }));

  const prompt = `Analyze and prioritize these social media comments for response.

CONTEXT:
${context.businessType ? `Business Type: ${context.businessType}` : ''}
${context.goals ? `Response Goals: ${context.goals}` : 'General engagement and customer service'}

COMMENTS TO ANALYZE:
${JSON.stringify(commentsForAnalysis, null, 2)}

For each comment, determine:

1. PRIORITY LEVEL:
   - "urgent": Purchase intent, customer issues, influencer engagement, viral potential
   - "high": Genuine questions, constructive feedback, potential leads
   - "medium": Positive engagement, discussion contributions
   - "low": Generic positive comments, simple emojis
   - "skip": Spam, bots, self-promotion, irrelevant

2. CATEGORY:
   - "purchase_intent": Shows interest in buying
   - "question": Asks something specific
   - "complaint": Negative but addressable
   - "praise": Positive feedback
   - "suggestion": Feature request or idea
   - "influencer": High-follower account engagement
   - "discussion": Adds to conversation
   - "spam": Bot or self-promotion
   - "generic": Simple engagement (emojis, "nice!")

3. SUGGESTED_RESPONSE: Brief response template (null for skip)

4. REASONING: Why this priority level

Return JSON array with objects containing: index, priority, category, suggested_response, reasoning`;

  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 analyzed = analysis.comments || analysis.results || analysis.analysis || [];

    return comments.map((comment, i) => {
      const result = analyzed.find(a => a.index === i) || {};
      return {
        ...comment,
        priority: result.priority || 'low',
        category: result.category || 'generic',
        suggestedResponse: result.suggested_response || null,
        reasoning: result.reasoning || ''
      };
    });
  } catch (error) {
    console.error('Prioritization error:', error.message);
    return comments.map(c => ({
      ...c,
      priority: 'medium',
      category: 'unknown',
      suggestedResponse: null,
      reasoning: 'Could not analyze'
    }));
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Influencer Detection

function detectInfluencers(comments, thresholds = {}) {
  const defaults = {
    microInfluencer: 10000,    // 10K+ followers
    midTierInfluencer: 100000, // 100K+ followers
    macroInfluencer: 1000000   // 1M+ followers
  };

  const config = { ...defaults, ...thresholds };

  return comments.map(comment => {
    const followers = comment.authorFollowers || 0;

    let influencerTier = null;
    if (followers >= config.macroInfluencer) {
      influencerTier = 'macro';
    } else if (followers >= config.midTierInfluencer) {
      influencerTier = 'mid-tier';
    } else if (followers >= config.microInfluencer) {
      influencerTier = 'micro';
    }

    // Boost priority for influencers
    let priorityBoost = comment.priority;
    if (influencerTier === 'macro') {
      priorityBoost = 'urgent';
    } else if (influencerTier === 'mid-tier' && comment.priority !== 'skip') {
      priorityBoost = 'urgent';
    } else if (influencerTier === 'micro' && ['medium', 'low'].includes(comment.priority)) {
      priorityBoost = 'high';
    }

    return {
      ...comment,
      influencerTier,
      priority: priorityBoost
    };
  });
}

function detectPurchaseIntent(comments) {
  const purchaseKeywords = [
    'buy', 'purchase', 'price', 'cost', 'how much', 'where can i get',
    'link', 'shop', 'store', 'order', 'shipping', 'discount', 'code',
    'available', 'stock', 'sell', 'payment', 'website', 'checkout'
  ];

  return comments.map(comment => {
    const text = (comment.text || '').toLowerCase();
    const hasPurchaseIntent = purchaseKeywords.some(keyword => text.includes(keyword));

    return {
      ...comment,
      hasPurchaseIntent,
      priority: hasPurchaseIntent && comment.priority !== 'skip' ? 'urgent' : comment.priority
    };
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Generate Response Templates

async function generateResponses(comments, brandVoice = {}) {
  const needsResponse = comments.filter(c => 
    c.priority !== 'skip' && c.priority !== 'low'
  );

  if (needsResponse.length === 0) return comments;

  const prompt = `Generate response templates for these prioritized comments.

BRAND VOICE:
Tone: ${brandVoice.tone || 'friendly and helpful'}
Style: ${brandVoice.style || 'casual but professional'}
${brandVoice.examples ? `Examples of our voice: ${brandVoice.examples}` : ''}

COMMENTS NEEDING RESPONSES:
${needsResponse.map((c, i) => `
[${i}] Priority: ${c.priority} | Category: ${c.category}
Author: @${c.author} (${c.authorFollowers?.toLocaleString() || 0} followers)
Comment: "${c.text?.substring(0, 200)}"
`).join('\n')}

For each comment, generate:
1. A response template that matches our brand voice
2. Keep responses concise (under 150 chars for TikTok/Instagram)
3. For questions, provide helpful answers
4. For complaints, acknowledge and offer to help via DM
5. For influencers, be extra personable and offer value
6. Include relevant emoji sparingly

Return JSON array with: index, response`;

  try {
    const response = await openai.chat.completions.create({
      model: 'gpt-4o-mini',
      messages: [{ role: 'user', content: prompt }],
      response_format: { type: 'json_object' }
    });

    const generated = JSON.parse(response.choices[0].message.content);
    const responses = generated.responses || generated.comments || [];

    return comments.map((comment, i) => {
      const needsResponseIndex = needsResponse.findIndex(c => c.id === comment.id);
      const genResponse = responses.find(r => r.index === needsResponseIndex);

      return {
        ...comment,
        generatedResponse: genResponse?.response || comment.suggestedResponse
      };
    });
  } catch (error) {
    console.error('Response generation error:', error.message);
    return comments;
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Display Results

function displayResults(prioritizedComments) {
  console.log('\n═══════════════════════════════════════════════════════════');
  console.log('πŸ“‹ COMMENT PRIORITY QUEUE');
  console.log('═══════════════════════════════════════════════════════════\n');

  // Group by priority
  const urgent = prioritizedComments.filter(c => c.priority === 'urgent');
  const high = prioritizedComments.filter(c => c.priority === 'high');
  const medium = prioritizedComments.filter(c => c.priority === 'medium');
  const low = prioritizedComments.filter(c => c.priority === 'low');
  const skip = prioritizedComments.filter(c => c.priority === 'skip');

  // Summary stats
  console.log('πŸ“Š SUMMARY');
  console.log('─────────────────────────────────────────────────────────');
  console.log(`Total Comments: ${prioritizedComments.length}`);
  console.log(`πŸ”΄ Urgent: ${urgent.length}`);
  console.log(`🟑 High: ${high.length}`);
  console.log(`🟒 Medium: ${medium.length}`);
  console.log(`βšͺ Low: ${low.length}`);
  console.log(`β›” Skip: ${skip.length}\n`);

  // Urgent comments
  if (urgent.length > 0) {
    console.log('πŸ”΄ URGENT - Respond Immediately');
    console.log('─────────────────────────────────────────────────────────');
    urgent.forEach((c, i) => {
      displayComment(c, i + 1);
    });
    console.log('');
  }

  // High priority
  if (high.length > 0) {
    console.log('🟑 HIGH PRIORITY - Respond Today');
    console.log('─────────────────────────────────────────────────────────');
    high.slice(0, 10).forEach((c, i) => {
      displayComment(c, i + 1);
    });
    if (high.length > 10) {
      console.log(`   ... and ${high.length - 10} more\n`);
    }
    console.log('');
  }

  // Medium priority
  if (medium.length > 0) {
    console.log('🟒 MEDIUM - When Time Permits');
    console.log('─────────────────────────────────────────────────────────');
    medium.slice(0, 5).forEach((c, i) => {
      displayComment(c, i + 1);
    });
    if (medium.length > 5) {
      console.log(`   ... and ${medium.length - 5} more\n`);
    }
    console.log('');
  }

  // Influencer highlights
  const influencers = prioritizedComments.filter(c => c.influencerTier);
  if (influencers.length > 0) {
    console.log('⭐ INFLUENCER ENGAGEMENT OPPORTUNITIES');
    console.log('─────────────────────────────────────────────────────────');
    influencers.forEach(c => {
      const tierEmoji = c.influencerTier === 'macro' ? 'πŸ‘‘' : 
                        c.influencerTier === 'mid-tier' ? '⭐' : '✨';
      console.log(`${tierEmoji} @${c.author} (${formatNumber(c.authorFollowers)} followers)`);
      console.log(`   "${c.text?.substring(0, 100)}..."`);
      console.log(`   Tier: ${c.influencerTier} | Category: ${c.category}\n`);
    });
  }

  // Purchase intent
  const buyers = prioritizedComments.filter(c => c.hasPurchaseIntent);
  if (buyers.length > 0) {
    console.log('πŸ’° PURCHASE INTENT DETECTED');
    console.log('─────────────────────────────────────────────────────────');
    buyers.forEach(c => {
      console.log(`@${c.author}: "${c.text?.substring(0, 100)}..."`);
      if (c.generatedResponse) {
        console.log(`   β†’ ${c.generatedResponse}\n`);
      }
    });
  }
}

function displayComment(comment, num) {
  const tierBadge = comment.influencerTier ? ` [${comment.influencerTier.toUpperCase()}]` : '';
  const verifiedBadge = comment.authorVerified ? ' βœ“' : '';

  console.log(`\n${num}. @${comment.author}${verifiedBadge}${tierBadge}`);
  console.log(`   Followers: ${formatNumber(comment.authorFollowers)} | Likes: ${comment.likes}`);
  console.log(`   Category: ${comment.category}`);
  console.log(`   "${comment.text?.substring(0, 150)}${comment.text?.length > 150 ? '...' : ''}"`);

  if (comment.generatedResponse) {
    console.log(`   πŸ“ Suggested Response: "${comment.generatedResponse}"`);
  }

  if (comment.reasoning) {
    console.log(`   πŸ’‘ ${comment.reasoning}`);
  }
}

function formatNumber(num) {
  if (!num) return '0';
  if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
  if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
  return num.toString();
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run It

async function processComments(videoUrl, options = {}) {
  // Auto-detect platform
  let comments = [];

  if (videoUrl.includes('tiktok.com')) {
    comments = await getTikTokComments(videoUrl);
  } else if (videoUrl.includes('instagram.com')) {
    comments = await getInstagramComments(videoUrl);
  } else if (videoUrl.includes('youtube.com') || videoUrl.includes('youtu.be')) {
    comments = await getYouTubeComments(videoUrl);
  } else if (videoUrl.includes('twitter.com') || videoUrl.includes('x.com')) {
    comments = await getTwitterReplies(videoUrl);
  }

  if (comments.length === 0) {
    console.log('No comments found.');
    return;
  }

  console.log(`\nβœ… Found ${comments.length} comments\n`);

  // Step 1: AI prioritization
  console.log('πŸ€– Analyzing and prioritizing comments...\n');
  let prioritized = await prioritizeComments(comments, {
    businessType: options.businessType || 'e-commerce brand',
    goals: options.goals || 'increase sales and engagement'
  });

  // Step 2: Detect influencers
  prioritized = detectInfluencers(prioritized, options.influencerThresholds);

  // Step 3: Detect purchase intent
  prioritized = detectPurchaseIntent(prioritized);

  // Step 4: Generate response templates
  if (options.generateResponses !== false) {
    console.log('πŸ“ Generating response templates...\n');
    prioritized = await generateResponses(prioritized, options.brandVoice);
  }

  // Display results
  displayResults(prioritized);

  // Export option
  if (options.exportPath) {
    const fs = require('fs');
    fs.writeFileSync(options.exportPath, JSON.stringify(prioritized, null, 2));
    console.log(`\nπŸ“ Exported to ${options.exportPath}`);
  }

  return prioritized;
}

// Main
const videoUrl = process.argv[2] || 'https://www.tiktok.com/@example/video/123';

processComments(videoUrl, {
  businessType: 'DTC skincare brand',
  goals: 'convert browsers to buyers, build community',
  brandVoice: {
    tone: 'warm, fun, skincare-expert',
    style: 'use emoji, be helpful, never salesy'
  },
  generateResponses: true
});
Enter fullscreen mode Exit fullscreen mode

Run with:

node index.js https://www.tiktok.com/@brand/video/1234567890
Enter fullscreen mode Exit fullscreen mode

Sample Output

βœ… Found 87 comments

πŸ€– Analyzing and prioritizing comments...
πŸ“ Generating response templates...

═══════════════════════════════════════════════════════════
πŸ“‹ COMMENT PRIORITY QUEUE
═══════════════════════════════════════════════════════════

πŸ“Š SUMMARY
─────────────────────────────────────────────────────────
Total Comments: 87
πŸ”΄ Urgent: 8
🟑 High: 15
🟒 Medium: 32
βšͺ Low: 24
β›” Skip: 8

πŸ”΄ URGENT - Respond Immediately
─────────────────────────────────────────────────────────

1. @skincaresally [MICRO]
   Followers: 45.2K | Likes: 234
   Category: influencer
   "This is exactly what I've been looking for! Can you DM me about PR?"
   πŸ“ Suggested Response: "We'd love to connect! πŸ’• Sliding into your DMs now!"
   πŸ’‘ Micro-influencer engagement opportunity with partnership potential

2. @beautyjunkie
   Followers: 892 | Likes: 45
   Category: purchase_intent
   "Where can I buy this?? I NEED it! Is it available in Canada?"
   πŸ“ Suggested Response: "Yes! πŸ‡¨πŸ‡¦ Shop at sociavault.com - we ship worldwide! Link in bio πŸ’•"
   πŸ’‘ Clear purchase intent with shipping question

3. @unhappy_customer
   Followers: 234 | Likes: 12
   Category: complaint
   "Ordered last week, still no shipping confirmation. Anyone else?"
   πŸ“ Suggested Response: "So sorry about the delay! πŸ˜” DMing you now to sort this out ASAP!"
   πŸ’‘ Customer service issue - needs immediate resolution to prevent viral negative feedback

⭐ INFLUENCER ENGAGEMENT OPPORTUNITIES
─────────────────────────────────────────────────────────
⭐ @skincaresally (45.2K followers)
   "This is exactly what I've been looking for! Can you DM me about PR?"
   Tier: micro | Category: influencer

✨ @glowupqueen (12.8K followers)
   "Adding this to my routine immediately πŸ™Œ"
   Tier: micro | Category: praise

πŸ’° PURCHASE INTENT DETECTED
─────────────────────────────────────────────────────────
@beautyjunkie: "Where can I buy this?? I NEED it! Is it available in Canada?"
   β†’ Yes! πŸ‡¨πŸ‡¦ Shop at sociavault.com - we ship worldwide! Link in bio πŸ’•

@newbie_skincare: "How much is this? Looks amazing"
   β†’ It's $34.99! ✨ Link in bio to grab yours!

@momof3: "Do you have a discount code? Would love to try this"
   β†’ Use code GLOW15 for 15% off your first order! πŸ’•
Enter fullscreen mode Exit fullscreen mode

What You Just Built

Community management tools charge premium for this:

  • Sprout Social: $249+/month
  • Hootsuite: $99+/month
  • Agorapulse: $99+/month

Your version prioritizes with AI for cents per analysis.

Get Started

  1. Get your SociaVault API Key
  2. Run the prioritizer on your latest post
  3. Respond to what matters most

Stop drowning in comments. Start engaging strategically.


Every comment is an opportunity. Know which ones matter.

Top comments (0)