Referral programs pay real money. If someone discovers they can game your referral flow by generating fake installs or self-referring, they will. Click campaigns attract bots. Install attribution attracts fraud rings. The question isn't whether fraud will happen; it's whether you'll detect it before the monthly report or in real time.
Tolinku webhooks give you event-level data as it happens: every click, every install, every referral. That's exactly the data you need to build fraud detection rules. This guide covers the most common fraud patterns in deep link campaigns and how to detect them using webhook events. For the webhook setup, see the webhook setup guide.
The webhooks page with create form, webhook list, and delivery log.
Common Fraud Patterns
Click Fraud
Bots or click farms generate fake clicks to inflate campaign metrics, drain ad budgets, or manipulate analytics.
Signals in webhook data:
- High click volume from a single IP address
- Clicks with no subsequent installs (high click-to-install ratio)
- Clicks from data center IP ranges (not residential IPs)
- Identical user-agent strings across many clicks
- Clicks arriving at perfectly regular intervals (non-human timing)
Install Fraud
Fake installs generated by device farms, emulators, or SDK spoofing to claim attribution credit or referral rewards.
Signals in webhook data:
- Multiple installs from the same IP address
- Installs that happen suspiciously fast after a click (under 5 seconds, suggesting automation)
- Installs from known emulator fingerprints
- High install volume with zero downstream engagement
Referral Fraud
Users creating multiple accounts to refer themselves, or organized groups coordinating referrals for reward harvesting.
Signals in webhook data:
- A single referrer token generating an abnormal number of referrals in a short period
- Referral chains (A refers B, B refers C, C refers A)
- Referrals followed by immediate reward claims and account abandonment
- Multiple
referral.createdevents from the same IP
Building a Detection Pipeline
The architecture is straightforward: receive webhook events, check them against fraud rules, and flag or block suspicious activity.
Tolinku Webhook → Receiver → Fraud Rules Engine → Flag/Block
→ Store for analysis
Receiver with Fraud Checking
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use('/webhooks', express.raw({ type: 'application/json' }));
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET!;
app.post('/webhooks/tolinku', async (req, res) => {
const signature = req.headers['x-webhook-signature'] as string;
const expected = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(req.body)
.digest('hex');
if (signature !== expected) {
return res.status(401).send('Invalid signature');
}
res.status(200).send('OK');
const event = JSON.parse(req.body.toString());
const flags = await checkFraudRules(event);
if (flags.length > 0) {
await handleSuspiciousEvent(event, flags);
}
await storeEvent(event, flags);
});
Rule 1: IP Velocity
Flag events when a single IP generates too many events in a short window.
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL!);
async function checkIPVelocity(event: any): Promise<string | null> {
const ip = event.data.ip;
if (!ip) return null;
const key = `fraud:ip:${event.event}:${ip}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, 3600); // 1-hour window
}
const thresholds: Record<string, number> = {
'link.clicked': 100, // 100 clicks/hour from one IP is suspicious
'install.tracked': 5, // 5 installs/hour from one IP is very suspicious
'referral.created': 3, // 3 referrals/hour from one IP is suspicious
};
const threshold = thresholds[event.event] || 50;
if (count > threshold) {
return `IP velocity: ${count} ${event.event} events from ${ip} in 1 hour (threshold: ${threshold})`;
}
return null;
}
Rule 2: Referrer Velocity
Flag when a single referrer token generates too many referrals too quickly.
async function checkReferrerVelocity(event: any): Promise<string | null> {
if (event.event !== 'referral.created' && event.event !== 'referral.completed') {
return null;
}
const token = event.data.referrer_token;
if (!token) return null;
const key = `fraud:referrer:${token}`;
const count = await redis.incr(key);
if (count === 1) {
await redis.expire(key, 86400); // 24-hour window
}
// Normal users generate maybe 1-2 referrals per day. 10+ is suspicious.
if (count > 10) {
return `Referrer velocity: ${count} referrals from token ${token} in 24 hours`;
}
return null;
}
Rule 3: Click-to-Install Timing
Legitimate installs take time: the user clicks a link, visits the app store, downloads the app, and opens it. This process takes at minimum 30-60 seconds. An "install" within 5 seconds of a click suggests automation.
async function checkInstallTiming(event: any): Promise<string | null> {
if (event.event !== 'install.tracked') return null;
const token = event.data.token;
const installTime = new Date(event.timestamp).getTime();
// Look up the most recent click for this token
const clickKey = `fraud:lastclick:${token}`;
const lastClickTime = await redis.get(clickKey);
if (lastClickTime) {
const timeDiff = installTime - parseInt(lastClickTime);
if (timeDiff < 5000) { // Under 5 seconds
return `Suspiciously fast install: ${timeDiff}ms after click for token ${token}`;
}
}
return null;
}
// Record click timestamps for timing analysis
async function recordClickTime(event: any) {
if (event.event !== 'link.clicked') return;
const token = event.data.token;
const clickTime = new Date(event.timestamp).getTime();
await redis.set(`fraud:lastclick:${token}`, clickTime.toString(), 'EX', 86400);
}
Rule 4: Campaign Anomalies
Compare real-time campaign metrics against historical baselines.
async function checkCampaignAnomaly(event: any): Promise<string | null> {
if (event.event !== 'link.clicked') return null;
const campaign = event.data.campaign;
if (!campaign) return null;
const key = `fraud:campaign:${campaign}:hourly`;
const currentCount = await redis.incr(key);
if (currentCount === 1) {
await redis.expire(key, 3600);
}
// Get the baseline (stored separately, updated daily)
const baselineKey = `fraud:campaign:${campaign}:baseline`;
const baseline = parseInt(await redis.get(baselineKey) || '0');
// If current hourly rate is 5x the average, flag it
if (baseline > 0 && currentCount > baseline * 5) {
return `Campaign anomaly: ${campaign} has ${currentCount} clicks this hour (baseline: ${baseline}/hour)`;
}
return null;
}
Rule 5: Geographic Anomalies
If your app only serves users in specific regions, clicks from unexpected countries are suspicious.
import { Reader } from '@maxmind/geoip2-node';
let geoReader: Reader;
async function checkGeoAnomaly(event: any): Promise<string | null> {
const ip = event.data.ip;
if (!ip) return null;
try {
const geo = geoReader.country(ip);
const country = geo.country?.isoCode;
// Data center IP ranges (no country) are suspicious
if (!country) {
return `No geo data for IP ${ip} (possible data center)`;
}
// Check against allowed countries (configure per campaign)
const ALLOWED_COUNTRIES = new Set(['US', 'CA', 'GB', 'AU']);
if (!ALLOWED_COUNTRIES.has(country)) {
return `Unexpected country ${country} for IP ${ip}`;
}
} catch {
// GeoIP lookup failed; could be a reserved/private IP
return `GeoIP lookup failed for ${ip}`;
}
return null;
}
Combining Rules
Run all rules and aggregate the flags:
async function checkFraudRules(event: any): Promise<string[]> {
// Record click time for timing analysis
await recordClickTime(event);
const checks = await Promise.allSettled([
checkIPVelocity(event),
checkReferrerVelocity(event),
checkInstallTiming(event),
checkCampaignAnomaly(event),
checkGeoAnomaly(event),
]);
const flags: string[] = [];
for (const check of checks) {
if (check.status === 'fulfilled' && check.value) {
flags.push(check.value);
}
}
return flags;
}
Responding to Fraud
When fraud is detected, you have several options:
1. Flag and Log
The lowest-risk response. Flag the event for manual review without blocking anything.
async function handleSuspiciousEvent(event: any, flags: string[]) {
await db.query(
`INSERT INTO fraud_flags (event_hash, event_type, flags, event_data, flagged_at)
VALUES ($1, $2, $3, $4, NOW())`,
[
getEventHash(event),
event.event,
JSON.stringify(flags),
JSON.stringify(event),
]
);
// Alert the team
await sendSlackAlert(`Suspicious ${event.event} event flagged:\n${flags.join('\n')}`);
}
2. Delay Rewards
For referral fraud, don't block the referral; delay the reward. Hold rewards for 7-14 days and review flagged referrals before releasing payment.
3. Block the Source
For confirmed fraud (after manual review), block the IP, referral token, or campaign:
const BLOCKED_IPS = new Set<string>();
const BLOCKED_TOKENS = new Set<string>();
async function isBlocked(event: any): Promise<boolean> {
if (BLOCKED_IPS.has(event.data.ip)) return true;
if (BLOCKED_TOKENS.has(event.data.token)) return true;
return false;
}
Load the blocklists from your database on startup and refresh periodically.
Dashboard Queries
Review fraud flags with these queries:
Most Flagged IPs
SELECT
event_data->>'data'->>'ip' AS ip,
COUNT(*) AS flag_count,
array_agg(DISTINCT event_type) AS event_types
FROM fraud_flags
WHERE flagged_at > NOW() - INTERVAL '7 days'
GROUP BY ip
ORDER BY flag_count DESC
LIMIT 20;
Referral Fraud Candidates
SELECT
event_data->'data'->>'referrer_token' AS referrer,
COUNT(*) AS referral_count,
COUNT(DISTINCT event_data->'data'->>'ip') AS unique_ips
FROM fraud_flags
WHERE event_type IN ('referral.created', 'referral.completed')
AND flagged_at > NOW() - INTERVAL '30 days'
GROUP BY referrer
HAVING COUNT(*) > 5
ORDER BY referral_count DESC;
Click Fraud by Campaign
SELECT
event_data->'data'->>'campaign' AS campaign,
COUNT(*) AS flagged_clicks,
COUNT(DISTINCT event_data->'data'->>'ip') AS unique_ips
FROM fraud_flags
WHERE event_type = 'link.clicked'
AND flagged_at > NOW() - INTERVAL '7 days'
GROUP BY campaign
ORDER BY flagged_clicks DESC;
False Positives
Every fraud detection system generates false positives. A legitimate office building might have 50 employees clicking the same link from the same IP. A successful referrer might genuinely generate 15 referrals in a day.
Mitigate false positives by:
- Starting with alerts, not blocks. Flag first, block later. Review flagged events for a few weeks before automating responses.
- Using multiple signals. A single flag (high IP velocity) is weak. Multiple flags (high IP velocity + data center IP + no installs) are strong.
- Tuning thresholds. Start with generous thresholds and tighten them as you understand your traffic patterns.
- Whitelisting known IPs. Corporate VPNs and office buildings generate legitimate high-velocity traffic.
For webhook security (signature verification, secret rotation), see the webhook security guide. For monitoring delivery health, see the delivery monitoring guide.
Get deep linking tips in your inbox
One email per week. No spam.