---
title: "Player protection flags for operators | Snowplow Signals"
description: "Snowplow Signals tells your protection tooling when a player's stakes, session or deposits break from their own pattern, so your team acts in the session."
source: https://signals.snowplow.io/solutions/gaming/player-protection
---

[Gaming](https://signals.snowplow.io/solutions/gaming) / Player protection flags 

# Flag a change in play while the session is still running. 

Signals tells your player protection team and tooling when a player's stakes, session length or deposits have moved away from their own pattern, so the review happens during the session instead of after it.

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

Earlier player protection flags

## Raise the flag while the player is still in the session. 

A flag is early when it reaches your team while the session it describes is still running. Signals gives your player protection team and tooling how this player's stakes, session length and deposits compare with their own pattern, and how long since one of their own limits was reached. What counts as a marker, and what happens next, stay your rules under your own obligations.

Four ways operators use it

01 Stakes rising fast in one session Betting far above their usual stake, an hour in. Flag it now. 02 A session running long Three hours in, their usual is forty minutes. Flag it now. 03 Deposits piling up in a day Third deposit today, past their usual month. Flag it now. 04 Back minutes after a limit ended Break ended twelve minutes ago, first bet placed. Flag it now. 

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

### Flag a stake climbing past this player's own norm

A player whose stake has sat near the same figure for a month is betting far above it tonight, and the last few bets are each bigger than the one before. The scheduled risk review reads this session after it ends, so the pattern reaches your team as history. Against the whole player base this stake may look ordinary. Against this player's own month it is a change, and the moment to see it is while the bets are still going in.

What Signals knows

`mean_stake_1h` 

Mean stake over the last hour of this session

`mean_stake_30d` 

Mean stake over the last 30 days

`bets_1h` 

Bets placed in the last hour

What your team and tooling do

Compares the two means and, when this hour's stake passes the ratio your protection policy sets for a marker, opens a case in your player protection tooling with both figures attached. Your team decides what, if anything, happens next.

Define it · PythonUse it · TypeScript

protection.ts · 17 lines Copy Copied 

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

bet_placed = Event(name="bet_placed", vendor="com.example", version="1-0-0")
stake = EventProperty(vendor="com.example", name="bet_placed", major_version=1, path="stake")

mean_stake_1h = Attribute(
    name="mean_stake_1h",
    type="double",
    events=[bet_placed],
    aggregation="mean",
    property=stake,
    period=timedelta(hours=1),
)

mean_stake_30d = Attribute(
    name="mean_stake_30d",
    type="double",
    events=[bet_placed],
    aggregation="mean",
    property=stake,
    period=timedelta(days=30),
)

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

# Bets are sent from your server, so the player id is the key both windows share.
game_protection_stakes = StreamAttributeGroup(
    name="game_protection_stakes",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[mean_stake_1h, mean_stake_30d, bets_1h],
)
```

```
// In the service that checks protection markers after each bet
const player = await signals.getServiceAttributes({
  name: "game_protection_stakes",
  attribute_key: "user_id",
  identifier: playerId,
});
// { mean_stake_1h: 42.5, mean_stake_30d: 8.1, bets_1h: 37 }

const rising = player.mean_stake_1h >= player.mean_stake_30d * policy.stakeRatio;

if (player.bets_1h >= policy.minBets && rising) {
  protection.openCase({
    playerId,
    marker: "stake_rising",
    evidence: { thisHour: player.mean_stake_1h, usual: player.mean_stake_30d },
  });
}
```

### Flag a session running long past this player's usual length

This player's sessions have run about forty minutes for as long as you have records, and tonight's has passed three hours. A session length reaches a review once it has a start and an end, so the long one is counted after the player has logged off. Your own records hold what is usual for this player. Signals supplies how long this session has run so far, while it is still running.

What Signals knows

`minutes_in_session` 

Minutes since this session started

`sessions_today` 

Sessions started in the last 24 hours

What your team and tooling do

Checks minutes in session against the usual length your own records hold for this player, and when it passes the margin your policy sets, surfaces the session to your team or shows the prompt your tooling runs. Which of those, and whether either, is your rule.

Define it · PythonUse it · TypeScript

protection.ts · 17 lines Copy Copied 

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

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

# Time since the most recent session_start is how long this session has run.
minutes_in_session = Attribute(
    name="minutes_in_session",
    type="double",
    events=[session_start],
    aggregation="time_since_last",
    time_unit="min",
)

sessions_today = Attribute(
    name="sessions_today",
    type="int32",
    events=[session_start],
    aggregation="counter",
    period=timedelta(days=1),
    default_value=0,
)

game_protection_session = StreamAttributeGroup(
    name="game_protection_session",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[minutes_in_session, sessions_today],
)
```

```
// In the job that checks open sessions every few minutes.
// usualSessionMinutes is your own figure for this player, from your warehouse or risk platform.
const player = await signals.getServiceAttributes({
  name: "game_protection_session",
  attribute_key: "user_id",
  identifier: playerId,
});
// { minutes_in_session: 186.4, sessions_today: 2 }

if (player.minutes_in_session > usualSessionMinutes * policy.lengthRatio) {
  protection.flag({
    playerId,
    marker: "long_session",
    minutes: player.minutes_in_session,
    usual: usualSessionMinutes,
  });
}
```

### Flag a day of deposits out of step with this player's month

A player who deposits a modest amount a few times a month has deposited three times today, and the total has passed what they put in across the rest of the month. Deposits are read in the next scheduled pass, so a heavy day surfaces once it is over and the money has been staked. The comparison is this player's day against this player's month, and it is available at the third deposit rather than the next morning.

What Signals knows

`deposited_24h` 

Total deposited in the last 24 hours

`deposited_30d` 

Total deposited in the last 30 days

`deposits_24h` 

Separate deposits in the last 24 hours

What your team and tooling do

Sets today's total against the rest of the month and, when today passes the share your policy names, opens a review in your protection tooling with both totals. Your rules decide whether that means a case, a message from your team or nothing.

Define it · PythonUse it · TypeScript

protection.ts · 17 lines Copy Copied 

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

deposit = Event(name="deposit", vendor="com.example", version="1-0-0")
amount = EventProperty(vendor="com.example", name="deposit", major_version=1, path="amount")

deposited_24h = Attribute(
    name="deposited_24h",
    type="double",
    events=[deposit],
    aggregation="sum",
    property=amount,
    period=timedelta(days=1),
    default_value=0,
)

deposited_30d = Attribute(
    name="deposited_30d",
    type="double",
    events=[deposit],
    aggregation="sum",
    property=amount,
    period=timedelta(days=30),
    default_value=0,
)

deposits_24h = Attribute(
    name="deposits_24h",
    type="int32",
    events=[deposit],
    aggregation="counter",
    period=timedelta(days=1),
    default_value=0,
)

# The cashier sends deposit from the server, keyed on the same player id as the bets.
game_protection_deposits = StreamAttributeGroup(
    name="game_protection_deposits",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[deposited_24h, deposited_30d, deposits_24h],
)
```

```
// In the service that runs after each deposit is confirmed
const player = await signals.getServiceAttributes({
  name: "game_protection_deposits",
  attribute_key: "user_id",
  identifier: playerId,
});
// { deposited_24h: 900, deposited_30d: 1150, deposits_24h: 3 }

const restOfMonth = player.deposited_30d - player.deposited_24h;

if (player.deposits_24h >= 2 && player.deposited_24h > restOfMonth * policy.depositRatio) {
  protection.openCase({
    playerId,
    marker: "deposit_spike",
    evidence: { today: player.deposited_24h, restOfMonth },
  });
}
```

### Flag a return to play minutes after a limit ended

A player set a loss limit and reached it, and the break they chose has just ended. Twelve minutes later they are placing bets. Your limits tooling records the limit being reached, and a review reads the quick return as a line in tomorrow's report, after the session it belongs to. The gap between the break ending and the first bet is a figure your team may want in front of them tonight, with your rules deciding what it means.

What Signals knows

`minutes_since_limit` 

Minutes since a self-set limit was last reached

`bets_10m` 

Bets placed in the last ten minutes

What your team and tooling do

Takes the break length from your limits tooling off the minutes since the limit was reached, and when the first bets land inside the window your policy names, puts the return in front of your team with the gap attached. Whether that leads to a conversation, a prompt or nothing is your call.

Define it · PythonUse it · TypeScript

protection.ts · 18 lines Copy Copied 

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

# Your limits tooling sends limit_reached when a player hits a limit they set.
limit_reached = Event(name="limit_reached", vendor="com.example", version="1-0-0")
bet_placed = Event(name="bet_placed", vendor="com.example", version="1-0-0")

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

bets_10m = Attribute(
    name="bets_10m",
    type="int32",
    events=[bet_placed],
    aggregation="counter",
    period=timedelta(minutes=10),
    default_value=0,
)

game_protection_limits = StreamAttributeGroup(
    name="game_protection_limits",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[minutes_since_limit, bets_10m],
)
```

```
// In the service that checks protection markers after each bet.
// breakMinutes is the break length this player chose, from your limits tooling.
const player = await signals.getServiceAttributes({
  name: "game_protection_limits",
  attribute_key: "user_id",
  identifier: playerId,
});
// { minutes_since_limit: 1452.0, bets_10m: 4 }

const minutesSinceBreakEnded = player.minutes_since_limit - breakMinutes;

if (player.bets_10m > 0 && minutesSinceBreakEnded >= 0 && minutesSinceBreakEnded <= policy.returnWindowMinutes) {
  protection.flag({
    playerId,
    marker: "quick_return",
    minutesAfterBreak: minutesSinceBreakEnded,
  });
}
```

## 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)
