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: game.economy.balance returns the full wallet list for the player
local wallets = game.economy.balance(player_id)
-- each entry looks like { currency = "...", balance = N }%% Erlang: fetch the wallet for a single currency, then debit
case asobi_economy:get_or_create_wallet(PlayerId, <<"gold">>) of
{ok, #{balance := 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 historyItems
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 itemStore
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 listinggame.economy.purchase(player_id, "shop:starter_pack")asobi_economy:purchase(PlayerId, <<"shop:starter_pack">>).Server-side grants
%% grant currency (e.g. match rewards)
asobi_economy:grant(PlayerId, <<"gold">>, 100, #{reason => <<"match_reward">>}).
%% debit
asobi_economy:debit(PlayerId, <<"gold">>, 50, #{reason => <<"respawn_fee">>}).
%% items are granted via the store/purchase flow or by writing an
%% asobi_player_item row through asobi_repo — there is no grant_item/3 helper.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:8080/api/v1/iap/apple \
-H 'Authorization: Bearer <session_token>' \
-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:8080/api/v1/iap/google \
-H 'Authorization: Bearer <session_token>' \
-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:grant(PlayerId, <<"gems">>, 100, #{reason => <<"iap_apple">>});
{ok, #{valid := false}} ->
{error, invalid_receipt};
{error, Reason} ->
{error, Reason}
end.