Docs / Economy

Economy & IAP

Virtual economy primitives: wallets (multi-currency), item definitions, player inventory, store listings, and server-side validated in-app purchases (Apple + Google Play). All balance changes go through a transactional ledger.

Wallets

Each player can have multiple wallets, one per currency. Every change is a transaction in an audit-ready ledger.

Lua

-- Lua
local wallet = game.economy.balance(player_id)
if (wallet.gold or 0) >= 100 then
    game.economy.debit(player_id, "gold", 100, "shop_buy")
end

Erlang

%% Erlang
case asobi_economy:balance(PlayerId, <<"gold">>) of
    {ok, Bal} when Bal >= 100 ->
        asobi_economy:debit(PlayerId, <<"gold">>, 100, #{reason => store_purchase});
    _ ->
        {error, insufficient}
end.

REST

GET  /api/v1/wallets                   List wallets
GET  /api/v1/wallets/:currency/history Transaction history

Items

Items are defined globally (asobi_item_def) and granted to players as instances (asobi_player_item). Definitions have slug, name, category, rarity, stackable and arbitrary metadata.

GET  /api/v1/inventory                 List player items
POST /api/v1/inventory/consume         Consume an item

Store

Listings bind an item definition to a currency and price. Purchases are atomic — wallet debit and inventory grant run in one DB transaction via Kura Multi.

GET  /api/v1/store                     List store catalog
POST /api/v1/store/purchase            Purchase a listing

Lua

game.economy.purchase(player_id, "shop:starter_pack")

Erlang

asobi_economy:purchase(PlayerId, <<"shop:starter_pack">>).

Server-side grants

%% grant currency (e.g. match rewards)
asobi_economy:credit(PlayerId, <<"gold">>, 100, #{reason => match_reward}).

%% debit
asobi_economy:debit(PlayerId, <<"gold">>, 50, #{reason => respawn_fee}).

%% grant an item directly
asobi_economy:grant_item(PlayerId, <<"sword_of_fire">>, 1).

ACID. Every economy call uses a DB transaction. Double-spend, inconsistent inventory, or “currency went missing” bugs are architecturally prevented, not just tested for.

In-app purchases

Server-side receipt validation for Apple App Store and Google Play. Always validate on the server — client receipts can be spoofed. Grant currency/items only after validation returns valid: true.

Apple App Store

StoreKit 2 signed transactions (JWS). Client sends the JWS string after a purchase:

curl -X POST http://localhost:8082/api/v1/iap/apple \
  -H 'Authorization: Bearer ' \
  -H 'Content-Type: application/json' \
  -d '{"signed_transaction": "eyJhbGciOi..."}'
{
  "product_id": "com.example.game.gems_100",
  "transaction_id": "2000000123456789",
  "purchase_date": 1711700000000,
  "type": "Consumable",
  "valid": true
}

Config: {apple_bundle_id, <<"com.example.game">>} must match your app bundle.

Google Play

Google Play Developer API. Client sends product ID and purchase token:

curl -X POST http://localhost:8082/api/v1/iap/google \
  -H 'Authorization: Bearer ' \
  -H 'Content-Type: application/json' \
  -d '{"product_id": "gems_100", "purchase_token": "..."}'

Granting after validation

case asobi_iap:verify_apple(SignedTransaction) of
    {ok, #{product_id := <<"gems_100">>, valid := true}} ->
        asobi_economy:credit(PlayerId, <<"gems">>, 100, #{reason => iap_apple});
    {ok, #{valid := false}} ->
        {error, invalid_receipt};
    {error, Reason} ->
        {error, Reason}
end.

Where next?