Economy System
The economy system provides server-validated currency management, an auction house (Global Trade Link), and server-side shop transactions. All three services are initialized automatically by NakamaManager.
Architecture Overview
| Class | File | Responsibility |
|---|---|---|
WalletService | Economy/EconomyServices.cs | Multi-currency balance tracking via Nakama's wallet |
ShopService | Economy/EconomyServices.cs | Server-validated NPC shop buy/sell |
GTLService | Economy/EconomyServices.cs | Global Trade Link auction house for player-to-player trading |
Access all three via NakamaManager.Instance:
var wallet = NakamaManager.Instance.Wallet;
var shop = NakamaManager.Instance.Shop;
var gtl = NakamaManager.Instance.GTL;
WalletService
WalletService manages the player's multi-currency balance. Nakama's server-side wallet system ensures currency changes are atomic and tamper-proof.
Checking Balances
// Refresh from server
await wallet.RefreshBalanceAsync(userId);
// Get balance for a specific currency (defaults to "gold")
long gold = wallet.GetBalance("gold");
long premiumGems = wallet.GetBalance("gems");
// Access the full currency dictionary
Dictionary<string, long> currencies = wallet.Currencies;
Events
wallet.OnBalanceChanged += (long newBalance) => { /* gold changed */ };
wallet.OnCurrencyChanged += (string currencyId, long oldValue, long newValue) =>
{
Debug.Log($"{currencyId}: {oldValue} -> {newValue}");
};
Server Authority
When OnlineConfig.EnableServerAuthority is true, all money changes go through the OnlineCommandBridge:
// This is called internally by CommandGraph commands (GiveMoney, etc.)
bool approved = await bridge.RequestGiveMoneyAsync(amount: 500, source: "npc_reward");
The server validates the source and amount before applying the change. This prevents money duplication exploits.
ShopService
ShopService handles NPC shop transactions with server-side validation.
Buying Items
bool success = await shop.BuyFromShopAsync(
shopId: "pokemart_viridian",
itemId: "potion",
quantity: 5);
The server validates:
- The shop exists and contains the item.
- The player has enough currency.
- The item is available (not out of stock for limited items).
Selling Items
long revenue = await shop.SellToShopAsync(
itemId: "nugget",
quantity: 1);
// revenue = the amount of gold received
The server validates the player actually has the item in their inventory before crediting the currency.
Global Trade Link (GTL)
The GTL is an auction house system that allows players to list monsters and items for sale to other players globally. It functions similarly to the GTS/GTE in popular monster capture games, but with full item support and listing fees.
Configuration
| Config Field | Default | Description |
|---|---|---|
GTLListingFee | 0.05 (5%) | Fee percentage deducted from listing price |
GTLListingDurationHours | 48 | Hours before a listing expires |
MaxGTLListings | 20 | Maximum active listings per player |
Listing a Monster
GTLListing listing = await gtl.ListMonsterAsync(
monsterId: "monster_uuid_here",
price: 50000);
// listing.Id -- unique listing ID
// listing.Fee -- the listing fee (price * GTLListingFee)
// listing.ExpiresAt -- Unix timestamp when the listing expires
// listing.Status -- GTLListingStatus.Active
Listing an Item
GTLListing listing = await gtl.ListItemAsync(
itemId: "rare_candy",
quantity: 10,
price: 25000);
Searching the GTL
var filter = new GTLSearchFilter
{
Query = "fire",
Type = GTLItemType.Monster,
MinPrice = 1000,
MaxPrice = 100000,
SortBy = "price",
Ascending = true,
Page = 0,
PageSize = 20
};
List<GTLListing> results = await gtl.SearchAsync(filter);
GTLSearchFilter supports:
Query-- text search on item/monster name.Type-- filter byGTLItemType.MonsterorGTLItemType.Item.MinPrice/MaxPrice-- price range filter.SortBy-- sort field name.Ascending-- sort direction.Page/PageSize-- pagination (default page size: 20).
Buying a Listing
bool success = await gtl.BuyListingAsync(listingId);
Cancelling a Listing
bool cancelled = await gtl.CancelListingAsync(listingId);
Viewing Your Listings
List<GTLListing> myListings = await gtl.GetMyListingsAsync();
GTL Events
gtl.OnListingSold += (GTLListing listing) =>
{
Debug.Log($"Your {listing.ItemName} sold for {listing.Price}!");
};
gtl.OnListingExpired += (GTLListing listing) =>
{
Debug.Log($"Listing for {listing.ItemName} expired.");
};
GTL Data Model
The GTLListing class contains:
| Field | Type | Description |
|---|---|---|
Id | string | Unique listing ID |
SellerId | string | Seller's user ID |
SellerName | string | Seller's display name |
Type | GTLItemType | Monster or Item |
ItemId | string | ID of the monster or item |
ItemName | string | Display name |
Quantity | int | Quantity (for items) |
Price | long | Asking price |
Fee | long | Listing fee deducted |
Status | GTLListingStatus | Active, Sold, Expired, or Cancelled |
ExpiresAt | long | Unix timestamp (milliseconds) |
CreatedAt | long | Unix timestamp (milliseconds) |
Economy Balancing
MCE's economy design includes several built-in currency sinks:
- GTL listing fees (default 5%) -- removes currency from circulation with every trade.
- Server-validated shop prices -- the server controls buy/sell ratios, preventing arbitrage.
- Battle rewards controlled server-side -- the server determines EXP, money, and item drops.
- Anti-cheat inventory auditing -- the
InventoryAuditordetects item duplication and quantity mismatches (see the Anti-Cheat section for details).
Anti-Fraud Measures
When EnableServerAuthority is active:
- All currency changes go through
OnlineCommandBridge.RequestGiveMoneyAsync()which calls themce_give_moneyRPC. The server validates the source (battle reward, shop sale, quest, etc.) before applying. - All item changes go through
OnlineCommandBridge.RequestGiveItemAsync()which calls themce_give_itemRPC. - The
InventoryAuditortakes snapshots after every validated action and compares them against client-reported inventories. Mismatches triggerAuditViolationevents. - The
ActionRateLimiterprevents rapid-fire buy/sell exploits (default 0.3s as configured in AntiCheatManager; class default is 0.5s).