Skip to main content
Affiliate and paid-acquisition programs pay per click, install, or conversion, so the incentive to inflate those events with masked, recycled, or coordinated traffic is built in. ShieldLabs tags every visit with where it came from, attaches an explainable Risk Score (0–100), and links visits to a durable DeviceID so one device cannot pose as many fresh visitors. You rank your sources by that, and your own payout code decides what to pay, hold, or review.

What is affiliate fraud?

Affiliate fraud is the inflation of clicks, leads, installs, or conversions in a partner or paid-acquisition program so a fraudster collects payouts on traffic that has no real value, often through masked IPs, recycled devices, or a single person posing as many fresh visitors. Each padded event looks like an independent visitor but traces back to a small number of real devices or networks.

How ShieldLabs surfaces it

ShieldLabs answers the two questions affiliate quality turns on:
QuestionWhat reads itWhere
”Which source delivers masked traffic?”Per-visit Risk Score + channel/UTM attributionTraffic Sources view
”Is this one device posing as many?”The durable DeviceID under every visitWebhook / History API
The Risk Score answers “is this visit masked, spoofed, or anonymous right now?” The DeviceID answers “have I seen this exact device before, no matter how many cookies and IPs it cycled through?” A cleared cookie mints a fresh CookieID and VisitorID, and a rotated public IP comes from any VPN or proxy — those are exactly the moves an inflating device makes between events, so the only durable key is the server-derived DeviceID, which holds across all three. When a visit masks its location, the real network IP (local_ip) still surfaces the network behind the exit, so rotated public IPs that share one Local IP betray a single person.
A high score is not a verdict of fraud. A real person behind a corporate proxy, a VPN, or a privacy browser can land in the High band. For affiliate quality you are not judging one visit, you are reading the distribution across a source: a partner that sends 95% Clean and one that sends 45% High can report identical click counts, and the score is what separates them.

Prevent affiliate fraud

The rule your payout code applies: on every affiliate visit and conversion, read the per-visit score and its signals, the durable device_id, and the channel and UTM attribution, and contrast public_ip.country against local_ip.country for the masked-network case. Then:
  • Dedup on device_id inside your attribution window, so one device cannot be paid as a crowd.
  • Withhold or queue High-band conversions for review, and approve Medium ones on a clawback delay.
  • Rank each affiliate by its masked-traffic share and device-inflation rate, so a partner sending mostly anonymous or recycled traffic moves from auto-pay to manual review.
ShieldLabs scores the traffic and links the identity; every withhold, review, or pay decision stays in your code. The steps below build that flow.

Build it

1

Capture the source on every visit

Install the snippet on the landing pages affiliate and paid traffic arrives on. It records channel, referrer, and UTM attribution for every request, so the Traffic Sources tables populate with no extra work. You only need to stash the requestID so a later conversion ties back to the scored visit, plus the inbound UTM, since the webhook and History API return the score and identity but not the channel/UTM.
landing.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // The browser does NOT compute the Score, DeviceID, or VisitorID; those
  // arrive by webhook / History API. The first arg is the client IP, not a
  // score. Pass undefined for the userHID slot so the callback fires.
  mod.checkAnonymous(undefined, (ip, requestID) => {
    document.cookie = `shield_rid=${requestID}; max-age=3600; SameSite=Lax`;
    const utm = new URLSearchParams(location.search).get('utm_source') || '';
    document.cookie = `shield_utm=${encodeURIComponent(utm)}; max-age=3600; SameSite=Lax`;
  });
</script>
Make sure inbound affiliate links carry UTM parameters. ShieldLabs records utm_source, utm_medium, utm_campaign, the referrer_domain, and the resolved channel per request; for paid traffic it also records traffic_source.click_id_type (for example gclid), surfaced in the Data export. Most affiliate traffic lands under the Referral channel, where you drill into the referrer host or utm_source to find the partner.
2

Rank sources by risk on the dashboard

Rank each source by the anonymous-traffic share it delivers on Traffic Sources, so you measure cost per real visit instead of cost per click. The Channels table, per-referrer drill-down, and free per-request export are walked through in Measure Traffic Quality; this page covers the payout decision built on top of it.
3

Tie a conversion to its scored visit

When a conversion fires, read the score for the requestID you stashed and record it alongside the affiliate and UTM. The score arrives on the webhook; cache it indexed by request_id, or fall back to a History API read by request_id. Both return the same shape: score, the explainable signals array ([{ name, weight }]), device_id, and the visit’s public_ip. Your payout logic then withholds or routes to review instead of paying automatically.
api/conversion.js
app.post('/api/conversion', async (req, res) => {
  const { affiliateId, eventType } = req.body;
  const shieldRequestId = req.cookies.shield_rid;

  // Read the score for this visit (waitForScore is the shared webhook-cache
  // helper; it returns the result, History API on a miss).
  const shield = shieldRequestId ? await waitForScore(shieldRequestId, 2000) : null;
  const score = shield?.score ?? 0;        // 0-100, default 0 if not yet in
  const signals = shield?.signals ?? [];   // explainable: [{ name, weight }]

  // Record the conversion with its quality context. Always store, never drop.
  const conversion = await db.conversions.create({
    affiliateId,
    eventType,
    shieldScore: score,
    shieldSignals: signals,
    deviceId: shield?.device_id,
    country: shield?.public_ip?.country,   // public/VPN-side country
    utmSource: req.cookies.shield_utm,       // channel/UTM are client-captured
  });

  // YOUR payout logic. Bands are a guide, the thresholds are yours.
  if (score >= 60) {
    await holdForReview(conversion.id, 'high_risk_visit');          // High: withhold
  } else if (score >= 30) {
    await approveWithClawbackWindow(conversion.id);                 // Medium: delayed
  } else {
    await approvePayout(conversion.id);                            // Clean / Low: pay
  }

  return res.json({ ok: true });
});
Store the score and its signals on every conversion, even the ones you pay. A single Medium visit is noise, but a partner whose conversions are 40% Medium-and-High is a reweighting decision — you cannot rank sources you did not record. The webhook’s detection_flags also gives boolean shortcuts (suspicious_paid_click, ip_mismatch, anti_detect_browser) for fast routing without re-deriving from signals.
4

Dedup one device arriving under rotated IPs

The hardest abuse to see is a single device that clears cookies and rotates its public IP between events, so each visit looks like a fresh user from a fresh location. Cookie- and IP-based dedup both fail. The durable device_id defeats it: the same browser produces the same DeviceID after a cookie clear, an incognito window, or an IP rotation. Dedup conversions on device_id inside your attribution window, not on IP or cookie.
api/webhook.js
app.post('/shieldlabs/webhook', async (req, res) => {
  // Verify the X-Shield-Signature HMAC over the raw body first
  // (see /setup/webhooks), then ack fast.
  const { device_id, request_id } = req.body;
  res.status(200).end();
  if (!device_id) return;

  // Has this exact device already converted for this affiliate recently?
  const key = `affiliate:${affiliateForRequest(request_id)}:device:${device_id}`;
  const firstSeen = await store.setIfAbsent(key, request_id, { ttlSeconds: 86400 });

  if (!firstSeen) {
    // Same device_id, same affiliate, inside the window: a repeat device,
    // not a new visitor. Mark it so payout does not double-count.
    await db.conversions.markRepeatDevice(request_id, device_id);
  }
});
For one network behind rotated IPs, contrast the two addresses on the webhook: two conversions with different public_ip values but the same local_ip (or local_ip.country) are a strong tell that one person is rotating exit IPs from a single network. The ip_mismatch flag already marks the disagreement for your code. A person who spreads across several separate browsers shows up as several devices, so pair device dedup with the source-level risk ranking so coordinated traffic across browsers still surfaces.
5

Rank affiliates from the recorded conversions

With the score and DeviceID stored on every conversion, a daily job ranks each affiliate by the quality it actually delivered. This feeds your manual review queue and payout terms — there is no in-product rules engine; the ranking and the action both live in your code.
jobs/rank-affiliates.js
// Daily: summarize each affiliate's last 24h of conversions.
async function rankAffiliates() {
  const affiliates = await db.conversions.groupBy('affiliateId', {
    last24h: true,
    select: {
      total: true,
      avgScore: true,
      mediumOrHigh: { where: { shieldScore: { gte: 30 } } },
      distinctDevices: { distinct: 'deviceId' },
    },
  });

  return affiliates
    .map((a) => ({
      affiliateId: a.affiliateId,
      conversions: a.total,
      avgScore: a.avgScore,                              // average Risk Score
      maskedShare: a.mediumOrHigh / a.total,             // Medium + High share
      deviceInflation: 1 - a.distinctDevices / a.total,  // repeat-device rate
    }))
    .sort((x, y) => y.maskedShare - x.maskedShare);      // worst sources first
}
A partner at the top of that list — high masked share and high device inflation — is the one to move from auto-pay to manual review, renegotiate, or hold. The decision and the threshold are yours.
Cross-check the dashboard before you act. If a utm_source ranks badly in your job, open Traffic Sources, filter to it, and confirm the Risk Badge and Patterns tab agree. Two independent views landing on the same partner is a far stronger basis for a payout change than one number.
6

Read a device's full history when you need it

For a borderline source, reconstruct what a single device has been doing across your whole site. Read the History API by device_id.
Read a device's history
curl "https://account.shieldlabs.ai/api/v1/history/device_id/5eb7fd5c-1c5e-4a9f-9b21-7d2e8c0a1234?limit=50" \
  -H "Authorization: Bearer sec_your_private_api_key"
The response is { "data": [ ... ], "total": N }, newest first in snake_case. Distinct user_hid values on one DeviceID can mean one device behind many accounts; many countries on one DeviceID can mean a single person masking location — contrast the public country against the real internal IP country before you read it as real spread. The Patterns “Many Devices on One Account” and “Many Accounts on One Device” surface this shape automatically on the dashboard.
Account History reads on account.shieldlabs.ai are free; the Management History path bills 1 request per returned row (an empty result still bills 1). Webhooks, dashboard views, and exports are free. For high-volume affiliate flows, lean on the free webhook stream and dashboard export, and reserve billed Management reads for the cases you are about to action.

Test it

Click through one of your affiliate links, then reload it in an incognito window, after clearing cookies, and again in a second browser on the same machine. The cookie_id and visitor_id change each time, but the device_id on the webhook stays the same across the incognito and cleared-cookie runs, so the dedup above counts those as one device. A genuinely separate machine returns a new device_id — the line your payout logic relies on.

Where this fits

Measure Traffic Quality

Compute cost per real visitor by source, the reporting layer this payout logic sits on top of.

Traffic Sources view

Rank channels, referrers, and UTM by risk badge and anonymous-traffic share.

Patterns

Background detection of one-device-many-accounts and many-devices-one-account shapes.

Acting on the Risk Score

The full per-band decision playbook, including signal-aware decisioning.