monetization
SKILL.md
Roblox Monetization Systems
When implementing monetization, follow these patterns for secure and player-friendly purchases.
MarketplaceService Basics
local MarketplaceService = game:GetService("MarketplaceService")
local Players = game:GetService("Players")
-- IDs from your game's monetization page
local GAME_PASSES = {
VIP = 123456789,
DoubleCash = 234567890,
SpeedBoost = 345678901
}
local DEV_PRODUCTS = {
Cash_100 = 111111111,
Cash_500 = 222222222,
Cash_1000 = 333333333,
Revive = 444444444
}
Game Passes
Checking Ownership
local function ownsGamePass(player, passId)
local success, owns = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, passId)
end)
if success then
return owns
else
warn("Failed to check game pass ownership:", owns)
return false
end
end
-- Cache ownership to reduce API calls
local gamePassCache = {}
local function getGamePassOwnership(player, passId)
local key = player.UserId .. "_" .. passId
if gamePassCache[key] ~= nil then
return gamePassCache[key]
end
local owns = ownsGamePass(player, passId)
gamePassCache[key] = owns
return owns
end
-- Clear cache when player leaves
Players.PlayerRemoving:Connect(function(player)
for key in pairs(gamePassCache) do
if key:find(tostring(player.UserId)) then
gamePassCache[key] = nil
end
end
end)
Prompting Purchase
local function promptGamePass(player, passId)
local success, err = pcall(function()
MarketplaceService:PromptGamePassPurchase(player, passId)
end)
if not success then
warn("Failed to prompt game pass:", err)
end
end
-- Handle purchase completion
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, wasPurchased)
if wasPurchased then
-- Update cache
local key = player.UserId .. "_" .. passId
gamePassCache[key] = true
-- Grant benefits immediately
applyGamePassBenefits(player, passId)
print(player.Name, "purchased game pass:", passId)
end
end)
Applying Benefits
local function applyGamePassBenefits(player, passId)
if passId == GAME_PASSES.VIP then
-- VIP tag
player:SetAttribute("VIP", true)
-- VIP chat tag
local tags = player:GetAttribute("ChatTags") or ""
player:SetAttribute("ChatTags", "[VIP] " .. tags)
-- Daily bonus multiplier
player:SetAttribute("DailyBonusMultiplier", 2)
elseif passId == GAME_PASSES.DoubleCash then
player:SetAttribute("CashMultiplier", 2)
elseif passId == GAME_PASSES.SpeedBoost then
player:SetAttribute("SpeedBoost", 1.5)
-- Apply to character
local character = player.Character
if character then
local humanoid = character:FindFirstChildOfClass("Humanoid")
if humanoid then
humanoid.WalkSpeed = 16 * 1.5
end
end
end
end
-- Apply on join (for returning players)
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function()
for name, passId in pairs(GAME_PASSES) do
if getGamePassOwnership(player, passId) then
applyGamePassBenefits(player, passId)
end
end
end)
end)
Developer Products (Consumables)
Processing Receipts (CRITICAL)
-- This MUST be set and handle ALL purchases
local purchaseHistory = {} -- In production, use DataStore
MarketplaceService.ProcessReceipt = function(receiptInfo)
-- Prevent duplicate processing
local purchaseKey = receiptInfo.PlayerId .. "_" .. receiptInfo.PurchaseId
if purchaseHistory[purchaseKey] then
return Enum.ProductPurchaseDecision.PurchaseGranted
end
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
if not player then
-- Player left, try again later
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local productId = receiptInfo.ProductId
local success = false
-- Grant the product
if productId == DEV_PRODUCTS.Cash_100 then
success = grantCash(player, 100)
elseif productId == DEV_PRODUCTS.Cash_500 then
success = grantCash(player, 500)
elseif productId == DEV_PRODUCTS.Cash_1000 then
success = grantCash(player, 1000)
elseif productId == DEV_PRODUCTS.Revive then
success = revivePlayer(player)
else
warn("Unknown product:", productId)
return Enum.ProductPurchaseDecision.NotProcessedYet
end
if success then
-- Record purchase
purchaseHistory[purchaseKey] = true
-- In production: save to DataStore
savePurchaseRecord(player, receiptInfo)
return Enum.ProductPurchaseDecision.PurchaseGranted
else
return Enum.ProductPurchaseDecision.NotProcessedYet
end
end
local function grantCash(player, amount)
local currentCash = DataManager.get(player, "cash") or 0
DataManager.set(player, "cash", currentCash + amount)
-- Apply multipliers from game passes
local multiplier = player:GetAttribute("CashMultiplier") or 1
if multiplier > 1 then
local bonus = amount * (multiplier - 1)
DataManager.set(player, "cash", currentCash + amount + bonus)
-- Notify about bonus
CashNotificationRemote:FireClient(player, amount, bonus)
else
CashNotificationRemote:FireClient(player, amount, 0)
end
-- Save immediately after purchase
DataManager.save(player)
return true
end
local function revivePlayer(player)
local character = player.Character
if not character then return false end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid then return false end
if humanoid.Health > 0 then
return false -- Not dead, don't charge
end
-- Revive
humanoid.Health = humanoid.MaxHealth
-- Clear ragdoll/death state if applicable
player:SetAttribute("IsDead", false)
ReviveEffectRemote:FireAllClients(character)
return true
end
Prompting Products
local function promptProduct(player, productId)
local success, err = pcall(function()
MarketplaceService:PromptProductPurchase(player, productId)
end)
if not success then
warn("Failed to prompt product:", err)
end
end
-- Example: Revive prompt on death
local function onPlayerDied(player)
task.delay(2, function()
-- Show revive option
RevivePromptRemote:FireClient(player, DEV_PRODUCTS.Revive)
end)
end
Product Info Display
local function getProductInfo(productId)
local success, info = pcall(function()
return MarketplaceService:GetProductInfo(productId, Enum.InfoType.Product)
end)
if success then
return {
name = info.Name,
description = info.Description,
price = info.PriceInRobux,
icon = "rbxassetid://" .. info.IconImageAssetId
}
end
return nil
end
-- Cache product info
local productInfoCache = {}
local function getCachedProductInfo(productId)
if not productInfoCache[productId] then
productInfoCache[productId] = getProductInfo(productId)
end
return productInfoCache[productId]
end
Premium Benefits
local function isPremium(player)
return player.MembershipType == Enum.MembershipType.Premium
end
local function applyPremiumBenefits(player)
if isPremium(player) then
-- Premium badge
player:SetAttribute("IsPremium", true)
-- Premium-only benefits
player:SetAttribute("CashMultiplier",
(player:GetAttribute("CashMultiplier") or 1) * 1.5)
player:SetAttribute("XPMultiplier",
(player:GetAttribute("XPMultiplier") or 1) * 1.5)
-- Premium daily bonus
player:SetAttribute("DailyBonusMultiplier",
(player:GetAttribute("DailyBonusMultiplier") or 1) * 2)
-- Premium-only items unlocked
player:SetAttribute("PremiumItemsUnlocked", true)
end
end
-- Handle subscription changes mid-game
Players.PlayerMembershipChanged:Connect(function(player)
if isPremium(player) then
applyPremiumBenefits(player)
PremiumWelcomeRemote:FireClient(player)
end
end)
-- Prompt to upgrade to Premium
local function promptPremium(player)
local success, err = pcall(function()
MarketplaceService:PromptPremiumPurchase(player)
end)
if not success then
warn("Failed to prompt premium:", err)
end
end
Shop UI Patterns
Shop Item Template
-- Server: Shop configuration
local ShopItems = {
cash = {
{id = DEV_PRODUCTS.Cash_100, amount = 100, icon = "rbxassetid://123"},
{id = DEV_PRODUCTS.Cash_500, amount = 500, icon = "rbxassetid://124", bonus = 50},
{id = DEV_PRODUCTS.Cash_1000, amount = 1000, icon = "rbxassetid://125", bonus = 200}
},
gamePasses = {
{id = GAME_PASSES.VIP, name = "VIP", description = "VIP tag + 2x daily bonus"},
{id = GAME_PASSES.DoubleCash, name = "2x Cash", description = "Double all cash earnings"},
{id = GAME_PASSES.SpeedBoost, name = "Speed Boost", description = "50% faster movement"}
}
}
-- Client: Fetch and display
GetShopItemsRemote.OnClientEvent:Connect(function(items)
for _, item in ipairs(items.cash) do
local info = MarketplaceService:GetProductInfo(item.id, Enum.InfoType.Product)
local button = createShopButton()
button.Icon.Image = item.icon
button.Amount.Text = item.amount .. (item.bonus and " +" .. item.bonus or "")
button.Price.Text = info.PriceInRobux .. " R$"
button.MouseButton1Click:Connect(function()
MarketplaceService:PromptProductPurchase(Players.LocalPlayer, item.id)
end)
end
end)
Purchase Confirmation UI
-- Client: Confirm before expensive purchases
local function confirmPurchase(productName, robuxCost)
local result = showConfirmDialog(
"Confirm Purchase",
"Buy " .. productName .. " for " .. robuxCost .. " Robux?",
{"Yes", "No"}
)
return result == "Yes"
end
-- Modified purchase flow
PurchaseButton.MouseButton1Click:Connect(function()
local info = getCachedProductInfo(selectedProductId)
if info.price >= 100 then -- Confirm expensive purchases
if not confirmPurchase(info.name, info.price) then
return
end
end
MarketplaceService:PromptProductPurchase(LocalPlayer, selectedProductId)
end)
Purchase Persistence
Save Purchase Records
local PurchaseStore = DataStoreService:GetDataStore("Purchases_v1")
local function savePurchaseRecord(player, receiptInfo)
local key = "Player_" .. player.UserId
pcall(function()
PurchaseStore:UpdateAsync(key, function(data)
data = data or {purchases = {}}
table.insert(data.purchases, {
productId = receiptInfo.ProductId,
purchaseId = receiptInfo.PurchaseId,
time = os.time(),
robuxSpent = receiptInfo.CurrencySpent
})
-- Keep last 100 purchases
while #data.purchases > 100 do
table.remove(data.purchases, 1)
end
return data
end)
end)
end
local function getPurchaseHistory(player)
local key = "Player_" .. player.UserId
local success, data = pcall(function()
return PurchaseStore:GetAsync(key)
end)
if success and data then
return data.purchases or {}
end
return {}
end
Grant Missed Purchases
-- On player join, check for ungranted purchases
local function checkPendingPurchases(player)
local history = getPurchaseHistory(player)
local grantedIds = DataManager.get(player, "grantedPurchases") or {}
for _, purchase in ipairs(history) do
if not grantedIds[purchase.purchaseId] then
-- Grant this purchase
local granted = grantProduct(player, purchase.productId)
if granted then
grantedIds[purchase.purchaseId] = true
end
end
end
DataManager.set(player, "grantedPurchases", grantedIds)
end
Ethical Monetization Guidelines
DO:
- Clearly display prices before purchase
- Allow players to earn most things through gameplay
- Make premium items cosmetic or time-saving, not power-increasing
- Provide value at every price point
- Respect player's time and money
DON'T:
- Create artificial scarcity or FOMO
- Hide true costs behind multiple currencies
- Require purchases to progress or compete
- Target children with manipulative dark patterns
- Make gameplay frustrating to encourage purchases
Fair Pricing Examples
-- GOOD: Clear value proposition
local SHOP_CONFIG = {
-- Currency packs with bonus for larger purchases
cashPacks = {
{robux = 25, cash = 100, bonus = 0}, -- Base rate
{robux = 50, cash = 250, bonus = 50}, -- 20% bonus
{robux = 100, cash = 600, bonus = 100}, -- 40% bonus
{robux = 200, cash = 1500, bonus = 300} -- 50% bonus
},
-- Game passes are permanent, one-time purchases
gamePasses = {
vip = {robux = 199, benefits = "Permanent VIP status + 2x daily bonus"},
doubleCash = {robux = 149, benefits = "Permanent 2x cash multiplier"}
}
}
-- BAD: Don't do this
-- local PREDATORY_PATTERNS = {
-- limitedTimeOffer = true, -- Creates FOMO
-- spinToWin = true, -- Gambling mechanics
-- payToProgress = true, -- Gameplay gating
-- obscurePricing = true -- Multiple currencies
-- }
Spending Limits (Self-Regulation)
-- Track spending for responsible monetization
local function trackSpending(player, robuxSpent)
local today = os.date("%Y-%m-%d")
local spendingKey = "Spending_" .. today
local todaySpent = player:GetAttribute(spendingKey) or 0
todaySpent = todaySpent + robuxSpent
player:SetAttribute(spendingKey, todaySpent)
-- Optional: Warn at spending thresholds
if todaySpent >= 1000 then
SpendingWarningRemote:FireClient(player,
"You've spent " .. todaySpent .. " Robux today. Consider taking a break!")
end
end
-- In ProcessReceipt
MarketplaceService.ProcessReceipt = function(receiptInfo)
-- ... existing code ...
if success then
trackSpending(player, receiptInfo.CurrencySpent)
end
-- ... existing code ...
end
Complete Shop Implementation
-- Server: MonetizationService.lua
local MonetizationService = {}
-- Configuration
MonetizationService.GAME_PASSES = {
VIP = {id = 123456789, benefits = {"VIPTag", "DoubleDailyBonus"}},
DoubleCash = {id = 234567890, benefits = {"CashMultiplier"}},
PetSlots = {id = 345678901, benefits = {"ExtraPetSlots"}}
}
MonetizationService.DEV_PRODUCTS = {
Cash_100 = {id = 111111111, grant = {"cash", 100}},
Cash_500 = {id = 222222222, grant = {"cash", 500}},
Gems_10 = {id = 333333333, grant = {"gems", 10}},
Revive = {id = 444444444, action = "revive"}
}
-- Ownership cache
local ownershipCache = {}
function MonetizationService.init()
-- Set up ProcessReceipt
MarketplaceService.ProcessReceipt = function(receiptInfo)
return MonetizationService.processReceipt(receiptInfo)
end
-- Handle game pass purchases
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, passId, purchased)
if purchased then
MonetizationService.onGamePassPurchased(player, passId)
end
end)
-- Apply benefits on join
Players.PlayerAdded:Connect(function(player)
player.CharacterAdded:Connect(function()
MonetizationService.applyAllBenefits(player)
end)
end)
end
function MonetizationService.ownsGamePass(player, passName)
local passConfig = MonetizationService.GAME_PASSES[passName]
if not passConfig then return false end
local cacheKey = player.UserId .. "_" .. passConfig.id
if ownershipCache[cacheKey] ~= nil then
return ownershipCache[cacheKey]
end
local success, owns = pcall(function()
return MarketplaceService:UserOwnsGamePassAsync(player.UserId, passConfig.id)
end)
if success then
ownershipCache[cacheKey] = owns
return owns
end
return false
end
function MonetizationService.applyAllBenefits(player)
-- Game passes
for passName, config in pairs(MonetizationService.GAME_PASSES) do
if MonetizationService.ownsGamePass(player, passName) then
for _, benefit in ipairs(config.benefits) do
MonetizationService.applyBenefit(player, benefit)
end
end
end
-- Premium
if player.MembershipType == Enum.MembershipType.Premium then
MonetizationService.applyBenefit(player, "Premium")
end
end
function MonetizationService.applyBenefit(player, benefit)
if benefit == "VIPTag" then
player:SetAttribute("VIP", true)
elseif benefit == "DoubleDailyBonus" then
player:SetAttribute("DailyBonusMultiplier", 2)
elseif benefit == "CashMultiplier" then
player:SetAttribute("CashMultiplier", 2)
elseif benefit == "ExtraPetSlots" then
player:SetAttribute("MaxPets", 10) -- Default is 5
elseif benefit == "Premium" then
player:SetAttribute("IsPremium", true)
player:SetAttribute("XPMultiplier", 1.5)
end
end
function MonetizationService.processReceipt(receiptInfo)
local player = Players:GetPlayerByUserId(receiptInfo.PlayerId)
if not player then
return Enum.ProductPurchaseDecision.NotProcessedYet
end
local productId = receiptInfo.ProductId
local granted = false
-- Find matching product
for productName, config in pairs(MonetizationService.DEV_PRODUCTS) do
if config.id == productId then
if config.grant then
local currency, amount = config.grant[1], config.grant[2]
granted = MonetizationService.grantCurrency(player, currency, amount)
elseif config.action == "revive" then
granted = MonetizationService.revivePlayer(player)
end
break
end
end
if granted then
DataManager.save(player)
return Enum.ProductPurchaseDecision.PurchaseGranted
end
return Enum.ProductPurchaseDecision.NotProcessedYet
end
function MonetizationService.grantCurrency(player, currency, amount)
local current = DataManager.get(player, currency) or 0
-- Apply multipliers
local multiplier = player:GetAttribute(currency:sub(1,1):upper() .. currency:sub(2) .. "Multiplier") or 1
local finalAmount = math.floor(amount * multiplier)
DataManager.set(player, currency, current + finalAmount)
CurrencyGrantedRemote:FireClient(player, currency, amount, finalAmount - amount)
return true
end
function MonetizationService.revivePlayer(player)
local character = player.Character
if not character then return false end
local humanoid = character:FindFirstChildOfClass("Humanoid")
if not humanoid or humanoid.Health > 0 then
return false
end
humanoid.Health = humanoid.MaxHealth
return true
end
return MonetizationService
Weekly Installs
1
Repository
taozhuo/game-dev-skillsFirst Seen
Jan 30, 2026
Security Audits
Installed on
cursor1