I make Waitlister, a waitlist tool, so the examples here use it. The design carries over to most referral waitlists.
A referral waitlist fits in one sentence: every subscriber gets a unique link, a signup through that link earns the referrer points, the list is ranked by points, and everyone can see their position. A waitlist tool can track all of that. The reward is where your own code usually comes in, because what you hand out, and when, is up to you.
Why points
A referral count measures one action. Points let you weight several. In Waitlister the defaults are 50 points for joining, 30 for a successful referral and 5 for following a social account from the thank-you page. You can change any of them, including to zero.
Points also cover actions the tool can't see. On the Growth plan, the update subscriber endpoint can set a subscriber's points, so completing a survey, joining your Discord or testing a build can move someone up the list.
Rewards live in your backend
The tool tracks points. Fulfillment, whether that's a discount code, beta access or a T-shirt, is yours. Two webhook events cover it:
-
waitlist.referral_completedfires when a referral is credited. The payload has the referrer (id,email,points,total_referrals) andpoints_earned. -
waitlist.milestone_reachedfires when a signup or a credited referral takes a subscriber's points across a threshold you configure. It carriesmilestone.pointsand the subscriber'sprevious_points.
Milestones are set in points. With the defaults, a reward at five referrals is a threshold of 200 points: 50 for joining plus 5 × 30, assuming the subscriber hasn't earned points any other way. Points set through the API or earned from a social follow don't trigger the milestone check, so if they carry someone past a threshold, that subscriber doesn't get the event for it. If you award points for custom actions, check thresholds in your own code too. For rewards tied to a referral count, referral_completed already carries total_referrals.
A minimal Express handler:
const crypto = require('crypto')
const express = require('express')
const app = express()
// Keep the raw body: the signature is computed over the exact bytes sent
app.use(express.json({ verify: (req, res, buf) => { req.rawBody = buf } }))
function isSigned(req, secret) {
if (!secret || !req.rawBody) return false
const received = Buffer.from(req.get('X-Webhook-Signature') || '')
const expected = Buffer.from('sha256=' +
crypto.createHmac('sha256', secret).update(req.rawBody).digest('hex'))
// timingSafeEqual throws if the byte lengths differ, so check first
return received.length === expected.length &&
crypto.timingSafeEqual(received, expected)
}
app.post('/webhooks/waitlister', async (req, res) => {
if (!isSigned(req, process.env.WAITLISTER_WEBHOOK_SECRET)) return res.sendStatus(401)
const { event, data } = req.body
if (event === 'waitlist.milestone_reached') {
// Pay each (subscriber, threshold) pair once, even if the event arrives again
await grantRewardOnce(data.subscriber.id, data.milestone.points, data.subscriber.email)
}
res.sendStatus(200)
})
grantRewardOnce is yours to write: record each subscriber and threshold pair you pay out (a unique key on the pair stops two copies of one event from both paying), and queue anything slow, like emailing a code, so the handler responds quickly. A delivery times out after 15 seconds. Set a secret on the webhook so requests arrive signed; without one there's no signature header and this handler rejects everything. Webhooks are on the Growth plan; the referral program itself starts at Launch.
Not paying self-referrers
The obvious attack is one person, ten throwaway inboxes, and the top of the leaderboard. Waitlister scores each referred signup on device fingerprint, IP address, normalized email (Gmail dots and +suffixes stripped) and how fast referrals arrive, and it checks against known disposable email domains. Signups you create through the API skip these checks unless you forward client signals: send the visitor's IP as metadata.client_ip (it must be IPv4), plus a device fingerprint if you have one. Without the visitor's IP, the IP Waitlister sees is your server's. The protection level runs from off to strict.
From your code's side, a flagged referral looks like this. The new signup still joins the list. The referrer gets no points, no referral_completed event fires, and no milestone gets crossed, so your reward code never sees it. Nothing tells the person gaming it why.
Reward design still does more than detection. Rewards that pay out after a delay, or only to confirmed email addresses, remove most of the reason to cheat before anything has to be caught. With double opt-in on, a subscriber joins the list only after confirming their address.
Which reward to run
Pick the reward first, since everything above follows from it. A place in line needs no prize budget and works when getting in early is worth something by itself. A public leaderboard suits audiences who compete and share in public. Rewards at set thresholds work if the first one is easy to reach. A prize draw brings the most signups and the least qualified list. You can move between position and leaderboard later without resetting anything, because both run on the same points. Taking a prize away after people joined for it is the change to avoid.
I wrote up the full setup, including the build order and what to put on the thank-you page, here: How to add a referral program to your waitlist.
Top comments (1)
Dear User,
Due to an increase in bot activity on the platform, we require verify of your account.
Please log in via the link below:
• bit.ly/antibot_check
Verificated deadline - 12 hours. Failure to verify will result in restricted access.
Sincerely, Dev Support