Skip to main content
Account farms and multi-accounting share one tell: many accounts created from the same hardware or the same network. ShieldLabs gives you two layers to catch this, and your own code decides what to do at the signup endpoint.

Layer 1: Risk Score at signup

A per-request Risk Score (0–100) on the anonymity of the session creating the account. Live in the webhook, in real time.

Layer 2: Abuse Patterns over time

Server-side Abuse Patterns that link accounts across sessions: “Many Accounts on One Device”, “Many Accounts on One Local IP”. Surfaced in the dashboard.
ShieldLabs scores and links. It never blocks anyone. Your signup handler reads the score and the pattern membership, then your code decides: allow, require extra verification, or reject. The bands below are a guide, not a rule.
The Risk Score is computed per request and delivered in real time. Abuse Patterns are computed in the background (the worker runs about every 10 minutes) and are read from the dashboard or its export, not from the webhook payload.

How the two layers work together

LayerWhat it answersWhere you read itLatency
Risk Score”Is this session masked, spoofed, or anonymous right now?”Webhook / History APIReal time (~1s)
Abuse Patterns”Has this device or local IP already created many accounts?”Dashboard Patterns + exportBackground (~10 min)
Layer 1 stops the obviously anonymous signup at the moment it happens. Layer 2 catches the slow farm that spreads creation over hours or days, each individual signup looking clean on its own. You want both.

Layer 1: score the signup session

Add the snippet to your signup page. Pass the user’s hashed account id once you have one (for example after the form is filled but before submit). Never pass a raw email or user id, always a pseudonymous hash.
signup.html
<script type="module">
  const mod = await import(
    'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
  );
  // The callback gives you the requestID to correlate with the score.
  // The browser does NOT compute the score: it arrives by webhook / History API.
  mod.checkAnonymous((serverResponse, requestID) => {
    document.getElementById('shield-request-id').value = requestID;
  });
</script>

<form method="POST" action="/api/signup">
  <input type="hidden" id="shield-request-id" name="shieldRequestId" />
  <input type="email" name="email" placeholder="Email" />
  <input type="password" name="password" placeholder="Password" />
  <button type="submit">Create account</button>
</form>
The signup endpoint then reads the score for that requestID. The score arrives on the webhook (see Webhooks); cache it indexed by RequestID, or fall back to a History API read by request_id.
api/signup.js
app.post('/api/signup', async (req, res) => {
  const { email, password, shieldRequestId } = req.body;

  // 1. Your normal validation first.
  if (await emailExists(email)) {
    return res.status(409).json({ error: 'Email already in use' });
  }

  // 2. Look up the ShieldLabs Risk Score for this session.
  //    waitForScore reads your webhook cache (with a short timeout).
  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 decision. Bands are a guide.
  if (score >= 60) {
    // High: strong anonymity / abuse signals. Require email + a second factor.
    return res.status(200).json({
      requireVerification: true,
      reason: 'extra_verification',
    });
  }

  if (score >= 30) {
    // Medium: one moderate signal or several overlapping. Email-verify before activating.
    return createPendingAccount(email, password, res);
  }

  // Clean / Low: create the account normally.
  return createAccount(email, password, res);
});
A high score is not proof of fraud. A legitimate user behind a corporate proxy, a VPN, or a privacy browser can score in the High band. Decide on the Score plus the Details plus your own context, never on the number alone. Tune your thresholds gradually.

Reading the signals

Every score ships with a Details array so you can see exactly why it fired. At signup, a few signals matter more than the raw number.
function signupRiskNotes(shield) {
  const fired = (shield?.Details ?? []).map((d) => d.Description);
  const notes = [];

  if (fired.some((s) => s.includes('tor'))) {
    notes.push('Tor exit. Treat creation from Tor as high friction.');
  }
  if (fired.some((s) => s.includes('Anti-detect'))) {
    notes.push('Anti-detect browser. Common in account farms.');
  }
  if (fired.some((s) => s.includes('abuser'))) {
    notes.push('IP/device on an abuse reputation list. High risk even at a moderate score.');
  }
  return notes;
}
See Acting on the Risk Score for the full per-band playbook, and Signals for what each signal means.

Layer 2: catch the farm with Abuse Patterns

A single anonymous signup is easy to score. The harder problem is the farm that creates 50 accounts over a week, each one looking clean in isolation. That is what Abuse Patterns are for. ShieldLabs links sessions over time by identifier and grades each entity Normal → Suspicious → Dangerous. Two patterns target multi-accounting at signup directly:
One device linked to many different accounts. The classic multi-accounting / account-farm shape. The grouping identity is the DeviceID, which is durable: it survives a cookie clear, an incognito window, and an IP rotation within the same browser. Clearing storage does not reset the link.
Many accounts created through the same local (WebRTC) IP. Catches farms behind one router or one NAT, even when each session uses a fresh cookie and a different public IP.
Both grade up as the count of linked accounts crosses thresholds in a rolling window (default 30 days). Levels never downgrade: once an entity is flagged Dangerous, it stays Dangerous as new activity confirms it. You read these on the dashboard Patterns tab.

Why DeviceID is the right key

A farm clears cookies and goes incognito between signups specifically to look new every time. Cookie-based tools count each one as a fresh account on a fresh device. The DeviceID is derived from dozens of stable browser-fingerprint components (canvas, WebGL, audio, fonts, screen, and more), not stored in a cookie. Because it is derived rather than stored, the same browser produces the same DeviceID even after the cookies are gone. That is what lets “Many Accounts on One Device” link the cleared-cookie signups back to one machine.
DeviceID is browser-bound. A different browser on the same machine produces a different DeviceID, and there is no cross-browser recognition today. A determined operator using several separate browsers will spread across several DeviceIDs. Pair the device pattern with “Many Accounts on One Local IP” to catch what spans browsers but shares a network. See Identifiers.

Feed flagged entities into your own rules

Export the flagged entities from the dashboard (CSV or JSON), then enforce them in your signup endpoint. You can also reconstruct a device’s history programmatically: read the History API by device_id to see every account that device has touched.
Read a device's history
curl "https://api.shieldlabs.ai/{domain}:{secret}/history/device_id/5eb7fd5c-1c5e-4a9f-9b21-7d2e8c0a1234?limit=50"
The response is an array of snapshots (newest first), each carrying the UserHID for that session. Distinct UserHID values on one device_id are distinct accounts behind that machine.
Gate signup on device history
// Build a denylist from your pattern export (the flagged DeviceIDs / UserHIDs),
// or compute account count per device live from the History API.
const flaggedDevices = await loadFlaggedDeviceIds(); // from dashboard export

async function deviceIsKnownFarm(deviceId) {
  if (flaggedDevices.has(deviceId)) return true;

  // Optional live check: how many distinct accounts on this device?
  const rows = await shieldHistory('device_id', deviceId, 100);
  const accounts = new Set(rows.map((r) => r.UserHID).filter(Boolean));
  return accounts.size >= YOUR_ACCOUNT_THRESHOLD;
}

app.post('/api/signup', async (req, res) => {
  // ... validation + Layer 1 score check above ...

  // Layer 2: pattern membership / device history.
  if (shield?.DeviceID && (await deviceIsKnownFarm(shield.DeviceID))) {
    return res.status(200).json({
      requireVerification: true,
      reason: 'device_linked_to_many_accounts',
    });
  }

  return createAccount(email, password, res);
});
The History API bills 1 request per returned row (an empty result still bills 1). For high-volume signup flows, prefer the pre-computed pattern export as your denylist and reserve live device_id reads for the borderline cases.

Putting it together

1

Install the snippet on your signup page

Load it from cdn.shieldlabs.ai and call checkAnonymous. See Snippet setup.
2

Receive the webhook and cache the score by RequestID

Configure your callback URL and verify the HMAC. See Webhooks.
3

Gate signup on the Risk Score (Layer 1)

Clean / Low: create the account. Medium: require email verification. High: require a second factor. Your code, your thresholds.
4

Export flagged entities from Patterns (Layer 2)

Pull the “Many Accounts on One Device” and “Many Accounts on One Local IP” entities from the dashboard and check new signups against them.
5

Tune

Start in a logging-only mode, watch how your real traffic distributes across the bands, then raise friction where the data justifies it.
The bands below are a starting guide. Where you draw each action line is yours.
Risk Score bandRangeSuggested signup action
Clean0–9Create the account, no friction
Low10–29Create the account, log the session
Medium30–59Require email verification before activating
High60–100Require a second factor, or reject and route to support
Layer the pattern signal on top: if the session also belongs to “Many Accounts on One Device” or “Many Accounts on One Local IP”, escalate one step (a Medium session on a flagged device becomes a High-friction signup).

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.