Cookie and email trial gates fail the moment a user clears storage or opens a new browser. This is the end-to-end architecture for a device-bound gate: where the fingerprint is captured, what you store, and how the lookup-and-decide step runs on each new signup.
The job
You offer a free trial. A share of users exhaust it, then sign up again to start over: clear cookies, open a different browser, or switch to incognito. A trial gate keyed to storage or email loses them at the first step, because the user controls both.
The durable key is the device itself. This page covers the full architecture for a device-bound trial gate, end to end: where you capture the signal, what you persist, and how the match-and-decide step runs on every new signup. For the narrative walkthrough of why each storage-based defense fails, see the companion how-to linked at the foot of the page.
What makes a device key hold
`getFingerprint` returns two hashes. The full `fingerprint` is per-browser: Chrome and Firefox on the same machine produce different values because their rendering and audio pipelines differ. The `hardwareFingerprint` is derived only from hardware-bound signals, so it is the same value across browsers on one physical device, and it survives cookie clears and incognito mode.
For a trial gate, the `hardwareFingerprint` is the key you store and match on. It is the thing an abuser cannot change without changing machines.
The end-to-end flow
| Step | What happens |
|---|---|
| 1. Capture | At signup, call getFingerprint() client-side. You get fingerprint, hardwareFingerprint, and consistency. No server involved yet. |
| 2. Send | POST the two hashes plus the consistency result to your backend over your existing API. Decide server-side: never trust a verdict computed in the browser. |
| 3. Store | Persist a record keyed by hardwareFingerprint: { hardwareFingerprint, fingerprint, accountId, firstSeen, lastSeen }. One device can map to many accounts. You own this store; the library is client-only. |
| 4. Look up | On each new signup, re-capture and query your store for the incoming hardwareFingerprint. For tolerance to minor hardware drift, run compareFingerprints in cross-browser mode against the stored record for a matchScore. |
| 5. Decide | If the device already has a trial record, the device is not eligible. Layer in consistency.spoofLikelihood to catch anti-detect browsers generating fresh identities. |
| 6. Act and log | Raise friction, deny, or allow. Log which signals contributed to the match for support review, and update lastSeen. |
The decision rule
The eligibility rule for trial abuse is the simplest of the device-identity jobs: a device gets one trial. Express it as a count so you can tune strictness without re-architecting.
Trial eligibility by device
| Condition | Outcome |
|---|---|
| No record for this hardwareFingerprint | Eligible: start the trial, write a record. |
| Record exists, low spoofLikelihood | Not eligible: device already used its trial. Raise friction or deny. |
| Record exists, high spoofLikelihood | Treat as evasion: challenge before allowing any signup. |
| No exact record, but a high matchScore in cross-browser mode | Probable same device: route to manual review or step-up verification. |
npm install doorman-bennyimport { getFingerprint } from 'doorman-benny';
// STEP 1-2: capture in the browser, send hashes to your backend.
async function submitSignup(email: string): Promise<void> {
const result = await getFingerprint();
await fetch('/api/signup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email,
hardwareFingerprint: result.hardwareFingerprint,
fingerprint: result.fingerprint,
spoofLikelihood: result.consistency.spoofLikelihood,
}),
});
}Client side. Send the hashes and the spoof-likelihood label; do the eligibility decision on the server.
// STEP 3-6: store, look up, and decide on the server.
// `db` is your own datastore, indexed by hardwareFingerprint.
async function evaluateTrial(payload: {
email: string;
hardwareFingerprint: string;
fingerprint: string;
spoofLikelihood: 'low' | 'medium' | 'high';
}): Promise<{ eligible: boolean; reason: string }> {
const existing = await db.devices.findByHardwareId(payload.hardwareFingerprint);
if (payload.spoofLikelihood === 'high') {
return { eligible: false, reason: 'challenge-required' };
}
if (existing) {
return { eligible: false, reason: 'device-already-trialed' };
}
await db.devices.insert({
hardwareFingerprint: payload.hardwareFingerprint,
fingerprint: payload.fingerprint,
accountId: payload.email,
firstSeen: new Date(),
lastSeen: new Date(),
});
return { eligible: true, reason: 'new-device' };
}Server side. The store is keyed by hardwareFingerprint; the decision combines an existing-record check with the spoof-likelihood label.
Which signals and which mode
The trial gate keys on the hardware-bound subset of signals, which is what makes the match survive a browser switch. You do not need to enumerate or weight them yourself: the `hardwareFingerprint` already bundles that subset, and `compareFingerprints` in `cross-browser` mode compares only that subset when you want a graded `matchScore` instead of a raw equality check.
Use exact `hardwareFingerprint` equality for the fast path. Fall back to `cross-browser` mode comparison when you want to tolerate minor hardware drift (an external monitor, a USB hub, an OS update) without missing a returning device.
Handling false positives
No device identity is perfect. A hardware-bound key can shift on legitimate events: an OS reinstall, a hardware upgrade, or a major OS update that changes reported platform values. Two practices keep this manageable.
First, treat a match as a risk signal, not an automatic block. On a match, raise friction (require email verification or a brief manual review) rather than silently refusing the signup. Second, when you have a stored prior fingerprint, use the graded `matchScore` from `cross-browser` mode rather than raw equality, so a device that drifted slightly still resolves to the same person.
Legitimate incognito use is not fraud. The incognito likelihood on the result is context for support staff, not an input to the trial gate on its own.
Frequently asked questions
Where do I store the fingerprints?
In your own datastore, indexed by hardwareFingerprint. The library is client-only and keeps no server state, so a device record is a row you write: the hardware hash, the full fingerprint, the account it belongs to, and first and last seen timestamps. A standard relational or key-value store is enough.
Does clearing cookies or switching browsers reset the trial?
No. The hardwareFingerprint is derived from signals the browser cannot clear and that are the same across browsers on one device. Clearing cookies or opening a different browser does not change it, so the device still matches its existing trial record.
What stops an abuser from using incognito mode?
Incognito blocks cookies and storage writes, but the hardware-bound signals are still available and still produce the same hash. The hardwareFingerprint matches between a normal and an incognito session on the same device, so an incognito signup resolves to the existing trial record.
How is this different from the blog post on stopping trial abuse?
The blog post is the narrative how-to: why cookies, IP blocks, and single-browser fingerprints each fail. This page is the architecture: the capture-store-match-decide flow, what you persist, and the server-side decision logic. Read the blog post for the why, this page for the wiring.
Is this compliant with GDPR or ePrivacy?
Fingerprinting for fraud prevention generally qualifies for a security and fraud-prevention exemption under GDPR and most ePrivacy frameworks, provided the processing is proportionate and disclosed in a privacy notice. The laws/fingerprinting-anti-fraud-exemption page covers the landscape. This is engineering documentation, not legal advice.
Get started
Build the trial gate
npm install doorman-benny. Hardware-bound device identity, cross-browser matching, and free anti-spoof scoring in one package. No cookies, no server round-trip for the fingerprint.
Last reviewed June 20, 2026

