Finance System
This page explains the complete financial model of KioskGaming — wallet architecture, currency units, how deposits and withdrawals flow through the role hierarchy, fee structures, and payment gateway integrations.
Currency Units
KioskGaming uses two currency units in parallel:
| Unit | Used by | Meaning |
|---|---|---|
| Credit | Players, shops, agents, super agents | Internal platform currency. All in-game balances are in credits. |
| USD | Shops, agents, super agents | Real money. Used to buy credits from the system or withdraw. |
From the player's perspective, the exchange rate is always:
1 USD deposited = 1 credit received
The margin between what a shop pays per credit and what a player pays per credit is the shop's profit. See Cost Rate below.
Wallet Architecture
Every account in the system may hold one or more wallet types depending on their role.
Master Wallet (wallets table)
The primary credit balance for every account type.
| Field | Type | Description |
|---|---|---|
ownerType | player | agent | shop | super_agent | Who owns this wallet |
balance | DECIMAL 18,8 | Available credit balance |
reservedBalance | DECIMAL 18,8 | Locked credits (pending transactions) |
currency | STRING | Always USD (maps to credits internally) |
status | active | frozen | closed | Wallet operational state |
version | INTEGER | Optimistic lock — prevents double-spend |
Each owner has exactly one master wallet (owner_type, owner_id) is unique.
USD Wallet (usd_wallets table)
Tracks real-money (online) balances, separate from game credits. Used primarily by shops and agents.
| Field | Type | Description |
|---|---|---|
balanceGross | DECIMAL 18,8 | Total USD before fees |
balanceNet | DECIMAL 18,8 | USD after fees are deducted |
reservedGross | DECIMAL 18,8 | Gross amount locked in pending payouts |
reservedNet | DECIMAL 18,8 | Net amount locked in pending payouts |
status | active | frozen | closed | Wallet state |
The USD wallet is how shop income from player deposits accumulates. Shops use this balance to buy more credits or withdraw.
Game Wallet (user_game_wallets table)
Each player has one game wallet per game provider they've ever interacted with.
| Field | Type | Description |
|---|---|---|
userId | UUID | Player |
gameProviderId | UUID | Game platform (e.g., Fire Kirin, Game Room) |
walletBalance | DECIMAL 15,2 | Credits allocated from master wallet to this provider |
inGameBalance | DECIMAL 15,2 | Balance inside the game's own environment |
winningBalance | DECIMAL 15,2 | Withdrawable winnings (River Pay only) |
syncStatus | ENUM | never_synced | in_sync | pending | failed |
lastSyncedAt | DATE | Last successful sync with game provider API |
walletBalance and inGameBalance can diverge when a sync is pending or failed. The system reconciles them via gameWalletSyncService.
Wallet Summary by Role
| Role | Master Wallet | USD Wallet | Game Wallet |
|---|---|---|---|
| Player | ✅ (credits) | — | ✅ (per game) |
| Shop | ✅ (credits) | ✅ (USD) | — |
| Agent | ✅ (credits) | ✅ (USD) | — |
| Super Agent | ✅ (credits) | ✅ (USD) | — |
Cost Rate (costRate)
costRate is a decimal multiplier (0–1) that defines the effective cost per credit for each level of the hierarchy.
costRate = 0.80 → this entity pays $0.80 to acquire 1 credit
Cost rate is inherited top-down: super agent → agent → shop. A child's rate must always be ≥ their parent's rate (no inverted margins allowed).
Profit example
shopRate = 0.80 (shop pays $0.80 per credit)
Player deposits $10 via payment gateway
Player receives: 10 credits (1 USD = 1 credit, always)
Shop's credit cost: 10 credits × $0.80 = $8.00
Shop collects: $10.00 (player's deposit)
Shop gross profit: $10.00 − $8.00 = $2.00
The margin between a parent's rate and a child's rate determines how profit is shared up the chain.
Deposit Flows
Flow 1 — Free Player Online Deposit (via Payment Gateway)
Used when a player not linked to a shop deposits through the Player Web.
Player selects amount and payment method on Player Web
│
▼
Backend creates PaymentTransaction { type: deposit, status: pending }
│
▼
Player redirected to payment gateway (LinkMePay / BTCPay / 0x)
│
▼
Payment gateway processes payment
│
▼
IPN / callback received by backend
│
▼
Status: pending → processing → completed
│
▼
Platform fee deducted (platformFeeRate × amount = platformFeeAmount)
│
▼
Net amount credited to player's master wallet
│
├── No game selected → credits stay in master wallet
│
└── Game selected → walletFlow: master_to_game
GameWalletTransaction created
credits forwarded to inGameBalance
Flow 2 — Shop Player Deposit (CDN Flow)
Used when a player linked to a shop deposits. The shop's credit wallet backs the player's balance.
Payment confirmed (same gateway flow as Flow 1)
│
▼
Backend identifies player as shop-linked
│
▼
Check shop's master wallet (credit balance)
│
├── Shop HAS sufficient credit ──────────────────────────┐
│ ▼
│ Shop master wallet −10 credits
│ Player master wallet +10 credits
│ Shop USD wallet +$X (player's USD, after fee)
│
└── Shop LACKS sufficient credit ───────────────────────┐
▼
Online deposit is **rejected** before payment gateway
(API + deposit-precheck return HTTP 400)
Message (EN): "The shop currently cannot process online deposit transactions.
Please visit the shop in person to complete this action."
Player must deposit in person at the shop (Flow 3)
Shop-linked players cannot complete online gateway deposits unless the shop's master wallet has enough available credit (balance − reservedBalance ≥ amount — 1:1 with USD face value). Deposit platform fees are deducted from the shop's USD wallet at settlement, not from extra CDN credits. Settlement uses strictShopPayer so the system does not cover shortfalls for shop players.
Flow 3 — Shop Counter Deposit (Cashier)
A physical deposit made by shop staff on behalf of a player at the kiosk counter. No payment gateway is involved.
Shop staff opens Shop Portal → "Transfer to Player"
│
▼
Staff enters player + amount
│
▼
Shop master wallet debited
Player master wallet credited
CashierTransaction recorded { type: deposit_credits, paymentMethod: cash | card | bank_transfer }
Flow 4 — Direct-to-Game Deposit
The player selects a specific game platform on the deposit screen. Credits go directly into the game instead of stopping in the master wallet.
Flows 1 or 2 run first (payment confirmed, master wallet credited)
│
▼
GameWalletTransaction created { direction: master_to_game }
│
▼
master wallet balance −N credits
inGameBalance +N credits
The game provider's API is called to sync the balance. syncStatus tracks the result.
Deposit Fee
Fees are configured per payment provider and method in the payment_provider_fee_configs table.
feeRate = paymentGateFeeRate + systemFeeRate
Fee is deducted from the USD amount the shop receives, not from the credits given to the player.
Example — shopRate = 0.80, feeRate = 10%
Player deposits: $10.00
Platform fee (10%): $1.00
Shop receives (USD): $9.00 ← deposited amount − fee
Player receives: 10 credits ← always full face value
Shop credit cost: 10 × $0.80 = $8.00
Shop net profit: $9.00 − $8.00 = $1.00
The fee is borne by the shop, not the player. From the player's perspective, depositing $10 always yields 10 credits regardless of the configured fee rate.
Fee lookup resolution
For every transaction the system resolves the applicable fee rate in this order:
1. Exact match: (provider, operation, methodId)
│
└── Not found →
2. Fallback: (provider, operation, 'all')
│
└── Not found →
3. Default: feeRate = 0
Withdrawal Flow
Players can withdraw credits back to USD via bank transfer or cryptocurrency. Withdrawals use the NET receive model: the player requests how much they want to receive (netPayoutUsd); fees are calculated on top of that amount.
Player submits withdrawal request
{ netUsd, withdrawalType: bank_transfer | bitcoin_transfer | zerox_* }
│
▼
Withdrawal fee calculated (NET receive model):
netPayoutUsd = amount player wants to receive
systemFeeAmount = netPayoutUsd × systemFeeRate
gatewayFeeAmount = max(netPayoutUsd × paymentGateFeeRate, paymentGateFeeMinUsd)
networkFeeAmount = percent or fixed (tier bears it; not sent to gateway)
gatewayPayoutUsd = netPayoutUsd + gatewayFeeAmount ← sent to payment provider
tierFundingUsd = net + system + gateway + network fees
│
▼
PaymentTransaction created { type: withdrawal, status: pending, feeModel: net_receive }
Player's master wallet: reservedBalance += netPayoutUsd (credits locked)
│
▼
Admin reviews in Admin Panel
│
├── Approved ────────────────────────────────────────────┐
│ ▼
│ status → approved → processing
│ Payout adapter called (LinkMePay / BTCPay / 0x)
│ Gateway receives gatewayPayoutUsd
│ status → completed
│ Player wallet debited
│
└── Rejected ───────────────────────────────────────────┐
▼
status → rejected
Reserved balance released back to available
Withdrawal fee components
| Component | Config field | Who pays | Sent to gateway? |
|---|---|---|---|
| Payment gate fee | paymentGateFeeRate + paymentGateFeeMinUsd | Funding tier (shop/agent/super agent) | Yes — included in gatewayPayoutUsd |
| System fee | systemFeeRate | Funding tier | No |
| Network fee | networkFeeType / networkFeeRate / networkFeeAmount | Funding tier | No |
Payment gate minimum fee: Some gateways charge a flat minimum per payout. When configured, the gateway fee is the greater of the percent calculation and the minimum:
gatewayFeeAmount = max(netPayoutUsd × paymentGateFeeRate, paymentGateFeeMinUsd)
gatewayPayoutUsd = netPayoutUsd + gatewayFeeAmount
Example — paymentGateFeeRate = 1%, paymentGateFeeMinUsd = $2.60, player withdraws $10 NET:
Percent gateway fee: $10 × 1% = $0.10
Applied gateway fee: max($0.10, $2.60) = $2.60 ← minimum wins
gatewayPayoutUsd: $10 + $2.60 = $12.60 ← sent to payment provider
If the player withdraws $500 NET with the same config:
Percent gateway fee: $500 × 1% = $5.00
Applied gateway fee: max($5.00, $2.60) = $5.00 ← percent wins
gatewayPayoutUsd: $500 + $5.00 = $505.00
paymentGateFeeMinUsd applies to withdrawals only (NET receive model). Deposit fees continue to use percent on gross only.
Pending withdrawals that exceed the configured timeout are auto-rejected by the system.
Payment Gateways
Deposit adapters
| Provider | Currency | Method |
|---|---|---|
| LinkMePay | VND | Bank transfer, QR code |
| BTCPay | BTC | On-chain Bitcoin |
| 0x Processing | USDC, WBTC | Crypto (Arbitrum, Base) |
Withdrawal adapters
| Provider | Payout method |
|---|---|
| LinkMePay | Bank transfer (VND) |
| BTCPay | On-chain Bitcoin payout |
| 0x Processing | USDC (Base), WBTC (Arb1) |
Transaction State Machine
All PaymentTransaction records follow strict state transitions:
Deposit:
pending → processing → completed
└──► failed
└──► expired
└──► cancelled
Withdrawal:
pending → approved → processing → completed
└──► rejected
Invalid state transitions are rejected by transactionStateMachine.js before any wallet mutation occurs.
Transaction Ledger
Every wallet balance change is recorded as a double-entry ledger in wallet_transactions:
| Field | Description |
|---|---|
fromWalletId | Source wallet (debited) |
toWalletId | Destination wallet (credited) |
amount | Credit amount transferred |
costRate | Rate applied at time of transfer |
platformFeeRate | Fee rate snapshot |
platformFeeAmount | Fee amount snapshot |
balanceBefore / balanceAfter | Audit trail of wallet state |
referenceType / referenceId | Links back to the originating transaction |
The transactionId field uses semantic prefixes for traceability:
player_deposit:{UUID} ← CDN deposit from shop to player
Transaction Limits & Fraud Flags
Configurable limits are stored in transaction_limits_configs as JSONB:
| Config group | Examples |
|---|---|
transactionLimits | Max deposit per transaction, max daily deposit |
rateLimits | Max deposits per hour, max withdrawals per hour |
fraudFlags | Suspicious amount thresholds, velocity windows |
Suspicious transactions are flagged in transaction_fraud_flags for admin review.
USD Wallet Operations (Shop)
The shop's USD wallet supports two operations:
| Operation | Effect |
|---|---|
| Buy credits | Shop converts USD → credits at their costRate. USD wallet debited, master credit wallet credited. |
| Withdraw | Shop requests payout of USD to an external bank or crypto address. Routed through the same withdrawal adapter flow as player withdrawals. |
Example — shop costRate = 0.80, USD wallet balance = $100
Buy 100 credits:
USD wallet: − $80.00
Credit wallet: +100 credits
Remaining USD: $20.00
Cash Settlement (offline reconciliation)
Credit movement between hierarchy levels creates an accounts-receivable ledger — not a new wallet. See Cash Settlement Management.
| Concept | Meaning |
|---|---|
| Not a wallet | Marking a settlement Paid does not change Credit Wallet, USD Wallet, or Player balance |
| Auto-create | Completed parent→child credit transfer → one settlement record |
| Expected Cash | Credit Amount × buyer.costRate (rate snapshotted at completion) |
| Scope | Phase 1: Admin → Shop. Phase 2: Shop/Cashier → Player (deposit). Phase 3: Player → Shop/Cashier (redemption). |
Summary — Who Gets What on a Player Deposit
| Scenario | Player receives | Shop receives | System receives |
|---|---|---|---|
| Free player, no fee | Full credits | — | Full USD |
| Shop player, shop has credit, no fee | Full credits | Player's USD | — |
| Shop player, shop has credit, fee 10% | Full credits | Player's USD − 10% | Fee amount |
| Shop player, shop has NO credit | Full credits | Nothing | Player's USD |
| Cashier (counter) deposit | Full credits | — (internal transfer) | — |
Related documentation
- Database code review — schema, migrations, partitioning, idempotency, DECIMAL precision
- Backend code review (full) — application layer, services, controllers