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")
endErlang
%% 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 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 listingLua
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.