> ## 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.

# Free Trial Abuse

> Learn how to detect and prevent free trial abuse: link new accounts cycling one device to re-claim free trials and free-tier quotas.

Trial abuse is one person taking your free trial again and again: a new email each run, the timer reset to zero, never a paid plan at the end. ShieldLabs gives you a durable [DeviceID](/features/identification) that survives the cookie clears and IP rotations between runs, so your signup code can see how many "different" trials actually sit behind one machine.

## What is free trial abuse?

Free trial abuse is when one person repeatedly creates new accounts to re-claim a product's free trial or free-tier quota without ever converting to paid. Each account looks like a distinct customer, but they all originate from one person behind a single device or local network, cycling identities to keep the free benefit running.

## How ShieldLabs surfaces it

ShieldLabs resolves each signup to a set of [identifiers](/features/identification) and grades how many accounts a device has spread across. Four layers answer four different questions:

| Layer                   | What it answers                                                                    | Where you read it                                                                        | Latency               |
| ----------------------- | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------- | --------------------- |
| **Identification**      | "Is this the same device, even after cleared cookies, incognito, or a new IP?"     | The durable `device_id` on the [webhook](/api/webhooks) / [History API](/api/server-api) | About a second        |
| **Anonymity detection** | "Is this trial start masked or anonymous right now?"                               | The `signals` array on the [webhook](/api/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](/api/webhooks) / [History API](/api/server-api)            | About a second        |
| **Patterns**            | "How many accounts has this device or local network already started a trial with?" | [Dashboard Patterns](/features/patterns) + export                                        | Background (\~10 min) |

The anchor for the rest is the durable **DeviceID** — derived server-side from stable device characteristics, not stored in the browser, so a cycler cannot reset it by clearing cookies, opening an incognito window, or switching networks. The cookie-bound **CookieID** and **VisitorID** reset on every run, so counting trials by cookie always undercounts; the DeviceID holds steady, and the distinct **UserHID** count behind it is how many trials one machine has actually started.

## Prevent free trial abuse

The rule your code applies: read the durable `device_id` and your own `user_hid` on every trial start, count the distinct `user_hid` values behind one `device_id` (and behind one `local_ip.ip` for the local-network case), and when that count crosses your trial limit, require verification or deny the trial instead of minting another free run. Fold in the session [Risk Score (0–100)](/features/risk-scoring) as weight: a clean device with a first trial passes, while a machine already cycling several accounts, or a high-band masked session, gets held. When a cycler hides behind a VPN to look like a new region, the real network IP (`local_ip`) still exposes the network behind a faked `public_ip.country`, and `detection_flags.ip_mismatch` is set to `true` so your code can act on it. ShieldLabs surfaces the count and the score; your signup handler owns the allow, verify, or deny. The steps below wire it up.

## Build it

<Steps>
  <Step title="Identify at trial start">
    Load the [snippet](/setup/snippet) on the page where the trial begins — the signup form or "start free trial" button — and re-identify on the action itself so you score the session actually starting the trial. Pass the account's hashed id (**UserHID**), never a raw email.

    ```html start-trial.html theme={null}
    <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', (ip, requestID) => {
        document.getElementById('shield-request-id').value = requestID;
      });
    </script>

    <form method="POST" action="/api/start-trial">
      <input type="hidden" id="shield-request-id" name="shieldRequestId" />
      <button type="submit">Start free trial</button>
    </form>
    ```
  </Step>

  <Step title="Read the scored result on your server">
    The score arrives on the [webhook](/api/webhooks). Verify the signature, cache it by `request_id`, and read it back with the shared [`waitForScore` helper](/use-case), or fall back to a [History API](/api/server-api) read by `request_id`. The fields your trial gate reads:

    ```json theme={null}
    {
      "request_id": "13f84f05-7c2a-4e9b-9f1d-2a6b8c0e4d11",
      "device_id": "5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f",
      "visitor_id": "161dfbad-8e7f-4a6b-9c5d-0e1f2a3b4c5d",
      "user_hid": "8a9f-hashed-account-id",
      "public_ip": { "ip": "203.0.113.10", "country": "US" },
      "local_ip": { "ip": "203.0.113.10", "country": "US" },
      "risk_score": 30,
            "signals": [
        { "name": "Proxy", "weight": 10 },
        { "name": "Datacenter IP", "weight": 10 },
        { "name": "Abuser Flag", "weight": 10 }
      ],
      "detection_flags": { "proxy": true, "datacenter_ip": true, "abuser": true, "ip_mismatch": false },
      "observed_at": "2026-06-16T18:00:45Z"
    }
    ```

    The weights in `signals` always add up to `score`; the [webhooks reference](/api/webhooks) has the full schema and every [`detection_flags`](/glossary#detection-flags) boolean.
  </Step>

  <Step title="Count the trials behind the device and decide">
    Read the session `score` for anonymity, then link related accounts by the durable `device_id` to count how many trials that machine has already started. A high score alone can be an honest prospect on a VPN; a high score **and** several accounts behind one device is the cycling shape worth gating.

    ```js api/start-trial.js theme={null}
    app.post('/api/start-trial', async (req, res) => {
      const { accountId, shieldRequestId } = req.body;

      // 1. Your normal eligibility checks first (not already trialed by this
      //    account, within campaign window, terms accepted).
      if (!(await trialIsAvailable(accountId))) {
        return res.status(409).json({ error: 'trial_not_available' });
      }

      // 2. Read the ShieldLabs result for this session (snake_case fields).
      const shield   = await waitForScore(shieldRequestId, 2000);
      const score    = shield?.score ?? 0;        // 0-100, default to 0 if not yet in
      const deviceId = shield?.device_id;

      // The all-zero DeviceID is "no device" (JS disabled or blocked), not a
      // new one. Route it to review and skip the device-count link.
      const NIL = '00000000-0000-0000-0000-000000000000';
      const usableDevice = deviceId && deviceId !== NIL;

      // 3. Count distinct accounts behind the durable DeviceID. A live History
      //    read by device_id reconstructs every account this machine touched.
      const trialsOnDevice = usableDevice
        ? await accountsBehindDevice(deviceId)
        : 0;

      // 4. YOUR code owns the verdict. Branch on the band and the count, never
      //    on a signal label string.
      if (trialsOnDevice >= YOUR_TRIAL_LIMIT) {
        return res.json({ requireVerification: true, reason: 'trials_on_device' });
      }
      if (score >= 60) {
        return res.json({ requireVerification: true, reason: 'anonymity' });
      }

      // Clean / Low / Medium and a fresh device: grant the trial.
      return grantTrial(req, res);
    });

    // Distinct hashed accounts seen on one machine, from a live History read
    // against your own store of what you have already counted.
    async function accountsBehindDevice(deviceId) {
      const rows = await shieldHistory('device_id', deviceId, 100);
      return new Set(rows.map((r) => r.user_hid).filter(Boolean)).size;
    }
    ```

    ```bash Read a device's trial history theme={null}
    curl "https://account.shieldlabs.ai/api/v1/history/device_id/5eb7fd5c-2a1b-4c3d-9e8f-7a6b5c4d3e2f?limit=100" \
      -H "Authorization: Bearer sec_your_private_api_key"
    ```

    For the local-network shape, run the same distinct-`user_hid` count keyed on `local_ip.ip` instead of `device_id`.
  </Step>

  <Step title="See the spread over time">
    The per-session check catches a trial right now. The standing view is [Patterns](/features/patterns), graded server-side over a rolling window and exported as CSV or JSON. Two map directly to trial cycling.

    <AccordionGroup>
      <Accordion title="Many Accounts on One Device" icon="mobile-screen">
        One device linked to many distinct accounts: the core trial-cycling shape. The grouping identity is the durable DeviceID, so a fresh email and a private window do not reset it. It grades **Suspicious**, then **Dangerous**, as the account count climbs.
      </Accordion>

      <Accordion title="Many Accounts on One Local IP" icon="network-wired">
        Many accounts starting trials through the same local network address, even when each session uses a fresh cookie and a different public IP. This catches a person who spreads across several separate browsers but still sits behind one router or NAT.
      </Accordion>
    </AccordionGroup>

    <Note>
      History reads through `account.shieldlabs.ai` are free; the Management History path bills 1 request per returned row (an empty result still bills 1). For high-volume signup flows, use the [pattern export](/features/patterns) as a fast denylist of cycling DeviceIDs and Local IPs, and reserve live reads for the borderline trials that are expensive to give away by mistake.
    </Note>
  </Step>

  <Step title="Tune to your product">
    Start in logging-only mode, watch how your real signups distribute, then set the device and local-IP limits that match your trial terms before you raise friction. A real prospect on a corporate VPN can fire the same anonymity signals, so weigh the count, the score, and your own context together.
  </Step>
</Steps>

## Test it

You do not need a real farm to see this work. Start a trial once in your normal browser and note the `device_id` on the webhook. Then play the cycler: clear cookies, open a private window, or switch to a second browser profile, and start a trial again as a different account. The `cookie_id` and `visitor_id` change every time, but the **same `device_id` returns**, and the distinct-`user_hid` count off that device climbs with each run — exactly the count your gate reads. Toggling a VPN adds the matching anonymity signals to the score without changing the durable DeviceID.

## Next

<CardGroup cols={2}>
  <Card title="Promo Abuse" icon="ticket" href="/use-case/promo-abuse">
    The reward-time sibling: the same device-count logic applied to coupons, signup bonuses, and referral credit.
  </Card>

  <Card title="New Account Fraud" icon="user-plus" href="/use-case/new-account-fraud">
    Join accounts to the DeviceID at registration so a farm is thinned before it ever reaches a trial.
  </Card>

  <Card title="Multi-Accounting" icon="users" href="/use-case/multi-accounting">
    The general shape behind trial cycling: one person, many accounts, one machine.
  </Card>

  <Card title="Risk Scoring" icon="gauge" href="/features/risk-scoring">
    How the 0-100 explainable score and the Clean / Low / Medium / High bands work.
  </Card>
</CardGroup>
