Skip to main content
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 actionCookie / VisitorID meterPer-DeviceID meter
Reads another articleCount goes upCount goes up
Clears cookiesCount resets to 0Same DeviceID, count holds
Opens an incognito windowCount resets to 0Same DeviceID, count holds
Switches networks or uses a VPNOften resets to 0Same DeviceID, count holds
Switches to a different browserNew countNew 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.
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.

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

1

Create a ShieldLabs account and get your keys

Sign up for free 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 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, and each webhook endpoint has its own whsec_… signing secret. See Keys.
2

Identify on every article view

Load the snippet on every metered article page and run an anonymous identify. Stash the requestID so your backend can resolve the DeviceID for the view.
article.html
<!-- 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>
3

Count per DeviceID and wall at your free limit

Your backend reads the scored result for that RequestID from the shared waitForScore helper, 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.
api/meter.js
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 read by request_id, so a dropped webhook does not leak a free view.
4

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

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.
To catch readers who slip the per-device meter by rotating browsers or devices, watch the dashboard 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 attemptPer-DeviceID meter result
Clear cookiesBlocked, count holds
Incognito or private windowBlocked, count holds
New network or VPNBlocked, count holds
Different browser, same machineNew count (browser-bound DeviceID)
Different deviceNew count (new DeviceID)
Scripts off (snippet present)All-zero DeviceID, scores 90+, fall back to cookie or IP
Snippet never loadsNo 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:
1

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

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

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.
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.
A guide, not a rule. The right limit depends on your content and your conversion goals.
SituationSuggested action
Device under the free limitServe the article, increment the count
Device at or over the free limitShow 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 IPTreat as one reader with scripts off, apply the IP cap
Same reader across two browsersAccept 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, and the 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 your meter reads. The Billing page covers the per-request model. The same durable DeviceID powers neighboring playbooks: Ban Enforcement keys a banlist on the device a returning reader cannot wipe, and Returning Visitor recognizes the device across sessions. To grade where your readers come from, Measure Traffic Quality scores each acquisition source by its anonymous-traffic share.