I keep seeing "build a SaaS in a weekend" posts that end with a login page and a Stripe checkout. Cool. What about the actual product?
Here's a real one. An influencer vetting tool that agencies and brands actually pay for. The whole thing — frontend, backend, Stripe, the works — in under 400 lines of meaningful code.
The Product
A simple tool: paste a creator's Instagram or TikTok handle, get a one-page report that tells you if this creator is worth partnering with.
The report shows:
- Real engagement rate (calculated from actual posts, not estimated)
- Audience quality signals (fake follower indicators)
- Posting consistency
- Growth trajectory
- A simple Go / Caution / Avoid recommendation
Free tier: 3 reports/month. Paid: $19/month for unlimited.
Why This Makes Money
Agencies vet 50-100 creators per campaign. They currently do it manually — checking profiles, calculating engagement rates in spreadsheets, gut-feeling whether followers look real.
Charge $19/month. Get 100 agencies. That's $1,900/month from a weekend project.
The Stack
- Next.js 14 (App Router) – frontend + API routes
- SociaVault API – all the social data
- Stripe – payments
- Clerk – auth (or roll your own, but why)
- Vercel – deploy
Project Structure
influencer-vetter/
├── app/
│ ├── page.tsx # Landing + search
│ ├── report/[handle]/page.tsx # The report page
│ ├── api/
│ │ ├── vet/route.ts # Generate report
│ │ └── webhook/route.ts # Stripe webhook
│ └── layout.tsx
├── lib/
│ ├── scoring.ts # The scoring engine
│ ├── stripe.ts # Stripe helpers
│ └── rate-limit.ts # Usage tracking
└── package.json
The Scoring Engine
This is the brain. Everything else is plumbing.
// lib/scoring.ts
interface CreatorReport {
handle: string;
platform: string;
followers: number;
engagementRate: number;
avgLikes: number;
avgComments: number;
likeCommentRatio: number;
postingFrequency: number; // posts per week
consistencyScore: number; // 0-100
authenticityScore: number; // 0-100
overallScore: number; // 0-100
verdict: 'go' | 'caution' | 'avoid';
flags: string[];
}
export function analyzeCreator(profile: any, posts: any[]): CreatorReport {
const followers = profile.followersCount || profile.followerCount || 0;
const flags: string[] = [];
// --- Engagement Rate ---
const totalLikes = posts.reduce((s, p) => s + (p.likesCount || p.diggCount || 0), 0);
const totalComments = posts.reduce((s, p) => s + (p.commentsCount || p.commentCount || 0), 0);
const avgLikes = posts.length > 0 ? totalLikes / posts.length : 0;
const avgComments = posts.length > 0 ? totalComments / posts.length : 0;
const engagementRate = followers > 0 ? ((avgLikes + avgComments) / followers) * 100 : 0;
// --- Like-to-Comment Ratio ---
// Healthy: 10:1 to 50:1. Above 100:1 = likely bought likes
const likeCommentRatio = avgComments > 0 ? avgLikes / avgComments : 0;
if (likeCommentRatio > 100) flags.push('Suspiciously high like-to-comment ratio');
if (likeCommentRatio > 200) flags.push('Likely purchased likes or bot engagement');
// --- Posting Frequency ---
const timestamps = posts
.map(p => new Date(p.timestamp || p.createTime * 1000).getTime())
.filter(t => !isNaN(t))
.sort((a, b) => b - a);
let postsPerWeek = 0;
if (timestamps.length >= 2) {
const spanDays = (timestamps[0] - timestamps[timestamps.length - 1]) / 86400000;
postsPerWeek = spanDays > 0 ? (posts.length / spanDays) * 7 : 0;
}
// --- Consistency Score ---
// Measures how regular their posting cadence is
let consistencyScore = 50;
if (timestamps.length >= 3) {
const gaps = [];
for (let i = 0; i < timestamps.length - 1; i++) {
gaps.push(timestamps[i] - timestamps[i + 1]);
}
const avgGap = gaps.reduce((s, g) => s + g, 0) / gaps.length;
const variance = gaps.reduce((s, g) => s + Math.pow(g - avgGap, 2), 0) / gaps.length;
const stdDev = Math.sqrt(variance);
const cv = avgGap > 0 ? stdDev / avgGap : 1; // Coefficient of variation
// Lower CV = more consistent. CV of 0.3 = very consistent, 1.0+ = chaotic
consistencyScore = Math.max(0, Math.min(100, Math.round((1 - Math.min(cv, 1)) * 100)));
}
if (postsPerWeek < 1) flags.push('Posts less than once per week');
if (consistencyScore < 30) flags.push('Very inconsistent posting schedule');
// --- Authenticity Score ---
let authenticityScore = 100;
// Follower/following ratio
const following = profile.followingCount || 0;
const ffRatio = following > 0 ? followers / following : followers;
if (ffRatio < 1 && followers > 1000) {
authenticityScore -= 20;
flags.push('Following more people than followers (follow-for-follow pattern)');
}
// Engagement vs follower count sanity check
if (followers > 100000 && engagementRate < 0.5) {
authenticityScore -= 25;
flags.push('Very low engagement for follower count — possible ghost followers');
}
// Like-comment ratio penalty
if (likeCommentRatio > 100) authenticityScore -= 15;
if (likeCommentRatio > 200) authenticityScore -= 20;
authenticityScore = Math.max(0, authenticityScore);
// --- Overall Score ---
// Weighted: engagement quality matters most
const overallScore = Math.round(
authenticityScore * 0.4 +
consistencyScore * 0.2 +
Math.min(engagementRate * 10, 100) * 0.3 + // Cap at 10% ER = 100 points
Math.min(postsPerWeek * 15, 100) * 0.1 // Cap at ~7 posts/week = 100 points
);
// --- Verdict ---
let verdict: 'go' | 'caution' | 'avoid';
if (overallScore >= 70 && flags.length <= 1) verdict = 'go';
else if (overallScore >= 40 || flags.length <= 2) verdict = 'caution';
else verdict = 'avoid';
return {
handle: profile.username || profile.uniqueId,
platform: profile.platform || 'unknown',
followers,
engagementRate: parseFloat(engagementRate.toFixed(2)),
avgLikes: Math.round(avgLikes),
avgComments: Math.round(avgComments),
likeCommentRatio: parseFloat(likeCommentRatio.toFixed(1)),
postingFrequency: parseFloat(postsPerWeek.toFixed(1)),
consistencyScore,
authenticityScore,
overallScore,
verdict,
flags,
};
}
That's the entire scoring engine. ~100 lines. No AI, no machine learning. Just math that makes sense.
The API Route
// app/api/vet/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { analyzeCreator } from '@/lib/scoring';
const SOCIAVAULT_API = 'https://api.sociavault.com/v1/scrape';
const API_KEY = process.env.SOCIAVAULT_API_KEY!;
async function fetchFromSociaVault(endpoint: string) {
const res = await fetch(`${SOCIAVAULT_API}${endpoint}`, {
headers: { 'x-api-key': API_KEY },
});
if (!res.ok) throw new Error(`API error: ${res.status}`);
return res.json();
}
export async function GET(req: NextRequest) {
const handle = req.nextUrl.searchParams.get('handle');
const platform = req.nextUrl.searchParams.get('platform') || 'instagram';
if (!handle) {
return NextResponse.json({ error: 'Missing handle' }, { status: 400 });
}
try {
let profile, posts;
if (platform === 'instagram') {
[profile, posts] = await Promise.all([
fetchFromSociaVault(`/instagram/profile?username=${handle}`),
fetchFromSociaVault(`/instagram/posts?username=${handle}&limit=12`),
]);
} else {
[profile, posts] = await Promise.all([
fetchFromSociaVault(`/tiktok/profile?username=${handle}`),
fetchFromSociaVault(`/tiktok/profile-videos?username=${handle}&limit=12`),
]);
}
const profileData = profile.data || profile;
const postsData = posts.data || posts.posts || [];
const report = analyzeCreator(
{ ...profileData, platform },
postsData
);
return NextResponse.json(report);
} catch (err: any) {
return NextResponse.json({ error: err.message }, { status: 500 });
}
}
The Frontend (Simplified)
I'm not going to paste an entire React frontend — you've seen search bars before. The key parts:
// app/page.tsx — the search
'use client';
import { useState } from 'react';
import { useRouter } from 'next/navigation';
export default function Home() {
const [handle, setHandle] = useState('');
const [platform, setPlatform] = useState('instagram');
const router = useRouter();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (handle.trim()) {
router.push(`/report/${handle.trim().replace('@', '')}?platform=${platform}`);
}
};
return (
<main className="min-h-screen flex items-center justify-center">
<form onSubmit={handleSubmit} className="w-full max-w-md">
<h1 className="text-3xl font-bold mb-8 text-center">
Should you partner with this creator?
</h1>
<div className="flex gap-2 mb-4">
<button
type="button"
onClick={() => setPlatform('instagram')}
className={platform === 'instagram' ? 'bg-pink-500 text-white px-4 py-2 rounded' : 'bg-gray-200 px-4 py-2 rounded'}
>
Instagram
</button>
<button
type="button"
onClick={() => setPlatform('tiktok')}
className={platform === 'tiktok' ? 'bg-black text-white px-4 py-2 rounded' : 'bg-gray-200 px-4 py-2 rounded'}
>
TikTok
</button>
</div>
<input
type="text"
value={handle}
onChange={(e) => setHandle(e.target.value)}
placeholder="Enter username..."
className="w-full px-4 py-3 border rounded-lg text-lg"
/>
<button type="submit" className="w-full mt-4 bg-blue-600 text-white py-3 rounded-lg text-lg font-medium">
Analyze Creator
</button>
</form>
</main>
);
}
The report page calls /api/vet?handle=xxx&platform=yyy and renders the result. Show the overall score big. Color code the verdict. List the flags as warnings. Done.
Monetization with Stripe
// lib/rate-limit.ts
// Track usage per user per month using a simple in-memory map
// (For production, use Redis or your database)
const usage = new Map<string, { count: number; resetAt: number }>();
export function checkUsage(userId: string, isPaid: boolean): boolean {
const limit = isPaid ? Infinity : 3;
const now = Date.now();
const key = userId;
const record = usage.get(key);
if (!record || record.resetAt < now) {
// New month
usage.set(key, { count: 1, resetAt: now + 30 * 86400000 });
return true;
}
if (record.count >= limit) return false;
record.count++;
return true;
}
Deploy
# Push to GitHub, connect to Vercel, done
git init && git add -A && git commit -m "initial"
vercel
Total cost:
- Vercel: $0 (free tier)
- SociaVault API: $29/month (Starter plan, 5K credits)
- Stripe: 2.9% + $0.30 per transaction
- Clerk: $0 (free tier up to 10K users)
At 10 paying users ($19/month each), you're making $190/month and spending ~$30. That's a 6:1 return from a weekend project.
What Makes This Win
It's not the code. It's the positioning.
HypeAuditor charges $299/month for this. CreatorIQ won't even talk to you without a demo call. You're offering the same core value for $19/month with zero friction.
The scoring engine above does 80% of what the expensive tools do. The other 20% is pretty charts and PDF exports — which you can add later.
Read the Full Guide
Build an Influencer Vetting Micro-SaaS → SociaVault Blog
Power your SaaS with social media data from SociaVault — one API for TikTok, Instagram, YouTube, and 10+ platforms. Profiles, posts, engagement, followers — all endpoints, one API key.
Discussion
Have you shipped a micro-SaaS in a weekend? What was it, and how did it go? I'm always curious about the gap between "launched" and "first paying customer."
Top comments (0)