> ## Documentation Index
> Fetch the complete documentation index at: https://docs.fingerprint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Bonus Abuse

> Learn how to prevent bonus abuse and limit welcome offers to one per device

## Overview

This tutorial walks through implementing Fingerprint to prevent bonus abuse, where players create multiple accounts to claim the same welcome offer, free bet, deposit match, referral, or sign-up credits again and again. It's a common problem for online casinos, sportsbooks, betting and prediction market platforms, sweepstakes sites, and games that reward new players.

You'll begin with a starter app that includes a mock prediction market sign-up page that automatically grants a welcome bonus of free trading credits to every new account. From there, you'll add the JavaScript agent to identify each visitor and use server-side logic with Fingerprint data to grant the bonus only once per device. New accounts are still created, so players aren't locked out, but a visitor who has already claimed the bonus won't get it again. Each account also stores the visitor ID, so your support or risk team can later link accounts created by the same visitor.

The same check works at other points in the player journey, such as first deposit or withdrawal, so you can apply it wherever a promotion is paid out.

By the end, you'll have a sample app that limits welcome bonuses to one per visitor and can be adapted to fit your own promotions and business logic.

This tutorial uses just plain JavaScript and a Node server with SQLite on the backend. For language- or framework-specific setups, see the quickstarts.

> Estimated time: \< 15 minutes

## Prerequisites

Before you begin, make sure you have the following:

* A copy of the [starter repository](https://github.com/fingerprintjs/use-case-tutorials) (clone with Git or download as a ZIP)
* [Node.js](https://nodejs.org/) (v22 or later) and npm installed
* Your favorite code editor
* Basic knowledge of JavaScript

## 1. Create a Fingerprint account and get your API keys

1. [Sign up](https://dashboard.fingerprint.com/signup) for a free Fingerprint trial, or log in if you already have an account.
2. After signing in, go to the [**API keys**](https://dashboard.fingerprint.com/api-keys) page in the dashboard.
3. Save your **public API key**, which you'll use to initialize the JavaScript agent.
4. Create and securely store a **server API key** for your server. Never expose it on the client side. You'll use this key on the backend to retrieve full visitor information through the Fingerprint Server API.

## 2. Set up your project

1. Clone or download the [starter repository](https://github.com/fingerprintjs/use-case-tutorials) and open it in your editor.

```bash Terminal theme={"theme":"github-dark-dimmed"}
git clone https://github.com/fingerprintjs/use-case-tutorials.git
```

2. This tutorial will be using the `bonus-abuse` folder. The project is organized as follows:

**public**

**index.html - Prediction market sign-up page**

**index.js - Frontend logic to handle sign-up**

**server**

**server.js - Serves static files and sign-up endpoint**

**db.js - SQLite database connection**

**accounts.js - Sign-up and welcome bonus logic**

**.env.example - Example environment variables**

3. Install dependencies:

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm install
```

4. Copy or rename `.env.example` to `.env`, then add your Fingerprint API keys:

```bash Terminal theme={"theme":"github-dark-dimmed"}
FP_PUBLIC_API_KEY=your-public-key
FP_SERVER_API_KEY=your-server-key
```

5. Start the server:

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm run dev
```

6. Visit `http://localhost:3000` to view the mock prediction market sign-up page from the starter app. Create a test account by entering a username and password and clicking **Sign up**. You'll see that the account is created and the welcome bonus is applied. Create a few more accounts with different usernames, then open the page in an incognito window and try again from the same browser. By default, the app grants the welcome bonus to every new account, no matter how many times the same visitor signs up.

## 3. Add Fingerprint to the frontend

In this step, you'll load the JavaScript agent when the page loads and trigger identification when the user clicks **Sign up**. The JavaScript agent returns both a `visitor_id` and an `event_id`. Instead of relying on the `visitor_id` from the browser, you'll send the `event_id` to your server along with the signup payload. The server will then call the [Fingerprint Events API](https://docs.fingerprint.com/reference/server-api-get-event.md) to securely retrieve the full identification details, including the verified visitor ID and risk signals such as browser tampering or bot activity.

1. At the top of `public/index.js`, load the JavaScript agent:

```javascript public/index.js theme={"theme":"github-dark-dimmed"}
const fpPromise = import(`https://fpjscdn.net/v4/${window.FP_PUBLIC_API_KEY}`).then((Fingerprint) =>
  Fingerprint.start({ region: 'us' }),
);
```

2. Make sure to change `region` to match your workspace region (e.g., `eu` for Europe, `ap` for Asia, `us` for Global (default)).
3. Near the bottom of `public/index.js`, the **Sign up** button already has an event handler for submitting the credentials. Inside this handler, request visitor identification from Fingerprint using the `get()` method and include the returned `event_id` when sending the signup to the server:

```javascript public/index.js theme={"theme":"github-dark-dimmed"}
signupBtn.addEventListener("click", async () => {
  // ...

  const fp = await fpPromise; // [!code ++]
  const { event_id } = await fp.get(); // [!code ++]

  try {
    const res = await fetch("/api/signup", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ username, password, event_id }), // [!code ++]
    });
    const data = await res.json();

    // ...
  }
});
```

> [!NOTE]
>
> **Linking your own identifiers:** if you already have an identifier for the user at this point, like an account, order, or session ID, pass it as `linkedId`. It's returned as `linked_id` on the event and indexed, so you can retrieve every event tied to that identifier through the Server API. Learn more about adding more context to your identification events in [Linking and tagging information](https://docs.fingerprint.com/docs/tagging-information.md).

The `get()` method sends signals collected from the browser to Fingerprint servers, where they are analyzed to identify the visitor. The returned `event_id` acts as a reference to this specific identification event, which your server can later use to fetch the full visitor details.

For lower latency in production, use [Sealed Client Results](https://docs.fingerprint.com/docs/sealed-client-results.md) to return full identification details as an encrypted payload from the `get()` method.

## 4. Receive and use the event ID to get visitor insights

Next, pass the `event_id` through to your signup logic, install and initialize the [Fingerprint Node Server SDK](https://docs.fingerprint.com/reference/node-server-sdk.md), and fetch the full visitor identification event so you can access the trusted visitor ID and [Smart Signals](https://fingerprint.com/products/smart-signals/).

1. In the backend, the `server/server.js` file defines the API routes for the app. Update the `/api/signup` route there to also extract `event_id` from the request body and pass it into the `attemptSignup` function:

```javascript server/server.js theme={"theme":"github-dark-dimmed"}
app.post('/api/signup', async (req, reply) => {
  const { username, password, event_id } = req.body || {}; // [!code ++]
  const result = await attemptSignup({ username, password, event_id }); // [!code ++]
  return reply.send(result);
});
```

2. Install the Fingerprint Node Server SDK:

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm install @fingerprint/node-sdk
```

3. The `server/accounts.js` file contains the logic for handling signups and granting the welcome bonus. Start by importing and initializing the Fingerprint Node Server SDK there, and load your environment variables with `dotenv`.

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
import { db } from './db.js';
import { config } from 'dotenv'; // [!code ++]
import { FingerprintServerApiClient, Region } from '@fingerprint/node-sdk'; // [!code ++]

config(); // [!code ++]

const fpServerApiClient = new FingerprintServerApiClient({
  // [!code ++]
  apiKey: process.env.FP_SERVER_API_KEY, // [!code ++]
  region: Region.Global, // [!code ++]
}); // [!code ++]
```

4. Make sure to change `region` to match your workspace region (e.g., `EU` for Europe, `AP` for Asia, `Global` for Global (default)).
5. Update the `attemptSignup` function to accept `event_id` and use it to fetch the full identification event details from Fingerprint:

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
export async function attemptSignup({ username, password, event_id }) {
  // [!code ++]
  if (!username || !password || !event_id) {
    // [!code ++]
    console.error('Missing one or more inputs.');
    return { success: false, error: 'Signup failed.' };
  }

  const user = findAccountByUsername(username);
  if (user) {
    console.error('Account already exists');
    return { success: false, error: 'Account already exists.' };
  }

  const event = await fpServerApiClient.getEvent(event_id); // [!code ++]

  // ...
}
```

Using the `event_id`, the getEvent method will retrieve the full data for the visitor identification event. The returned object will contain the visitor ID, IP address, device, and browser details, and Smart Signals like bot detection, browser tampering detection, proxy detection, and more.

You can see a full example of the event structure and test it with your own device in the [demo playground](https://demo.fingerprint.com/playground).

For additional checks to ensure the validity of the data coming from your frontend, view [how to protect from client-side tampering and replay attacks](https://docs.fingerprint.com/docs/protecting-from-client-side-tampering.md).

## 5. Block bots and withhold bonuses from suspicious devices

> [!NOTE]
>
> This optional step uses the Bot Detection Smart Signal, which is available during the free trial
> and on paid plans.

Bonus abuse is often automated, with scripts creating accounts in bulk to farm sign-up credits. A simple but powerful way to stop this is to block bot signups entirely. The `event` object includes the [Bot Detection Smart Signal](https://fingerprint.com/products/bot-detection/) that flags automated activity, making it easy to reject bot traffic.

This signal returns `good` for known bots like search engines, `bad` for automation tools, headless browsers, or other signs of automation, and `not_detected` when no bot activity is found.

1. Continuing in the `attemptSignup` function in `server/accounts.js`, check the bot signal returned in the `event` object:

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
export async function attemptSignup({ username, password, event_id }) {
  // ...

  const event = await fpServerApiClient.getEvent(event_id);

  const botDetected = event.bot !== 'not_detected'; // [!code ++]

  if (botDetected) {
    // [!code ++]
    console.error('Bot detected.'); // [!code ++]
    return { success: false, error: 'Signup failed.' }; // [!code ++]
  } // [!code ++]

  // ...
}
```

You can also use Fingerprint's [Suspect Score](https://docs.fingerprint.com/docs/suspect-score.md) to flag high-risk signups. The Suspect Score is a weighted representation of all Smart Signals present in the identification payload, helping to identify suspicious activity, such as a player using a proxy, a tampered browser, or a virtual machine to look like a new device.

Rather than blocking the signup outright, a high Suspect Score is a good reason to hold back the bonus. The player can still create an account and use the platform, while your team reviews the account or asks for additional verification before releasing the bonus.

2. Below the bot detection check, add a condition that reads the Suspect Score from the `event` object and sets a flag to withhold the bonus if it exceeds a chosen threshold (for example, 20):

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
export async function attemptSignup({ username, password, event_id }) {
  // ...

  const event = await fpServerApiClient.getEvent(event_id);

  const botDetected = event.bot !== 'not_detected';

  if (botDetected) {
    console.error('Bot detected.');
    return { success: false, error: 'Signup failed.' };
  }

  const suspectScore = event.suspect_score || 0; // [!code ++]
  let withholdBonus = false; // [!code ++]

  if (suspectScore > 20) {
    // [!code ++]
    console.error(`High Suspect Score detected: ${suspectScore}`); // [!code ++]
    withholdBonus = true; // [!code ++]
  } // [!code ++]

  // ...
}
```

You'll use the `withholdBonus` flag in the next step when deciding whether to grant the bonus.

## 6. Limit bonuses to one per visitor

Next, use the trusted visitor ID from the `event` object to grant the welcome bonus only once per visitor. Every signup that passes the bot check still creates an account, but if the same visitor ID has already received the bonus, the new account is created without it. In production, you might hold the bonus for review, require identity verification, or allow a set number of bonuses per visitor over time instead. *(This example simplifies account creation and bonus logic for demonstration purposes.)*

This makes bonus rules enforceable at the visitor level, not just the account level. Even if someone signs up with a new username or email address, clears cookies, or switches to incognito mode, the bonus is still linked to their visitor ID, keeping one-per-player offers truly one per player.

Note: The starter app includes a SQLite database with a table already created for you. The `bonusGranted` column records whether each account received the welcome bonus:

```text SQLite database tables theme={"theme":"github-dark-dimmed"}
accounts - Stores account details
  username TEXT PRIMARY KEY
  password TEXT NOT NULL
  visitorId TEXT
  bonusGranted INTEGER NOT NULL DEFAULT 0
  createdAt INTEGER NOT NULL
```

1. Add a helper function to the bottom of the `server/accounts.js` file to check if the visitor has already received the welcome bonus:

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
// Check if any account on this device has already received the welcome bonus
function hasReceivedBonus(visitorId) {
  return !!db
    .prepare(`SELECT 1 FROM accounts WHERE visitorId = ? AND bonusGranted = 1 LIMIT 1`)
    .get(visitorId);
}
```

2. Update `attemptSignup` to retrieve the `visitorId` and use it to decide whether the new account should receive the bonus. The bonus is applied only if the Suspect Score is under the threshold and the visitor hasn't already claimed it:

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
export async function attemptSignup({ username, password, event_id }) {
  // ...

  if (suspectScore > 20) {
    console.error(`High Suspect Score detected: ${suspectScore}`);
    withholdBonus = true;
  }

  const visitorId = event.identification.visitor_id; // [!code ++]
  const alreadyClaimed = hasReceivedBonus(visitorId); // [!code ++]

  if (alreadyClaimed) {
    // [!code ++]
    console.error('Welcome bonus already claimed on this device.'); // [!code ++]
  } // [!code ++]

  const bonusApplied = !withholdBonus && !alreadyClaimed; // [!code ++]

  // ...
}
```

3. The starter app's SQL statement sets `bonusGranted` to `1` for every new account. Update it to store the `visitorId` and only mark the bonus as granted when `bonusApplied` is true. Then return the result, including a warning message when the bonus is withheld:

```javascript server/accounts.js theme={"theme":"github-dark-dimmed"}
export async function attemptSignup({ username, password, event_id }) {
  // ...

  const bonusApplied = !withholdBonus && !alreadyClaimed;

  db.prepare(
    `INSERT INTO accounts (username, password, visitorId, bonusGranted, createdAt) VALUES (?, ?, ?, ?, ?)`, // [!code ++]
  ).run(username, password, visitorId, bonusApplied ? 1 : 0, Date.now()); // [!code ++]

  if (!bonusApplied) {
    // [!code ++]
    return {
      // [!code ++]
      success: true, // [!code ++]
      bonusApplied: false, // [!code ++]
      type: 'warning', // [!code ++]
      message: alreadyClaimed // [!code ++]
        ? 'Account created. The welcome bonus has already been claimed on this device.' // [!code ++]
        : 'Account created. The welcome bonus could not be applied to this account.', // [!code ++]
    }; // [!code ++]
  } // [!code ++]

  return {
    success: true,
    bonusApplied: true,
    bonusCredits: WELCOME_BONUS_CREDITS,
    type: 'success',
    message: 'Account created. Welcome bonus applied.',
  };
}
```

The account is always stored with its `visitorId`, even when the bonus is withheld. This lets your support or risk team look up every account created by the same visitor, which is useful for reviewing suspected multi-accounting or investigating bonus abuse after the fact.

This gives you a basic system to detect and prevent bonus abuse. You can extend it by allowing a limited number of bonuses per visitor, adding verification steps before releasing a withheld bonus, setting time-based limits, or flagging linked accounts for review.

> [!NOTE]
>
> This is a minimal example to show how to implement Fingerprint. In a real application, make sure
> to use proper security practices, input validation, password handling, and account management
> logic that align with your production standards.

## 7. Test your implementation

Now that everything is wired up, you can test the full protected signup and bonus flow.

1. Start your server if it isn't already running and open [http://localhost:3000](http://localhost:3000):

```bash Terminal theme={"theme":"github-dark-dimmed"}
npm run dev
```

2. Reset the database by clicking on **Reset demo DB** at the bottom of the page.
3. Create a new account in your browser. You'll see "Account created. Welcome bonus applied."
4. Create another account with a different username in the same browser. The account is created, but you'll see "Account created. The welcome bonus has already been claimed on this device."
5. Open an incognito/private window and create another account. The account is still created, but the bonus is denied because your visitor ID remains the same for that browser.
6. Bonus: Test the flow using a headless browser or automation tool to see bot detection in action. A sample script is available in `test-bot.js` that attempts to create 10 new accounts. While your app is running, run the script with `node test-bot.js` in your terminal and observe that every automated signup is blocked entirely.
7. Click **Reset demo DB** again to clear all accounts, then sign up once more. The first signup receives the welcome bonus again.

## Next steps

You now have a working signup flow that limits welcome bonuses to one per visitor with Fingerprint. From here, you can expand the logic with more [Smart Signals](https://docs.fingerprint.com/docs/smart-signals-reference.md), fine-tune rules based on your business policies, or layer in additional checks for suspicious visitors.

To support multiple promotions, such as deposit matches, free bets, or referral credits, track claims per promo by recording the promo and visitor ID for each claim, similar to the redemptions table in the coupon abuse tutorial. You can apply the same pattern to catch self-referrals, where the referring and referred accounts share a visitor ID, and to check visitor IDs at withdrawal before bonus funds are paid out.

To dive deeper, explore the other use case tutorials for more step-by-step examples.

Check out these related resources:

* [Node SDK Reference](https://docs.fingerprint.com/reference/node-server-sdk.md)
* [Vue frontend quickstart](https://docs.fingerprint.com/docs/vue-quickstart.md)
* [React frontend quickstart](https://docs.fingerprint.com/docs/react-quickstart.md)
* [API reference for the Events endpoint](https://docs.fingerprint.com/reference/server-api-get-event.md)
* [Use case tutorial: Detecting new account fraud](https://docs.fingerprint.com/docs/new-account-fraud-use-case-tutorial.md)
* [Use case tutorial: Preventing coupon abuse](https://docs.fingerprint.com/docs/coupon-abuse-use-case-tutorial.md)
* [Use case tutorial: Enforcing bans](https://docs.fingerprint.com/docs/ban-enforcement-use-case-tutorial.md)
* [Low-latency identification with Sealed Client Results](https://docs.fingerprint.com/docs/sealed-client-results.md)
