Skip to main content
Signup farms exist for one payoff: the reward. The same operator spins up fresh accounts to claim a signup bonus, burn through a coupon code, or restart a free trial. The catch happens not when the account is born but when it reaches for the reward, so this recipe lives at the redemption endpoint. Joining accounts at registration is a separate job that the signup recipe already covers, so wire that up for the create-account moment and treat this page as the reward-time gate on top of it.

Score this redemption

The per-request Risk Score (0-100) on the session reaching for the reward, live in the webhook.

Count accounts behind the device

The Abuse Patterns that tie many accounts to one device or one local IP, read from the dashboard or reconstructed from history.
ShieldLabs scores the session and links the accounts. It never blocks anyone. Your redemption handler reads the score and the account count behind the device, then your code decides: grant the reward, require verification first, or deny it. The thresholds below are a starting point, not a rule.

Where this differs from signup

The signup recipe scores the account at the moment it is created. That stops the obviously anonymous registration, but a patient farm creates accounts slowly and quietly, each one clean on its own, and only cashes them in later. The reward moment is where the farm reveals itself: ten “different” customers redeeming the same coupon from one machine is a shape that no single clean signup ever shows. So run both. Score at signup to thin the farm early, and check again at redemption to count how many accounts that one device or local IP has actually used to claim the reward.

Step 1: score the redemption session

Add the snippet to the page where the reward is claimed (the cart with the coupon applied, the “start trial” screen, the bonus-claim button). Re-identify on the action itself so you score the session that is redeeming, not a stale page load. Pass the account’s hashed id, never a raw email.
redeem.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // Pass the hashed account id, never a raw email or user id.
  // The callback hands you the requestID to correlate with the score.
  mod.checkAuthenticatedUser('8a9f-hashed-account-id', (serverResponse, requestID) => {
    document.getElementById('shield-request-id').value = requestID;
  });
</script>

<form method="POST" action="/api/redeem">
  <input type="hidden" id="shield-request-id" name="shieldRequestId" />
  <input type="text" name="couponCode" placeholder="Coupon code" />
  <button type="submit">Apply reward</button>
</form>
The redemption endpoint reads the score for that requestID. The score arrives on the webhook, so cache it by RequestID and read it back with the shared waitForScore helper defined in the cookbook overview, or fall back to a History API read by request_id.
api/redeem.js
app.post('/api/redeem', async (req, res) => {
  const { accountId, couponCode, shieldRequestId } = req.body;

  // 1. Your normal redemption checks first (code valid, not already used by
  //    this account, within campaign window).
  if (!(await couponIsRedeemable(couponCode, accountId))) {
    return res.status(409).json({ error: 'Coupon not redeemable' });
  }

  // 2. Look up the ShieldLabs Risk Score for this session.
  const shield = await waitForScore(shieldRequestId, 2000);
  const score = shield?.Score ?? 0;       // 0-100, default to 0 if not yet in
  const signals = shield?.Details ?? [];  // explainable: [{ Value, Description }]

  // 3. YOUR code owns the verdict. Branch on the band, never on a label string.
  if (score >= 60) {
    // High band: strong anonymity signals on the redeeming session.
    // Hold the reward and require verification before granting it.
    return res.status(200).json({ requireVerification: true, reason: 'anonymity' });
  }

  // Clean / Low / Medium: carry on to the account-count check in Step 2.
  return grantOrGate(req, res, shield);
});
A high score is not a fraud verdict. A real customer on a corporate proxy, a VPN, or a privacy browser can land in the High band. Decide on the Score plus its Details plus your own redemption context, never on the number alone, and tune thresholds gradually. Anti-detect browser, proxy, and VPN signals often ride along with farms, but each one can be innocent in isolation, so treat them as weight, not proof.
The score tells you whether this one session looks masked. It does not, on its own, tell you how many accounts sit behind the device. That is the next step.

Step 2: count the accounts behind the device and the local IP

A bonus farm clears cookies and goes incognito between accounts to look new every time. The score on each individual redemption can look fine. What gives it away is the count: how many distinct accounts has this one device, or this one local IP, already used to claim the reward? Two Abuse Patterns answer exactly that, and ShieldLabs grades each flagged entity Suspicious or Dangerous. An entity that crosses no threshold stays the unflagged baseline and is never recorded.
One device linked to many different accounts: the classic bonus-farm shape. The grouping identity is the DeviceID, which is durable and browser-bound. It survives a cookie clear, an incognito window, and an IP rotation, so clearing storage does not reset the link. Suspicious at 3 to 5 accounts in a month, Dangerous at 6 or more.
Many accounts redeeming through the same local IP, even when each session uses a fresh cookie and a different public IP. Catches the farm sitting behind one router or one NAT. Suspicious at 3 in 24 hours, 4 to 5 in 7 days, or 6 to 9 in 30 days; Dangerous at 4 or more in 24 hours, 6 or more in 7 days, or 10 or more in 30 days.
These patterns grade up as the count crosses the thresholds in a rolling window, and levels never downgrade once flagged. You read them on the dashboard Patterns tab and export the flagged entities as CSV or JSON.

Count it live at redemption

For the reward decision you often want the count right now, not on the next pattern run. Read the History API by device_id to reconstruct every account that device has touched. Each snapshot carries the UserHID for that session, so the number of distinct UserHID values is the number of accounts behind the machine.
Read a device's history
curl "https://api.shieldlabs.ai/{domain}:{secret}/history/device_id/5eb7fd5c-1c5e-4a9f-9b21-7d2e8c0a1234?limit=50"
Gate the reward on account count
// Prefer the pre-computed pattern export as a fast denylist, and reserve live
// device_id reads for the borderline redemptions that are worth the cost.
const flaggedDevices = await loadFlaggedDeviceIds(); // from dashboard export

async function accountsBehindDevice(deviceId) {
  const rows = await shieldHistory('device_id', deviceId, 100);
  // Distinct hashed accounts seen on this one machine.
  return new Set(rows.map((r) => r.UserHID).filter(Boolean)).size;
}

async function grantOrGate(req, res, shield) {
  const deviceId = shield?.DeviceID;

  // Known farm device from the pattern export: hold the reward.
  if (deviceId && flaggedDevices.has(deviceId)) {
    return res.status(200).json({ requireVerification: true, reason: 'device_linked_to_many_accounts' });
  }

  // Live count for this redemption. YOUR_ACCOUNT_LIMIT is your policy.
  if (deviceId && (await accountsBehindDevice(deviceId)) >= YOUR_ACCOUNT_LIMIT) {
    return res.status(200).json({ requireVerification: true, reason: 'reward_already_claimed_on_device' });
  }

  // Clear: grant the reward.
  return grantReward(req, res);
}
The History API bills 1 request per returned row, and an empty result still bills 1, while the webhook delivery is free. For high-volume redemption flows, lean on the pattern export as your denylist and reserve live device_id reads for the rewards that are expensive to give away by mistake.

Weigh the device count with the local IP

A determined operator who uses several genuinely separate browsers shows up as several devices, because the DeviceID is browser-bound. That is the gap the device count alone leaves open. Close it by weighing three things together: the “Many Accounts on One Device” signal, the “Many Accounts on One Local IP” signal, and your own redemption limits on the code or campaign. A reward claimed by ten accounts that share one local IP is a strong shape even when each one reports a different device, and a coupon you have capped at one redemption per customer does not need a perfect farm detector to hold the line.

Putting it together

1

Wire the signup gate first

Join accounts to the DeviceID at registration with the signup recipe so the farm is already thinned before it reaches the reward.
2

Score the redemption session

Load the snippet on the claim page and call checkAuthenticatedUser on the action, following the snippet setup.
3

Cache the score and read it back

Receive the webhook, verify the HMAC, and read the result with the shared waitForScore helper from the cookbook overview.
4

Count accounts behind the device and local IP

Check the redeeming device against the “Many Accounts on One Device” and “Many Accounts on One Local IP” exports, or count distinct UserHID values from a live device_id history read.
5

Decide and tune

Grant, verify, or deny in your own code. Start in a logging-only mode, watch how real redemptions distribute, then raise friction where the data justifies it.
The four bands are defined in Risk Scoring, and the per-band playbook lives in Acting on the Risk Score. Mapped to a reward gate, with the account count layered on top:
Signal at redemptionSuggested reward action
Clean / Low score, no pattern flagGrant the reward
Medium score, no pattern flagGrant, but log and watch the device
High scoreRequire verification before granting
Device or local IP flagged SuspiciousRequire verification, regardless of the session score
Device or local IP flagged DangerousDeny the reward and route to review
Where you draw each line is yours, and your own per-code or per-campaign redemption caps sit alongside these as a second, simpler backstop.

Next: Acting on the Risk Score

The full per-band decision playbook, including Details-aware decisioning and how to combine the score with specific signals.