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.
Promo abuse defense depends on the stable visitor_fingerprint.id. You don’t need to call waitForFingerprint() to get it, because getSession() waits for fingerprinting to finish before it returns a handoff, and Foil doesn’t score a session without a completed fingerprint. If the script fails to load or getSession() fails, the page sends the claim without a handoff, and the server rejects it as shown below.

Server verification

Node.js
The server SDKs don’t check a token’s age. Reject a token whose decision.evaluated_at is more than a few minutes old, so that an attacker can’t replay a token captured from one session. See Verify the token on your server for the check in each language. Store the session_id from the verified token, as the example does, rather than the sessionId that the browser sent. 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.
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

Foil resolves the visitor fingerprint before it issues a token, so a token from the browser SDK carries visitor_fingerprint.id. The server SDKs still type the field as nullable, so the example handles a missing ID by applying your normal one-per-account check. A request that arrives without a valid token at all is a different case. It usually means that the request didn’t come from your page, so the example rejects it.

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.