| name | In-Game Purchases |
| description | Enabling monetization through virtual goods and currencies with virtual economy design, store implementation, payment integration, and purchase validation for gaming applications. |
In-Game Purchases
Current Level: Intermediate
Domain: Gaming / Payments
Overview
In-game purchases enable monetization through virtual goods and currencies. This guide covers virtual economy, store implementation, and payment integration for building monetization systems that provide value to players while generating revenue.
Virtual Economy Design
enum CurrencyType {
SOFT = 'soft',
HARD = 'hard',
PREMIUM = 'premium'
}
const PRICE_TIERS = {
small: { amount: 100, price: 0.99 },
medium: { amount: 500, price: 4.99 },
large: { amount: 1200, price: 9.99 },
mega: { amount: 3000, price: 19.99 }
};
Database Schema
CREATE TABLE currencies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
player_id UUID REFERENCES players(id) ON DELETE CASCADE,
soft_currency BIGINT DEFAULT 0,
hard_currency BIGINT DEFAULT 0,
premium_currency BIGINT DEFAULT 0,
updated_at TIMESTAMP DEFAULT NOW(),
UNIQUE(player_id)
);
CREATE TABLE items (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
key VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(255) NOT NULL,
description TEXT,
icon_url VARCHAR(500),
category VARCHAR(100),
rarity VARCHAR(50),
price_soft INTEGER,
price_hard INTEGER,
price_usd DECIMAL(10,2),
stackable BOOLEAN DEFAULT TRUE,
max_stack INTEGER,
metadata JSONB,
active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT NOW(),
INDEX idx_category (category),
INDEX idx_key (key)
);
player_inventory (
id UUID gen_random_uuid(),
player_id UUID players(id) CASCADE,
item_id UUID items(id) CASCADE,
quantity ,
acquired_at NOW(),
(player_id, item_id),
INDEX idx_player (player_id)
);
transactions (
id UUID gen_random_uuid(),
player_id UUID players(id) CASCADE,
type () ,
item_id UUID items(id),
currency_type (),
amount ,
price_usd (,),
platform (),
platform_transaction_id (),
receipt TEXT,
status () ,
created_at NOW(),
INDEX idx_player (player_id),
INDEX idx_platform_tx (platform_transaction_id)
);
Store Implementation
export class StoreService {
async getStoreItems(category?: string): Promise<StoreItem[]> {
const items = await db.item.findMany({
where: {
active: true,
...(category && { category })
}
});
return items.map(item => this.toStoreItem(item));
}
async purchaseWithSoftCurrency(
playerId: string,
itemKey: string,
quantity: number = 1
): Promise<Purchase> {
const item = await db.item.findUnique({ where: { key: itemKey } });
if (!item || !item.priceSoft) {
throw new Error('Item not available for soft currency');
}
const totalCost = item.priceSoft * quantity;
const currency = db..({ : { playerId } });
(!currency || currency. < totalCost) {
();
}
db..({
: { playerId },
: {
: { : totalCost }
}
});
.(playerId, item., quantity);
db..({
: {
playerId,
: item.,
: ,
: ,
: totalCost,
:
}
});
{
: item.,
quantity,
: totalCost,
:
};
}
(
: ,
: ,
: =
): <> {
item = db..({ : { : itemKey } });
(!item || !item.) {
();
}
totalCost = item. * quantity;
currency = db..({ : { playerId } });
(!currency || currency. < totalCost) {
();
}
db..({
: { playerId },
: {
: { : totalCost }
}
});
.(playerId, item., quantity);
db..({
: {
playerId,
: item.,
: ,
: ,
: totalCost,
:
}
});
{
: item.,
quantity,
: totalCost,
:
};
}
(
: ,
: ,
:
): <> {
db..({
: {
: { playerId, itemId }
},
: {
playerId,
itemId,
quantity
},
: {
: { : quantity }
}
});
}
(: ): {
{
: item.,
: item.,
: item.,
: item.,
: item.,
: item.,
: item.,
: {
: item.,
: item.,
: item.
}
};
}
}
{
: ;
: ;
: ;
: ;
: ;
: ;
: ;
: {
: | ;
: | ;
: | ;
};
}
{
: ;
: ;
: ;
: ;
}
Payment Integration
export class PaymentService {
async initiatePurchase(
playerId: string,
productId: string,
platform: 'ios' | 'android' | 'web'
): Promise<PurchaseIntent> {
const product = await db.item.findUnique({ where: { key: productId } });
if (!product || !product.priceUsd) {
throw new Error('Product not available');
}
const transaction = await db.transaction.create({
data: {
playerId,
itemId: product.id,
type: 'iap',
priceUsd: product.priceUsd,
platform,
status: 'pending'
}
});
return {
transactionId: transaction.id,
productId,
price: product.priceUsd,
platform
};
}
async completePurchase(
transactionId: ,
: ,
:
): <> {
transaction = db..({
: { : transactionId }
});
(!transaction) {
();
}
isValid = .(
receipt,
transaction.!,
platformTransactionId
);
(!isValid) {
();
}
db..({
: { : transactionId },
: {
receipt,
platformTransactionId,
:
}
});
.(transaction);
}
(
: ,
: ,
:
): <> {
(platform === ) {
.(receipt);
} (platform === ) {
.(receipt, transactionId);
}
;
}
(: ): <> {
response = (, {
: ,
: { : },
: .({
: receipt,
: process..
})
});
data = response.();
data. === ;
}
(
: ,
:
): <> {
{ google } = ();
androidpublisher = google.();
{
result = androidpublisher...({
: process..,
: receipt,
: transactionId,
: process..
});
result.. === ;
} (error) {
;
}
}
(: ): <> {
item = db..({
: { : transaction. }
});
(!item) ;
(item. === ) {
db..({
: { : transaction. },
: {
: { : item.. }
}
});
} {
db..({
: {
: {
: transaction.,
: item.
}
},
: {
: transaction.,
: item.,
:
},
: {
: { : }
}
});
}
}
}
{
: ;
: ;
: ;
: ;
}
Inventory Management
export class InventoryService {
async getInventory(playerId: string): Promise<InventoryItem[]> {
const items = await db.playerInventory.findMany({
where: { playerId },
include: { item: true }
});
return items.map(i => ({
id: i.id,
itemId: i.item.id,
name: i.item.name,
iconUrl: i.item.iconUrl,
quantity: i.quantity,
rarity: i.item.rarity,
acquiredAt: i.acquiredAt
}));
}
async useItem(playerId: string, itemId: string): Promise<void> {
const inventoryItem = await db.playerInventory.findUnique({
: {
: { playerId, itemId }
}
});
(!inventoryItem || inventoryItem. <= ) {
();
}
.(playerId, itemId);
(inventoryItem. === ) {
db..({
: { : inventoryItem. }
});
} {
db..({
: { : inventoryItem. },
: {
: { : }
}
});
}
}
(: , : ): <> {
item = db..({ : { : itemId } });
(!item || !item.) ;
(item..) {
:
.(playerId, item..);
;
:
.(playerId, item..);
;
}
}
(: , : ): <> {
}
(: , : ): <> {
}
}
{
: ;
: ;
: ;
: ;
: ;
: ;
: ;
}
Gifting System
export class GiftingService {
async sendGift(
senderId: string,
recipientId: string,
itemId: string
): Promise<void> {
const senderItem = await db.playerInventory.findUnique({
where: {
playerId_itemId: { playerId: senderId, itemId }
}
});
if (!senderItem || senderItem.quantity <= 0) {
throw new Error('Item not in inventory');
}
await db.playerInventory.update({
where: { id: senderItem.id },
data: {
quantity: { decrement: 1 }
}
});
await db.playerInventory.upsert({
where: {
playerId_itemId: { playerId: recipientId, itemId }
},
create: {
playerId: recipientId,
itemId,
:
},
: {
: { : }
}
});
io.().(, {
senderId,
itemId
});
}
}
Best Practices
- Economy Balance - Balance earning vs spending
- Receipt Validation - Always validate receipts
- Fraud Prevention - Detect and prevent fraud
- Clear Pricing - Show clear prices
- Inventory Limits - Set reasonable limits
- Transaction Logs - Log all transactions
- Refunds - Handle refund requests
- Analytics - Track purchase metrics
- Testing - Test with sandbox accounts
- Compliance - Follow platform guidelines
Quick Start
Virtual Store
interface StoreItem {
id: string
name: string
description: string
price: {
currency: 'soft' | 'hard' | 'premium'
amount: number
}
category: 'consumable' | 'permanent' | 'subscription'
}
async function purchaseItem(
playerId: string,
itemId: string
): Promise<PurchaseResult> {
const item = await getStoreItem(itemId)
const player = await getPlayer(playerId)
if (player.currency[item.price.currency] < item.price.amount) {
throw new Error('Insufficient funds')
}
await deductCurrency(playerId, item.price.currency, item.price.amount)
(playerId, itemId)
{ : , item }
}
Receipt Validation
async function validateIOSReceipt(receiptData: string): Promise<boolean> {
const response = await fetch('https://buy.itunes.apple.com/verifyReceipt', {
method: 'POST',
body: JSON.stringify({
'receipt-data': receiptData,
'password': process.env.IOS_SHARED_SECRET
})
})
const result = await response.json()
return result.status === 0
}
Production Checklist
Anti-patterns
❌ Don't: Trust Client
if (player.coins >= item.price) {
player.coins -= item.price
grantItem(item)
}
if (player.coins >= item.price) {
await deductCurrency(playerId, 'soft', item.price)
await grantItem(playerId, itemId)
}
❌ Don't: No Receipt Validation
await processPurchase(purchaseData)
const isValid = await validateReceipt(purchaseData.receipt)
if (isValid) {
await processPurchase(purchaseData)
}
Integration Points
- Payment Gateways (
30-ecommerce/payment-gateways/) - Payment processing
- Game Analytics (
38-gaming-features/game-analytics/) - Purchase analytics
- Achievements (
38-gaming-features/achievements/) - Purchase rewards
Further Reading
Resources