Gate account creation with two layers: the per-request Risk Score at signup and the dashboard Abuse Patterns that link many accounts to one device or one local IP over time.
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.
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.
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.
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;}
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:
Many Accounts on One Device
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 on One Local IP
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.
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.
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.
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 exportasync 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.
The bands below are a starting guide. Where you draw each action line is yours.
Risk Score band
Range
Suggested signup action
Clean
0–9
Create the account, no friction
Low
10–29
Create the account, log the session
Medium
30–59
Require email verification before activating
High
60–100
Require 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.