SDK / Unity

Purchases

UnstableUnity 6000.6+

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

  1. 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.
  2. Show the product's price, never one of your own.
  3. Say when the player sees the offer. That's the funnel's offered step; reading the list records nothing.
  4. Buy it. The answer is granted, cancelled, pending or failed.
  5. Read what the player owns again whenever it changes.
using Ase.Brain;
using Newtonsoft.Json.Linq;

void Start()
{
    Brain.Purchases.Changed += Refresh;
    Refresh();
}

void Refresh()
{
    string json = Brain.Purchases.Product("supporter");
    bool onSale = json != null && !Brain.Purchases.Has("supporter");
    buyButton.gameObject.SetActive(onSale);
    if (onSale) buyLabel.text = "Become a supporter: " + (string)JObject.Parse(json)["price"];
    creditsLabel.text = Brain.Purchases.Credits("ranked").ToString();
}

public void OnShopOpened() { Brain.Purchases.Offered("supporter"); }

public void OnBuyPressed()
{
    Brain.Purchases.Buy("supporter", result =>
    {
        if (result == "granted") ShowThanks();
        else if (result == "pending") ShowMessage("Your purchase is waiting for approval.");
    });
}

Products() answers a JSON array of {id, store_product, kind, name, about, price, credits, deliver, unlocks, period}, and Product(id) one of them, or null while it isn't on sale. Every call returns at once; answers run on the engine thread. Brain.Purchases.Source names the store this build sells through (appstore, play, steam or dev), or null without one.

On Steam, Brain starts Steam itself unless your game's own Steamworks binding already did. If yours did, forward the two callbacks Brain needs:

BrainSteamworks.Current.ForwardWebApiTicket(handle, result, ticket);
BrainSteamworks.Current.ForwardMicroTxnAuthorization(orderId, authorized);

What the player owns

Read Unity
Has an entitlement now Has(name)
Until when, in seconds since the epoch: -1 forever, 0 not at all Until(name)
A credit balance Credits(name)
Free matches left today in a queue, -1 without a limit DailyLeft(queue)
Read again from Brain Refresh(done)
Something changed Changed

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.

Brain.Purchases.Delivery += grant =>
{
    var delivery = JObject.Parse(grant.Json);
    save.coins += (int)(delivery["deliver"]["coins"] ?? 0);
    SaveGame();
    Brain.Purchases.Delivered((string)delivery["source"], (string)delivery["transaction_id"]);
};

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.

Unity
Restore Brain.Purchases.Restore(done)
Manage Brain.Purchases.Manage()

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.Purchases.DevFake("pending");
Brain.Purchases.DevStoreEvent(Brain.Purchases.TransactionId("pass"), "refund", result => { });

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.