Skip to main content

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

ClassFileResponsibility
WalletServiceEconomy/EconomyServices.csMulti-currency balance tracking via Nakama's wallet
ShopServiceEconomy/EconomyServices.csServer-validated NPC shop buy/sell
GTLServiceEconomy/EconomyServices.csGlobal 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.

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 FieldDefaultDescription
GTLListingFee0.05 (5%)Fee percentage deducted from listing price
GTLListingDurationHours48Hours before a listing expires
MaxGTLListings20Maximum 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 by GTLItemType.Monster or GTLItemType.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:

FieldTypeDescription
IdstringUnique listing ID
SellerIdstringSeller's user ID
SellerNamestringSeller's display name
TypeGTLItemTypeMonster or Item
ItemIdstringID of the monster or item
ItemNamestringDisplay name
QuantityintQuantity (for items)
PricelongAsking price
FeelongListing fee deducted
StatusGTLListingStatusActive, Sold, Expired, or Cancelled
ExpiresAtlongUnix timestamp (milliseconds)
CreatedAtlongUnix timestamp (milliseconds)

Economy Balancing

MCE's economy design includes several built-in currency sinks:

  1. GTL listing fees (default 5%) -- removes currency from circulation with every trade.
  2. Server-validated shop prices -- the server controls buy/sell ratios, preventing arbitrage.
  3. Battle rewards controlled server-side -- the server determines EXP, money, and item drops.
  4. Anti-cheat inventory auditing -- the InventoryAuditor detects 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 the mce_give_money RPC. The server validates the source (battle reward, shop sale, quest, etc.) before applying.
  • All item changes go through OnlineCommandBridge.RequestGiveItemAsync() which calls the mce_give_item RPC.
  • The InventoryAuditor takes snapshots after every validated action and compares them against client-reported inventories. Mismatches trigger AuditViolation events.
  • The ActionRateLimiter prevents rapid-fire buy/sell exploits (default 0.3s as configured in AntiCheatManager; class default is 0.5s).