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

# Paywall Bypass

> Learn how to detect and prevent paywall bypass: meter free views on a DeviceID readers cannot reset by clearing cookies or going incognito.

A metered paywall lets a reader see a few free articles, then asks them to subscribe. The classic dodge is to reset the count: clear cookies, open an incognito window, and the meter starts over from zero. That works because most meters key off a first-party cookie, and a cookie is the one thing the reader controls. This pattern meters on the DeviceID instead, an identifier the reader cannot reset that way.

## What is paywall bypass?

Paywall bypass is when a reader gets past a metered or subscription wall without paying — most often by resetting the free-view count. They clear cookies, open a private window, or rotate their IP so the meter forgets them and starts over. Each reset mints a fresh `CookieID` and therefore a fresh `VisitorID`, which is exactly why a cookie-keyed meter forgets the reader.

## How ShieldLabs surfaces it

ShieldLabs derives a durable, server-derived **DeviceID** for each view that holds through those resets, so you can count metered views per device on an identifier the reader cannot wipe. Each view also carries a `RequestID`, the join key your snippet hands to your backend to look up that view's DeviceID before it serves or walls the article.

A cookie meter and a DeviceID meter behave identically until the reader tries to game them. Then they diverge:

| Reader action                   | Cookie / VisitorID meter | Per-DeviceID meter                         |
| ------------------------------- | ------------------------ | ------------------------------------------ |
| Reads another article           | Count goes up            | Count goes up                              |
| Clears cookies                  | Count resets to 0        | Same DeviceID, count holds                 |
| Opens an incognito window       | Count resets to 0        | Same DeviceID, count holds                 |
| Switches networks or uses a VPN | Often resets to 0        | Same DeviceID, count holds                 |
| Switches to a different browser | New count                | New DeviceID, new count (see limits below) |

The `CookieID` is minted in the browser and lost the moment cookies are cleared; the `VisitorID` is built partly from that cookie, so it resets too. The DeviceID is the durable, browser-bound handle, and metering on it closes the reset loophole. An IP is no steadier: a VPN, proxy, or mobile carrier hands the same reader a fresh address on demand. ShieldLabs returns the identity; your code owns the wall.

<Note>
  This is a metering policy, not a fraud verdict. A reader hitting the wall after their free views is doing nothing wrong, they have simply read what the plan allows. The DeviceID lets you count honestly across cookie resets; the limit and what the wall says are yours.
</Note>

## Stop paywall bypass

Read the durable `DeviceID` off the webhook for every article view. The rule your code applies: increment a per-`DeviceID` counter, compare it against your free limit, and show the wall once a device passes the limit. The outcome is a meter the reader cannot reset by clearing cookies, opening incognito, or rotating their IP, because all three keep the same `DeviceID`.

## 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="Identify on every article view">
    Load the [snippet](/setup/snippet) on every metered article page and run an anonymous identify. Stash the `requestID` so your backend can resolve the DeviceID for the view.

    ```html article.html theme={null}
    <!-- Every metered article page -->
    <script type="module">
      const mod = await import(
        'https://cdn.shieldlabs.ai/snippet.js?publicKey=YOUR_PUBLIC_KEY'
      );
      // Anonymous read. The callback gives you the IP and the requestID.
      // The DeviceID and Score are not returned here, they arrive by webhook.
      mod.checkAnonymous(undefined, (ip, requestID) => {
        document.getElementById('shield-request-id').value = requestID;
      });
    </script>
    ```
  </Step>

  <Step title="Count per DeviceID and wall at your free limit">
    Your backend reads the scored result for that `RequestID` from the shared [`waitForScore` helper](/use-case), then bumps the counter for the DeviceID it carries and compares it against your free limit. Because clearing cookies and opening incognito both keep the same DeviceID, the reader cannot zero the count by resetting browser storage.

    ```js api/meter.js theme={null}
    const FREE_LIMIT = 5; // your free articles per device per period
    const ZERO_DEVICE = '00000000-0000-0000-0000-000000000000';

    app.post('/api/meter', async (req, res) => {
      const { articleId, shieldRequestId } = req.body;

      // Pull the ShieldLabs result for this view from the shared cache.
      // waitForScore returns the raw snake_case payload (device_id,
      // public_ip, detection_flags, risk_score, signals, ...).
      const shield = await waitForScore(shieldRequestId, 2000);
      const deviceId = shield?.device_id;

      // No DeviceID, or the all-zero one: fall back to a cookie or IP meter
      // for this view rather than serving it free forever (see next step).
      if (!deviceId || deviceId === ZERO_DEVICE) {
        return res.json(meterByCookieOrIp(req, articleId));
      }

      // Per-DeviceID counter in your own store.
      const views = await incrementDeviceViews(deviceId, articleId);

      if (views > FREE_LIMIT) {
        return res.json({ action: 'paywall', reason: 'free_limit_reached', views });
      }
      return res.json({ action: 'allow', reason: 'within_free_limit', views });
    });
    ```

    Metering reads the DeviceID straight off the webhook, which is free; each `checkAnonymous` call is the one request that decrements your domain balance. If a webhook is dropped (delivery is at-most-once, no retries), `waitForScore` falls back to a free [History API](/api/server-api) read by `request_id`, so a dropped webhook does not leak a free view.
  </Step>

  <Step title="Fall back for views the DeviceID meter cannot cover">
    Two cases have no usable DeviceID, and pretending otherwise leaks free views:

    * **Scripts off** — the snippet is present but JavaScript is unavailable. The view comes back with the all-zero `device_id` and scores **90 or higher**, and `detection_flags.javascript_disabled` is `true` with the `JavaScript Disabled` signal. Use that flag, not just the all-zero DeviceID, to route the view to your cookie or IP counter.
    * **Snippet never loads** — no snapshot is posted, so no webhook arrives and `waitForScore` returns null.

    For the per-IP fallback, prefer the real network IP (`local_ip.ip`) over the public `public_ip.ip` when present. A VPN hands out a fresh public IP on demand — exactly the reset a per-IP cap is meant to slow — but the real network IP often reveals the underlying network behind it. When `detection_flags.ip_mismatch` is `true` the public IP is masked, so an `public_ip` cap alone is weak there.

    The other limit is by design: the DeviceID is **browser-bound**, so the same reader on Chrome and on Firefox is two DeviceIDs, and a determined reader can earn fresh free views by switching browsers or machines. That is a far higher bar than clearing cookies. To raise it further, pair the per-DeviceID counter with a per-IP cap and a soft cookie meter, and require a free account for continued access once any of them trips.
  </Step>

  <Step title="Tune to your product">
    Start by logging the per-DeviceID distribution of your real readers before you enforce, so your free limit matches how people actually read rather than a guess.
  </Step>
</Steps>

To catch readers who slip the per-device meter by rotating browsers or devices, watch the dashboard [Patterns](/features/patterns) view: **Many Devices on One Visitor** and **Changing IDs on One Account** surface that rotation across history, and heavy cookie-resetting still maps to **Many Accounts on One Device**. Export the flagged IDs and apply your own policy — Patterns are export-only, there is no rules engine.

| Reset attempt                   | Per-DeviceID meter result                                |
| ------------------------------- | -------------------------------------------------------- |
| Clear cookies                   | Blocked, count holds                                     |
| Incognito or private window     | Blocked, count holds                                     |
| New network or VPN              | Blocked, count holds                                     |
| Different browser, same machine | New count (browser-bound DeviceID)                       |
| Different device                | New count (new DeviceID)                                 |
| Scripts off (snippet present)   | All-zero DeviceID, scores 90+, fall back to cookie or IP |
| Snippet never loads             | No webhook arrives, fall back to cookie or IP            |

## Test it

Confirm the meter holds before you enforce. Read past your free limit in a normal window, then try the resets a reader would try:

<Steps>
  <Step title="Clear cookies and reload">
    Read enough articles to trip the wall, clear cookies, and reload. The `device_id` in the webhook is the same one, so your per-device count keeps climbing instead of resetting to zero.
  </Step>

  <Step title="Open the article in incognito">
    Open the same page in a private window. The `cookie_id` and `visitor_id` change, but the `device_id` returns identical — proof your meter keys off the durable handle.
  </Step>

  <Step title="Switch to a second browser">
    Open the article in a different browser on the same machine. Here the `device_id` *does* change, because it is browser-bound. That is the honest limit, and the per-IP cap is what slows it.
  </Step>
</Steps>

<Note>
  Do not test with an automated client or a scripts-off fetch — those return the all-zero `device_id` and exercise the cookie/IP fallback, not the device meter. Test as a real reader resetting their own browser.
</Note>

## Recommended starting policy

A guide, not a rule. The right limit depends on your content and your conversion goals.

| Situation                                 | Suggested action                                                  |
| ----------------------------------------- | ----------------------------------------------------------------- |
| Device under the free limit               | Serve the article, increment the count                            |
| Device at or over the free limit          | Show the paywall                                                  |
| All-zero DeviceID (scripts off / blocked) | Meter by cookie or IP for this view, do not serve free forever    |
| Many all-zero views from one IP           | Treat as one reader with scripts off, apply the IP cap            |
| Same reader across two browsers           | Accept as the browser-bound limit, or add a per-IP cap to slow it |

## Where to go next

To understand why the DeviceID holds through cookie clears and incognito while the VisitorID does not, read [Identification](/features/identification), and the [anonymity signals](/features/anonymity-signals) reference covers the masking flags that travel alongside it. The exact `device_id` field and the rest of the scored body live in the [webhook payload](/api/webhooks) your meter reads. The [Billing](/billing) page covers the per-request model.

The same durable DeviceID powers neighboring playbooks: [Ban Enforcement](/use-case/ban-evasion) keys a banlist on the device a returning reader cannot wipe, and [Returning Visitor](/use-case/returning-visitor) recognizes the device across sessions. To grade where your readers come from, [Measure Traffic Quality](/use-case/traffic-quality) scores each acquisition source by its anonymous-traffic share.
