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.
ShieldLabs gives you a counter the reader cannot reset that way. The DeviceID is derived on the server from the device environment, not stored in the browser, so it holds through cleared cookies, a private window, and a rotated IP. Count metered views per DeviceID and the meter survives every reset trick that defeats a cookie. ShieldLabs returns the identity; your own code owns the paywall.
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; what the limit is and what the wall says are yours.
Why the cookie meter resets and the DeviceID meter does not
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. That is exactly why you do not meter on a cookie or on the VisitorID. The DeviceID is the durable, browser-bound handle, and metering on it is what closes the reset loophole.
Count metered views per DeviceID
Load the snippet on every article page and run an anonymous identify. The DeviceID arrives on your webhook; you increment a per-DeviceID counter and compare it against your free limit before rendering the article.
<!-- 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) => {
// Hand the requestID to your backend so it can look up the DeviceID
// for this view and decide whether to meter or wall it.
document.getElementById('shield-request-id').value = requestID;
});
</script>
Your backend reads the scored result for that RequestID from the shared webhook cache, then bumps the counter for the DeviceID it carries. This reuses the canonical waitForScore and scoreCache helper from the cookbook overview; it is not redefined here.
const FREE_LIMIT = 5; // your free articles per device per period
// uuid.Nil: a blocked or JS-disabled view returns the all-zero DeviceID
// and scores 90. You cannot meter an identity that was never built.
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.
const shield = await waitForScore(shieldRequestId, 2000);
const deviceId = shield?.deviceID;
// 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 the limits below.
if (!deviceId || deviceId === ZERO_DEVICE) {
return res.json(meterByCookieOrIp(req, articleId));
}
// Per-DeviceID counter in your own store. Clearing cookies or opening
// incognito does not change the DeviceID, so this count is not resettable
// that way.
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 });
});
The counter lives in your datastore keyed by DeviceID. Because clearing cookies and opening incognito both keep the same DeviceID, the reader cannot zero the count by resetting their browser storage. A cookie meter would have started fresh on either action.
Decrement your domain balance is automatic: each checkAnonymous call is one request. Metering reads the DeviceID straight off the webhook, which is free, so a busy article page does not also rack up History API charges. Reserve History reads for the occasional audit of one reader’s view history.
Handle the cases the DeviceID meter cannot cover
The DeviceID closes the cookie-reset loophole, but it is not a universal counter. Two cases need an explicit fallback, and pretending otherwise leaks free views.
A view that blocks the snippet or runs with JavaScript disabled comes back with the all-zero DeviceID (00000000-0000-0000-0000-000000000000) and scores 90, because no stable device characteristics were collected. You cannot meter an identity that was never built. Do not serve those views free indefinitely: fall back to a cookie or IP counter for the all-zero case, and treat a run of all-zero views from one IP as someone reading with scripts off.
The other honest limit is by design. The DeviceID is browser-bound: the same reader on Chrome and on Firefox is two DeviceIDs, and a brand-new device is a new DeviceID. So a determined reader can still earn fresh free views by switching browsers or machines. That is a far higher bar than clearing cookies, and it is the boundary of what device-derived metering can do. If you need to raise the bar 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.
| 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 blocked | All-zero DeviceID, fall back to cookie or IP |
Putting it together
Identify on every article view
Load the snippet on metered pages and call checkAnonymous. Stash the requestID so your backend can resolve the DeviceID for the view. Count per DeviceID, not per cookie
On each webhook, read the DeviceID and increment that device’s view count in your own store. This is the count a reader cannot reset by clearing cookies or going incognito.
Wall at your free limit
When a device passes your free limit, return the paywall instead of the article. The limit and the wall copy are yours.
Fall back for the unmeterable
Route an all-zero DeviceID to a cookie or IP counter so a scripts-off reader is not waved through. Add a per-IP cap if you want to slow browser-switching.
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 guessing.
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. The Billing page covers the per-request model, including why webhook-driven metering is free and History reads bill per row. If you also want to grade where your readers come from, Measure Traffic Quality scores each acquisition source by its anonymous-traffic share.