Guides

Prevent promo abuse, referral fraud and free trial abuse

Stop coupon, referral and free trial abuse with one redemption per device: tag each redemption, check it on your server, and enforce it with a unique index.

A promotion meant for one customer gets redeemed by one person many times: a first-order discount on ten new accounts, a referral reward for referring yourself, a free trial started again every month. Promo abuse survives limits per account or per email address, because accounts and addresses are free. A limit per device does not.

This guide enforces one redemption per device for promo codes, referral rewards and free trials with Fingerly, from the client to a database constraint.

Where to identify

MomentTagWhy
Applying a coupon or promotion codepromo:<code>The tag binds the identification to one promotion.
Claiming a referral rewardreferral:<referrer id>Compare the new customer's device with the referrer's.
Starting a free trialtrial-startOne trial per device.

Enforce one redemption per device

  1. Step 1: Identify when the visitor redeems

    import { load } from '@fingerly/web-js'
    
    // consent: 'granted' only once you have consent (or another lawful basis)
    const fingerly = await load({ apiKey: 'fly_pk_development_…', consent: 'granted' })
    
    async function submit() {
      let requestId: string | undefined
      try {
        ({ requestId } = await fingerly.identify({ tag: 'promo:' + code }))
      } catch {
        // Carry on: your server treats a missing request ID as missing evidence.
      }
      await fetch('/api/promotions/redeem', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ code, requestId }) })
    }
    
  2. Step 2: Let the database enforce the limit

    Store the visitor ID with every redemption, and put a unique index on the promotion and the visitor ID. Two redemptions racing each other cannot both succeed, because the database refuses the second.

    SQL
    CREATE TABLE redemptions (
      code        text   NOT NULL,
      visitor_id  text   NOT NULL,
      account_id  bigint NOT NULL,
      PRIMARY KEY (code, visitor_id)
    );
    
  3. Step 3: Read the event and apply the promotion

    The server code below uses two helpers from the use-case recipes: verifiedEvent(requestId, tag), which reads the stored event with a secret key and returns it only when it belongs to the action, is recent and has not been used before, and fired(event, ...groups), which says whether a signal group fired.

    Node.js
    import { fired, verifiedEvent } from './fingerly.server'
    
    export async function redeem(accountId: string, code: string, requestId: unknown) {
      const event = await verifiedEvent(requestId, 'promo:' + code)
    
      // A promotion is optional: without evidence, ask for more before applying it.
      if (!event || fired(event, 'fingerprint_suppressed')) return 'verify'
      if (event.suspect_level === 'high') return 'refuse'
    
      const inserted = await redemptions.insertIfAbsent({ code, visitorId: event.visitor_id, accountId })
      return inserted ? 'apply' : 'already-redeemed'
    }
    

    redemptions.insertIfAbsent is your own query: an insert that reports whether the row was new. Without a usable identification, the device cannot be counted, so ask for verification before applying the offer.

Stop referral fraud

A referral reward is abused when the referrer and the new customer are the same person. Identify when the reward is claimed, with the referrer in the tag, and compare the claiming device with the devices the referrer has used.

Node.js
export async function isSelfReferral(referrerId: string, requestId: unknown) {
  const event = await verifiedEvent(requestId, 'referral:' + referrerId)
  if (!event) return true // no evidence, no reward
  return accounts.hasUsedDevice(referrerId, event.visitor_id)
}

Create the account either way, and pay no reward when it returns true. For the same reason, store the visitor ID with every account at sign-up, as in detect multiple accounts.

Stop free trial abuse

Use the same tag and table with a trial instead of a code: identify with trial-start, store the visitor ID with the trial, and allow one trial per device. A device that starts a second trial is told it has been used, or is offered the paid plan.

A starting policy

SituationAction
The device already redeemed this promotionRefuse the discount, and say it has already been used on this device.
A referral where the new customer's device has been used by the referrerCreate the account, but pay no reward.
high levelRefuse the promotion.
fingerprint_suppressed fired, or no usable identificationAsk for verification, such as a phone number, before applying it.
OtherwiseApply it.

Signals that matter for promotions

  • fingerprint_suppressed: the device hides enough to get a new visitor ID every time, which would defeat a per-device limit.
  • device_farm, android_emulator, ios_simulator and cloned_app: many "devices" that are really one person's setup.
  • bot: redemptions scripted at scale.
  • residential_proxy and vpn: used to make repeated sign-ups look like different households.

Common questions

How do I prevent promo code abuse?

Identify when the code is applied, read the stored event on your server, and store the visitor ID with the redemption under a unique index on the code and the visitor ID. A second redemption from the same device then fails, whatever account or email it uses.

Can people get around a per-device limit?

A determined user can use another device, so the limit raises the cost of abuse rather than ending it. Combine it with the level and the signals above, and with your own checks such as a verified phone number.

What about customers on a shared device?

They share a visitor ID. If the promotion should allow a household two redemptions, count redemptions per visitor and set the limit above one, or ask for verification when a second arrives.

Does it work for in-app promotions?

Yes. Use the iOS, Android, React Native or Flutter SDK with the same tag, and enforce the same index on your server. A reinstalled Android app gets a new visitor ID, so pair the limit with verification for high-value offers.

How do I handle a visitor that cannot be identified?

Treat it as missing evidence. The answer has identifiable: false and the fingerprint_suppressed signal fires, so ask for verification before applying the offer rather than refusing.