Virtual-currency wallets with an append-only ledger.
Currencies are free-form string codes ("gold", "gems", "energy") — the
game decides which exist. Every balance change is atomic and recorded in the
ledger, so two concurrent spends can never overspend and every mutation is
auditable.
Usage (server-side / hooks)
Economy.grant(user_id, "gold", 100, reason: "match_reward")
case Economy.spend(user_id, "gold", 30, reason: "store_purchase") do
{:ok, balance} -> :ok
{:error, :insufficient_funds} -> :not_enough_gold
end
Economy.balance(user_id, "gold") #=> 70
Economy.balances(user_id) #=> %{"gold" => 70}Idempotency
Pass :idempotency_key so a retried request (network retry, at-least-once job)
can't double-apply — the second call is a no-op that returns the current
balance:
Economy.grant(user_id, "gems", 5, idempotency_key: "purchase:#{order_id}")Safety
These are server-authoritative: expose them from hooks and admin tools, never as a raw client "add currency" endpoint. Clients only read their wallet.
Note: This is an SDK stub. Calling these functions will raise an error. The actual implementation runs on the GameServer.
Summary
Functions
Current balance of one currency (0 when the user has no wallet for it).
All non-zero balances for a user, as a %{currency => balance} map.
Add amount of currency to a user's wallet.
Remove amount of currency from a user's wallet, atomically.
Subscribe the calling process to a user's live wallet updates.
Stop receiving a user's wallet updates.
Types
@type currency() :: String.t()
@type user_id() :: Ecto.UUID.t()
Functions
@spec balance(user_id(), currency()) :: non_neg_integer()
Current balance of one currency (0 when the user has no wallet for it).
@spec balances(user_id()) :: %{required(currency()) => non_neg_integer()}
All non-zero balances for a user, as a %{currency => balance} map.
@spec grant(user_id(), currency(), pos_integer(), keyword()) :: {:ok, non_neg_integer()} | {:error, term()}
Add amount of currency to a user's wallet.
Options: :reason (ledger label), :idempotency_key, :metadata.
Returns {:ok, new_balance}.
@spec spend(user_id(), currency(), pos_integer(), keyword()) :: {:ok, non_neg_integer()} | {:error, :insufficient_funds | term()}
Remove amount of currency from a user's wallet, atomically.
Returns {:ok, new_balance} or {:error, :insufficient_funds} — the balance
is never left negative.
Subscribe the calling process to a user's live wallet updates.
@spec unsubscribe(user_id()) :: :ok
Stop receiving a user's wallet updates.