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.
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.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
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_claimstable. 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
/v1/fingerprints/:visitorId:
Scoring patterns
A singleisLikelyRepeatClaim → 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.
When the visitor ID is missing
Foil resolves the visitor fingerprint before it issues a token, so a token from the browser SDK carriesvisitor_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.