> ## Documentation Index
> Fetch the complete documentation index at: https://docs.shieldlabs.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# New Account Fraud (Fake Signups)

> Learn how to detect and prevent new account fraud and fake signups: catch account farms by linking registrations to a small set of real devices.

Account farms and fake signups share one tell on the wire: many accounts created from the same hardware or the same network, often through a VPN, proxy, an anti-detect browser, or a cleared-cookie incognito session that tries to look new every time. ShieldLabs gives you four layers to catch this at registration, and your own code decides what to do at the signup endpoint.

## What is new account fraud?

New account fraud, also called fake signup fraud, is the mass creation of bogus or duplicate accounts by one person to abuse signup-bound perks (free trials, promos, referral payouts) or to seed downstream abuse. The accounts look distinct on the surface but trace back to a small number of real devices or networks.

## How ShieldLabs surfaces it

ShieldLabs gives you four things at each signup, and your code decides what to do with them:

| Layer                   | What it answers                                                                                        | Where you read it                                                                          | Latency               |
| ----------------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------- |
| **Identification**      | "Is this the same device behind a 'new' signup, even after cleared cookies, incognito, or a fresh IP?" | The durable `device_id` on the [webhook](/setup/webhooks) / [History API](/api/server-api) | About a second        |
| **Anonymity detection** | "Is this signup masked, spoofed, or anonymous right now?"                                              | The `signals` array on the [webhook](/setup/webhooks) / [History API](/api/server-api)     | About a second        |
| **Risk Score**          | "How risky is the visit overall, as one 0-100 number?"                                                 | `risk_score` on the [webhook](/setup/webhooks) / [History API](/api/server-api)            | About a second        |
| **Patterns**            | "Has this device or local IP already created many accounts?"                                           | [Dashboard Patterns](/features/patterns) + export                                          | Background (\~10 min) |

Identification is the anchor the others ride on: the naive keys reset on demand (a fresh `cookie_id` and `visitor_id` per cleared-cookie or incognito session, and a new IP each time the VPN exit node changes), but the server-derived **DeviceID** does not, so it collapses those "new" signups back to one machine. The Risk Score flags the obviously anonymous signup the moment it happens, so your code can stop it, and Patterns catch the slow farm that spreads creation over hours or days, each signup looking clean on its own. You want all four.

<Note>
  A high [Risk Score (0–100)](/features/risk-scoring) is not proof of fraud. A legitimate user behind a corporate proxy, a VPN, or a privacy browser can score in the High band. ShieldLabs surfaces the score and the reasons; your signup handler owns the threshold and the verdict.
</Note>

## Prevent fake signups

The rule your code applies: read the session's Risk Score and named signals at your signup endpoint, before you activate the account — a High-band score requires a second factor, a Medium-band score holds the account for email verification, a Clean or Low score passes through. Layer the patterns on top: a signup whose DeviceID or local IP already carries many accounts escalates one step. The outcome is that the obviously masked signup, and the farm spreading creation across sessions, both meet friction while a real first-time visitor sails in.

Two signal sources drive that decision:

* **Per session (the Risk Score):** the [Risk Score (0–100)](/features/risk-scoring) and the [anonymity signals](/features/anonymity-signals) behind it — VPN, Proxy, Tor, Privacy Relay, Browser VPN/Proxy, Datacenter IP, Abuser Flag, plus consistency tells like OS Mismatch, Timezone Mismatch, and environment tells like Anti-detect Browser and JavaScript Disabled. Each signal adds its weight to the score, so the automation tells carry the heaviest weights and one alone can push a session into High, while the pure network signals are light, so a VPN-only or Datacenter-only session lands in Low until they stack. You branch on the band, and read each signal's weight for context.
* **Across sessions (Patterns):** the dashboard [Patterns](/features/patterns) **Many Accounts on One Device** (keyed on the durable DeviceID) and **Many Accounts on One Local IP** (the farm behind one router or NAT, even when each session uses a fresh cookie and a different public IP) link the farm the per-session score cannot see in one request.

A scripted or headless farm surfaces through anonymity signals like JavaScript Disabled and Anti-detect Browser, and through the correlation patterns above — you act on those explainable signals and the score.

## Build it

<Steps>
  <Step title="Create a ShieldLabs account and get your keys">
    [Sign up for free](https://app.shieldlabs.ai/) and get 5,000 identifications, or log in if you already have an account. Register the domain you want to identify visitors on, then open the [Keys](/setup/keys) page. Use the **Public Key** to initialize the snippet in the browser, and keep your server-side credentials on your backend: the **Private API Key** authenticates the [History API](/api/server-api), and each webhook endpoint has its own `whsec_…` signing secret. See [Keys](/setup/keys).
  </Step>

  <Step title="Install the snippet on your signup page">
    Add the [snippet](/setup/snippet) to your signup page and call `checkAnonymous`. The callback hands you the `requestID` to correlate with the score later; the first argument is the client IP, not a score (the score arrives by webhook). Pass `undefined` for the userHID slot so the callback lands in place.

    ```html signup.html theme={null}
    <script type="module">
      const mod = await import(
        'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
      );
      mod.checkAnonymous(undefined, (ip, 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>
    ```
  </Step>

  <Step title="Gate signup on the Risk Score">
    Receive the [webhook](/setup/webhooks), verify its HMAC, and cache the score indexed by `RequestID`. At the signup endpoint, read the score for that session with the shared [`waitForScore` helper](/use-case) (your webhook cache, with a short timeout; falls back to a [History API](/api/server-api) read by `request_id`), then branch on its band.

    ```js api/signup.js theme={null}
    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.
      const shield = await waitForScore(shieldRequestId, 2000);
      const score = shield?.score ?? 0;            // 0–100, default 0 if not yet in
      const flags = shield?.detection_flags ?? {}; // booleans for quick branching

      // 3. YOUR code owns the decision. Bands are a guide.
      if (score >= 60) {
        // High: strong anonymity signals. Require email + a second factor.
        return res.status(200).json({ requireVerification: true, reason: 'extra_verification' });
      }
      if (score >= 30) {
        // Medium: email-verify before activating.
        return createPendingAccount(email, password, res);
      }

      // Clean / Low: create the account normally.
      return createAccount(email, password, res);
    });
    ```

    Branch on the `score` band and, when you need a fast condition for a specific tell, on the boolean [`detection_flags`](/glossary#detection-flags) (`vpn`, `proxy`, `tor`, `anti_detect_browser`, `javascript_disabled`, `ip_mismatch`, and more) — never on the human-readable `signal` labels, which can change.

    **Read the real network behind a mask.** The webhook carries two IPs. `public_ip` is the public IP and its country, which any VPN or proxy can fake per signup. `local_ip` is the real network IP, which can expose the network behind the mask. When the two disagree, `detection_flags.ip_mismatch` is set to `true`. For a farm rotating fresh public IPs, a shared `local_ip` is often the durable network tell — the same value the "Many Accounts on One Local IP" pattern correlates on.
  </Step>

  <Step title="Catch the farm with Patterns">
    A single anonymous signup is easy to score. The harder problem is the farm that creates 50 accounts over a week, each one clean in isolation. [Patterns](/features/patterns) link sessions over time and grade flagged entities **Suspicious**, then **Dangerous**, as the linked account count crosses thresholds in a rolling window (default 30 days). Levels never downgrade. An entity below the first threshold stays the unflagged baseline, which is never recorded.

    <AccordionGroup>
      <Accordion title="Many Accounts on One Device" icon="mobile-screen">
        One device linked to many different accounts — the classic account-farm shape. The grouping identity is the DeviceID, durable across a cookie clear, an incognito window, and an IP rotation. A farm clears cookies and goes incognito between signups specifically to look new every time; the DeviceID links those signups back to one machine anyway.
      </Accordion>

      <Accordion title="Many Accounts on One Local IP" icon="network-wired">
        Many accounts created through the same local IP. Catches farms behind one router or one NAT, even when each session uses a fresh cookie and a different public IP. Pair it with the device pattern to catch what spans several browsers but shares a network.
      </Accordion>
    </AccordionGroup>

    Export the flagged entities from the [dashboard](/features/patterns) (CSV or JSON) as a denylist, then check new signups against them. You can also reconstruct a device's history live: read the History API by `device_id` to see every account that device has touched. The response is `{ data, total }`; each element of `data` is a snapshot (newest first) carrying the `user_hid` for that session, so distinct `user_hid` values on one `device_id` are distinct accounts behind that machine.

    ```js Gate signup on device history theme={null}
    // Build a denylist from your pattern export (the flagged DeviceIDs),
    // or compute account count per device live from the History API.
    const flaggedDevices = await loadFlaggedDeviceIds(); // from dashboard export — your own store

    async function deviceIsKnownFarm(deviceId) {
      if (flaggedDevices.has(deviceId)) return true;
      const rows = await shieldHistory('device_id', deviceId, 100);
      const accounts = new Set(rows.map((r) => r.user_hid).filter(Boolean));
      return accounts.size >= YOUR_ACCOUNT_THRESHOLD;
    }

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

      if (shield?.device_id && (await deviceIsKnownFarm(shield.device_id))) {
        return res.status(200).json({
          requireVerification: true,
          reason: 'device_linked_to_many_accounts',
        });
      }

      return createAccount(email, password, res);
    });
    ```

    <Note>
      Account History reads on `account.shieldlabs.ai` and webhook delivery are free; the Management History path 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 any billed Management reads for the borderline cases.
    </Note>
  </Step>

  <Step title="Tune to your product">
    Start in a logging-only mode, watch how your real traffic distributes across the bands and patterns, then raise friction where the data justifies it.
  </Step>
</Steps>

## Test it

Reproduce a farm signup before you trust the gate. Load your signup page, complete it once, and note the `device_id` from the webhook. Then clear cookies (or open a new incognito window, or rotate your IP through a VPN) and sign up again: the `cookie_id` and `visitor_id` change each time, but the same `device_id` comes back, which is exactly what "Many Accounts on One Device" links on. Open a genuinely different browser and you will see a new DeviceID — the limit to be aware of when one person uses several separate browsers.

## Recommended starting thresholds

The four bands are defined in [Risk Scoring](/features/risk-scoring), and the per-band action playbook lives in [Acting on the Risk Score](/guides/acting-on-risk-score). Mapped to a signup gate:

| Risk Score band    | 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 |

Where you draw each action line is yours. 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).

<Card title="Next: Acting on the Risk Score" icon="arrow-right" href="/guides/acting-on-risk-score">
  The full per-band decision playbook, including signal-aware decisioning and how to combine the score with specific signals.
</Card>
