Skip to content
Engine

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.

SystemServerClient

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.

Server
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" })
Client
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
OptionDescription
DisplayName, Symbol, IconHow the currency is shown.
DefaultStarting balance for new players.
Min, MaxLimits (default 0 and MaxBalance).
DecimalsDecimal places; amounts are rounded to this.
LeaderstatMirror the balance into the player list (LeaderboardSystem stat).
Leaderboardtrue or leaderboard options: keep a global leaderboard of balances, synced every LeaderboardSyncSeconds.
ReplicateSend 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

MethodDescription
: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?): ResultAdds money.
:Withdraw(player, currency, amount, options?): ResultRemoves 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.

EventFires 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

MethodDescription
: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 optionDescription
IdUnique within the shop.
Name, Description, Icon, DataFor your UI; replicated to clients.
PriceA number, or function(player, quantity) -> total for dynamic prices such as discounts or scaling costs.
CurrencyOverrides the shop's currency.
LimitMost units one player can ever buy.
Requiresfunction(player, item, quantity) -> (boolean, reason?), for example a level requirement.
Grantfunction(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 memberDescription
: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? }): Reward

A 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 memberDescription
: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? }?): Trade

A 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 memberDescription
: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? }): Product

A 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

MemberDescription
: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, OpenTradeCountRead-only.
LogToStatisticsSettable.

Client

MemberDescription
: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

Defaults from DefaultConfiguration/Systems/EconomySystem22 settings
SettingDefaultWhat 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.
ProfileTimeout30Seconds :GetBalanceAsync / :DepositAsync wait for a player's profile session before rejecting.
MaxBalance1e15Ceiling a currency gets when Max is omitted.
DefaultTransactionType"Gameplay"Roblox transaction type a deposit/withdrawal is logged under when the call passes none.
LogToStatisticstrueForward balance changes and product grants to StatisticsService.
WarnOnRejectedfalseAlso warn() when a withdrawal, purchase, claim or trade is rejected.
LeaderboardSyncSeconds120Seconds between pushes of changed balances to a currency's leaderboard.
LeaderboardRefreshInterval60Seconds between top-list refreshes of a currency leaderboard when its options pass none.
ShopMaxQuantity99Most 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.
TradeTimeoutSeconds120Seconds an open trade may sit without completing before it cancels itself.
TradeMaxCurrencies10Distinct 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.
ReplicateCatalogtrueSend currency definitions, shop catalogs and reward definitions to clients.
ClientRequestRatePerSec2Per-player rate limit on client purchase / claim / price / catalog requests.
ClientRequestBurst5Burst that request limiter allows.
ClientTimeout10Seconds 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