Systems
EconomySystem
Currencies saved in player profiles, shops with limits and dynamic prices, timed rewards with streaks, player trades, Robux products and safe offline transactions.
Getting it
local EconomySystem = Engine:GetSystem("EconomySystem")Overview
EconomySystem is a complete in-game economy on top of DataService:
- Currencies with limits, decimals, formatting, and optional leaderstats and leaderboards.
- Transactions (deposit, withdraw, set) that never take a balance below its minimum or above its maximum, are reported to StatisticsService, and replicate to the player.
- Shops with fixed or dynamic prices, purchase limits, requirements and grant callbacks.
- Timed rewards such as daily rewards, with streaks.
- Trades between two players in the same server.
- Robux products that grant currency through safe receipts.
- Offline and cross-server deposits, withdrawals and transfers that are applied exactly once.
Balances live in player profiles. By default the system creates and runs its own profile store (Engine.Economy) and starts a session for every player. If your game already has a player profile store, call :BindProfileStore(store) before players join, and balances are stored in your profiles under ProfilePath (default "Economy"). The system adds the tables it needs there itself. When your data already holds a value that is not a table at ProfilePath, it errors instead of overwriting that value.
local EconomySystem = Engine:GetSystem("EconomySystem")
EconomySystem:CreateCurrency("Coins", { Symbol = "C", Leaderstat = true })
EconomySystem:CreateCurrency("Gems", { Symbol = "G", Max = 100000 })
EconomySystem:CreateShop("Weapons", {
Currency = "Coins",
Items = {
{ Id = "Sword", Price = 250, Limit = 1, Grant = function(player) giveTool(player, "Sword") end },
{ Id = "Potion", Price = 40 },
},
})
EconomySystem:CreateReward("Daily", {
Currency = "Coins",
Amount = function(player, streak)
return 100 + streak * 25
end,
Cooldown = 24 * 3600,
MaxStreak = 7,
})
-- Somewhere in your game
EconomySystem:Deposit(player, "Coins", 50, { Transaction = "Gameplay", Sku = "EnemyKill" })local EconomySystem = Engine:GetSystem("EconomySystem")
EconomySystem:OnBalanceChanged("Coins", function(balance)
coinsLabel.Text = EconomySystem:FormatBalance("Coins", balance)
end)
buySwordButton.Activated:Connect(function()
EconomySystem:GetShop("Weapons"):PurchaseAsync("Sword"):catch(function(reason)
showError(reason)
end)
end)Currencies
CreateCurrency
EconomySystem:CreateCurrency(name: string, options: CurrencyOptions?): Currency| Option | Description |
|---|---|
DisplayName, Symbol, Icon | How the currency is shown. |
Default | Starting balance for new players. |
Min, Max | Limits (default 0 and MaxBalance). |
Decimals | Decimal places; amounts are rounded to this. |
Leaderstat | Mirror the balance into the player list (LeaderboardSystem stat). |
Leaderboard | true or leaderboard options: keep a global leaderboard of balances, synced every LeaderboardSyncSeconds. |
Replicate | Send the definition to clients. |
A Currency has :Round(amount), :Clamp(amount), :Format(amount), :Describe(), :Destroy() and read-only fields for every option. :GetCurrency(name), :FindCurrency(name) and :GetCurrencies() look them up.
Balances and transactions
| Method | Description |
|---|---|
:GetBalance(player, currency) | The balance (needs the player's profile to be loaded). |
:GetBalanceAsync(player, currency) | Waits for the profile first. |
:GetBalances(player) | Every balance by currency. |
:CanAfford(player, currency, amount) | |
:Deposit(player, currency, amount, options?): Result | Adds money. |
:Withdraw(player, currency, amount, options?): Result | Removes money; fails with "Insufficient" instead of going below the minimum. |
:SetBalance(player, currency, value, options?): Result |
A Result is { Ok, Balance, Delta, Reason? }. Deposits are clamped to the currency's maximum, so Delta tells you how much actually changed. Reasons: "Insufficient", "NoProfile", "Invalid".
Transaction options: { Transaction?, Sku?, Fields? }. Transaction is the Roblox Analytics transaction type (default DefaultTransactionType); Sku names what the money was for.
| Event | Fires when |
|---|---|
BalanceChanged: Signal<(player, currency, balance, delta, info)> | A balance changed. info has the flow, transaction type, SKU and previous balance. |
TransactionRejected: Signal<(player?, currency, amount, reason)> | A withdrawal, purchase or claim was refused. |
Offline and cross-server
| Method | Description |
|---|---|
:DepositAsync(playerOrUserId, currency, amount, options?) | Works for players on any server or offline, through a DataService edit that is applied exactly once. |
:WithdrawAsync(playerOrUserId, currency, amount, options?) | The same for withdrawals. |
:TransferAsync(from, to, currency, amount, options?) | Moves money between two players, wherever they are. |
They resolve with { Ok, Status?, Balance?, Delta?, Reason? }, where Status is the edit result ("applied", "queued", "rejected", "missing"). Options are the transaction options plus WaitForApply. Use them for gifts, admin tools and rewards from other places.
Shops
CreateShop
EconomySystem:CreateShop(name: string, options: { Currency, Items, OnPurchase?, MaxQuantity?, Replicate? }): Shop| Item option | Description |
|---|---|
Id | Unique within the shop. |
Name, Description, Icon, Data | For your UI; replicated to clients. |
Price | A number, or function(player, quantity) -> total for dynamic prices such as discounts or scaling costs. |
Currency | Overrides the shop's currency. |
Limit | Most units one player can ever buy. |
Requires | function(player, item, quantity) -> (boolean, reason?), for example a level requirement. |
Grant | function(player, item, quantity, total) called after payment, to give the item. |
OnPurchase is called for every purchase in the shop. MaxQuantity caps units per purchase.
| Shop member | Description |
|---|---|
:PurchaseAsync(player, itemId, quantity?): Promise<PurchaseResult> | Checks everything, takes the money, calls the grant functions and records the count. Rejects with a reason. |
:CanPurchase(player, itemId, quantity?): (boolean, reason?) | |
:GetPrice(player, itemId, quantity?) | |
:GetPurchaseCount(player, itemId) | |
:GetItem(id), :FindItem(id), Items, :Describe() | |
Purchased: Signal<(player, result)>, Rejected: Signal<(player, itemId, reason)> |
A PurchaseResult is { Item, Quantity, Total, Balance, Count }. If a grant function errors, the payment is refunded.
Timed rewards
CreateReward
EconomySystem:CreateReward(name: string, options: { Currency, Amount, Cooldown, StreakWindow?, MaxStreak?, Replicate? }): RewardA reward the player can claim once per Cooldown seconds. Claiming again within StreakWindow after it became available raises the streak (up to MaxStreak); waiting longer resets it. Amount is a number or function(player, streak) -> amount.
| Reward member | Description |
|---|---|
:ClaimAsync(player): Promise<{ Amount, Streak, Balance }> | |
:GetStatus(player) | { Available, SecondsLeft, Streak, NextStreak, NextAmount?, LastClaim? }. |
:Reset(player), :Describe(), :Destroy() | |
Claimed: Signal<(player, result)> |
Trades
CreateTrade
EconomySystem:CreateTrade(a: Player, b: Player, options: { Timeout? }?): TradeA trade window between two players in this server. Each side offers amounts of currencies; any change resets both acceptances, and when both players accept the exchange happens in one step, so nobody can be scammed by a last-second change.
| Trade member | Description |
|---|---|
:Offer(player, currency, amount) | Sets what this player offers (0 removes it). |
:GetOffer(player) | { [currency] = amount }. |
:Accept(player), :Unaccept(player), :HasAccepted(player) | |
:Cancel(reason?) | |
:Other(player) | The other participant. |
Id, Players, Status ("Open", "Completed", "Cancelled"), Reason, IsOpen | |
Changed, Completed, Cancelled(reason) | Signals. |
Open trades cancel themselves after TradeTimeoutSeconds, and when a participant leaves. :GetTrade(id), :FindTrade(id) and :FindTradeFor(player) look trades up; TradeCreated fires for each new trade.
Robux products
DefineProduct
EconomySystem:DefineProduct(productId: number, { Currency, Amount, Name? }): ProductA developer product that grants currency. :CreateReceiptProcessor(handlers?, options?) returns a ProcessReceipt callback that grants these products exactly once (built on DataService receipts); pass extra handlers for other products. ProductGranted: Signal<(player, product, receipt)> fires after each grant.
If you use MonetizationService, let it own the receipt callback and deposit currency from its product handlers instead.
Profiles
| Member | Description |
|---|---|
:BindProfileStore(store) | Use your own profile store. Must happen before the first balance is touched. |
:HasProfile(player), :GetProfileAsync(player, timeout?) | |
ProfileStoreName, ProfilePath, IsOwnedStore, CurrencyCount, ShopCount, OpenTradeCount | Read-only. |
LogToStatistics | Settable. |
Client
| Member | Description |
|---|---|
:GetBalance(currency), :GetBalanceAsync(currency, timeout?), :GetBalances() | The local player's balances, kept up to date. |
:OnBalanceChanged(currency, fn(balance, delta)) | |
:FormatBalance(currency, amount) | Uses the currency's symbol and decimals. |
:GetCurrency(name), :FindCurrency(name), :GetCurrencies() | Replicated definitions. |
:GetShop(name), :FindShop(name), :GetShops() | A client Shop has Items, :GetItem(id), :GetQuoteAsync(itemId, quantity?) and :PurchaseAsync(itemId, quantity?). |
:GetReward(name), :FindReward(name), :GetRewards() | A client Reward has :GetStatusAsync() and :ClaimAsync(). |
:WaitForCatalogAsync(timeout?) | |
BalanceChanged: Signal<(currency, balance, delta)>, CatalogChanged |
Purchases and claims requested by the client are validated and executed on the server; the client cannot change balances. Requests are rate-limited (ClientRequestRatePerSec).
Configuration
DefaultConfiguration/Systems/EconomySystem22 settings| Setting | Default | What it does |
|---|---|---|
| ProfileStoreName | "Engine.Economy" | ProfileStore the system owns when no store is bound with :BindProfileStore. |
| ProfilePath | "Economy" | Path inside a profile's Data the system keeps its state under. |
| ProfileTimeout | 30 | Seconds :GetBalanceAsync / :DepositAsync wait for a player's profile session before rejecting. |
| MaxBalance | 1e15 | Ceiling a currency gets when Max is omitted. |
| DefaultTransactionType | "Gameplay" | Roblox transaction type a deposit/withdrawal is logged under when the call passes none. |
| LogToStatistics | true | Forward balance changes and product grants to StatisticsService. |
| WarnOnRejected | false | Also warn() when a withdrawal, purchase, claim or trade is rejected. |
| LeaderboardSyncSeconds | 120 | Seconds between pushes of changed balances to a currency's leaderboard. |
| LeaderboardRefreshInterval | 60 | Seconds between top-list refreshes of a currency leaderboard when its options pass none. |
| ShopMaxQuantity | 99 | Most units of one item a single purchase may buy when a shop passes no MaxQuantity. |
| ShopTransactionType | "Shop" | Roblox transaction type shop purchases are logged under. |
| RewardTransactionType | "TimedReward" | Roblox transaction type timed-reward claims are logged under. |
| ProductTransactionType | "IAP" | Roblox transaction type Robux product grants are logged under. |
| TradeTransactionType | "Gameplay" | Roblox transaction type trade legs are logged under. |
| TradeTimeoutSeconds | 120 | Seconds an open trade may sit without completing before it cancels itself. |
| TradeMaxCurrencies | 10 | Distinct currencies one side of a trade may offer. |
| AdjustEditName | "Engine.Economy.Adjust" | DataService edit name offline deposits/withdrawals travel under. |
| TransferName | "Engine.Economy.Transfer" | DataService transfer name :TransferAsync uses between profiles. |
| ReplicateCatalog | true | Send currency definitions, shop catalogs and reward definitions to clients. |
| ClientRequestRatePerSec | 2 | Per-player rate limit on client purchase / claim / price / catalog requests. |
| ClientRequestBurst | 5 | Burst that request limiter allows. |
| ClientTimeout | 10 | Seconds the client waits for the catalog or its profile before rejecting. |
Types
export type Currency
export type CurrencyOptions
export type TransactionOptions = { Transaction: string?, Sku: string?, Fields: { [string]: any }? }
export type TransactionInfo
export type Result = { Ok: boolean, Balance: number, Delta: number, Reason: string? }
export type Flow = "Source" | "Sink"
export type Shop
export type ShopOptions
export type ShopItem
export type ShopItemOptions
export type PurchaseResult
export type Trade
export type TradeStatus = "Open" | "Completed" | "Cancelled"
export type Reward
export type RewardOptions
export type RewardStatus
export type ClaimResult
export type Product
export type RemoteResult