Skip to main content
Promo abuse is the use case where a single session verdict is not the right question. An attacker creating a hundred accounts on one device to claim a hundred trial credits looks like a human on every individual session - because they are. The signal is cross-session: one visitor fingerprint claiming the offer too many times, too fast.

The threat

Promotional abuse covers a family of scams that share one property: the offer is valuable enough per account to justify creating many accounts. Common shapes:
  • Free-trial farming. A new account gets 30 days of access, or $X of credit, or a hardware sample. Attackers spin up accounts until the offer dries up and resell credits or access.
  • Referral bonus fraud. “Refer a friend, get $10.” Attackers self-refer by creating both sides of the transaction.
  • Coupon / promo-code abuse. A one-per-customer code is tested, shared, or stacked. The same device redeems it twenty times under twenty identities.
  • Signup-only bonuses. Platforms that hand out tokens, NFTs, credits, or airdrop allocations at account creation.
Detection: catching any one fraudulent claim is hard, because the attacker has gone to some effort to make it look real. Catching the second and subsequent claims from the same device is straightforward, because the visitor fingerprint persists across account creation, cookie clears, and incognito sessions.

The flow

1

Run Foil normally at signup and/or claim

You need a session verdict for the fraud signal, and you need the visitor fingerprint.
2

Extract visitor_fingerprint.id from the verified token

This ID is stable across sessions on the same device.
3

Look up prior activity for that fingerprint

GET /v1/fingerprints/:visitorId returns lifecycle, session count, and recent verdict history.
4

Check your own record of prior claims

Cross-reference the visitor ID against your own promo_claims table.
5

Apply the claim policy

One claim per visitor fingerprint over a window you define - often 30–90 days.

Client integration

The browser-side integration is the same as any other sensitive action - the difference is entirely on the server.
waitForFingerprint() matters here. Promo abuse defense depends on getting a stable visitor_fingerprint.id, and that ID is only assigned once Foil has frozen the fingerprint server-side. Submitting before that can return a token without a visitor ID, which defeats the whole integration.

Server verification

Node.js
The equivalent in other languages follows the same shape - see Server verification for the full language matrix.

The cross-session check

isLikelyRepeatClaim is the piece that does the work. It combines two sources:
  • Your promo_claims table. The ground truth for “this offer has already been given to this device.” Query it first - it’s cheap and decisive when positive.
  • Foil’s fingerprint record. Even if your own table says “no prior claim of this code,” the visitor fingerprint’s history can still flag the device as a serial claimer across other offers, or as part of a suspicious account constellation.
Node.js
Key fields used here, from /v1/fingerprints/:visitorId:

Scoring patterns

A single isLikelyRepeatClaim → true is a strong block signal. You can go further by composing a few facts into a score:
  • New fingerprint, many sessions in minutes. Classic “spin up ten browsers, claim ten promos” pattern.
  • Seen many times, claim never before. A long-tenured device that hasn’t claimed this offer: probably fine.
  • Seen many times, multiple claims across different offers. A device that keeps claiming offers under different accounts: very suspicious, even without a bot verdict.
  • lifecycle.expires_at in the past. The fingerprint record has aged out - treat as a new device.
Some of these only make sense if you have enough traffic that the denominator isn’t trivially small. Layer them on after the basic repeat-claim check is in place and generating useful signal.
Don’t deny promos purely on visitor-fingerprint match if you have a policy of “one per household.” Real families share devices. The fingerprint signal is great evidence but shouldn’t be the only input - combine with payment instrument, shipping address, or email domain as appropriate for your business.

When the visitor ID is missing

visitor_fingerprint is null on sessions where Foil couldn’t establish a stable ID - typically hardened privacy browsers (Firefox in resistFingerprinting mode, Brave with aggressive shields), very short sessions, or mobile webviews with storage disabled. Treat a missing visitor ID as “apply your normal one-per-account check, but don’t grant a generous promo” rather than as a block signal in itself. Most missing-ID sessions are real privacy-conscious users; some are intentional evasion. Don’t punish the first group to catch the second.

What’s next

Signup protection

Stop the account factory that feeds promo abuse.

Server verification

Reference for token verification and session readback.

Fingerprints API

Full API shape for GET /v1/fingerprints/:visitorId.

Going to production

Rollout plan and monitoring.