Purchases
Your game declares what it sells as data. The SDK sells it through the platform's store, Brain checks every purchase on its server and keeps what each player owns, and your game reads the result. You never handle receipts or store product ids in game code.
Declare your products
Products live in brain/game.toml:
[purchases]
public = ["supporter"] # entitlements other players see, as badges
[[purchases.product]]
id = "supporter"
kind = "unlock"
unlocks = "supporter"
name = "Supporter"
about = "A golden frame everyone sees."
[[purchases.product]]
id = "ranked_5"
kind = "consumable"
credits = { ranked = 5 } # Brain keeps the balance and spends it
[[purchases.product]]
id = "coins_500"
kind = "consumable"
deliver = { coins = 500 } # handed to your save once
[[purchases.product]]
id = "pass"
kind = "subscription"
unlocks = "pass"
period = "month" # week, month or year
| Kind | Grants | Ends |
|---|---|---|
unlock |
An entitlement forever; a store restore brings it back on a new device | Refund |
consumable with credits |
A balance Brain keeps and spends, such as match credits | When spent |
consumable with deliver |
An amount handed to your game's save once | Your game's to handle |
subscription |
An entitlement until the paid period ends, grace included; optional credits each period |
Expiry or refund |
Ids and granted names are 1 to 16 characters of a-z, 0-9 and _. Prices aren't in
game.toml: your game shows the store's localized price, and you set prices in the console. Store
product ids follow a convention (<bundle id>.<id> on the App Store, <id> on Google Play); see
store integrations to set up each store.
Products turn accounts on, because what a player owns belongs to an account. A game without online play gets a silent sign-in and nothing more.
Check the file after a change:
ase games check --project .
Stores
The SDK picks the build's store on its own:
| Store | Where | Notes |
|---|---|---|
| App Store | iOS 15 and macOS 12 onwards, through StoreKit 2 | Ask to Buy answers pending; the grant comes later |
| Google Play | Android | Brain acknowledges or consumes each purchase; your game never does |
| Steam | Desktop builds while the player is signed in to Steam | Steam's overlay asks the player to approve; prices come from Brain, such as 2.99 EUR |
| Dev store | Debug and development builds, or any build run with --brain-dev-store (-brain-dev-store in Unreal) |
Free, answers on the next frame; never replaces a real store |
A release build without a store offers nothing. A purchase waits on the device until Brain grants it, and the store's transaction is finished only after that grant, so a purchase is never lost to a crash or a dropped connection.
Sell a product
- Offer only what Brain says is on sale. A product is missing while it isn't on sale, for example before you set its price.
- Show the product's
price, never one of your own. - Say when the player sees the offer. That's the funnel's
offeredstep; reading the list records nothing. - Buy it. The answer is
granted,cancelled,pendingorfailed. - Read what the player owns again whenever it changes.
#include "BrainSubsystem.h"
void UMyShop::Open()
{
UBrainSubsystem* Brain = GetGameInstance()->GetSubsystem<UBrainSubsystem>();
Brain->OnPurchasesChanged.AddDynamic(this, &UMyShop::Refresh);
Brain->OnProductBought.AddDynamic(this, &UMyShop::HandleBought);
Refresh();
Brain->OfferedProduct(TEXT("supporter"));
}
// UFUNCTION()
void UMyShop::Refresh()
{
UBrainSubsystem* Brain = GetGameInstance()->GetSubsystem<UBrainSubsystem>();
const FString Unlocks = Brain->GetProductsJson(TEXT("unlocks"), TEXT("supporter"));
const bool bOwned = Brain->HasEntitlement(TEXT("supporter"));
const int64 Credits = Brain->GetCredits(TEXT("ranked"));
// Show Unlocks' price on the button, unless bOwned.
}
void UMyShop::Buy()
{
GetGameInstance()->GetSubsystem<UBrainSubsystem>()->BuyProduct(TEXT("supporter"));
}
// UFUNCTION()
void UMyShop::HandleBought(const FString& Product, const FString& Result, const FString& TransactionId,
const FString& Reason)
{
if (Result == TEXT("granted")) ShowThanks();
}
GetProductsJson(FilterKey, FilterValue) answers a JSON array of offers, each with the store's
localized price, filtered by kind, unlocks, credits or deliver; empty strings list them all.
Blueprint has the same nodes and events under ASE Brain > Purchases.
Unreal sells through store modules: AseBrainSteam (enable Unreal's OnlineSubsystemSteam, which
starts Steam), AseBrainApple for the App Store and AseBrainAndroid for Google Play.
What the player owns
| Read | Unreal |
|---|---|
| Has an entitlement now | HasEntitlement(Name) |
| Until when, in seconds since the epoch: -1 forever, 0 not at all | GetEntitlementUntil(Name) |
| A credit balance | GetCredits(Name) |
| Free matches left today in a queue, -1 without a limit | GetDailyLeft(Queue) |
| Read again from Brain | RefreshPurchases() |
| Something changed | OnPurchasesChanged |
What a player owns is cached on the device, so these work offline. Read them again on every change signal: a grant, a renewal, an expiry or a refund.
Delivered products
A deliver product hands its amounts to your game once. Add them to your save, save, then confirm
the delivery, so Brain never hands it over twice. Until you confirm, it comes again at every launch.
// UFUNCTION(), bound to Brain->OnPurchaseDelivery
void UMyShop::HandleDelivery(const FString& Source, const FString& TransactionId, const FString& Product,
const FString& DeliverJson)
{
AddToSave(DeliverJson); // {"coins":500}
WriteSave();
GetGameInstance()->GetSubsystem<UBrainSubsystem>()->ConfirmDelivery(Source, TransactionId);
}
Restore and manage
Every game with purchases puts Restore purchases in its settings, and Manage subscriptions when it sells subscriptions. Restore brings back unlocks and subscriptions bought before, from the store and from Brain. Manage opens the store's own subscriptions page.
| Unreal | |
|---|---|
| Restore | RestorePurchases(), then OnPurchasesRestored |
| Manage | ManageSubscriptions() |
Restore answers false when Brain couldn't be reached.
Credits and the daily limit
A matchmaking queue can give each player a few free matches a day and spend credits past them:
[[matchmaking.queue]]
id = "ranked"
daily = 1 # free matches a local day
credits = "ranked" # spent past the free ones
Once the free matches and the player's credits are used, queueing fails with daily_limit. Offer
the products that grant those credits. A match that ends before it really started gives back what
it spent. See Multiplayer.
Test with the dev store
Debug and development builds sell through the dev store: every purchase is free and answers on the next frame. Make the next purchase end another way, or pretend the store renewed, put in grace or billing retry, expired or refunded a subscription:
Brain->DevFakePurchase(TEXT("pending"));
Brain->DevStoreEvent(Brain->GetPurchaseTransaction(TEXT("pass")), TEXT("refund"));
The purchase funnel
The SDK records the core purchase event on its own: offered when your game says a product was
offered (once a minute per product at most), started when a buy begins, and finished with its
result. That's why you call offered yourself: reading the product list counts as nothing, so you can
check what's on sale anywhere without skewing the funnel.