AI chat tools, image generators, PDF converters and résumé checkers all face the same product decision: let people try it before they sign up. Three free generations without an account converts better than a signup wall. It also costs real money per use, and the first thing an enthusiastic guest learns is that a private window gives them three more.
This guide covers metering anonymous usage by device: what to key the counter on, how to keep the browser from forging it, and how to hand the count over at signup so registering is not a reset button.
Why the usual keys fail
| Key | What breaks it |
|---|---|
| Cookie / localStorage | Private window, clearing site data, another browser |
| IP address | VPNs and proxies (too loose); offices, campuses and mobile carrier NAT (too strict) |
| Browser fingerprint hash computed client-side | The client computes it, so the client can change it |
The last row is the subtle one. If your JavaScript computes an identifier and posts it to your API, a guest can post any value they like. Anything the browser sends you is a claim.
Key the quota on a server-resolved visitorId
Prynt gives each browser a stable visitorId that persists across cleared cookies and incognito. The important part is where you read it: not from the browser, but from your server, by fetching the event with your secret key. The browser only ever sends a requestId, which is useless without the secret key.
The flow:
- The page identifies once and sends the
requestIdto your server. - Your server fetches
GET /v1/events/{requestId}, readsvisitorId, and stores it in your server-side session. - Each guest request is metered against
guest_usage[visitorId].
// browser: once per page session
const prynt = await Prynt.load({ apiKey: 'pk_live_…', extendedResult: true }); // Smart Signals + decision
const { requestId } = await prynt.identify({ tag: { action: 'guest_session' } });
await fetch('/api/guest/session', { method: 'POST', body: JSON.stringify({ requestId }) });
// server (Express): resolve the device once, then meter per request
import { PryntServer } from '@prynt/node';
const prynt = new PryntServer({ secretKey: process.env.PRYNT_SECRET_KEY });
const FREE_GUEST_USES = 3;
app.post('/api/guest/session', async (req, res) => {
let event;
try {
event = await prynt.getEvent(req.body.requestId);
} catch {
// unknown id (e.g. a GPC opt-out, which stores no event) or Prynt unreachable
return res.status(401).json({ error: 'signup_required' });
}
const ageMs = Date.now() - Date.parse(event.createdAt);
if (ageMs > 10 * 60 * 1000) return res.status(400).json({ error: 'stale' });
req.session.guestDevice = event.visitorId;
req.session.guestRisk = event.decision;
res.json({ remaining: FREE_GUEST_USES - (await usage.get(event.visitorId)) });
});
app.post('/api/generate', async (req, res) => {
if (!req.user) {
const device = req.session.guestDevice;
if (!device) return res.status(401).json({ error: 'identify_first' });
if (req.session.guestRisk === 'block') return res.status(403).json({ error: 'signup_required' });
const used = await usage.incr(device); // e.g. Redis INCR with a TTL
if (used > FREE_GUEST_USES) return res.status(402).json({ error: 'signup_required' });
}
// … run the model
});
Two details in that code matter:
- The freshness check. A
requestIddoes not expire and is not single-use, so a guest could replay one captured on someone else’s device to borrow their identity. CheckingcreatedAtagainst the clock limits that window to a few minutes. Your own session store provides the rest. - One identify per session, not per request. Each identify counts against your monthly quota. Resolve the device once, store it server-side, and meter against the stored value. When the guest clears cookies, they lose the session, identify again, and get the same
visitorIdback, with the same count.
What survives, and what does not
A device-keyed quota holds through the cheap resets: private windows, cleared cookies, closing and reopening the browser, and IP changes. It is not unbreakable. A different physical device or a fresh virtual machine is a different device. That is fine: the goal is not to make a fourth free use impossible, but to make it cost more than signing up. Most people will choose the signup.
For the motivated minority, the event carries more than the id. smartSignals.incognito, vpn, datacenter and bot let you tighten the quota for sessions that look like they are working at it. A guest on a datacenter IP with automation tells does not need three free generations; zero is fine.
const ss = event.smartSignals || {};
const quota = (ss.bot?.result || ss.datacenter?.result) ? 0
: ss.incognito?.result ? 1
: FREE_GUEST_USES;
Bot detection is on every plan; incognito, VPN and datacenter signals are in Pro. See rate limiting by device for the same idea applied to per-minute limits instead of lifetime quotas.
Carry the count into the account
The most common leak in guest metering is the signup itself. A guest uses three free generations, signs up, and receives the full new-account allowance on top. Then they sign up again with another email. Now your guest limit and your freemium credit limit are stacking.
Close it at signup:
- The signup form sends a fresh
requestId. - The server fetches the event, and reads both
visitorIdandaccountsOnDevice. - If the device already has an account, apply your signup policy (block, verify, or create without free credits).
- Otherwise create the account, set its starting usage to the guest count recorded for that
visitorId, and link it withPUT /v1/events/{requestId}.
const event = await prynt.getEvent(requestId);
const guestUsed = await usage.get(event.visitorId);
const user = await createUser({ email, freeUsesConsumed: guestUsed });
await prynt.updateEvent(requestId, { linkedId: user.id });
Whether guest uses count against the account allowance is a product choice. Some teams deduct them; others grant the account allowance once per device and nothing for a second account. The important part is that the device decides, not the email.
Copy that does not feel hostile
Guests hitting the limit are your best prospects, so treat the wall as a prompt, not an accusation. “You’ve used your free tries on this device. Create a free account to keep going” is accurate and polite. Avoid mentioning fingerprints, private windows or detection; it only invites the people you want to deter to experiment.
Privacy considerations
Metering anonymous visitors is a legitimate interest for many products, but check it against your own legal basis. The agent honors Global Privacy Control by default, and a consent mode is available if you need to gate identification on a banner. Decide in advance what a guest without a resolvable device gets: a single free use, or a signup prompt. The privacy page and device fingerprinting vs cookies cover the trade-offs.
Ship it in an afternoon
A device-keyed guest quota is a session endpoint, a counter and a signup hook. Start with the free plan on the pricing page, meter three uses per device, and watch how many guests hit the wall versus how many convert. Those two numbers tell you whether three is the right number.
Try it free
Prynt is device intelligence with a free tier — visitor IDs, bot & fraud Smart Signals, and behavioral biometrics, powered by a cross-site network. Start free.