---
title: "In-game store offers for studios | Snowplow Signals"
description: "Snowplow Signals tells your game server what a player ran out of, where, and what they have bought before, so it builds the store offer that fits this session."
source: https://signals.snowplow.io/solutions/gaming/store
---

[Gaming](https://signals.snowplow.io/solutions/gaming) / In-game store offers 

# A store offer that fits what the player ran out of. 

Signals tells your game server what this player ran out of, where it happened and what they have bought before, so the offer it builds fits the session instead of the plan set before it started.

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

Higher ARPDAU

## Earn more per player per day by building the offer for the session they are in. 

ARPDAU moves when the offer a player sees matches what they need right now, rather than the bundle chosen before the session began. Signals gives your game server what this player ran out of and where, how many times they have opened the store this session, what they tend to buy and whether they have bought at all. Your server builds the offer under the rules your monetization team set.

Four ways studios use it

01 Players out of premium currency mid-level Out of gems two rooms from the end. Offer the small gem bundle, here. 02 Players who open the store and leave Three store opens this session, no purchase. Show the starter offer, once. 03 Players who buy one kind of cosmetic Buys skins, only skins. Lead the store with skins. 04 Daily players who have never bought Played every day for two weeks, never bought. Offer on the next level clear. 

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

### Offer the small bundle of what the player just ran out of

A player has run out of gems two rooms from the end of a level, and the store opens on the standard bundle because that is what was configured before the session began. The bundle is sized for a planned purchase. What this player wants is enough gems to finish the room they are standing in, and by the time the catalog is retuned the level is over. The small gem bundle, offered where they ran out, is the purchase that fits.

What Signals knows

`last_currency_depleted` 

The premium currency they most recently ran out of

`depleted_at` 

The level they were in when it ran out

`minutes_since_depleted` 

Minutes since it happened

What your game server does

Opens the store on the small bundle of the currency that just ran out, placed at the point in the level where it happened, when the depletion is a few minutes old. Which bundle counts as small, and its price, come from your monetization team's rules.

Define it · PythonUse it · TypeScript

game-server.ts · 14 lines Copy Copied 

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

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

last_currency_depleted = Attribute(
    name="last_currency_depleted",
    type="string",
    events=[currency_depleted],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="currency_depleted", major_version=1, path="currency"),
)

depleted_at = Attribute(
    name="depleted_at",
    type="string",
    events=[currency_depleted],
    aggregation="last",
    property=EventProperty(vendor="com.example", name="currency_depleted", major_version=1, path="level_id"),
)

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

# Players are signed in, so the game server asks by player id.
game_store_depleted = StreamAttributeGroup(
    name="game_store_depleted",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[last_currency_depleted, depleted_at, minutes_since_depleted],
)
```

```
// In the game server, when the client asks for the store
const player = await signals.getServiceAttributes({
  name: "game_store_depleted",
  attribute_key: "user_id",
  identifier: playerId,
});
// { last_currency_depleted: "gems", depleted_at: "w3_l7", minutes_since_depleted: 1.4 }

if (player.minutes_since_depleted < 10) {
  store.open({
    lead: bundles.small(player.last_currency_depleted),
    placement: { level: player.depleted_at },
  });
}
```

### Show the starter offer when the store keeps getting closed

A player has opened the store three times in this session and closed it each time without buying. The catalog is the same on the third open as on the first, and the starter offer the monetization team set aside for players like this sits behind a segment that gets rebuilt later, after the session has ended. Three opens and no purchase is the moment that offer was made for, and the player is in the store right now.

What Signals knows

`store_opens_session` 

Times they opened the store in the last hour

`purchases_session` 

Purchases in the last hour

What your game server does

Shows the starter offer your rules allow once per player when the store has been opened three times this session without a purchase, and records on your side that this player has had it, so the fourth open goes back to the catalog.

Define it · PythonUse it · TypeScript

game-server.ts · 14 lines Copy Copied 

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

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

# A session on this title runs well under an hour, so an hour-long window stands in for it.
store_opens_session = Attribute(
    name="store_opens_session",
    type="int32",
    events=[store_opened],
    aggregation="counter",
    period=timedelta(hours=1),
    default_value=0,
)

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

game_store_browsing = StreamAttributeGroup(
    name="game_store_browsing",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[store_opens_session, purchases_session],
)
```

```
// In the game server, when the client asks for the store
const player = await signals.getServiceAttributes({
  name: "game_store_browsing",
  attribute_key: "user_id",
  identifier: playerId,
});
// { store_opens_session: 3, purchases_session: 0 }

const eligible = await offers.notYetShown(playerId, "starter");

if (player.store_opens_session >= 3 && player.purchases_session === 0 && eligible) {
  store.open({ lead: offers.starter });
  offers.markShown(playerId, "starter");
}
```

### Lead the store with what this player buys

Every purchase this player has made in the last three months has been a skin. The store front leads with the currency bundles and the season pass, because the front page is laid out for the average player and changes on the content schedule. This player scrolls past the top row every time. Leading with skins puts the thing they buy where they look first.

What Signals knows

`purchases_by_type_90d` 

How many items of each type they bought in the last ninety days

`last_item_type` 

The type of the last item they bought

What your game server does

Moves the item type this player keeps buying to the top row of the store when it makes up most of their purchases this quarter, and keeps the rest of the layout as the content team set it.

Define it · PythonUse it · TypeScript

game-server.ts · 15 lines Copy Copied 

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

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

purchases_by_type_90d = Attribute(
    name="purchases_by_type_90d",
    type="dict",
    events=[purchase],
    aggregation="category_count",
    property=item_type,
    period=timedelta(days=90),
)

last_item_type = Attribute(
    name="last_item_type",
    type="string",
    events=[purchase],
    aggregation="last",
    property=item_type,
)

game_store_cosmetics = StreamAttributeGroup(
    name="game_store_cosmetics",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[purchases_by_type_90d, last_item_type],
)
```

```
// In the game server, when it lays out the store front
const player = await signals.getServiceAttributes({
  name: "game_store_cosmetics",
  attribute_key: "user_id",
  identifier: playerId,
});
// { purchases_by_type_90d: { skin: 6, emote: 1 }, last_item_type: "skin" }

const counts = Object.entries(player.purchases_by_type_90d);
const total = counts.reduce((sum, [, n]) => sum + n, 0);
const [topType, topCount] = counts.sort((a, b) => b[1] - a[1])[0];

if (topCount / total > 0.6) {
  store.lead({ row: topType });
}
```

### Make the first-purchase offer to a daily player on a level clear

A player has cleared levels on twelve of the last fourteen days and has never bought anything. The first-purchase offer is in the catalog, and it reaches players on a schedule that does not know how long their streak is or what they just did. A daily player who has just cleared a level is the one most open to it, and the level-clear screen is where it belongs, before they go back to the map.

What Signals knows

`active_days_14d` 

Days with a level cleared in the last fourteen

`lifetime_purchases` 

Purchases since the account was created

What your game server does

Puts the first-purchase offer on the level-clear screen when a player with no purchases has cleared a level on most of the last fourteen days, and leaves the screen alone for anyone who has bought before or plays now and then.

Define it · PythonUse it · TypeScript

game-server.ts · 11 lines Copy Copied 

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

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

active_days_14d = Attribute(
    name="active_days_14d",
    type="int32",
    events=[level_completed],
    aggregation="approx_count_distinct",
    property=AtomicProperty(name="derived_tstamp", date_part="active_day"),
    period=timedelta(days=14),
)

# No period, so the count runs over the whole account; the ttl expires it for a player who stops playing.
lifetime_purchases = Attribute(
    name="lifetime_purchases",
    type="int32",
    events=[purchase],
    aggregation="counter",
    ttl=timedelta(days=365),
    default_value=0,
)

game_store_first_purchase = StreamAttributeGroup(
    name="game_store_first_purchase",
    version=1,
    attribute_key=user_id,
    owner="you@example.com",
    attributes=[active_days_14d, lifetime_purchases],
)
```

```
// In the game server, when it builds the level-clear screen
const player = await signals.getServiceAttributes({
  name: "game_store_first_purchase",
  attribute_key: "user_id",
  identifier: playerId,
});
// { active_days_14d: 12, lifetime_purchases: 0 }

if (player.lifetime_purchases === 0 && player.active_days_14d >= 12) {
  levelClear.show({ offer: offers.firstPurchase });
}
```

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