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:
- Searches for brand mentions across TikTok, Instagram, Twitter, and Reddit
- Filters for authentic user content (not ads or sponsored posts)
- 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
Create .env:
SOCIAVAULT_API_KEY=your_sociavault_key
OPENAI_API_KEY=your_openai_key
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 [];
}
}
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;
}
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;
}
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`);
});
}
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;
}
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();
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!
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
- Get your SociaVault API Key
- Add your OpenAI key
- Search for your brand
- 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)