Tiny corner popups that show real activity — and drop a pin on the visitor's city. Zero dependencies. One script tag. Bring your own brand.
🧭 Setup wizard · Quick start · Config
Most social-proof widgets show a boring avatar and a line of text. ProofPin drops a real map of the visitor's city behind an Apple-style pin — so "Sarah in Austin, TX just signed up" actually looks like it happened somewhere real. It reads as trustworthy because it is: ProofPin only ever shows the events you feed it.
┌───────────────────────────────────┐
│ ▓▓▓▓▓ │ Sarah in Austin, TX │
│ ▓📍▓▓ │ Just started a free trial │
│ ▓▓▓▓▓ │ 9 min ago · ✓ Verified │
└───────────────────────────────────┘
▲ live map tile of their city
- 🗺️ The map is the hook — a live, keyless map tile centered on each visitor's city.
- 🪶 Zero dependencies — ~6kb gzipped, no framework, works on any site (HTML, WordPress, Shopify, React…).
- 🎨 Your brand in minutes — logo, colors, pin, position, and copy are all config.
- 🔌 Bring your own data — a static array, your own endpoint, or the included one-click backend.
- 🔒 Honest by design — it shows only the real events you give it. No fake-event generator here (see Honest proof).
- 🔔 5 notification types — visitor events (with the map), live count, announcements, countdown timers, and reviews.
| ProofPin | Provely | Fomo | typical $29/mo tool | |
|---|---|---|---|---|
| Price | Free, MIT | ~$17–95/mo | ~$19–99/mo | ~$29/mo |
| Live map of the visitor's city | ✅ | ❌ | ❌ | ❌ |
| Zero dependencies, one script tag | ✅ | ❌ | ❌ | ❌ |
| Self-hosted / your data | ✅ | ❌ | ❌ | ❌ |
| Visitor events + live count | ✅ | ✅ | ✅ | ✅ |
| Announcements / timers / reviews | ✅ | ✅ | ✅ | ✅ |
| Fabricated "past data" mode | ✅ never | ✅ (a feature) | ||
| Open source | ✅ | ❌ | ❌ | ❌ |
Same core features as the tools charging monthly — plus a live map they don't have, and no subscription.
→ Open the setup wizard — answer a few questions, watch the live preview, and copy a snippet that's already filled in with your brand, colors, and data source. No code to write.
Drop in one script tag and initialize. This CDN link works today — no npm install needed:
<script src="https://cdn.jsdelivr.net/npm/proofpin"></script>
<script>
ProofPin.init({
logo: '/logo.svg',
brandName: 'Acme',
defaultAction: 'Just started a free trial',
events: [
{ name: 'Sarah', place: 'Austin, TX', lat: 30.27, lon: -97.74, time: Date.now() - 540000 },
{ name: 'Marcus', place: 'Miami, FL', lat: 25.76, lon: -80.19, time: Date.now() - 120000 },
],
})
</script>That's the whole thing. No build step, no account. Paste it just before </body>.
Using a bundler instead? Install from npm:
npm install proofpinPrefer no inline JS? Add a
data-proofpinattribute with a JSON config to the script tag and ProofPin auto-initializes.
ProofPin renders whatever you give it. Pick one:
1. A static array — great for testimonials, demos, or when your backend already pushes events somewhere:
ProofPin.init({ events: [ /* {name, place, lat, lon, time, text} */ ] })2. Your own endpoint — ProofPin GETs it and expects { events: [...], viewsToday: 0 }:
ProofPin.init({ endpoint: '/api/proof' })3. The included backend (one click) — a Netlify Function + Netlify Blobs that captures real events, derives the city + map coordinates from the visitor's IP (server-side, never asked for), and counts a rolling 24h "viewing" number:
Then, from your app, record a real event whenever something real happens:
// after a genuine signup / purchase / booking:
fetch('/.netlify/functions/proofpin', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ type: 'event', name: 'Sarah', text: 'Just started a free trial' }),
})…and point the widget at it: ProofPin.init({ endpoint: '/.netlify/functions/proofpin' }).
Besides visitor events, pass extra cards via cards: [...]. They're mixed into the rotation.
ProofPin.init({
events: [ /* visitor events with the map */ ],
cards: [
{ kind: 'announcement', title: 'New: v2 is live', text: 'Ship faster today',
cta: 'Learn more', url: 'https://yoursite.com/v2' },
{ kind: 'timer', title: 'Launch ends in', until: Date.now() + 2*60*60*1000 },
{ kind: 'review', rating: 5, quote: 'Best tool we added all year.', name: 'Mia', place: 'NYC' },
],
})announcement—title,text, optionalcta+urltimer—title,until(epoch ms); counts down livereview—quote,rating(1–5), optionalname,placeevent— the visitor card (name,place,lat,lon,text,url)count— the live "X people viewing" card (fromviewsToday)
Any card with a url (or a global onClick) becomes clickable.
Fire a notification the moment something real happens — no reload:
ProofPin.init({ /* ... */ })
// later, e.g. inside your signup success handler:
ProofPin.push({ name: 'Sarah', place: 'Austin, TX', lat: 30.27, lon: -97.74, text: 'Just signed up' })
// or any card type:
ProofPin.push({ kind: 'review', rating: 5, quote: 'Love it!', name: 'Dev' })Every option, with defaults:
| Option | Default | What it does |
|---|---|---|
endpoint |
null |
URL to GET events from ({ events, viewsToday }). |
events |
null |
Static array of events instead of an endpoint. |
viewsToday |
0 |
Count to use with a static events array. |
recordViews |
true |
POST {type:'view'} to the endpoint on load. |
position |
'bottom-left' |
bottom-left, bottom-right, top-left, or top-right. |
cards |
null |
Extra cards (announcement / timer / review) mixed into the rotation. |
onClick |
null |
fn(item) fired when a card is clicked. |
maxDisplays |
0 |
Stop after N cards per session (0 = unlimited). |
showOnMobile |
true |
false hides the widget on screens under 640px. |
pages |
null |
Only show on matching paths, e.g. ['/pricing','/blog/*'] (null = everywhere). |
gradient |
['#7D4AFE','#4399EC','#07EBDB'] |
Brand accent gradient. |
accent |
'#4399EC' |
"Verified by" / accent color. |
pinColor |
'#FF3B30' |
The map drop pin. |
dark |
true |
Dark glass card (false = light card). |
logo |
null |
Small logo URL for the "Verified by" badge. |
brandName |
'us' |
Shown as "Verified by {brandName}" when no logo. |
verifiedText |
'Verified by' |
Label before the logo/brand. |
showVerified |
true |
Show the verified badge line. |
showMap |
true |
Show the map panel (falls back to gradient + pin). |
mapZoom |
12 |
Map zoom (higher = tighter on streets). |
defaultAction |
'Just signed up' |
Action line; per-event text overrides it. |
countText |
'{n} people viewing recently' |
Headline for the count card ({n} = viewsToday). |
countSubtext |
'in the last 24 hours' |
Sub-line for the count card. |
viewFloor |
8 |
Hide the count card below this number. |
maxAgeHours |
48 |
Ignore events older than this. |
firstDelay |
5000 |
Delay (ms) before the first popup. |
visibleMs |
6500 |
How long each popup stays (ms). |
gapMs |
2800 |
Gap between popups (ms). |
dismissible |
true |
Show the ✕ dismiss button. |
respectReducedMotion |
true |
Honor prefers-reduced-motion. |
zIndex |
2147483000 |
Stacking order. |
{
name: 'Sarah', // first name (kept short)
place: 'Austin, TX', // shown next to the name
lat: 30.27, // optional — enables the map tile
lon: -97.74, // optional
time: 1700000000000, // epoch ms (or `t`)
text: 'Just signed up' // optional per-event action line
}No lat/lon? The card gracefully falls back to your brand gradient with the pin.
const pin = ProofPin.init(config) // start
pin.push(item) // add a card/event live (see "Push events live")
pin.dismiss() // stop and hide
pin.destroy() // remove entirely
ProofPin.push(item) // push to the most recent instance
ProofPin.version // e.g. "0.2.0"ProofPin ships without a fake-event generator, on purpose. Invented social proof ("Someone just bought!" when nobody did) is deceptive and, in many places, illegal (e.g. the US FTC's rules on fake endorsements). ProofPin only renders the events you feed it — so feed it real ones, fired from real signups, purchases, or bookings. Honest proof converts and keeps you out of trouble. This tool just makes it look great.
Map tiles come from CARTO Voyager basemaps (no API key needed for light use). Attribution for OpenStreetMap and CARTO is required if you display the map — add a small line in your footer:
Maps © OpenStreetMap contributors, © CARTO
Higher traffic? Swap the one TILE constant in src/proofpin.js for a keyed provider
(Mapbox, MapTiler, etc.).
All modern browsers (Chrome, Safari, Firefox, Edge). Uses fetch and backdrop-filter;
degrades gracefully where backdrop-filter is unsupported.
npm install # dev deps (esbuild, @netlify/blobs)
npm run build # -> dist/proofpin.min.js (+ copies into demo/)Open docs/index.html to run the setup wizard locally.
Issues and PRs welcome. Keep it dependency-free and small.
MIT © Jay Awal (@mr.jayawal)