GS2-Money2
Tip
GS2-Money2 is, as its name suggests, a microservice that corresponds to version 2.
For documentation on the previous version, see GS2-Money.
A feature for handling in-game resources that have a value equivalent to cash. Be sure to use this feature when handling assets that fall under prepaid payment instruments (self-issued type) as defined by the Japanese Payment Services Act.
GS2-Money2 is not merely a service that handles “billing currency balances”; it is a microservice that takes on the following accounting and legal-compliance requirements as a whole.
- Balance management per deposit timestamp and unit price
- Prevention of fraudulent deposits via receipt validation
- Retention of wallet deposit/withdrawal history
- Isolation of currencies in accordance with platform guidelines
- State management of subscription (period-based) contracts
- Aggregation of unused balances (compliance with prepaid payment instruments)
Balance
GS2-Money2 does not manage a player’s billing currency balance as a simple quantity; instead, it manages quantities per the value at the time of purchase.
For example, if 100 units of billing currency are purchased for 100 yen, the value of one unit is equivalent to 1 yen. Now suppose 1,200 units of billing currency can be purchased simultaneously for 1,000 yen. One option is to keep the unit price at 1 yen and treat the extra 200 units as a free bonus; another option is to treat the units purchased for 1,000 yen as having a different unit price of 0.8334 yen.
If the latter approach is adopted, GS2-Money2 has the ability to separately manage the “balance of billing currency with a unit price of 1 yen” and the “balance of billing currency with a unit price of 0.8334 yen”.
What is the benefit of choosing the latter?
Obviously, the latter makes accounting more complex and may seem to offer no benefit. From the service provider’s perspective, that is entirely true. However, let us switch sides and think from the game player’s perspective.
Some games have a billing currency with a cash value of 0 yen (commonly called “free currency”) that is distributed by the operator. To encourage players to purchase billing currency, suppose an attractive product is sold for 300 units that can only be purchased with billing currency obtained by paying real money (commonly called “paid currency”).
If a player purchases 1,200 units of billing currency for 1,000 yen and the system grants 1,000 paid-currency units and 200 free-currency units, the player can only purchase the 300-unit product three times. On the other hand, if you treat all 1,200 units as paid currency with a unit price of 0.8334 yen, the player can purchase it four times. This difference has at least some impact on player psychology.
Whether to prioritize accounting convenience or player benefit is a specification that should be considered carefully.
Slots
GS2-Money2 supports multiple wallets per player. The key used to distinguish these wallets is the slot.
This feature exists in order to comply with the guidelines of platform providers that prohibit bringing in billing currency purchased on other platforms.
However, since these guidelines only apply to paid currency, there is a feature that allows free currency to be shared across all slots.
By enabling the namespace’s sharedFreeCurrency, free currency is treated as shared across all slots, and only paid currency is isolated per slot.
graph LR
subgraph Wallets[Wallets]
direction TB
S0["Slot 0 (iOS)<br/>Paid balance: 1,000"]
S1["Slot 1 (Android)<br/>Paid balance: 500"]
S2["Slot 2 (Steam)<br/>Paid balance: 800"]
end
Shared["Free balance: 200<br/>(sharedFreeCurrency=true)"] -.shared.-> S0
Shared -.shared.-> S1
Shared -.shared.-> S2
Purchase Currency
A change in GS2-Money2 compared to GS2-Money is that a single wallet can hold billing currency purchased in multiple currencies.
For example, paid currency purchased in JPY and paid currency purchased in USD can be held as separate balances within a single wallet.
In the daily aggregation process performed several times a day, DailyTransactionHistory is recorded per currency, allowing sales and consumption to be tracked separately per currency.
Consumption Priority
When a player consumes billing currency, you can choose whether to consume free currency first or paid currency first. Generally, the specification of consuming free currency first is adopted, but for accounting reasons paid currency can be prioritized.
When consuming paid currency, the oldest deposits are consumed first.
Specify one of the following for the namespace’s currencyUsagePriority.
| Value | Description |
|---|---|
PrioritizeFree |
Consume free currency first (typical) |
PrioritizePaid |
Consume paid currency first |
Receipt Validation
GS2-Money2 has a feature to validate receipts obtained when purchasing additional content from game distribution platforms. The receipt is validated to confirm that it was correctly issued by the platform provider and that it has not been used in the game before.
To validate receipts, you must register Apple App Store or Google Play authentication credentials in the namespace’s platformSetting.
By using this feature, you can prevent attacks that attempt to obtain billing currency using fraudulent receipts.
Supported Platforms
| Platform | Setting Key | Description |
|---|---|---|
| Apple App Store | appleAppStore |
Billing for iOS / iPadOS / macOS |
| Google Play | googlePlay |
Billing for Android |
| Fake | fake |
Dummy receipt issuance for development and QA |
During development, you can use fake to issue pseudo receipts and run test flows without executing actual purchases.
Transaction Log
Not only the receipt validation history but also all addition and subtraction of billing currency is recorded. Furthermore, several times a day, the system aggregates how much unused billing currency is currently pooled in the game in cash-equivalent terms.
Depending on the situation, you may be required to deposit a portion of the unused balance with a third-party institution; this calculated result can be used in such cases.
Main Log/History Data
| Data | Description |
|---|---|
DepositEvent |
History of deposit operations |
WithdrawEvent |
History of consumption operations |
VerifyReceiptEvent |
History of receipt validations |
RefundEvent |
History of refund notifications from the platform |
RefundHistory |
History of refund cancellations |
DailyTransactionHistory |
Daily aggregation of deposits/withdrawals per currency |
UnusedBalance |
Current unused balance per currency |
Legal Procedures
GS2-Money2 collects the data required for various legal procedures and makes it accessible via the API, but the legal procedures themselves must be performed by you or your organization as the user of GS2.
GS2 cannot take responsibility for what procedures are required and therefore cannot give advice on it. Please consult your legal counsel.
Transaction Actions
GS2-Money2 provides the following transaction actions.
- Consume action: Consume balance, Verify receipt
- Acquire action: Add to balance
By using “Add to balance” as an acquire action, you can safely run processes within a transaction that directly grant billing currency (paid or free) upon clearing a specific event or as a gacha “bonus”. This enables flexible measures combining purchases with in-game rewards.
By using “Verify receipt” as a consume action, you can safely handle, as a single transaction, the entire flow from purchase processing using a receipt to adding billing currency.
Subscriptions (Recurring Billing)
GS2-Money2 can handle not only per-purchase billing currency but also subscriptions (recurring billing).
By registering StoreSubscriptionContentModel master data in advance and linking with GS2-Schedule using scheduleNamespaceId and triggerName, the start, renewal, and end of contract periods can be reflected automatically.
graph LR Player["Player"] -->|Purchase| Store["App Store / Google Play"] Store -->|Receipt| Player Player -->|AllocateSubscriptionStatus| Money2[GS2-Money2] Money2 -->|Trigger| Schedule[GS2-Schedule] Schedule -.Expiration management.-> Money2 Money2 -->|Notification| Player
Contract Information Management
Each purchase is recorded in a SubscribeTransaction, allowing contract details and store information per transaction to be tracked.
Per-player contract status is stored in SubscriptionStatus, where you can check the current status (active / inactive) and expiresAt.
Use AllocateSubscriptionStatus to bind a contract status to a player using a receipt. You can also use TakeOverSubscriptionStatus to transfer the contract to another player. This can be used for operations such as sharing a contract within a family or transferring a contract from a child account to an adult account.
Script Triggers per Lifecycle
Scripts can be executed before and after contract operations. You can choose synchronous or asynchronous execution, and external service integration is also supported.
| Trigger Name | Main Use |
|---|---|
| subscribeScript | On new subscription (excluding user reassignment) |
| renewScript | On subscription renewal |
| unsubscribeScript | On cancellation (excluding user reassignment) |
| takeOverScript | When changing the user assigned to a contract |
Contract Status Notifications
When the subscription status changes, the push notification configured in changeSubscriptionStatusNotification can be sent.
Use this to handle changes such as expiration or cancellation during gameplay and reflect them in the UI.
Script Triggers
By configuring depositBalanceScript, withdrawBalanceScript, and verifyReceiptScript on the namespace, you can execute custom scripts before and after deposit, withdrawal, and receipt validation. Triggers can be set to synchronous or asynchronous execution and support external integration using Amazon EventBridge.
The main event triggers and the script setting names that can be configured are as follows.
depositBalanceScript(completion notification:depositBalanceDone): Before and after deposit processingwithdrawBalanceScript(completion notification:withdrawBalanceDone): Before and after withdrawal processingverifyReceiptScript(completion notification:verifyReceiptDone): Before and after receipt validation
You can use synchronous scripts to reject deposits/withdrawals when certain conditions apply, or use asynchronous scripts to send revenue data to BI tools in real time.
Push Notifications
The main push notifications and their setting names are as follows.
changeSubscriptionStatusNotification: Notifies when a subscription’s contract status changes
Master Data Management
By registering master data, you can configure the data and behaviors available in the microservice.
The types of master data are as follows.
StoreContentModel: Sale content definitionStoreSubscriptionContentModel: Subscription content definition
In addition to registering master data from the management console, you can also reflect data from GitHub or build a workflow that registers it from CI using GS2-Deploy.
Example of StoreContentModel
Defines the mapping between sale content and product IDs of the App Store / Google Play.
{
"version": "2022-07-13",
"storeContentModels": [
{
"name": "stone_300",
"metadata": "300 stones pack",
"appleAppStore": {
"productId": "io.gs2.sample.stone_300"
},
"googlePlay": {
"productId": "io.gs2.sample.stone_300"
}
}
]
}Buff-Based Adjustment
By integrating with GS2-Buff, you can apply buffs to the count of DepositByUserId or the withdrawCount of Withdraw/WithdrawByUserId to temporarily increase or decrease the deposit or consumption amount. You can flexibly adjust these according to events and campaigns.
For example, a billing currency bonus campaign such as “increase billing currency purchased during the campaign by 10%” can be implemented as a Rate Add 0.1 buff on the count of DepositByUserId.
Implementation Examples
Retrieving the Balance
var item = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Wallet(
slot: 0
).ModelAsync(); const auto Domain = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Wallet(
0 // slot
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Item = Future->GetTask().Result();Retrieving the List of Wallets
You can retrieve all of the player’s wallets per slot at once. This is useful for displaying balances when billing currency is separated across multiple platforms.
var items = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).WalletsAsync(
).ToListAsync(); const auto It = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Wallets();
TArray<Gs2::UE5::Money2::Model::FEzWalletPtr> Result;
for (auto Item : *It)
{
if (Item.IsError())
{
return false;
}
Result.Add(Item.Current());
}Adding to the Balance
Adding to the balance cannot be performed with game engine SDKs.
Implement it by, for example, adding to the balance as a GS2-Showcase purchase reward.
Consuming the Balance
We do not recommend consuming the balance directly via this API. Instead, we recommend performing some processing in exchange for consuming the billing currency via a service such as GS2-Showcase.
var result = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).Wallet(
slot: 0
).WithdrawAsync(
withdrawCount: 50,
paidOnly: false
);
var item = await result.ModelAsync(); const auto Future = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Wallet(
0 // slot
)->Withdraw(
50,
nullptr // paidOnly
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Retrieving Subscription Contract Status
var item = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).SubscriptionStatus(
contentName: "premium-0001"
).ModelAsync(); const auto Domain = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->SubscriptionStatus(
"premium-0001" // contentName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Item = Future->GetTask().Result();Allocating a Subscription Contract
Using a receipt obtained from the App Store / Google Play, assign a subscription contract to the player. Receipt validation is also performed, and contracts using fraudulent receipts are rejected.
var domain = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).AllocateSubscriptionStatusAsync(
receipt: receipt
); const auto Future = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->AllocateSubscriptionStatus(
Receipt
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;Taking Over a Subscription Contract
Changes the player a subscription contract is bound to from another player to the current player. The contract on the platform itself is not changed; only the in-game target of the contract is changed.
var domain = await gs2.Money2.Namespace(
namespaceName: "namespace-0001"
).Me(
gameSession: GameSession
).TakeOverSubscriptionStatusAsync(
receipt: receipt
); const auto Future = Gs2->Money2->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->TakeOverSubscriptionStatus(
Receipt
);
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;