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. These services are initialized automatically by NakamaManager (the Global Trade Station, Gts, and the inventory sync, Inventory, live next to them).

The Global Trade Link browser

The balance in the corner is not a number the client keeps: it is what the wallet service last heard from the server, which is also the only thing that can change it. The filters (category, price, sort) go into the query — the browser never downloads a catalogue and filters it locally, which is what keeps a large market cheap.

The peer-to-peer trade window

Direct trades run through the four stages named along the top: invite, offering, confirm, done. Both padlocks at the bottom left have to close before Confirm Trade means anything, and the server owns the transition — a client that says "done" out of turn is refused, which is the whole reason the state machine lives on the server side rather than in the window you are looking at.

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 nodes (GivePlayerMoney, 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)

Client UI​

The GTL ships with a real, wired client UI. GTLBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GTLBrowserUgui.cs) is a UGUI auction-house browser that calls NakamaManager.Instance.GTL directly — SearchAsync, GetMyListingsAsync, BuyListingAsync — and subscribes to OnListingSold. It falls back to an honest "Offline — the GTL is unavailable" state when not connected. Alongside the bool-returning methods documented above, GTLService also exposes *WithResultAsync variants (SearchListingsAsync, BuyListingWithResultAsync, CancelListingWithResultAsync, …) that return OnlineResult<T> so callers can surface a server error instead of a bare false.

GTS — Global Trade Station​

A second exchange, the GTS, is a monster-for-monster asynchronous trade. GtsService (NakamaManager.Instance.Gts, in Economy/EconomyServices.cs) is its client transport over the server RPCs gts_deposit, gts_search and gts_withdraw:

MethodRPCWhat it does
DepositAsync / DepositWithResultAsyncgts_depositEscrow a roster monster out of the player's roster and post what is wanted in return (a GtsWantSpec). The server immediately looks for a deposit that satisfies both sides: if there is one, the two monsters swap atomically in that same call; otherwise the deposit rests on the board
SearchAsync / SearchWithResultAsyncgts_searchRead the board of active, unexpired deposits (read-only: nothing is swapped here). What you offer and what you want may pre-filter the board
WithdrawAsync / WithdrawWithResultAsyncgts_withdrawReclaim one of your own active deposits

Like the GTL, each operation has a UI-facing overload that degrades to an empty list or false, and a *WithResultAsync overload returning an OnlineResult<T> with the server's error. The matchable card of a monster is derived server-side from the escrowed monster, so no client-sent card is trusted. IGtsTransport is the interface behind it, so a UI can be tested against a fake.

GtsBrowserUgui (Assets/OpenMon/Online/UI/UGUI/GtsBrowserUgui.cs) is the UGUI front end: it assembles a GtsWantSpec from its filter controls and uses the pure GtsMonsterMatcher (no Unity, no Nakama) to filter the deposit board.

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).