"10,000+ followers on Twitter" means nothing if visitors can't verify it.
Social proof widgets do one thing: show real numbers that build trust instantly.
But most widget tools either look terrible, cost $50/month, or show stale data from 3 weeks ago.
In this tutorial, we'll build a Real-Time Social Proof Widget that:
- Pulls live follower counts from multiple platforms
- Renders a clean, embeddable widget
- Auto-updates without manual refreshes
Perfect for landing pages, creator portfolios, and SaaS homepages.
Why Social Proof Matters
Quick psychology lesson:
- 92% of consumers trust peer recommendations over advertising
- Pages with social proof convert 34% better
- "Join 50,000+ users" outperforms "Sign up today"
Numbers create credibility. Real-time numbers create more credibility.
The Stack
- Node.js + Express: Backend API
- SociaVault API: To fetch social stats
- HTML/CSS/JS: Embeddable widget
- Optional: Redis: For caching (recommended)
Step 1: Setup
mkdir social-proof-widget
cd social-proof-widget
npm init -y
npm install express axios dotenv cors
Create .env:
SOCIAVAULT_API_KEY=your_sociavault_key
PORT=3000
Step 2: The Stats API
Create server.js:
require('dotenv').config();
const express = require('express');
const axios = require('axios');
const cors = require('cors');
const app = express();
app.use(cors()); // Allow embedding on other sites
app.use(express.static('public'));
const SOCIAVAULT_BASE = 'https://api.sociavault.com';
const headers = { 'Authorization': `Bearer ${process.env.SOCIAVAULT_API_KEY}` };
// Cache to avoid hammering the API
const cache = new Map();
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
async function getCachedOrFetch(key, fetchFn) {
const cached = cache.get(key);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
return cached.data;
}
const data = await fetchFn();
cache.set(key, { data, timestamp: Date.now() });
return data;
}
// TikTok stats
async function getTikTokStats(handle) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/tiktok/profile`, {
params: { handle },
headers
});
const data = response.data.data;
return {
platform: 'tiktok',
handle,
followers: data.followerCount || data.fans || 0,
likes: data.heartCount || data.heart || 0,
videos: data.videoCount || data.video || 0,
verified: data.verified || false
};
} catch (error) {
console.error(`TikTok error for @${handle}:`, error.message);
return null;
}
}
// Instagram stats
async function getInstagramStats(handle) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/instagram/profile`, {
params: { handle },
headers
});
const data = response.data.data;
return {
platform: 'instagram',
handle,
followers: data.follower_count || data.followers || 0,
following: data.following_count || 0,
posts: data.media_count || 0,
verified: data.is_verified || false
};
} catch (error) {
console.error(`Instagram error for @${handle}:`, error.message);
return null;
}
}
// Twitter stats
async function getTwitterStats(handle) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/twitter/profile`, {
params: { handle },
headers
});
const data = response.data.data;
return {
platform: 'twitter',
handle,
followers: data.followers_count || data.followers || 0,
following: data.friends_count || data.following || 0,
tweets: data.statuses_count || data.tweets || 0,
verified: data.verified || data.is_blue_verified || false
};
} catch (error) {
console.error(`Twitter error for @${handle}:`, error.message);
return null;
}
}
// YouTube stats
async function getYouTubeStats(channelUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/youtube/channel`, {
params: { url: channelUrl },
headers
});
const data = response.data.data;
return {
platform: 'youtube',
handle: data.name || data.title,
subscribers: data.subscriberCount || data.subscribers || 0,
videos: data.videoCount || data.videos || 0,
views: data.viewCount || data.totalViews || 0,
verified: data.verified || false
};
} catch (error) {
console.error(`YouTube error:`, error.message);
return null;
}
}
// LinkedIn stats
async function getLinkedInStats(profileUrl) {
try {
const response = await axios.get(`${SOCIAVAULT_BASE}/v1/scrape/linkedin/profile`, {
params: { url: profileUrl },
headers
});
const data = response.data.data;
return {
platform: 'linkedin',
handle: data.username || data.public_id,
followers: data.follower_count || data.followers || 0,
connections: data.connections_count || 0
};
} catch (error) {
console.error(`LinkedIn error:`, error.message);
return null;
}
}
Step 3: API Endpoints
Add routes to serve the stats:
// Single platform endpoint
app.get('/api/stats/:platform/:handle', async (req, res) => {
const { platform, handle } = req.params;
const cacheKey = `${platform}:${handle}`;
try {
const stats = await getCachedOrFetch(cacheKey, async () => {
switch (platform) {
case 'tiktok': return getTikTokStats(handle);
case 'instagram': return getInstagramStats(handle);
case 'twitter': return getTwitterStats(handle);
default: return null;
}
});
if (!stats) {
return res.status(404).json({ error: 'Profile not found' });
}
res.json(stats);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Multi-platform bundle
app.get('/api/stats/bundle', async (req, res) => {
const { tiktok, instagram, twitter, youtube, linkedin } = req.query;
const promises = [];
if (tiktok) {
promises.push(getCachedOrFetch(`tiktok:${tiktok}`, () => getTikTokStats(tiktok)));
}
if (instagram) {
promises.push(getCachedOrFetch(`instagram:${instagram}`, () => getInstagramStats(instagram)));
}
if (twitter) {
promises.push(getCachedOrFetch(`twitter:${twitter}`, () => getTwitterStats(twitter)));
}
if (youtube) {
promises.push(getCachedOrFetch(`youtube:${youtube}`, () => getYouTubeStats(youtube)));
}
if (linkedin) {
promises.push(getCachedOrFetch(`linkedin:${linkedin}`, () => getLinkedInStats(linkedin)));
}
const results = await Promise.all(promises);
const stats = results.filter(Boolean);
// Calculate totals
const totalFollowers = stats.reduce((sum, s) => {
return sum + (s.followers || s.subscribers || 0);
}, 0);
res.json({
platforms: stats,
totals: {
platforms: stats.length,
followers: totalFollowers
}
});
});
app.listen(process.env.PORT || 3000, () => {
console.log(`Stats API running on port ${process.env.PORT || 3000}`);
});
Step 4: The Embeddable Widget
Create public/widget.js:
(function() {
// Widget configuration
const WIDGET_API = 'YOUR_API_URL'; // Replace with your deployed URL
class SocialProofWidget {
constructor(config) {
this.container = document.querySelector(config.selector);
this.handles = config.handles || {};
this.theme = config.theme || 'light';
this.style = config.style || 'horizontal';
this.showTotal = config.showTotal !== false;
this.init();
}
async init() {
this.injectStyles();
this.render('loading');
try {
const stats = await this.fetchStats();
this.render('loaded', stats);
} catch (error) {
this.render('error', error.message);
}
}
async fetchStats() {
const params = new URLSearchParams(this.handles).toString();
const response = await fetch(`${WIDGET_API}/api/stats/bundle?${params}`);
if (!response.ok) throw new Error('Failed to fetch stats');
return response.json();
}
formatNumber(num) {
if (num >= 1000000) {
return (num / 1000000).toFixed(1) + 'M';
}
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
}
getPlatformIcon(platform) {
const icons = {
tiktok: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z"/></svg>',
instagram: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>',
twitter: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z"/></svg>',
youtube: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/></svg>',
linkedin: '<svg viewBox="0 0 24 24" fill="currentColor"><path d="M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z"/></svg>'
};
return icons[platform] || '';
}
getPlatformColor(platform) {
const colors = {
tiktok: '#000000',
instagram: '#E4405F',
twitter: '#000000',
youtube: '#FF0000',
linkedin: '#0A66C2'
};
return colors[platform] || '#666666';
}
injectStyles() {
if (document.querySelector('#social-proof-styles')) return;
const styles = document.createElement('style');
styles.id = 'social-proof-styles';
styles.textContent = `
.sp-widget {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
display: flex;
gap: 16px;
padding: 16px;
border-radius: 12px;
background: ${this.theme === 'dark' ? '#1a1a1a' : '#ffffff'};
color: ${this.theme === 'dark' ? '#ffffff' : '#1a1a1a'};
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
.sp-widget.vertical { flex-direction: column; }
.sp-widget.horizontal { flex-direction: row; flex-wrap: wrap; justify-content: center; }
.sp-platform {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
background: ${this.theme === 'dark' ? '#2a2a2a' : '#f5f5f5'};
transition: transform 0.2s;
}
.sp-platform:hover { transform: translateY(-2px); }
.sp-platform svg { width: 20px; height: 20px; }
.sp-platform-stats { display: flex; flex-direction: column; }
.sp-followers { font-weight: 700; font-size: 18px; }
.sp-label { font-size: 12px; opacity: 0.7; }
.sp-total {
text-align: center;
padding: 12px;
border-top: 1px solid ${this.theme === 'dark' ? '#333' : '#eee'};
margin-top: 8px;
}
.sp-total-number { font-size: 24px; font-weight: 700; }
.sp-total-label { font-size: 14px; opacity: 0.7; }
.sp-loading { padding: 20px; text-align: center; opacity: 0.5; }
.sp-verified { color: #1DA1F2; margin-left: 4px; }
`;
document.head.appendChild(styles);
}
render(state, data) {
if (state === 'loading') {
this.container.innerHTML = `
<div class="sp-widget ${this.style}">
<div class="sp-loading">Loading social stats...</div>
</div>
`;
return;
}
if (state === 'error') {
this.container.innerHTML = `
<div class="sp-widget ${this.style}">
<div class="sp-loading">Unable to load stats</div>
</div>
`;
return;
}
const platformsHtml = data.platforms.map(p => `
<div class="sp-platform" style="color: ${this.getPlatformColor(p.platform)}">
${this.getPlatformIcon(p.platform)}
<div class="sp-platform-stats">
<span class="sp-followers">
${this.formatNumber(p.followers || p.subscribers)}
${p.verified ? '<span class="sp-verified">✓</span>' : ''}
</span>
<span class="sp-label">@${p.handle}</span>
</div>
</div>
`).join('');
const totalHtml = this.showTotal ? `
<div class="sp-total">
<div class="sp-total-number">${this.formatNumber(data.totals.followers)}</div>
<div class="sp-total-label">Total Followers</div>
</div>
` : '';
this.container.innerHTML = `
<div class="sp-widget ${this.style}">
${platformsHtml}
${totalHtml}
</div>
`;
}
}
// Expose globally
window.SocialProofWidget = SocialProofWidget;
})();
Step 5: The Embed Code
Users can embed the widget with:
<!-- Add this where you want the widget -->
<div id="social-proof"></div>
<!-- Add this before </body> -->
<script src="https://your-api.com/widget.js"></script>
<script>
new SocialProofWidget({
selector: '#social-proof',
handles: {
tiktok: 'garyvee',
instagram: 'garyvee',
twitter: 'garyvee',
youtube: 'https://youtube.com/@garyvee'
},
theme: 'light', // 'light' or 'dark'
style: 'horizontal', // 'horizontal' or 'vertical'
showTotal: true
});
</script>
Step 6: Self-Hosted Demo Page
Create public/index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Social Proof Widget Demo</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
max-width: 800px;
margin: 0 auto;
padding: 40px 20px;
background: #f5f5f5;
}
h1 { text-align: center; }
.demo-section {
background: white;
padding: 30px;
border-radius: 12px;
margin: 30px 0;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
}
h2 { margin-top: 0; }
.embed-code {
background: #1a1a1a;
color: #fff;
padding: 20px;
border-radius: 8px;
overflow-x: auto;
font-family: monospace;
font-size: 14px;
}
</style>
</head>
<body>
<h1>🏆 Social Proof Widget</h1>
<div class="demo-section">
<h2>Horizontal Layout (Light)</h2>
<div id="widget-horizontal"></div>
</div>
<div class="demo-section" style="background: #1a1a1a;">
<h2 style="color: white;">Vertical Layout (Dark)</h2>
<div id="widget-vertical"></div>
</div>
<div class="demo-section">
<h2>Your Embed Code</h2>
<div class="embed-code">
<div id="social-proof"></div>
<script src="https://your-domain.com/widget.js"></script>
<script>
new SocialProofWidget({
selector: '#social-proof',
handles: {
tiktok: 'your_handle',
instagram: 'your_handle',
twitter: 'your_handle'
}
});
</script>
</div>
</div>
<script src="/widget.js"></script>
<script>
// Demo with real data
new SocialProofWidget({
selector: '#widget-horizontal',
handles: {
tiktok: 'khaby.lame',
instagram: 'khabylame',
twitter: 'KhabyLame'
},
theme: 'light',
style: 'horizontal',
showTotal: true
});
new SocialProofWidget({
selector: '#widget-vertical',
handles: {
tiktok: 'charlidamelio',
instagram: 'charlidamelio'
},
theme: 'dark',
style: 'vertical',
showTotal: true
});
</script>
</body>
</html>
Step 7: Deploy
Deploy your API anywhere (Vercel, Railway, Render):
# Example: Deploy to Railway
npm install -g @railway/cli
railway login
railway init
railway up
Update WIDGET_API in widget.js to your deployed URL.
Adding Redis Caching (Production)
For high-traffic sites, add Redis:
const Redis = require('ioredis');
const redis = new Redis(process.env.REDIS_URL);
async function getCachedOrFetch(key, fetchFn, ttl = 300) {
// Try cache first
const cached = await redis.get(key);
if (cached) {
return JSON.parse(cached);
}
// Fetch fresh data
const data = await fetchFn();
// Cache with TTL
await redis.setex(key, ttl, JSON.stringify(data));
return data;
}
Customization Options
Different Widget Styles
// Minimal - just numbers
new SocialProofWidget({
selector: '#minimal',
handles: { twitter: 'username' },
style: 'minimal' // Shows "50K followers" only
});
// Badge style
new SocialProofWidget({
selector: '#badge',
handles: { tiktok: 'username' },
style: 'badge' // Compact badge format
});
Animation on Load
.sp-platform {
opacity: 0;
transform: translateY(20px);
animation: fadeUp 0.5s forwards;
}
.sp-platform:nth-child(1) { animation-delay: 0.1s; }
.sp-platform:nth-child(2) { animation-delay: 0.2s; }
.sp-platform:nth-child(3) { animation-delay: 0.3s; }
@keyframes fadeUp {
to {
opacity: 1;
transform: translateY(0);
}
}
What This Would Cost Elsewhere
Social proof widget services:
- SocialProof.io: $29/month
- Proof.com: $79/month
- UseProof: $99/month
Your self-hosted version:
- SociaVault: ~$0.004 per stat refresh
- Hosting: $0-5/month
- Total: Nearly free
Plus you own the data and can customize everything.
Real-World Use Cases
1. Landing Pages
Show follower counts to build credibility for a new product.
2. Creator Portfolios
Let potential sponsors see your real numbers instantly.
3. Media Kits
Embed live stats that auto-update (no more outdated screenshots).
4. SaaS Dashboards
Show user growth stats to your investors in real-time.
Get Started
- Get your SociaVault API Key
- Deploy the API
- Embed the widget on your site
- Watch your conversion rate climb
Real numbers. Real-time. Real credibility.
Social proof isn't just vanity metrics. It's trust at scale.
Top comments (0)