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 ───────────────────────┐
▼
System covers 10 credits (from system reserve)
Player master wallet +10 credits
Shop receives nothing
When the system covers a deposit because the shop lacks credit, the shop does not receive the player's USD payment. The USD is absorbed by the system.
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.
Player submits withdrawal request
{ amount, withdrawalType: bank_transfer | bitcoin_transfer }
│
▼
Withdrawal fee calculated:
grossUsd = amount
platformFeeAmount = grossUsd × platformFeeRate
netPayoutUsd = grossUsd − platformFeeAmount
│
▼
PaymentTransaction created { type: withdrawal, status: pending }
Player's master wallet: reservedBalance += amount (credits locked)
│
▼
Admin reviews in Admin Panel
│
├── Approved ────────────────────────────────────────────┐
│ ▼
│ status → approved → processing
│ Payout adapter called (LinkMePay / BTCPay / 0x)
│ status → completed
│ Player wallet debited
│
└── Rejected ───────────────────────────────────────────┐
▼
status → rejected
Reserved balance released back to available
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
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) | — |