Skip to main content
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 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 and grades how many accounts a device has spread across. Four layers answer four different questions:
LayerWhat it answersWhere you read itLatency
Identification”Is this the same device, even after cleared cookies, incognito, or a new IP?”The durable device_id on the webhook / History APIAbout a second
Anonymity detection”Is this trial start masked or anonymous right now?”The signals array on the webhook / History APIAbout a second
Risk Score”How risky is the visit overall, as one 0-100 number?”risk_score on the webhook / History APIAbout a second
Patterns”How many accounts has this device or local network already started a trial with?”Dashboard Patterns + exportBackground (~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) 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

1

Identify at trial start

Load the 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.
start-trial.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', (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>
2

Read the scored result on your server

The score arrives on the webhook. Verify the signature, cache it by request_id, and read it back with the shared waitForScore helper, or fall back to a History API read by request_id. The fields your trial gate reads:
{
  "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 has the full schema and every detection_flags boolean.
3

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.
api/start-trial.js
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;
}
Read a device's trial history
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.
4

See the spread over time

The per-session check catches a trial right now. The standing view is Patterns, graded server-side over a rolling window and exported as CSV or JSON. Two map directly to trial cycling.
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.
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.
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 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.
5

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.

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

Promo Abuse

The reward-time sibling: the same device-count logic applied to coupons, signup bonuses, and referral credit.

New Account Fraud

Join accounts to the DeviceID at registration so a farm is thinned before it ever reaches a trial.

Multi-Accounting

The general shape behind trial cycling: one person, many accounts, one machine.

Risk Scoring

How the 0-100 explainable score and the Clean / Low / Medium / High bands work.