DEV Community

Olamide Olaniyan
Olamide Olaniyan

Posted on

Build a UGC Content Finder to Discover Brand Mentions Across Platforms

User-generated content is marketing gold.

Real customers talking about your product? That's more convincing than any ad you could create.

But finding UGC is like searching for needles in a haystack. People don't tag brands. They misspell names. They post on platforms you forgot existed.

In this tutorial, we'll build a UGC Content Finder that:

  1. Searches for brand mentions across TikTok, Instagram, Twitter, and Reddit
  2. Filters for authentic user content (not ads or sponsored posts)
  3. Compiles a library of repostable content with creator info

Stop missing content your customers create for you.

Why UGC Matters

The numbers:

  • UGC is 9.8x more impactful than influencer content
  • 79% of people say UGC influences purchase decisions
  • UGC-based ads get 4x higher click-through rates
  • Brands using UGC see 29% higher web conversions

But here's the problem: most UGC goes undiscovered.

Someone posts a TikTok raving about your product. You never see it. They never hear from you. A potential brand ambassador just... disappears.

The Stack

  • Node.js: Runtime
  • SociaVault API: To search across platforms
  • OpenAI API: To filter and categorize content

Step 1: Setup

mkdir ugc-finder
cd ugc-finder
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: Multi-Platform Search

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}` };

// Search TikTok by keyword
async function searchTikTok(keyword) {
  console.log(`πŸ“± Searching TikTok for "${keyword}"...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/search`, {
      params: { keyword, type: 'video' },
      headers
    });

    const videos = response.data.data || [];

    return videos.map(v => ({
      platform: 'tiktok',
      type: 'video',
      id: v.id,
      author: v.author?.uniqueId || v.authorMeta?.name,
      authorFollowers: v.author?.followerCount || v.authorMeta?.fans || 0,
      description: v.desc || v.description,
      likes: v.diggCount || v.stats?.diggCount || 0,
      comments: v.commentCount || v.stats?.commentCount || 0,
      views: v.playCount || v.stats?.playCount || 0,
      url: `https://www.tiktok.com/@${v.author?.uniqueId}/video/${v.id}`,
      created: v.createTime ? new Date(v.createTime * 1000) : null
    }));
  } catch (error) {
    console.error('TikTok search error:', error.message);
    return [];
  }
}

// Search TikTok by hashtag
async function searchTikTokHashtag(hashtag) {
  console.log(`πŸ“± Searching TikTok hashtag #${hashtag}...`);

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

    const videos = response.data.data || [];

    return videos.map(v => ({
      platform: 'tiktok',
      type: 'video',
      id: v.id,
      author: v.author?.uniqueId || v.authorMeta?.name,
      authorFollowers: v.author?.followerCount || 0,
      description: v.desc || v.description,
      likes: v.diggCount || v.stats?.diggCount || 0,
      comments: v.commentCount || v.stats?.commentCount || 0,
      views: v.playCount || v.stats?.playCount || 0,
      url: `https://www.tiktok.com/@${v.author?.uniqueId}/video/${v.id}`,
      hashtag: hashtag
    }));
  } catch (error) {
    console.error('TikTok hashtag error:', error.message);
    return [];
  }
}

// Search Reddit
async function searchReddit(query) {
  console.log(`πŸ”΄ Searching Reddit for "${query}"...`);

  try {
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/reddit/search`, {
      params: { query, sort: 'relevance' },
      headers
    });

    const posts = response.data.data || [];

    return posts.map(p => ({
      platform: 'reddit',
      type: 'post',
      id: p.id,
      author: p.author,
      subreddit: p.subreddit,
      title: p.title,
      description: p.selftext || p.body || '',
      upvotes: p.score || p.ups || 0,
      comments: p.num_comments || 0,
      url: p.url || `https://reddit.com${p.permalink}`,
      created: p.created_utc ? new Date(p.created_utc * 1000) : null
    }));
  } catch (error) {
    console.error('Reddit search error:', error.message);
    return [];
  }
}

// Search Twitter
async function searchTwitter(query) {
  console.log(`🐦 Searching Twitter for "${query}"...`);

  try {
    // Get tweets from users mentioning the brand
    const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/search`, {
      params: { query },
      headers
    });

    const tweets = response.data.data || [];

    return tweets.map(t => ({
      platform: 'twitter',
      type: 'tweet',
      id: t.id || t.rest_id,
      author: t.user?.screen_name || t.author_handle,
      authorFollowers: t.user?.followers_count || 0,
      description: t.full_text || t.text,
      likes: t.favorite_count || t.likes || 0,
      retweets: t.retweet_count || 0,
      views: t.views_count || 0,
      url: `https://twitter.com/${t.user?.screen_name}/status/${t.id}`,
      created: t.created_at ? new Date(t.created_at) : null
    }));
  } catch (error) {
    console.error('Twitter search error:', error.message);
    return [];
  }
}

// Get Instagram posts with hashtag (via profile posts that use the hashtag)
async function searchInstagramHashtag(hashtag) {
  console.log(`πŸ“Έ Searching Instagram hashtag #${hashtag}...`);

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

    const posts = response.data.data || [];

    return posts.map(p => ({
      platform: 'instagram',
      type: p.media_type === 'VIDEO' ? 'reel' : 'post',
      id: p.id || p.pk,
      author: p.user?.username || p.owner?.username,
      authorFollowers: p.user?.follower_count || 0,
      description: p.caption || '',
      likes: p.like_count || p.likes || 0,
      comments: p.comment_count || p.comments || 0,
      url: `https://instagram.com/p/${p.shortcode || p.code}`,
      hashtag: hashtag
    }));
  } catch (error) {
    console.error('Instagram hashtag error:', error.message);
    return [];
  }
}
Enter fullscreen mode Exit fullscreen mode

Step 3: UGC Quality Filter

Not all mentions are UGC. We need to filter out:

  • Sponsored posts
  • Brand's own content
  • Competitor mentions
  • Low-quality spam
async function filterForUGC(content, brandName, config = {}) {
  console.log(`\nπŸ” Filtering ${content.length} posts for authentic UGC...`);

  const {
    minEngagement = 10,
    maxFollowers = 100000, // Filter out big influencers (they're not "users")
    excludeSponsored = true,
    excludeOwnBrand = true
  } = config;

  // First pass: rule-based filtering
  let filtered = content.filter(c => {
    // Minimum engagement
    const engagement = (c.likes || 0) + (c.comments || 0) * 2;
    if (engagement < minEngagement) return false;

    // Not a mega-influencer
    if (c.authorFollowers > maxFollowers) return false;

    // Not from the brand itself
    if (excludeOwnBrand && c.author?.toLowerCase().includes(brandName.toLowerCase())) {
      return false;
    }

    return true;
  });

  // Second pass: AI-powered classification
  if (filtered.length > 0 && excludeSponsored) {
    filtered = await classifyContent(filtered, brandName);
  }

  console.log(`βœ… Found ${filtered.length} authentic UGC posts`);
  return filtered;
}

async function classifyContent(content, brandName) {
  // Process in batches
  const batchSize = 20;
  const results = [];

  for (let i = 0; i < content.length; i += batchSize) {
    const batch = content.slice(i, i + batchSize);

    const prompt = `
      Classify these social media posts about "${brandName}".

      For each post, determine:
      1. Is it authentic UGC (real user sharing genuine experience)?
      2. Is it sponsored/paid content?
      3. Is it a review or testimonial?
      4. Sentiment: positive, neutral, or negative
      5. Content type: unboxing, tutorial, review, lifestyle, complaint, question

      Posts:
      ${JSON.stringify(batch.map((c, idx) => ({
        index: idx,
        author: c.author,
        followers: c.authorFollowers,
        text: c.description?.substring(0, 300),
        platform: c.platform
      })))}

      Return JSON array:
      [
        {
          "index": 0,
          "isUGC": true/false,
          "isSponsored": true/false,
          "sentiment": "positive/neutral/negative",
          "contentType": "type",
          "ugcScore": 0-100,
          "reason": "brief explanation"
        }
      ]
    `;

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

    const classifications = JSON.parse(completion.choices[0].message.content);
    const classified = Array.isArray(classifications) ? classifications : classifications.results || [];

    // Merge classifications with original content
    batch.forEach((item, idx) => {
      const classification = classified.find(c => c.index === idx) || {};
      if (classification.isUGC && !classification.isSponsored && classification.ugcScore >= 60) {
        results.push({
          ...item,
          classification: {
            sentiment: classification.sentiment,
            contentType: classification.contentType,
            ugcScore: classification.ugcScore,
            reason: classification.reason
          }
        });
      }
    });
  }

  return results;
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Content Categorization

function categorizeUGC(content) {
  const categories = {
    reviews: [],
    testimonials: [],
    tutorials: [],
    unboxings: [],
    lifestyle: [],
    comparisons: [],
    other: []
  };

  content.forEach(c => {
    const type = c.classification?.contentType?.toLowerCase() || 'other';

    if (type.includes('review')) {
      categories.reviews.push(c);
    } else if (type.includes('testimonial') || type.includes('recommendation')) {
      categories.testimonials.push(c);
    } else if (type.includes('tutorial') || type.includes('how-to')) {
      categories.tutorials.push(c);
    } else if (type.includes('unboxing') || type.includes('haul')) {
      categories.unboxings.push(c);
    } else if (type.includes('lifestyle') || type.includes('daily')) {
      categories.lifestyle.push(c);
    } else if (type.includes('comparison') || type.includes('vs')) {
      categories.comparisons.push(c);
    } else {
      categories.other.push(c);
    }
  });

  return categories;
}

function rankByRepostPotential(content) {
  return content.sort((a, b) => {
    // Score based on multiple factors
    const scoreA = calculateRepostScore(a);
    const scoreB = calculateRepostScore(b);
    return scoreB - scoreA;
  });
}

function calculateRepostScore(item) {
  let score = 0;

  // Engagement weight
  score += Math.min((item.likes || 0) / 100, 50);
  score += Math.min((item.comments || 0) / 10, 30);

  // UGC quality score
  score += (item.classification?.ugcScore || 50) / 2;

  // Positive sentiment bonus
  if (item.classification?.sentiment === 'positive') score += 20;

  // Platform preferences (TikTok/Instagram reels are more repostable)
  if (item.platform === 'tiktok') score += 15;
  if (item.platform === 'instagram' && item.type === 'reel') score += 15;

  // Smaller creators = more authentic feel
  if (item.authorFollowers < 10000) score += 10;
  if (item.authorFollowers < 1000) score += 10;

  return score;
}
Enter fullscreen mode Exit fullscreen mode

Step 5: The Main UGC Finder

async function findUGC(config) {
  const { brandName, hashtags = [], keywords = [], platforms = ['tiktok', 'instagram', 'twitter', 'reddit'] } = config;

  console.log('\nπŸ”Ž UGC CONTENT FINDER\n');
  console.log('═══════════════════════════════════════════\n');
  console.log(`Brand: ${brandName}`);
  console.log(`Hashtags: ${hashtags.map(h => `#${h}`).join(', ')}`);
  console.log(`Keywords: ${keywords.join(', ')}`);
  console.log(`Platforms: ${platforms.join(', ')}\n`);

  let allContent = [];

  // Search each platform
  for (const platform of platforms) {
    // Hashtag searches
    for (const hashtag of hashtags) {
      let results = [];

      if (platform === 'tiktok') {
        results = await searchTikTokHashtag(hashtag);
      } else if (platform === 'instagram') {
        results = await searchInstagramHashtag(hashtag);
      }

      allContent = [...allContent, ...results];
      await new Promise(r => setTimeout(r, 1000));
    }

    // Keyword searches
    for (const keyword of [...keywords, brandName]) {
      let results = [];

      if (platform === 'tiktok') {
        results = await searchTikTok(keyword);
      } else if (platform === 'twitter') {
        results = await searchTwitter(keyword);
      } else if (platform === 'reddit') {
        results = await searchReddit(keyword);
      }

      allContent = [...allContent, ...results];
      await new Promise(r => setTimeout(r, 1000));
    }
  }

  console.log(`\nπŸ“Š Total content found: ${allContent.length}\n`);

  // Remove duplicates
  const uniqueContent = removeDuplicates(allContent);
  console.log(`πŸ“Š After deduplication: ${uniqueContent.length}\n`);

  // Filter for authentic UGC
  const ugcContent = await filterForUGC(uniqueContent, brandName, {
    minEngagement: 10,
    maxFollowers: 100000,
    excludeSponsored: true
  });

  // Categorize
  const categorized = categorizeUGC(ugcContent);

  // Rank by repost potential
  const ranked = rankByRepostPotential(ugcContent);

  // Display results
  displayResults(categorized, ranked);

  return { all: ugcContent, categorized, topPicks: ranked.slice(0, 10) };
}

function removeDuplicates(content) {
  const seen = new Set();
  return content.filter(c => {
    const key = `${c.platform}-${c.id}`;
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
}

function displayResults(categorized, ranked) {
  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ“‚ UGC BY CATEGORY');
  console.log('═══════════════════════════════════════════\n');

  Object.entries(categorized).forEach(([category, items]) => {
    if (items.length > 0) {
      console.log(`${category.toUpperCase()}: ${items.length} posts`);
    }
  });

  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ† TOP 10 REPOSTABLE UGC');
  console.log('═══════════════════════════════════════════\n');

  ranked.slice(0, 10).forEach((item, i) => {
    console.log(`${i + 1}. [${item.platform.toUpperCase()}] @${item.author}`);
    console.log(`   ${item.description?.substring(0, 80)}...`);
    console.log(`   πŸ‘ ${item.likes} | πŸ’¬ ${item.comments} | Score: ${item.classification?.ugcScore || 'N/A'}`);
    console.log(`   Type: ${item.classification?.contentType || 'Unknown'} | Sentiment: ${item.classification?.sentiment || 'Unknown'}`);
    console.log(`   πŸ”— ${item.url}\n`);
  });
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Export for Outreach

function exportForOutreach(ugcContent) {
  const fs = require('fs');

  // CSV for spreadsheet
  const headers = 'Platform,Author,Followers,Content,Likes,Comments,Sentiment,Type,URL\n';
  const rows = ugcContent.map(c => 
    `"${c.platform}","${c.author}","${c.authorFollowers}","${c.description?.replace(/"/g, '""').substring(0, 200)}","${c.likes}","${c.comments}","${c.classification?.sentiment || ''}","${c.classification?.contentType || ''}","${c.url}"`
  ).join('\n');

  fs.writeFileSync('ugc-content.csv', headers + rows);
  console.log('\nβœ… Exported to ugc-content.csv');

  // JSON for programmatic use
  fs.writeFileSync('ugc-content.json', JSON.stringify(ugcContent, null, 2));
  console.log('βœ… Exported to ugc-content.json');
}

// Generate outreach templates
async function generateOutreachTemplates(topUGC) {
  console.log('\nπŸ“§ Generating outreach templates...');

  const templates = [];

  for (const content of topUGC.slice(0, 5)) {
    const prompt = `
      Write a friendly DM to ask permission to repost this user's content.

      Context:
      - Platform: ${content.platform}
      - Their post: "${content.description?.substring(0, 200)}"
      - They have ${content.authorFollowers} followers

      Requirements:
      - Be genuine and appreciative
      - Mention you'll credit them
      - Keep it under 150 words
      - Don't be corporate or stiff

      Return JSON: { "message": "the DM text" }
    `;

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

    const result = JSON.parse(completion.choices[0].message.content);
    templates.push({
      author: content.author,
      platform: content.platform,
      template: result.message
    });
  }

  console.log('\n═══════════════════════════════════════════');
  console.log('πŸ“§ OUTREACH TEMPLATES');
  console.log('═══════════════════════════════════════════\n');

  templates.forEach((t, i) => {
    console.log(`To: @${t.author} (${t.platform})`);
    console.log(`─────────────────────────────────────────`);
    console.log(t.template);
    console.log('\n');
  });

  return templates;
}
Enter fullscreen mode Exit fullscreen mode

Step 7: Run It

async function main() {
  const results = await findUGC({
    brandName: 'Notion',
    hashtags: ['notion', 'notiontemplate', 'notiontips'],
    keywords: ['notion app', 'notion setup', 'notion review'],
    platforms: ['tiktok', 'instagram', 'twitter', 'reddit']
  });

  // Export results
  exportForOutreach(results.all);

  // Generate outreach templates for top picks
  await generateOutreachTemplates(results.topPicks);
}

main();
Enter fullscreen mode Exit fullscreen mode

Sample Output

πŸ”Ž UGC CONTENT FINDER
═══════════════════════════════════════════

Brand: Notion
Hashtags: #notion, #notiontemplate, #notiontips
Keywords: notion app, notion setup, notion review
Platforms: tiktok, instagram, twitter, reddit

πŸ“± Searching TikTok hashtag #notion...
πŸ“± Searching TikTok hashtag #notiontemplate...
πŸ“Έ Searching Instagram hashtag #notion...
🐦 Searching Twitter for "notion app"...
πŸ”΄ Searching Reddit for "notion review"...

πŸ“Š Total content found: 247
πŸ“Š After deduplication: 189

πŸ” Filtering 189 posts for authentic UGC...
βœ… Found 67 authentic UGC posts

═══════════════════════════════════════════
πŸ“‚ UGC BY CATEGORY
═══════════════════════════════════════════

REVIEWS: 12 posts
TUTORIALS: 23 posts
TESTIMONIALS: 8 posts
LIFESTYLE: 15 posts
UNBOXINGS: 3 posts
OTHER: 6 posts

═══════════════════════════════════════════
πŸ† TOP 10 REPOSTABLE UGC
═══════════════════════════════════════════

1. [TIKTOK] @productivitysarah
   just organized my entire life in notion and i'm literally crying it's so...
   πŸ‘ 45,200 | πŸ’¬ 892 | Score: 95
   Type: tutorial | Sentiment: positive
   πŸ”— https://www.tiktok.com/@productivitysarah/video/7234567890

2. [INSTAGRAM] @studywithjess
   POV: you finally set up your notion dashboard and everything just clicks...
   πŸ‘ 12,400 | πŸ’¬ 234 | Score: 92
   Type: lifestyle | Sentiment: positive
   πŸ”— https://instagram.com/p/ABC123

3. [REDDIT] u/minimalist_mike
   Switched from Evernote to Notion 6 months ago - here's my honest review...
   πŸ‘ 2,340 | πŸ’¬ 187 | Score: 88
   Type: review | Sentiment: positive
   πŸ”— https://reddit.com/r/Notion/comments/xyz

[... continues ...]

═══════════════════════════════════════════
πŸ“§ OUTREACH TEMPLATES
═══════════════════════════════════════════

To: @productivitysarah (tiktok)
─────────────────────────────────────────
Hey Sarah! πŸ‘‹

Just came across your Notion setup video and honestly it's SO good. The way you organized your dashboard is exactly what our community loves to see.

Would you be cool with us reposting it on our official account? We'd obviously tag you and give full credit!

Either way, thanks for making such awesome content. You're making productivity look fun πŸ™Œ

Let me know!
Enter fullscreen mode Exit fullscreen mode

Cost Analysis

Manual UGC discovery:

  • Time: 5-10 hours per week scrolling platforms
  • Miss rate: 80%+ of UGC goes undiscovered
  • No systematic categorization

UGC platforms:

  • TINT: $500+/month
  • Yotpo: $299+/month
  • Stackla: Enterprise pricing

Your version:

  • SociaVault: ~$0.50 per search session
  • OpenAI: ~$0.10 for classification
  • Total: ~$0.60 per comprehensive search

What You Just Built

This is the core feature of UGC platforms that charge $500+/month:

  • Multi-platform search
  • Authenticity filtering
  • Quality scoring
  • Outreach automation

Now you have it for basically free.

Get Started

  1. Get your SociaVault API Key
  2. Add your OpenAI key
  3. Search for your brand
  4. Discover content you never knew existed

Your customers are already creating content for you. Start finding it.


The best marketing doesn't come from you. It comes from your users.

Top comments (0)