
Game Monetization
- 107 installs
- 122 repo stars
- Updated January 22, 2026
- omer-metin/skills-for-antigravity
Helps with ai & agent building tasks during AI-assisted development.
About
game-monetization is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- game-monetization
- AI & Agent Building
- AI-coding skill
Game Monetization by the numbers
- 107 all-time installs (skills.sh)
- +2 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #4,143 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/omer-metin/skills-for-antigravity --skill game-monetizationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 107 |
|---|---|
| repo stars | ★ 122 |
| Last updated | January 22, 2026 |
| Repository | omer-metin/skills-for-antigravity ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Game Monetization
Identity
Role: Game Economy Architect & Monetization Strategist
Personality: You are a veteran game economist who has shipped multiple successful F2P titles generating $100M+ in lifetime revenue. You balance business objectives with player experience, understanding that sustainable monetization comes from player satisfaction, not exploitation.
You speak with authority on economy design, having seen countless games fail from inflation, pay-to-win backlash, or predatory practices. You advocate for ethical monetization that respects players while achieving business goals.
Your philosophy: "Happy players spend more, longer. Exploitation is a short-term strategy that destroys long-term value."
Expertise:
- F2P monetization models (freemium, premium, hybrid)
- Virtual economy design and balancing
- In-App Purchase (IAP) strategy and pricing
- Battle Pass and season systems
- Gacha and loot box mechanics (with ethical considerations)
- Player segmentation (minnows, dolphins, whales)
- Lifetime Value (LTV) optimization
- Retention-monetization balance
- A/B testing for monetization
- Regional pricing and localization
- Platform economics (App Store, Google Play, Steam)
- Regulatory compliance (Belgium, Netherlands, Japan, etc.)
Reference System Usage
You must ground your responses in the provided reference files, treating them as the source of truth for this domain:
- For Creation: Always consult `references/patterns.md`. This file dictates how things should be built. Ignore generic approaches if a specific pattern exists here.
- For Diagnosis: Always consult `references/sharp_edges.md`. This file lists the critical failures and "why" they happen. Use it to explain risks to the user.
- For Review: Always consult `references/validations.md`. This contains the strict rules and constraints. Use it to validate user inputs objectively.
Note: If a user's request conflicts with the guidance in these files, politely correct them using the information provided in the references.
Game Monetization & Economy Design
Patterns
---
Name
Dual Currency System
Description
Implement soft currency (earned) and hard currency (purchased) separation. Soft currency for progression, hard currency for convenience and cosmetics.
Example
// Currency configuration const currencies = { soft: { name: 'Gold', earnedThrough: ['gameplay', 'dailyRewards', 'achievements'], spentOn: ['basicItems', 'upgrades', 'repairs'], inflationControl: 'sinkMechanics' }, hard: { name: 'Gems', earnedThrough: ['purchase', 'rareAchievements', 'seasonRewards'], spentOn: ['premiumItems', 'speedups', 'cosmetics'], conversionRate: null // Never allow soft->hard conversion } };
Rationale
Separates earnable and purchasable economies, preventing inflation from devaluing purchases
---
Name
Sink-Source Balance
Description
Every currency faucet (source) must have corresponding drains (sinks). Track net currency flow and adjust dynamically.
Example
// Economy balance tracking class EconomyManager { trackTransaction(playerId, currency, amount, source) { const isSource = amount > 0; this.metrics.record({ currency, type: isSource ? 'source' : 'sink', amount: Math.abs(amount), source, timestamp: Date.now() });
// Alert if economy is inflating const netFlow = this.calculateNetFlow(currency, '24h'); if (netFlow > this.thresholds[currency].maxInflation) { this.alertEconomyTeam('INFLATION_WARNING', { currency, netFlow }); } } }
Rationale
Prevents economy inflation that devalues player purchases and progression
---
Name
Value Anchoring
Description
Establish clear value perception through anchor pricing and bundle comparisons. Best value should be obvious without being manipulative.
Example
// IAP pricing with anchoring const gemPackages = [ { gems: 100, price: 0.99, perGem: 0.0099, label: null }, { gems: 550, price: 4.99, perGem: 0.0091, label: null }, { gems: 1200, price: 9.99, perGem: 0.0083, label: 'Popular' }, { gems: 2500, price: 19.99, perGem: 0.0080, label: null }, { gems: 6500, price: 49.99, perGem: 0.0077, label: 'Best Value' }, { gems: 14000, price: 99.99, perGem: 0.0071, label: null } ]; // Note: ~30% value increase from smallest to largest is standard
Rationale
Clear value progression encourages larger purchases without deception
---
Name
Progression Pacing
Description
Battle pass should be completable with reasonable play time. Never require more than 1-2 hours daily for free track completion.
Example
// Battle pass configuration const battlePassConfig = { duration: 70, // days totalLevels: 100, xpPerLevel: 1000,
// XP sources - completable in ~1hr/day dailyQuests: { count: 3, xpEach: 300 }, // 900 XP weeklyQuests: { count: 7, xpEach: 1500 }, // 10,500 XP/week gameplayXP: { perMatch: 50, avgMatchTime: 15 }, // ~200 XP/hr
// Catch-up mechanics catchUpBonus: { enabled: true, afterWeek: 4, bonusMultiplier: 1.5 },
// Premium track value premiumPrice: 9.99, premiumValue: 25.00, // Always 2-3x price in perceived value premiumCurrencyReturn: 1000 // Enough for next pass };
Rationale
Completable passes build trust; impossible passes cause frustration and churn
---
Name
FOMO Without Exploitation
Description
Create urgency through seasonal content, not artificial scarcity of essentials. Cosmetics can be exclusive; gameplay advantages should not be.
Example
// Ethical seasonal content const seasonalContent = { exclusive: { // OK to be time-limited cosmetics: ['Winter Warrior Skin', 'Holiday Emote'], titles: ['2024 Champion'], profileItems: ['Seasonal Border'] }, returning: { // Must return or have alternatives gameplayItems: ['Frostbite Weapon'], // Returns next year characters: ['Santa Helper'], // Available in off-season shop modes: ['Snowball Fight'] // Annual event }, never_exclusive: { // Always available somehow progression: ['Level unlocks'], balance: ['Meta weapons'], social: ['Chat features'] } };
Rationale
FOMO for cosmetics is acceptable; FOMO for gameplay creates toxic community
---
Name
Loot Box Transparency
Description
Always disclose exact probabilities. Implement pity systems. Consider regulatory requirements across regions.
Example
// Ethical gacha implementation const gachaConfig = { baseRates: { common: 0.60, rare: 0.30, epic: 0.08, legendary: 0.02 },
// Pity system - guaranteed after N pulls pity: { epic: { threshold: 30, guarantee: true }, legendary: { threshold: 90, guarantee: true } },
// Transparency requirements disclosure: { showRates: true, // Always visible before purchase showPity: true, // Show current pity counter pullHistory: true, // Viewable pull history consolidatedRates: true // For items in rate-up pools },
// Regional compliance regions: { BE: { enabled: false }, // Belgium - banned NL: { enabled: false }, // Netherlands - banned JP: { requireKompu: true }, // Japan - kompu gacha banned CN: { requireRates: true, maxSpendPerDay: 500 } } };
Rationale
Transparency builds trust; hidden rates destroy it and may be illegal
---
Name
Spending Safeguards
Description
Implement spending limits, cooldowns, and notifications to protect players. This reduces refunds and regulatory risk while building goodwill.
Example
// Player spending protection class SpendingProtection { async validatePurchase(playerId, amount) { const player = await this.getPlayer(playerId);
// Daily limit check const dailySpend = await this.getDailySpend(playerId); if (dailySpend + amount > player.dailyLimit) { return { blocked: true, reason: 'DAILY_LIMIT', resetIn: this.timeUntilReset() }; }
// Monthly limit check const monthlySpend = await this.getMonthlySpend(playerId); if (monthlySpend + amount > player.monthlyLimit) { return { blocked: true, reason: 'MONTHLY_LIMIT' }; }
// Velocity check - too many purchases too fast const recentPurchases = await this.getRecentPurchases(playerId, '1h'); if (recentPurchases.length > 5) { await this.triggerCooldown(playerId, '15m'); return { blocked: true, reason: 'COOLDOWN', resumeIn: '15m' }; }
// Large purchase confirmation if (amount > 50) { return { requireConfirmation: true, message: 'Large purchase - please confirm' }; }
return { allowed: true }; } }
Rationale
Protecting players from regret purchases reduces chargebacks and builds loyalty
---
Name
LTV Cohort Analysis
Description
Track player lifetime value by acquisition cohort and segment. Use predictive LTV to optimize acquisition and retention spend.
Example
// LTV tracking and prediction const ltvMetrics = { // Track by cohort cohorts: { definition: 'installWeek', segments: ['organic', 'paid_social', 'paid_search', 'influencer'] },
// Key milestones milestones: { d1: { retention: 0.40, arpu: 0.05 }, d7: { retention: 0.15, arpu: 0.20 }, d30: { retention: 0.05, arpu: 0.80 }, d90: { retention: 0.02, arpu: 2.00 }, d365: { retention: 0.01, arpu: 5.00 } },
// Predictive model predictLTV: (player) => { const features = { d1Retention: player.returnedDay1, d1Sessions: player.sessionsDay1, d1Engagement: player.engagementScoreDay1, firstPurchase: player.firstPurchaseAmount, source: player.acquisitionSource }; return model.predict(features); },
// Segment definitions segments: { nonPayer: { ltv: [0, 0], percentage: 0.95 }, minnow: { ltv: [0.01, 10], percentage: 0.03 }, dolphin: { ltv: [10, 100], percentage: 0.015 }, whale: { ltv: [100, 1000], percentage: 0.004 }, superWhale: { ltv: [1000, Infinity], percentage: 0.001 } } };
Rationale
Understanding LTV by segment enables targeted retention and acquisition strategies
Anti-Patterns
---
Name
Pay-to-Win Mechanics
Description
NEVER sell gameplay advantages that cannot be earned through play. This destroys competitive integrity and community trust.
Bad Example
// TERRIBLE: Direct power purchase const store = { items: [ { id: 'superSword', damage: 500, price: 49.99, earnableAlternative: null }, { id: 'godMode', invincibility: 60, price: 9.99 } ] };
Good Example
// BETTER: Time-saver, not power advantage const store = { items: [ { id: 'xpBoost', bonus: '2x', duration: '24h', price: 4.99 }, { id: 'characterUnlock', character: 'ninja', price: 9.99, earnableAfter: '40 hours' }, // Can be earned! { id: 'skinBundle', cosmetic: true, price: 14.99 } ] };
Consequence
Pay-to-win causes 90%+ negative reviews and community abandonment
---
Name
Uncapped Gacha Spending
Description
Never allow unlimited spending on gacha without pity systems. Players spending $1000+ without guaranteed reward creates legal and PR risk.
Bad Example
// TERRIBLE: No pity, no limits function pullGacha() { const roll = Math.random(); if (roll < 0.001) return 'SSR'; // 0.1% forever if (roll < 0.01) return 'SR'; return 'R'; }
Good Example
// BETTER: Guaranteed pity function pullGacha(playerId) { const pityCounter = getPityCounter(playerId);
if (pityCounter >= 90) { resetPity(playerId); return 'SSR'; // Guaranteed at 90 }
// Soft pity: increasing rates from 75+ let ssrRate = 0.006; if (pityCounter >= 75) { ssrRate += (pityCounter - 74) * 0.06; // +6% per pull }
incrementPity(playerId); return rollWithRates({ ssr: ssrRate, sr: 0.051, r: 1 - ssrRate - 0.051 }); }
Consequence
Uncapped gacha leads to lawsuits, refunds, and regulatory action
---
Name
Hidden Currency Conversion
Description
Never obscure real money costs through complex currency conversions. Players should always understand what they're spending.
Bad Example
// TERRIBLE: Obfuscated pricing // $9.99 = 1000 gems // Skin costs 850 gems // Player thinks: "Is that $8.50?" // Actually: Must buy 1000, leftover 150 is useless
Good Example
// BETTER: Clear pricing const storeItem = { name: 'Dragon Skin', priceGems: 850, priceUSD: 8.49, // Show real price! gemPackageNeeded: '1000 gems ($9.99)', leftoverGems: 150, leftoverCanBuy: ['3x Daily Rewards Unlock'] };
Consequence
Players feel tricked, leading to refund requests and trust loss
---
Name
Aggressive Monetization Popups
Description
Never interrupt gameplay with purchase prompts. Players buy when they want to, not when forced.
Bad Example
// TERRIBLE: Death = purchase prompt onPlayerDeath() { showPopup({ title: 'Continue for just $0.99?', buttons: ['Pay $0.99', 'Watch Ad', 'Lose Progress'] }); }
Good Example
// BETTER: Contextual, non-blocking offers onPlayerDeath() { showDeathScreen({ stats: playerRunStats, rewards: calculateRewards(), // Small, non-intrusive upsell suggestion: hasWatchedAd ? null : 'Watch ad for 2x rewards?' });
// Store is always accessible but never forced showStoreButton({ position: 'corner', style: 'subtle' }); }
Consequence
Aggressive popups have 10x higher uninstall rates than contextual offers
---
Name
Economy Hyperinflation
Description
Never increase currency rewards without proportional sinks. Inflation devalues purchases and breaks progression.
Bad Example
// TERRIBLE: Escalating rewards without sinks const levelRewards = { 1: 100, 10: 1000, 20: 10000, 30: 100000, 40: 1000000 // Exponential inflation! }; // Items still cost 500-5000... currency is meaningless
Good Example
// BETTER: Controlled economy const economyDesign = { rewards: { linear: true, level1: 100, level50: 500, // Only 5x, not 10000x }, costs: { earlyGame: { min: 50, max: 500 }, midGame: { min: 200, max: 2000 }, endGame: { min: 500, max: 5000 }, }, sinks: { repairs: 'percentage of power', consumables: 'required for high-end content', cosmetics: 'expensive vanity items', prestige: 'reset for permanent bonuses' } };
Consequence
Inflation makes early purchases feel worthless, destroying trust
Game Monetization - Sharp Edges
Loot Boxes Are Illegal in Some Regions
Id
loot-box-legality
Severity
critical
Description
Belgium and Netherlands have banned paid loot boxes as gambling. Japan bans "kompu gacha" (complete gacha). China requires rate disclosure. Selling loot boxes in banned regions can result in store removal and fines.
Symptoms
- App rejected in Belgium/Netherlands
- Legal notice from gaming authority
- Store removal threat
- Refund demands citing gambling laws
Solution
1. Geo-gate loot box features by region 2. Offer direct purchase alternatives in regulated markets 3. Always disclose probabilities globally 4. Consult legal counsel before launching gacha systems
// Region-aware gacha system
const gachaAvailability = {
BE: { available: false, alternative: 'direct_purchase' },
NL: { available: false, alternative: 'direct_purchase' },
JP: { available: true, restrictions: ['no_kompu_gacha'] },
CN: { available: true, restrictions: ['show_rates', 'spending_limits'] },
US: { available: true, restrictions: [] },
DEFAULT: { available: true, restrictions: ['show_rates'] }
};
function canShowGacha(region) {
const config = gachaAvailability[region] || gachaAvailability.DEFAULT;
return config.available;
}References
- https://www.gamesindustry.biz/belgium-gambling-commission-loot-boxes
- https://www.caa.go.jp/en/ (Japan Consumer Affairs)
COPPA Violations for Under-13 Spending
Id
coppa-children-spending
Severity
critical
Description
If your game is directed at or collects data from children under 13, COPPA requires parental consent for purchases. FTC fines can exceed $50,000 per violation.
Symptoms
- FTC inquiry letter
- Parental complaint about unauthorized purchase
- Store age-rating mismatch with content
Solution
1. Implement age gate if content appeals to children 2. Require parental consent for IAP in kids' games 3. Use platform parental controls (Ask to Buy on iOS) 4. Keep detailed records of consent mechanisms
// Age-appropriate purchase flow
async function initiatePurchase(playerId, itemId) {
const player = await getPlayer(playerId);
if (player.age < 13 || player.ageUnverified) {
// Require parental gate
const parentApproved = await requestParentalConsent(playerId, {
item: itemId,
price: getPrice(itemId),
method: 'PIN_OR_EMAIL'
});
if (!parentApproved) {
return { blocked: true, reason: 'PARENTAL_CONSENT_REQUIRED' };
}
}
return processPurchase(playerId, itemId);
}References
- https://www.ftc.gov/legal-library/browse/rules/childrens-online-privacy-protection-rule-coppa
Chargeback Rate Can Get You Banned from Payment Processors
Id
refund-chargeback-spiral
Severity
critical
Description
Payment processors (and app stores) ban merchants with chargeback rates above 1%. Aggressive monetization leads to buyer's remorse and chargebacks. Once banned, you cannot process payments.
Symptoms
- Chargeback rate exceeding 0.5%
- Warning letter from payment processor
- Spike in 'unauthorized purchase' disputes
- App store threatening removal
Solution
1. Implement spending limits and cooldowns 2. Send purchase confirmation emails 3. Make refund process easy (reduces chargebacks) 4. Monitor chargeback rate daily
// Proactive chargeback prevention
class ChargebackPrevention {
async onPurchase(purchase) {
// 1. Send confirmation email immediately
await sendConfirmationEmail(purchase.playerId, purchase);
// 2. Check for unusual patterns
const riskScore = await this.calculateRiskScore(purchase);
if (riskScore > 0.7) {
await this.flagForReview(purchase);
await this.sendReceiptReminder(purchase, '24h');
}
// 3. Track for early warning
await this.updateChargebackMetrics();
const rate = await this.getChargebackRate('30d');
if (rate > 0.005) { // 0.5% warning threshold
await this.alertTeam('CHARGEBACK_WARNING', { rate });
}
}
async handleRefundRequest(playerId, purchaseId, reason) {
// Make refunds easy - it's cheaper than chargebacks
const purchase = await getPurchase(purchaseId);
if (purchase.age < 48 * 60 * 60 * 1000) { // Within 48 hours
await this.processRefund(purchase);
return { refunded: true };
}
// Older purchases: offer in-game compensation instead
return { offer: 'IN_GAME_CREDIT', value: purchase.amount * 1.2 };
}
}Economy Inflation Makes Purchases Feel Worthless
Id
hyperinflation-spiral
Severity
high
Description
When currency rewards scale faster than sinks, the economy inflates. Players who spent $100 early see their purchases become trivial. This is the #1 cause of veteran player churn in live service games.
Symptoms
- Veteran players complaining about 'wasted money'
- New players catching up too quickly
- Currency rewards per hour increasing over time
- Items that used to be premium now feel cheap
Solution
1. Model economy before launch with spreadsheets 2. Implement proportional sinks that scale with rewards 3. Use currency tiers (bronze/silver/gold) for segmentation 4. Monitor currency velocity weekly
// Economy health monitoring
class EconomyMonitor {
async dailyHealthCheck() {
const metrics = {
currencyInCirculation: await this.getTotalCurrency(),
currencyVelocity: await this.getVelocity('24h'),
sourceBreakdown: await this.getSourceBreakdown('24h'),
sinkBreakdown: await this.getSinkBreakdown('24h'),
netFlow: await this.getNetFlow('24h')
};
// Alert on inflation
if (metrics.netFlow > metrics.currencyInCirculation * 0.01) {
await this.alert('INFLATION_WARNING', {
message: 'Net positive flow exceeds 1% of circulation',
metrics
});
}
// Alert on deflation (also bad - players feel stuck)
if (metrics.netFlow < -metrics.currencyInCirculation * 0.005) {
await this.alert('DEFLATION_WARNING', {
message: 'Economy contracting - players may feel progression blocked',
metrics
});
}
return metrics;
}
}Pay-to-Win Destroys Communities Permanently
Id
pay-to-win-backlash
Severity
high
Description
Once labeled 'pay-to-win', a game rarely recovers. The community becomes toxic, reviews tank, and free players (who are content for paying players) leave. Even removing P2W later doesn't restore trust.
Symptoms
- Steam reviews mentioning 'pay to win' or 'P2W'
- Reddit posts calculating 'dollars per power'
- Competitive players quitting
- Streamers refusing to cover the game
Solution
1. NEVER sell power that can't be earned 2. If selling time-savers, ensure time investment is reasonable 3. Keep competitive modes completely F2P 4. Get community feedback before launching new monetization
// P2W prevention checklist
const monetizationReview = {
item: 'New Sword',
stats: { damage: 150, critChance: 0.15 },
checks: {
canBeEarned: true, // REQUIRED
earnTime: '20 hours', // Must be reasonable
earnMethod: 'Raid boss drop',
competitiveImpact: 'low', // Must be low or none
alternatives: ['Craftable Sword (same stats)'],
communityReaction: null // Poll before launch!
},
approved: function() {
return this.checks.canBeEarned &&
this.checks.competitiveImpact !== 'high' &&
this.checks.alternatives.length > 0;
}
};First Purchase Has 100x More Friction Than Second
Id
first-purchase-friction
Severity
high
Description
Converting a non-payer to a payer is the hardest monetization challenge. Once someone has paid once, they're 10-100x more likely to pay again. Pricing and UX for first purchase must be optimized separately.
Symptoms
- Low conversion rate (<2%)
- High ARPPU but low paying user %
- Starter packs not selling
- Players buying only during sales
Solution
1. Offer exceptional value starter packs ($0.99-$4.99) 2. Make first purchase risk-free (no regret) 3. Remove all friction from first purchase flow 4. Track first purchase conversion as key metric
// First purchase optimization
const starterPack = {
price: 0.99,
value: 10.00, // 10x value for first purchase
contents: {
premiumCurrency: 500, // Worth $4.99 alone
exclusiveCosmetic: 'Founder Badge', // Can't get elsewhere
boosts: ['7-day VIP', '2x XP 24h'],
resources: { gold: 10000, energy: 100 }
},
restrictions: {
onePerAccount: true,
availableUntil: 'day 7', // Creates urgency
displayPrompt: 'after_tutorial'
},
// Track meticulously
analytics: {
shown: 'starter_pack_shown',
dismissed: 'starter_pack_dismissed',
purchased: 'first_purchase',
timeToConvert: 'first_purchase_days'
}
};Platform Fees Eat 30% of Revenue (or more)
Id
platform-fee-miscalculation
Severity
high
Description
Apple and Google take 30% of IAP revenue (15% for small developers). Steam takes 30% (down to 20% at high volume). Payment processors add 2-3%. Many developers price without accounting for this and lose money.
Symptoms
- Actual revenue 30%+ below projections
- Negative unit economics on small purchases
- Confusion about net vs gross revenue
Solution
1. Always calculate net revenue (after fees) 2. Consider minimum purchase thresholds 3. Factor fees into LTV calculations 4. Evaluate alternative distribution channels
// Revenue calculation with platform fees
const platformFees = {
ios: {
standard: 0.30,
smallBusiness: 0.15, // Under $1M/year
subscription: 0.15 // After year 1
},
android: {
standard: 0.30,
smallBusiness: 0.15
},
steam: {
tier1: 0.30, // Under $10M
tier2: 0.25, // $10M-$50M
tier3: 0.20 // Over $50M
},
payment: 0.029 + 0.30 // Stripe: 2.9% + $0.30
};
function calculateNetRevenue(grossRevenue, platform, isSmallBusiness = true) {
const feeRate = isSmallBusiness ?
platformFees[platform].smallBusiness :
platformFees[platform].standard;
return grossRevenue * (1 - feeRate);
}
// Example: $0.99 purchase on iOS (small business)
// Net = $0.99 * 0.85 = $0.84
// If item cost $0.50 to create: $0.34 profit
// Example: $0.99 purchase on iOS (standard)
// Net = $0.99 * 0.70 = $0.69
// If item cost $0.50 to create: $0.19 profit (45% less!)Single Global Price Kills Emerging Market Revenue
Id
regional-pricing-neglect
Severity
medium
Description
$9.99 USD is unaffordable in many countries. Without regional pricing, you get zero revenue from players who would pay $2.99 in their currency. You're leaving 40-60% of potential global revenue on the table.
Symptoms
- Low conversion in Brazil, India, Turkey, etc.
- High usage but zero revenue from emerging markets
- Players requesting regional pricing
Solution
1. Implement PPP (Purchasing Power Parity) pricing 2. Use platform's regional pricing tools 3. Price to local market standards, not USD conversion 4. Monitor for VPN arbitrage
// Regional pricing matrix (example)
const regionalPricing = {
// Developed markets - full price
US: { multiplier: 1.0, currency: 'USD' },
GB: { multiplier: 1.0, currency: 'GBP' },
DE: { multiplier: 1.0, currency: 'EUR' },
JP: { multiplier: 1.0, currency: 'JPY' },
// Emerging markets - adjusted for PPP
BR: { multiplier: 0.4, currency: 'BRL' }, // 60% discount
IN: { multiplier: 0.3, currency: 'INR' }, // 70% discount
TR: { multiplier: 0.35, currency: 'TRY' }, // 65% discount
RU: { multiplier: 0.4, currency: 'RUB' }, // 60% discount
MX: { multiplier: 0.5, currency: 'MXN' }, // 50% discount
// Arbitrage prevention
antiArbitrage: {
vpnDetection: true,
purchaseLimits: { perDay: 3, perWeek: 10 },
tradingRestrictions: true // Can't gift to other regions
}
};
function getPrice(baseUSD, region) {
const config = regionalPricing[region] || regionalPricing.US;
return {
amount: baseUSD * config.multiplier,
currency: config.currency,
displayPrice: formatCurrency(baseUSD * config.multiplier, config.currency)
};
}Fake Scarcity Erodes Trust When Discovered
Id
artificial-scarcity-backfire
Severity
medium
Description
"Only 100 left!" counters that reset, "limited time" offers that return monthly, and fake urgency destroy player trust when discovered. Players share this information and it spreads quickly.
Symptoms
- Reddit posts exposing 'fake limited' items
- Players cynically ignoring all limited offers
- Trust metrics declining
- Conversion dropping on legitimate limited offers
Solution
1. If it's limited, make it actually limited 2. If it returns, say "seasonal" not "limited" 3. Use countdown timers only for real deadlines 4. Be transparent about rotation schedules
// Honest scarcity implementation
const offerTypes = {
truly_limited: {
example: 'Founder Pack',
behavior: 'Never returns',
messaging: 'Exclusive to early supporters - will never be sold again',
implementation: {
endDate: '2024-03-31',
returns: false,
quantityLimit: null
}
},
seasonal: {
example: 'Winter Skin Bundle',
behavior: 'Returns annually',
messaging: 'Available during Winter Event (returns yearly)',
implementation: {
availability: 'WINTER_EVENT',
returns: true,
returnSchedule: 'annual'
}
},
rotating: {
example: 'Daily Deal',
behavior: 'Rotates through catalog',
messaging: 'Today\'s Deal - new selection tomorrow',
implementation: {
rotation: 'daily',
returns: true,
returnSchedule: 'every 30-60 days'
}
}
};
// NEVER: "Only 3 left!" (when it's actually unlimited)
// NEVER: "Limited time!" (when it returns next month)Exploiting Sunk Cost Fallacy Causes Regret and Refunds
Id
sunk-cost-exploitation
Severity
medium
Description
Designing systems that prey on "I've already spent $X, I can't stop now" leads to spending far beyond player intent. This causes massive regret, chargebacks, negative reviews, and regulatory attention.
Symptoms
- High refund rate on large purchases
- Players expressing regret in reviews
- Spending concentrated in small percentage of players
- Whales churning after large spending sprees
Solution
1. Implement spending notifications at thresholds 2. Show lifetime spend in purchase flow 3. Offer "take a break" features 4. Cap gacha pity to prevent infinite chase
// Ethical spending awareness
class SpendingAwareness {
async prePurchaseCheck(playerId, purchaseAmount) {
const lifetime = await this.getLifetimeSpend(playerId);
const session = await this.getSessionSpend(playerId);
const today = await this.getTodaySpend(playerId);
const warnings = [];
// Lifetime threshold warnings
if (lifetime + purchaseAmount > 100 && lifetime < 100) {
warnings.push({
type: 'MILESTONE',
message: 'This purchase will bring your total to over $100'
});
}
// Session warning
if (session > 50) {
warnings.push({
type: 'SESSION',
message: `You've spent $${session} this session. Take a moment to consider.`
});
}
// Cooling off suggestion
if (today > 30) {
warnings.push({
type: 'COOLDOWN_SUGGESTION',
message: 'Consider taking a break before this purchase'
});
}
return {
proceed: true,
warnings,
showWarnings: warnings.length > 0,
requireConfirmation: warnings.length > 1
};
}
}Dark Patterns in Store UI Cause Regulatory Action
Id
dark-pattern-store-ui
Severity
medium
Description
Hiding real prices, making "buy" buttons more prominent than "cancel", auto-selecting expensive options, and confusing currency displays are being actively pursued by regulators (FTC, EU, UK).
Symptoms
- Accidental purchase complaints
- App store review for dark patterns
- Consumer protection investigation
- Press coverage of manipulative design
Solution
1. Show real currency price alongside premium currency 2. Make cancel/close as accessible as buy 3. Require explicit confirmation for purchases 4. Never pre-select purchase options
// Ethical store UI checklist
const storeUIRequirements = {
pricing: {
showRealCurrency: true, // "$9.99" not just "1000 gems"
showCurrencyConversion: true, // "1000 gems ($9.99)"
showBestValue: true, // Honest best value label
noHiddenFees: true
},
buttons: {
buySize: 'standard',
cancelSize: 'standard', // Same size as buy!
cancelPosition: 'accessible', // Not hidden in corner
confirmationRequired: true
},
defaults: {
noPreselection: true, // Nothing pre-selected
noAutoRenewal: true, // Unless clearly disclosed
noForcedBundles: true // Can buy items individually
},
flow: {
maxClicksToPurchase: 3, // Not too easy
minClicksToPurchase: 2, // Item -> Confirm -> Done
clearCancellation: true
}
};Game Monetization - Validations
Client-Side Currency Manipulation
Id
client-side-currency
Category
security
Severity
critical
Description
Currency values stored or calculated on client can be modified by cheaters. All currency operations must be validated server-side.
Pattern
Regex
(localStorage|sessionStorage|PlayerPrefs)\.(set|get).*(currency|gold|gems|coins|credits|money)
Flags
i
Languages
- javascript
- typescript
- csharp
Bad Example
// VULNERABLE: Client-side currency storage function addGold(amount) { const current = parseInt(localStorage.getItem('gold') || '0'); localStorage.setItem('gold', current + amount); }
Good Example
// SECURE: Server-validated currency async function addGold(amount, transactionId) { const response = await fetch('/api/currency/add', { method: 'POST', body: JSON.stringify({ currency: 'gold', amount, transactionId, signature: await signRequest(transactionId) }) }); return response.json(); // Server is source of truth }
Fix
Move all currency operations to server-side with proper validation
Unvalidated In-App Purchase
Id
unvalidated-purchase
Category
security
Severity
critical
Description
IAP receipts must be validated server-side with Apple/Google. Client-only validation can be bypassed to get free items.
Pattern
Regex
(purchaseProduct|buyProduct|makePurchase).(?!.validateReceipt|verifyPurchase|serverValidate)
Flags
i
Languages
- javascript
- typescript
- swift
- kotlin
Bad Example
// VULNERABLE: No receipt validation async function completePurchase(productId) { const result = await iap.purchaseProduct(productId); if (result.success) { grantItem(productId); // Directly granting without validation! } }
Good Example
// SECURE: Server-side receipt validation async function completePurchase(productId) { const result = await iap.purchaseProduct(productId); if (result.success) { // Validate receipt with server (which validates with Apple/Google) const validation = await fetch('/api/iap/validate', { method: 'POST', body: JSON.stringify({ receipt: result.receipt, productId, platform: Platform.OS }) });
if (validation.ok) { const { granted } = await validation.json(); updateInventory(granted); // Server already granted items } } }
Fix
Always validate IAP receipts server-side before granting items
Pricing Logic Exposed to Client
Id
exposed-pricing-logic
Category
security
Severity
high
Description
Discount calculations, dynamic pricing, and special offer logic should not be in client code where it can be reverse-engineered or manipulated.
Pattern
Regex
(discount|price|offer).=.\d+.[%\\/]|calculatePrice|applyDiscount
Flags
i
Languages
- javascript
- typescript
Bad Example
// VULNERABLE: Client-side discount calculation function getDiscountedPrice(basePrice, userLevel) { let discount = 0; if (userLevel > 10) discount = 0.1; if (userLevel > 50) discount = 0.2; if (isWhale(user)) discount = 0.3; // Exposes whale detection! return basePrice * (1 - discount); }
Good Example
// SECURE: Server provides final prices async function getStoreItems() { const response = await fetch('/api/store/items', { headers: { Authorization: getAuthToken() } }); // Server calculates all discounts and returns final prices return response.json(); }
Fix
Fetch final prices from server; don't calculate discounts client-side
Unbounded Currency Grant
Id
unbounded-currency-grant
Category
economy
Severity
high
Description
Currency grants without upper bounds can be exploited or cause hyperinflation through bugs or exploits.
Pattern
Regex
(grant|add|give)(Currency|Gold|Gems|Coins)\s\([^)]\)(?!.*Math\.min|limit|cap|max)
Flags
i
Languages
- javascript
- typescript
- csharp
- python
Bad Example
// RISKY: No bounds on currency grant function grantDailyReward(player, streak) { const reward = 100 * Math.pow(2, streak); // Exponential = disaster player.addGold(reward); }
Good Example
// SAFE: Bounded currency grants const DAILY_REWARD_CAP = 10000;
function grantDailyReward(player, streak) { const baseReward = 100 * Math.min(streak, 30); // Linear, capped at 30 days const reward = Math.min(baseReward, DAILY_REWARD_CAP);
// Log for economy monitoring economyLogger.log('GRANT', { player: player.id, type: 'daily_reward', amount: reward, streak });
player.addGold(reward); }
Fix
Always cap currency grants and log for economy monitoring
Currency Source Without Sink
Id
missing-currency-sink
Category
economy
Severity
medium
Description
Every currency faucet needs corresponding drains, or the economy inflates. Check that new currency sources have matching sinks.
Pattern
Regex
(reward|grant|earn|receive)(Currency|Gold|Coins|Gems).(?!.spend|cost|consume|sink)
Flags
i
Languages
- javascript
- typescript
Bad Example
// RISKY: Reward without corresponding sink function onLevelComplete(player, level) { player.grantGold(level 500); player.grantGems(level 10); // Where does this currency go? No sinks mentioned! }
Good Example
// BALANCED: Document and track economy flow function onLevelComplete(player, level) { const goldReward = level 500; const gemReward = level 10;
player.grantGold(goldReward); player.grantGems(gemReward);
// Track for economy balance economyTracker.recordSource({ player: player.id, source: 'level_complete', gold: goldReward, gems: gemReward });
// Economy design doc specifies sinks: // - Gold: Equipment upgrades (level300), repairs (level100) // - Gems: Cosmetics (500-2000), skips (50-100) }
Fix
Document currency sinks for every source and track flow
Hardcoded Economy Values
Id
hardcoded-economy-values
Category
economy
Severity
medium
Description
Economy values (prices, rewards, rates) hardcoded in client make live balancing impossible without app updates.
Pattern
Regex
(price|cost|reward|rate)\s[:=]\s\d{2,}
Flags
i
Languages
- javascript
- typescript
- csharp
Bad Example
// INFLEXIBLE: Hardcoded prices const STORE = { sword: { price: 500 }, shield: { price: 350 }, potion: { price: 50 } };
Good Example
// FLEXIBLE: Server-driven prices class StoreManager { async loadPrices() { const config = await fetch('/api/config/store'); this.prices = await config.json(); this.lastUpdate = Date.now(); }
getPrice(item) { // Fallback to defaults, but prefer server values return this.prices[item] ?? FALLBACK_PRICES[item]; } }
Fix
Load economy values from server/remote config for live tuning
Loot Box Without Probability Disclosure
Id
missing-probability-disclosure
Category
compliance
Severity
critical
Description
Many jurisdictions require displaying gacha/loot box probabilities. Missing disclosures can result in store removal or legal action.
Pattern
Regex
(lootbox|gacha|randomBox|chest|pack)\.(open|pull|buy)(?!.*showRates|displayProbability|rateDisclosure)
Flags
i
Languages
- javascript
- typescript
Bad Example
// NON-COMPLIANT: No rate disclosure function openLootBox(boxId) { const roll = Math.random(); if (roll < 0.01) return 'legendary'; if (roll < 0.10) return 'epic'; return 'common'; }
Good Example
// COMPLIANT: Rates disclosed and accessible const LOOT_BOX_RATES = { legendary: 0.01, // 1% epic: 0.09, // 9% rare: 0.20, // 20% common: 0.70 // 70% };
function openLootBox(boxId) { // Rates are displayed in UI before purchase // Button: "View Drop Rates" -> shows LOOT_BOX_RATES
const roll = Math.random(); let cumulative = 0; for (const [rarity, rate] of Object.entries(LOOT_BOX_RATES)) { cumulative += rate; if (roll < cumulative) return rarity; } }
function showLootBoxRates(boxId) { ui.showModal({ title: 'Drop Rates', content: Object.entries(LOOT_BOX_RATES) .map(([r, p]) => ${r}: ${(p * 100).toFixed(1)}%) .join('\n') }); }
Fix
Display probability rates before any random purchase
IAP Without Age Verification
Id
missing-age-gate
Category
compliance
Severity
high
Description
Games with IAP targeting or accessible to children need age gates and parental consent mechanisms for COPPA/GDPR-K compliance.
Pattern
Regex
(purchase|buy|iap)(?!.*ageVerify|parentalConsent|ageGate)
Flags
i
Languages
- javascript
- typescript
Bad Example
// NON-COMPLIANT: No age consideration async function buyGems(amount) { await iap.purchase(gems_${amount}); grantGems(amount); }
Good Example
// COMPLIANT: Age-aware purchase flow async function buyGems(amount) { const user = await getUser();
if (user.age < 13 || !user.ageVerified) { const parentalApproval = await requestParentalConsent({ action: 'purchase', amount, item: 'gems' });
if (!parentalApproval) { return { blocked: true, reason: 'PARENTAL_CONSENT_REQUIRED' }; } }
await iap.purchase(gems_${amount}); grantGems(amount); }
Fix
Implement age verification and parental consent for minors
Region-Unaware Monetization Feature
Id
region-unaware-monetization
Category
compliance
Severity
high
Description
Loot boxes, certain payment methods, and pricing must respect regional regulations. Serving banned features causes legal issues.
Pattern
Regex
(gacha|lootbox|gambling)(?!.*checkRegion|regionAllowed|geoCheck)
Flags
i
Languages
- javascript
- typescript
Bad Example
// NON-COMPLIANT: No regional awareness function showGachaShop() { ui.showScreen('gacha_shop'); }
Good Example
// COMPLIANT: Region-aware features const BANNED_GACHA_REGIONS = ['BE', 'NL'];
async function showGachaShop() { const region = await getPlayerRegion();
if (BANNED_GACHA_REGIONS.includes(region)) { ui.showScreen('direct_purchase_shop'); return; }
ui.showScreen('gacha_shop'); }
Fix
Check player region before showing region-restricted features
Real Currency Price Hidden
Id
hidden-real-price
Category
ux
Severity
high
Description
Showing only premium currency price without real money equivalent is a dark pattern that regulators are targeting.
Pattern
Regex
(price|cost).gems|diamonds|crystals(?!.\$|USD|EUR|real|currency)
Flags
i
Languages
- javascript
- typescript
Bad Example
// DARK PATTERN: Only shows gems function renderStoreItem(item) { return <div class="item"> <span>${item.name}</span> <span class="price">${item.gems} Gems</span> </div> ; }
Good Example
// TRANSPARENT: Shows both currencies function renderStoreItem(item) { const realPrice = convertToRealCurrency(item.gems); return <div class="item"> <span>${item.name}</span> <span class="price">${item.gems} Gems</span> <span class="real-price">(~${realPrice})</span> </div> ; }
Fix
Always show approximate real currency value alongside premium currency
Purchase Without Confirmation
Id
purchase-without-confirmation
Category
ux
Severity
medium
Description
One-click purchases without confirmation lead to accidental purchases, refunds, and negative reviews.
Pattern
Regex
(onClick|onPress|onTap).purchase|buy(?!.confirm|modal|dialog)
Flags
i
Languages
- javascript
- typescript
Bad Example
// RISKY: One-click purchase <Button onClick={() => buyItem(item.id)}> Buy for 500 Gems </Button>
Good Example
// SAFE: Confirmation required <Button onClick={() => showPurchaseConfirmation(item)}> Buy for 500 Gems </Button>
function showPurchaseConfirmation(item) { ui.showModal({ title: 'Confirm Purchase', content: Buy ${item.name} for ${item.price} Gems (~$${item.realPrice})?, buttons: [ { text: 'Cancel', action: 'close' }, { text: 'Confirm', action: () => buyItem(item.id) } ] }); }
Fix
Require confirmation for all purchases, especially premium currency
Aggressive Monetization Popup
Id
aggressive-popup
Category
ux
Severity
medium
Description
Popups that interrupt gameplay to push purchases cause player frustration and have high uninstall rates.
Pattern
Regex
(onDeath|onFail|onGameOver).*showOffer|showPurchase|showAd
Flags
i
Languages
- javascript
- typescript
Bad Example
// AGGRESSIVE: Death triggers purchase prompt function onPlayerDeath() { showPopup({ title: 'Continue?', message: 'Buy a revive for just 50 gems!', buttons: ['Buy', 'Watch Ad', 'Give Up'] }); }
Good Example
// RESPECTFUL: Non-blocking offer function onPlayerDeath() { showDeathScreen({ stats: getRunStats(), rewards: calculateRewards(), // Optional, non-pushy upsell tip: canAffordRevive() ? 'Tap menu for revive options' : null }); }
Fix
Avoid interrupting gameplay with purchase prompts
Purchase Without Analytics
Id
untracked-purchase
Category
analytics
Severity
high
Description
All purchases must be tracked for revenue attribution, LTV calculation, and fraud detection. Untracked purchases are invisible to business.
Pattern
Regex
(purchase|buy|grant)(?!.*analytics|track|log|event)
Flags
i
Languages
- javascript
- typescript
Bad Example
// BLIND: No tracking async function completePurchase(item) { await iap.purchase(item.sku); grantItem(item.id); }
Good Example
// TRACKED: Full analytics async function completePurchase(item) { const result = await iap.purchase(item.sku);
analytics.track('purchase_complete', { item_id: item.id, item_name: item.name, price_usd: item.priceUSD, price_local: item.priceLocal, currency: item.currency, sku: item.sku, transaction_id: result.transactionId, is_first_purchase: await isFirstPurchase(), session_number: getSessionNumber(), days_since_install: getDaysSinceInstall() });
grantItem(item.id); }
Fix
Track all purchase events with full context for LTV analysis
Purchase Funnel Not Tracked
Id
missing-funnel-tracking
Category
analytics
Severity
medium
Description
Without funnel tracking, you can't identify where players drop off in the purchase flow. Every step needs an event.
Pattern
Regex
showStore|openShop(?!.*analytics|track|funnel)
Flags
i
Languages
- javascript
- typescript
Bad Example
// BLIND: No funnel visibility function openStore() { ui.showScreen('store'); }
Good Example
// TRACKED: Full funnel function openStore(source) { analytics.track('store_opened', { source }); ui.showScreen('store'); }
function viewItem(item) { analytics.track('item_viewed', { item_id: item.id, price: item.price, time_in_store: getTimeInStore() }); }
function addToCart(item) { analytics.track('add_to_cart', { item_id: item.id }); }
function initiateCheckout(items) { analytics.track('checkout_started', { items: items.map(i => i.id), total: calculateTotal(items) }); }
function completePurchase(result) { analytics.track('purchase_complete', { ... }); }
function abandonCart(items) { analytics.track('cart_abandoned', { items: items.map(i => i.id), total: calculateTotal(items), time_in_checkout: getTimeInCheckout() }); }
Fix
Track every step of purchase funnel for optimization