---
title: "Promo abuse checks for operators | Snowplow Signals"
description: "Snowplow Signals tells your risk rules what a player did between bonus and withdrawal, and how many accounts a device opened, so they hold the payout first."
source: https://signals.snowplow.io/solutions/gaming/promotions
---

[Gaming](https://signals.snowplow.io/solutions/gaming) / Promo abuse checks 

# Catch the bonus abuse before the payout. 

Signals tells your risk rules what a player did between the bonus and the withdrawal request, and how many accounts share a device or a card this week, so they can hold the payout for review before the money moves.

[Start free](https://signals.snowplow.io/start) [Talk to us](https://signals.snowplow.io/start?intent=talk) 

Lower promo abuse losses

## Lose less to promo abuse by flagging the pattern before the payout. 

By the time a scheduled review finds promo abuse, the payout has gone out. Signals gives your risk rules how many bets a player placed between the bonus and the withdrawal request, which risk tiers those bets sat in, and how many accounts share this device or this card this week. Your rules set the thresholds and decide whether to hold, flag or let it through.

Four ways operators use it

01 A withdrawal minutes after a bonus Bonus credited, one small bet, withdrawal requested. Hold it for review. 02 A device on its fourth account Three accounts from this device since Monday, a fourth signing up. Flag it. 03 A bonus wagered only on the safest bets Fourteen bets since the reload bonus, all in the lowest tier. Flag the pattern. 04 Several accounts on one card Fifth account to deposit from this card today. Hold the bonus on it. 

[Read the docs →](https://docs.snowplow.io/docs/signals/introduction/) 

### Hold the payout when a bonus turns into a withdrawal without play

A player claimed a deposit bonus twenty minutes ago, placed one small bet and has just requested a withdrawal. The pattern is plain in the sequence of events, and the review that reads it usually runs on a schedule, so the first pass lands after the payment has cleared. Signals has the minutes since the bonus and the bets placed since the deposit at the moment the request arrives, which is when the hold has to be decided.

What Signals knows

`minutes_since_bonus` 

Minutes since this player's last bonus was credited

`bets_24h` 

Bets placed in the last 24 hours

`stake_24h` 

Total amount staked in the last 24 hours

What your risk rules do

Holds the withdrawal for review when the request arrives inside the window your team sets after a bonus and the bets since then fall under the floor your rules name. A player who has played the bonus through goes to the normal payout queue.

Define it · PythonUse it · TypeScript

risk.ts · 11 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, Event, EventProperty, StreamAttributeGroup, user_id,
)

bonus_claimed = Event(name="bonus_claimed", vendor="com.example", version="1-0-0")
bet_placed = Event(name="bet_placed", vendor="com.example", version="1-0-0")

minutes_since_bonus = Attribute(
    name="minutes_since_bonus",
    type="double",
    events=[bonus_claimed],
    aggregation="time_since_last",
    time_unit="min",
)

bets_24h = Attribute(
    name="bets_24h",
    type="int32",
    events=[bet_placed],
    aggregation="counter",
    period=timedelta(hours=24),
    default_value=0,
)

stake_24h = Attribute(
    name="stake_24h",
    type="double",
    events=[bet_placed],
    aggregation="sum",
    property=EventProperty(vendor="com.example", name="bet_placed", major_version=1, path="stake"),
    period=timedelta(hours=24),
    default_value=0,
)

# Players are logged in at the cashier, so the group keys on the player id your platform sets.
game_promos_withdrawal = StreamAttributeGroup(
    name="game_promos_withdrawal",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[minutes_since_bonus, bets_24h, stake_24h],
)
```

```
// In the handler that receives a withdrawal request
const player = await signals.getServiceAttributes({
  name: "game_promos_withdrawal",
  attribute_key: "user_id",
  identifier: playerId,
});
// { minutes_since_bonus: 22.4, bets_24h: 1, stake_24h: 5 }

if (player.minutes_since_bonus < rules.bonusWindowMinutes && player.bets_24h <= rules.minBetsAfterBonus) {
  risk.hold({ withdrawal: withdrawalId, reason: "bonus_without_play" });
}
```

### Flag the registration while the device count is fresh

A registration is completing on a device that opened three other accounts this week, each one claiming the welcome bonus and going quiet. A device check that runs against a list learns about the fourth account when the list is next built, and the welcome bonus has been credited by then. The count is known the moment the account is created, which is when your rules want it.

What Signals knows

`accounts_created_7d` 

Accounts created on this device in the last seven days

`accounts_total` 

Accounts created on this device over its lifetime

What your risk rules do

Flags the new account to your risk team when the device passes the number of accounts a week your rules allow, and can hold the welcome bonus on that account until someone has looked. What counts as too many, and whether anything is blocked, stays with your rules.

Define it · PythonUse it · TypeScript

risk.ts · 11 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, AttributeKey, EntityProperty, Event, StreamAttributeGroup

account_created = Event(name="account_created", vendor="com.example", version="1-0-0")
device = AttributeKey(
    name="device",
    property=EntityProperty(vendor="com.example", name="device", major_version=1, path="id"),
)

accounts_created_7d = Attribute(
    name="accounts_created_7d",
    type="int32",
    events=[account_created],
    aggregation="counter",
    period=timedelta(days=7),
    default_value=0,
)

accounts_total = Attribute(
    name="accounts_total",
    type="int32",
    events=[account_created],
    aggregation="counter",
    ttl=timedelta(days=365),
    default_value=0,
)

# Keyed on the device, not the player, so a new account is asked about from its first event.
game_promos_device = StreamAttributeGroup(
    name="game_promos_device",
    version=1,
    attribute_key=device,
    owner="you@example.com",
    attributes=[accounts_created_7d, accounts_total],
)
```

```
// In the handler that completes a registration
const device = await signals.getServiceAttributes({
  name: "game_promos_device",
  attribute_key: "device",
  identifier: deviceId,
});
// { accounts_created_7d: 4, accounts_total: 4 }

if (device.accounts_created_7d >= rules.accountsPerDeviceWeek) {
  risk.flag({ account: accountId, reason: "device_account_velocity", count: device.accounts_created_7d });
}
```

### Flag a bonus being wagered through on the lowest-risk bets

A player claimed a reload bonus this morning and has placed fourteen bets since, every one in your lowest risk tier, the bets that clear the wagering requirement with the least chance of losing the bonus. Each bet passes on its own. The pattern is in the mix, and it shows while the bonus is still locked. A report that groups bets by tier reaches the team after the requirement has cleared and the balance can be withdrawn.

What Signals knows

`bets_by_tier_24h` 

How many bets this player placed in each risk tier in the last 24 hours

`bonus_claims_24h` 

Bonuses claimed in the last 24 hours

What your risk rules do

Sends the player to your risk team when a bonus was claimed today and the share of bets in the lowest tier passes the level your rules set, before the wagering requirement is marked as met. Your team decides whether the bonus balance is released, voided or held.

Define it · PythonUse it · TypeScript

risk.ts · 14 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import Attribute, Event, EventProperty, StreamAttributeGroup, user_id

bonus_claimed = Event(name="bonus_claimed", vendor="com.example", version="1-0-0")
bet_placed = Event(name="bet_placed", vendor="com.example", version="1-0-0")

bets_by_tier_24h = Attribute(
    name="bets_by_tier_24h",
    type="dict",
    events=[bet_placed],
    aggregation="category_count",
    property=EventProperty(vendor="com.example", name="bet_placed", major_version=1, path="risk_tier"),
    period=timedelta(hours=24),
)

bonus_claims_24h = Attribute(
    name="bonus_claims_24h",
    type="int32",
    events=[bonus_claimed],
    aggregation="counter",
    period=timedelta(hours=24),
    default_value=0,
)

game_promos_wagering = StreamAttributeGroup(
    name="game_promos_wagering",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[bets_by_tier_24h, bonus_claims_24h],
)
```

```
// In the handler that marks a bonus as wagered through
const player = await signals.getServiceAttributes({
  name: "game_promos_wagering",
  attribute_key: "user_id",
  identifier: playerId,
});
// { bets_by_tier_24h: { low: 14 }, bonus_claims_24h: 1 }

const total = Object.values(player.bets_by_tier_24h).reduce((sum, n) => sum + n, 0);
const lowShare = (player.bets_by_tier_24h.low ?? 0) / total;

if (player.bonus_claims_24h > 0 && total >= rules.minBetsToJudge && lowShare >= rules.lowTierShare) {
  risk.flag({ player: playerId, bonus: bonusId, reason: "low_risk_wagering", share: lowShare });
}
```

### Hold the deposit bonus when one card is funding several accounts

Five accounts have deposited from one card today, each in time to claim the deposit bonus. Each deposit clears on its own, and the card does not become a pattern until something counts the accounts behind it. A reconciliation that runs after the fact finds the card once the bonuses have been credited and part of the money has gone back out. The count is there on the fifth deposit, before the bonus reaches that account.

What Signals knows

`accounts_depositing_24h` 

Separate accounts that deposited from this card in the last 24 hours

`deposits_24h` 

Deposits made from this card in the last 24 hours

What your risk rules do

Holds the deposit bonus on the account that just deposited when the number of accounts on this card today passes the limit your rules set, and sends the card to your risk team. Whether the earlier accounts are reviewed as well is your call.

Define it · PythonUse it · TypeScript

risk.ts · 11 lines Copy Copied 

```
from datetime import timedelta
from snowplow_signals import (
    Attribute, AttributeKey, AtomicProperty, Event, EventProperty, StreamAttributeGroup,
)

deposit_completed = Event(name="deposit_completed", vendor="com.example", version="1-0-0")
# The fingerprint is your payment processor's token for the card, so no card number reaches Signals.
card = AttributeKey(
    name="card",
    property=EventProperty(vendor="com.example", name="deposit_completed", major_version=1, path="card_fingerprint"),
)

accounts_depositing_24h = Attribute(
    name="accounts_depositing_24h",
    type="int32",
    events=[deposit_completed],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="user_id"),
    period=timedelta(hours=24),
)

deposits_24h = Attribute(
    name="deposits_24h",
    type="int32",
    events=[deposit_completed],
    aggregation="counter",
    period=timedelta(hours=24),
    default_value=0,
)

game_promos_card = StreamAttributeGroup(
    name="game_promos_card",
    version=1,
    attribute_key=card,
    owner="you@example.com",
    attributes=[accounts_depositing_24h, deposits_24h],
)
```

```
// In the handler that credits a deposit bonus
const card = await signals.getServiceAttributes({
  name: "game_promos_card",
  attribute_key: "card",
  identifier: cardFingerprint,
});
// { accounts_depositing_24h: 5, deposits_24h: 6 }

if (card.accounts_depositing_24h >= rules.accountsPerCardDay) {
  risk.hold({ bonus: bonusId, account: accountId, reason: "shared_card" });
}
```

## Try it on your own site. 

Add the SDK, define one thing you want to know about a visitor, and watch it update while you click around. The free tier runs on the real engine: no card, no sales call.

[Start free](https://signals.snowplow.io/start) [Talk to an engineer](https://signals.snowplow.io/start?intent=engineer)
