Documentation index for AI agents

GS2-Money2 Transaction Actions

Specification of verify/consume/acquire transaction actions

Combining actions, and concurrency

The background common to every service is collected in Combining Transaction Actions. Read that first; the rest of this section is what GS2-Money2 adds to it.

The transaction actions of GS2-Money2 address one wallet, identified by the combination of namespace, user, and slot. A wallet holds the paid balance and the free balance together.

An update that moves only the free balance is written as an increment, so those combine. An update that moves the paid balance rewrites the wallet as a whole.

OperationRepeated in one transactionAcross a nested transactionBoundary that separates targets
Depositing and withdrawing free currency only
DepositByUserId WithdrawByUserId
Any mix; combined and the amounts added upAdded upnamespace, user, slot
Depositing or withdrawing where paid currency moves
DepositByUserId WithdrawByUserId
Deposits are combined with their deposit transactions gathered into one list; withdrawals are combined and the amounts added up. A deposit together with a withdrawal failsFailsnamespace, user, slot
Verifying a receipt
VerifyReceiptByUserId
FailsFails when they resolve to the same transaction idnamespace, user (the row itself is per transaction id)

Because free-currency updates are increments, a deposit and a withdrawal of free currency may sit in one transaction and both apply. Where the paid balance moves, the wallet is rewritten as a whole and the two rows cannot be placed together.

A withdrawal that free currency alone cannot cover reaches into the paid balance, so it belongs to the second row even when you did not intend to spend paid currency.

These updates fall back to rewriting the wallet as a whole in several cases, and then behave like the second row: where the paid lots change, where the wallet was stored in the older format, where the free lots are not exactly one, or where the summary does not agree with the recorded lots. The last two only arise from data brought in by import.

Where sharedFreeCurrency is enabled, the free balance of every slot is held in one row belonging to slot 0. Operations on slot 0 take the increment path; free-currency operations issued against another slot are written as a whole instead.

The balance is judged against the total after combining. A withdrawal that fits on its own can still be rejected when combined with another one.

A paid-only withdrawal and an ordinary one cannot be placed against the same slot. Adding them up while they disagree on which balance to draw from would take the money out of a balance you did not intend, so withdrawals with differing paidOnly against one wallet are rejected when the transaction is issued. Specify them separately.

VerifyReceiptByUserId is named like a verify action but its kind is consume. It leaves the wallet untouched, but it does record the receipt as an event row of its own, and it belongs in the consume list. It looks at the state as of the start of the transaction, so it cannot verify a receipt recorded by the same transaction.

Two receipt verifications against one user are rejected when the transaction is issued, even when they carry different receipts. A receipt reaches its row through a transaction id, and how that id is read out of the receipt differs by store — on Google Play the transactionId you send is never looked at, and the orderId inside the payload decides the row. Two receipts that look different can therefore be the same charge, and two that look the same can be different charges. Rather than guess, GS2 asks you to keep one per transaction. Buying a display item that verifies a receipt with a quantity of 2 or more runs into this as well, because the actions are stacked once per unit.

Placing this verification in the same transaction as a deposit lets you deposit only after confirming that the receipt is genuine. Use that shape to prevent double counting.

Take care with nested transactions

Free-currency updates are safe across a nested transaction: they are applied together as one update whichever route they arrive by.

Where the paid balance moves, the wallet is rewritten as a whole, so touching it both from the inside and from the outside makes the transaction fail. Currency is a common cost, so this is easy to hit: a purchase that charges the wallet on the outside and a prize that also charges it on the inside will not go through together.

Deposits of the same amount arriving from the inside and from the outside do not fail: they are gathered into one. Two deposits of 100 credit only 100, with no error. This applies to the second row, where the wallet is rewritten as a whole; differing amounts fail, so the silent mismatch happens only when the amounts agree.

If you want to avoid these restrictions

Depositing into a wallet is an acquire action; withdrawing and verifying a receipt are consume actions. Turning acquireActionUseJobQueue on clears collisions between deposits, but a deposit together with a withdrawal needs enableAtomicCommit turned off. Since currency is involved, weigh that decision carefully.

Verifying a receipt has no such escape. The check runs before either setting is applied, and a receipt is one charge that must not be verified twice, so turning them on changes nothing here. Split it into separate transactions, and buy a display item that verifies a receipt one at a time.

Concurrency and retries

Free-currency updates do not conflict however many concurrent requests overlap, as long as the result stays within range. This is what the increment path buys: rewards, login bonuses, and mission payouts that all grant free currency no longer collide with one another.

Where the paid balance moves, the wallet is written with a revision check, so the request confirmed later returns a conflict (409). Nothing is wrong with the request, so retrying will succeed, and the balance is re-evaluated on the retry.

A different slot is a different wallet, so requests for the same user do not conflict when the slots differ. The exception is sharedFreeCurrency, where the free balance of every slot lives in one row.


Consume Action

Gs2Money2:WithdrawByUserId

Withdraw balance from Wallet by User ID

Withdraws the specified amount of currency from the wallet for the specified user. If paidOnly is false, free currency is consumed first, then paid currency.

Quantity specification supported: YES

Reversible action: YES

TypeConditionRequiredDefaultValue LimitsDescription
namespaceNamestring
~ 128 charsNamespace name
Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.).
userIdstring
~ 128 charsUser ID
Specify #{userId} to substitute the currently logged-in user’s ID.
slotint
0 ~ 100000000Slot Number
Identifies the wallet slot. Different slots can be used to manage currency separately per platform (e.g., one slot for iOS, another for Android) when cross-platform balance sharing is not allowed.
withdrawCountint
1 ~ 2147483646Quantity of premium currency to be consumed
paidOnlyboolfalseWhether to target only paid currency
timeOffsetTokenstring~ 1024 charsTime offset token
{
    "action": "Gs2Money2:WithdrawByUserId",
    "request": {
        "namespaceName": "[string]Namespace name",
        "userId": "[string]User ID",
        "slot": "[int]Slot Number",
        "withdrawCount": "[int]Quantity of premium currency to be consumed",
        "paidOnly": "[bool]Whether to target only paid currency",
        "timeOffsetToken": "[string]Time offset token"
    }
}
action: Gs2Money2:WithdrawByUserId
request:
  namespaceName: "[string]Namespace name"
  userId: "[string]User ID"
  slot: "[int]Slot Number"
  withdrawCount: "[int]Quantity of premium currency to be consumed"
  paidOnly: "[bool]Whether to target only paid currency"
  timeOffsetToken: "[string]Time offset token"
transaction.service("money2").consume.withdraw_by_user_id({
    namespaceName="[string]Namespace name",
    userId="[string]User ID",
    slot="[int]Slot Number",
    withdrawCount="[int]Quantity of premium currency to be consumed",
    paidOnly="[bool]Whether to target only paid currency",
    timeOffsetToken="[string]Time offset token",
})

Gs2Money2:VerifyReceiptByUserId

Mark a receipt as used by User ID

Verifies a purchase receipt for the specified user and records it as used. An event log is recorded and the verify receipt script configured in the Namespace is executed.

Quantity specification supported: NO

Reversible action: NO

TypeConditionRequiredDefaultValue LimitsDescription
namespaceNamestring
~ 128 charsNamespace name
Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.).
userIdstring
~ 128 charsUser ID
Specify #{userId} to substitute the currently logged-in user’s ID.
contentNamestring
~ 128 charsStore Content Model name
receiptReceipt
Receipt
timeOffsetTokenstring~ 1024 charsTime offset token
{
    "action": "Gs2Money2:VerifyReceiptByUserId",
    "request": {
        "namespaceName": "[string]Namespace name",
        "userId": "[string]User ID",
        "contentName": "[string]Store Content Model name",
        "receipt": {
            "Store": "[string]Store",
            "TransactionID": "[string]Transaction ID",
            "Payload": "[string]Payload"
        },
        "timeOffsetToken": "[string]Time offset token"
    }
}
action: Gs2Money2:VerifyReceiptByUserId
request:
  namespaceName: "[string]Namespace name"
  userId: "[string]User ID"
  contentName: "[string]Store Content Model name"
  receipt: 
    Store: "[string]Store"
    TransactionID: "[string]Transaction ID"
    Payload: "[string]Payload"
  timeOffsetToken: "[string]Time offset token"
transaction.service("money2").consume.verify_receipt_by_user_id({
    namespaceName="[string]Namespace name",
    userId="[string]User ID",
    contentName="[string]Store Content Model name",
    receipt={
        store="[string]Store",
        transactionID="[string]Transaction ID",
        payload="[string]Payload"
    },
    timeOffsetToken="[string]Time offset token",
})

Acquire Action

Gs2Money2:DepositByUserId

Deposit balance to Wallet by User ID

Adds the specified deposit transactions to the wallet for the specified user. An event log is recorded for each deposit transaction.

Quantity specification supported: YES

Reversible action: YES

TypeConditionRequiredDefaultValue LimitsDescription
namespaceNamestring
~ 128 charsNamespace name
Unique Namespace name. Specified using alphanumeric characters, hyphens (-), underscores (_), and periods (.).
userIdstring
~ 128 charsUser ID
Specify #{userId} to substitute the currently logged-in user’s ID.
slotint
0 ~ 100000000Slot Number
Identifies the wallet slot. Different slots can be used to manage currency separately per platform (e.g., one slot for iOS, another for Android) when cross-platform balance sharing is not allowed.
depositTransactionsList<DepositTransaction>
1 ~ 1000 itemsList of Deposit transactions
timeOffsetTokenstring~ 1024 charsTime offset token
{
    "action": "Gs2Money2:DepositByUserId",
    "request": {
        "namespaceName": "[string]Namespace name",
        "userId": "[string]User ID",
        "slot": "[int]Slot Number",
        "depositTransactions": [
            {
                "price": "[double]Purchase Price",
                "currency": "[string]Currency Code",
                "count": "[int]Quantity of premium currency",
                "depositedAt": "[long]Deposit Date"
            }
        ],
        "timeOffsetToken": "[string]Time offset token"
    }
}
action: Gs2Money2:DepositByUserId
request:
  namespaceName: "[string]Namespace name"
  userId: "[string]User ID"
  slot: "[int]Slot Number"
  depositTransactions: 
    - price: "[double]Purchase Price"
      currency: "[string]Currency Code"
      count: "[int]Quantity of premium currency"
      depositedAt: "[long]Deposit Date"
  timeOffsetToken: "[string]Time offset token"
transaction.service("money2").acquire.deposit_by_user_id({
    namespaceName="[string]Namespace name",
    userId="[string]User ID",
    slot="[int]Slot Number",
    depositTransactions={
        {
            price="[double]Purchase Price",
            currency="[string]Currency Code",
            count="[int]Quantity of premium currency",
            depositedAt="[long]Deposit Date"
        }
    },
    timeOffsetToken="[string]Time offset token",
})