---
title: "Cashier bonus timing for operators | Snowplow Signals"
description: "Snowplow Signals tells your cashier who opened it twice and closed it, who deposits weekly and whose card failed, so your bonus rules offer what moves them."
source: https://signals.snowplow.io/solutions/gaming/cashier
---

[Gaming](https://signals.snowplow.io/solutions/gaming) / Cashier bonus timing 

# A cashier that can tell hesitation from habit. 

Signals tells your cashier and bonus rules how many times a player has opened the cashier this session, how often they deposit and whether the last attempt failed, so the bonus goes to the player it would move.

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

Higher deposit conversion

## Convert more deposits by offering the bonus to the player it would move. 

Deposit conversion moves when the cashier can tell a player who is hesitating from one who was going to deposit anyway. Signals gives your bonus rules how many times this player has opened the cashier this session, how often they deposit without a bonus, whether the last attempt failed and whether your protection tooling has flagged them. Your rules decide who sees an offer, which one, and who sees none.

Four ways operators use it

01 Players who open the cashier and close it Opened the cashier twice this session, no deposit. Offer the bonus now. 02 Players who deposit every week Four deposits this month, none with a bonus. Hold the bonus. 03 Players whose deposit failed Card declined a minute ago, back at the cashier. Offer another way to pay. 04 Players your protection tooling flagged Flagged by your protection tooling this session. Show no bonus, whatever the rules say. 

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

### Offer the bonus to the player who keeps opening the cashier

A player has opened the cashier twice this session and closed it both times without depositing. The bonus was set before the session for a whole segment, so it either showed on the first open, before this player had hesitated, or it waits for tomorrow's campaign. The second close is the moment a bonus would change the answer, and the player is still in the session.

What Signals knows

`cashier_opens_1h` 

Times the cashier was opened in the last hour

`deposits_1h` 

Deposits completed in the last hour

`deposits_30d` 

Deposits completed in the last 30 days

What your cashier and bonus rules do

Shows the deposit bonus your rules allow for this player once the cashier has been opened twice this session without a deposit, and keeps the standard cashier for a player who has opened it once.

Define it · PythonUse it · TypeScript

cashier.tsx · 14 lines Copy Copied 

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

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

# Keyed on the player, so a one-hour window stands in for the session.
cashier_opens_1h = Attribute(
    name="cashier_opens_1h",
    type="int32",
    events=[cashier_opened],
    aggregation="counter",
    period=timedelta(hours=1),
    default_value=0,
)

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

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

game_cashier_hesitation = StreamAttributeGroup(
    name="game_cashier_hesitation",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[cashier_opens_1h, deposits_1h, deposits_30d],
)
```

```
// In the code that renders the cashier
const player = await signals.getServiceAttributes({
  name: "game_cashier_hesitation",
  attribute_key: "user_id",
  identifier: playerId,
});
// { cashier_opens_1h: 2, deposits_1h: 0, deposits_30d: 1 }

if (player.cashier_opens_1h >= 2 && player.deposits_1h === 0) {
  cashier.show({
    bonus: bonusRules.offerFor(playerId),
    headline: "Add to your next deposit",
  });
}
```

### Hold the bonus from the player who deposits anyway

A player has deposited every week this month and did it without a bonus each time. A cashier bonus set for everyone in their tier pays them for a deposit they were about to make. The budget is spent where it changes nothing, and the margin on this player goes with it. For them the right cashier is the standard one.

What Signals knows

`deposits_30d` 

Deposits completed in the last 30 days

`bonus_deposits_30d` 

Deposits in the last 30 days that used a bonus

`days_since_deposit` 

Days since their last deposit

What your cashier and bonus rules do

Keeps the standard cashier, with no bonus, for a player whose deposits this month came in without one, and leaves the bonus budget for the player who hesitates.

Define it · PythonUse it · TypeScript

cashier.tsx · 13 lines Copy Copied 

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

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

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

bonus_deposits_30d = Attribute(
    name="bonus_deposits_30d",
    type="int32",
    events=[deposit],
    aggregation="counter",
    criteria=Criteria(all=[
        Criterion.eq(EventProperty(vendor="com.example", name="deposit", major_version=1, path="bonus_applied"), True),
    ]),
    period=timedelta(days=30),
    default_value=0,
)

days_since_deposit = Attribute(
    name="days_since_deposit",
    type="double",
    events=[deposit],
    aggregation="time_since_last",
    time_unit="d",
)

game_cashier_regulars = StreamAttributeGroup(
    name="game_cashier_regulars",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[deposits_30d, bonus_deposits_30d, days_since_deposit],
)
```

```
// In the code that renders the cashier
const player = await signals.getServiceAttributes({
  name: "game_cashier_regulars",
  attribute_key: "user_id",
  identifier: playerId,
});
// { deposits_30d: 4, bonus_deposits_30d: 0, days_since_deposit: 6.8 }

if (player.deposits_30d >= 3 && player.bonus_deposits_30d === 0) {
  cashier.show({ bonus: null });
} else {
  cashier.show({ bonus: bonusRules.offerFor(playerId) });
}
```

### Offer another payment method, not a bigger bonus

A player's first deposit attempt failed a minute ago and they are back at the cashier. Bonus rules that read a second open as hesitation will raise the offer, but this player was not hesitating. They were trying to pay. A bigger bonus leaves the declined card where it is, and a different payment method at the top of the cashier gets the deposit through.

What Signals knows

`last_deposit_failure` 

Why their last deposit attempt failed

`last_failed_method` 

The payment method that failed

`minutes_since_deposit_failed` 

Minutes since a deposit attempt failed

What your cashier and bonus rules do

Puts the other payment methods at the top of the cashier when a deposit attempt failed in the last few minutes, keeps the bonus at whatever your rules had set rather than raising it, and names the failed method so the player knows which one to skip.

Define it · PythonUse it · TypeScript

cashier.tsx · 15 lines Copy Copied 

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

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

last_deposit_failure = Attribute(
    name="last_deposit_failure",
    type="string",
    events=[deposit_failed],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="deposit_failed", major_version=1, path="reason"),
    period=timedelta(hours=1),
)

last_failed_method = Attribute(
    name="last_failed_method",
    type="string",
    events=[deposit_failed],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="deposit_failed", major_version=1, path="payment_method"),
    period=timedelta(hours=1),
)

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

game_cashier_failed = StreamAttributeGroup(
    name="game_cashier_failed",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[last_deposit_failure, last_failed_method, minutes_since_deposit_failed],
)
```

```
// In the code that renders the cashier
const player = await signals.getServiceAttributes({
  name: "game_cashier_failed",
  attribute_key: "user_id",
  identifier: playerId,
});
// { last_deposit_failure: "card_declined", last_failed_method: "card", minutes_since_deposit_failed: 1.2 }

if (player.minutes_since_deposit_failed <= 10) {
  cashier.show({
    methods: paymentMethods.without(player.last_failed_method),
    bonus: bonusRules.currentFor(playerId),
    note: `Your ${player.last_failed_method} payment did not go through. Try another way to pay.`,
  });
}
```

### Show no bonus once your protection tooling has flagged the player

Your player protection tooling has flagged this player during the session, under rules your team wrote. The bonus rules run on their own view of the session and read the same player as one worth encouraging, so the flag reaches the cashier after the offer has been shown. The flag has to arrive at the cashier before the bonus does. Your tooling sends the flag as an event, Signals carries it to the cashier, and what counts as a marker and what happens next stay with your team.

What Signals knows

`protection_flags_1h` 

Flags your protection tooling raised in the last hour

`last_protection_flag` 

The kind of flag it raised most recently

`cashier_opens_1h` 

Times the cashier was opened in the last hour

What your cashier and bonus rules do

Shows the plain cashier with no bonus of any kind when your protection tooling has flagged the player this session, before any other bonus rule runs, and shows the support options your protection rules set for that kind of flag.

Define it · PythonUse it · TypeScript

cashier.tsx · 17 lines Copy Copied 

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

# Sent by your protection tooling from the server when a rule of yours fires.
protection_flag = Event(name="protection_flag", vendor="com.example", version="1-0-0")
cashier_opened = Event(name="cashier_opened", vendor="com.example", version="1-0-0")

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

last_protection_flag = Attribute(
    name="last_protection_flag",
    type="string",
    events=[protection_flag],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="protection_flag", major_version=1, path="flag_type"),
    period=timedelta(hours=1),
)

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

game_cashier_protection = StreamAttributeGroup(
    name="game_cashier_protection",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[protection_flags_1h, last_protection_flag, cashier_opens_1h],
)
```

```
// In the code that renders the cashier, before any bonus rule runs
const player = await signals.getServiceAttributes({
  name: "game_cashier_protection",
  attribute_key: "user_id",
  identifier: playerId,
});
// { protection_flags_1h: 1, last_protection_flag: "session_length", cashier_opens_1h: 3 }

if (player.protection_flags_1h > 0) {
  cashier.show({
    bonus: null,
    support: protectionRules.optionsFor(player.last_protection_flag),
  });
  return;
}

cashier.show({ bonus: bonusRules.offerFor(playerId) });
```

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