Monetizar seu jogo

Produto de desenvolvedor: o ProcessReceipt é o que separa venda de prejuízo

Produto de desenvolvedor é o que o jogador compra mais de uma vez. Criar leva cinco minutos; o que decide se você entrega ou perde a compra é o ProcessReceipt, e ele tem uma regra de resposta que quase todo mundo descobre tarde.

por Gustavo Cantino9 min de leituraIniciante

Produzido com IA a partir das fontes citadas abaixo, e conferido por checagens automáticas contra essas mesmas fontes. Nenhuma pessoa leu este texto antes da publicação. Política Editorial

Placa tipográfica com a categoria Monetizar seu jogo em destaque, o nível iniciante abaixo, e um campo de barras verticais verde-limão sobre fundo escuro.

Criar um produto de desenvolvedor leva cinco minutos no painel. O que decide se a compra vira item na mão do jogador ou vira reclamação é uma função só, escrita no servidor, e ela tem uma regra de resposta que quase todo mundo descobre depois do primeiro prejuízo.

Este guia cobre o caminho inteiro — o que o produto é, como criar, como vender por script — mas o coração dele é o ProcessReceipt, porque é onde a venda acontece de verdade.

O problema

Primeiro, escolher a coisa certa. Produto de desenvolvedor é item ou habilidade que o usuário pode comprar mais de uma vez: os exemplos da própria documentação são moeda do jogo, munição e poções. Para o que se compra uma vez só — uma arma especial, um poder permanente —, a documentação manda ver Passes.

Essa escolha é estrutural e cara de desfazer: passe e produto têm APIs diferentes, telas diferentes e formas diferentes de o jogador reaver. Trocar depois é refazer a loja.

O segundo problema é o que ninguém vê no painel. Quando o jogador compra, a Roblox não entrega nada — ela emite um recibo e pergunta ao seu jogo o que fazer com ele. Se o seu jogo não responde direito, o dinheiro sai da conta do jogador e o item não aparece. O painel continua mostrando a venda.

Esse desenho tem um motivo. A compra acontece na infraestrutura da Roblox, que não tem como saber o que "cem moedas" significa dentro do seu jogo — quem sabe é o seu código. Então ela cobra, emite o recibo e devolve a pergunta. A entrega é responsabilidade sua, do começo ao fim, e o único sinal de que ela aconteceu é o que o seu servidor responde.

A consequência prática é desconfortável e vale ser dita: o painel de vendas não prova entrega. Ele mostra que a Roblox cobrou. Se o seu jogo estava com um bug naquele momento, a linha da venda está lá igual, e não há nada na interface que distinga a compra que chegou da que não chegou.

Passo a passo

1. Publique o jogo. A documentação é explícita: o jogo precisa estar publicado e acessível antes de criar o produto. Não dá para preparar a loja num lugar que ainda não existe.

2. Crie o produto. No painel, em Creations, escolha o jogo e vá em Monetization e depois em Developer Products. Clique em criar e preencha:

  • Ícone. Não pode passar de 512x512 pixels, precisa estar em .jpg, .png ou .bmp, e detalhe importante não deve ficar fora dos limites circulares.
  • Nome e descrição.
  • Categoria. Ela serve para organizar os itens na loja dentro do jogo — não é enfeite de painel, é o que agrupa a vitrine.
  • Preço em Robux. O mínimo é 1 Robux e o máximo é 1 bilhão de Robux. A faixa é larga de propósito; o preço que funciona não sai dela, sai do seu jogo.

3. Pegue o id. Para usar scripting é preciso ter o id do produto. Ele sai passando o mouse sobre a miniatura, clicando nos três pontos e escolhendo Copy Asset ID no menu de contexto. É o número que vai aparecer no código — e vale guardar num módulo de constantes, não espalhado em cada script.

4. Mostre o produto. O GetProductInfo devolve informação do produto, como nome e preço, para você montar a vitrine sem repetir o texto à mão. Um detalhe que custa uma hora de depuração a quem não leu: para produto de desenvolvedor, o segundo parâmetro precisa ser Enum.InfoType.Product. Com o enum errado a chamada não devolve o que você espera, e o erro não diz isso.

Se você tem muitos produtos, o GetDeveloperProductsAsync devolve todos os produtos associados ao jogo, num objeto Pages que dá para percorrer e filtrar para montar uma loja inteira sem listar nada manualmente.

Essa diferença entre as duas funções decide a manutenção da sua loja. Com GetProductInfo você escolhe item por item, o que é previsível mas obriga a mexer no código toda vez que um produto entra ou sai. Com GetDeveloperProductsAsync a vitrine se monta a partir do que existe no painel — criar um produto novo passa a ser trabalho de painel, não de deploy. O preço é ter que lidar com paginação e com o fato de que um produto mal configurado aparece na loja sozinho.

As duas chamadas conversam com o servidor da Roblox e podem falhar. Os exemplos oficiais envolvem as duas em pcall por isso mesmo, e não é zelo decorativo: sem pcall, uma instabilidade momentânea derruba a montagem da vitrine inteira e o jogador vê uma loja vazia em vez de uma loja com um item a menos.

5. Abra a compra. O PromptProductPurchase é o que abre a janela de compra dentro do jogo — ligado a um botão, a um NPC vendedor, ao que fizer sentido.

Se você usa otimização de preço, existe uma exigência que muda onde o código mora: o script precisa rodar no cliente para o usuário ver o preço personalizado. Um script de servidor montando a vitrine mostra o preço padrão para todo mundo.

6. Processe o recibo. Esta é a parte que não é opcional, e é a próxima seção.

Artefato

O script abaixo é o ProcessReceipt completo, com a ordem que importa: conferir se o recibo já foi entregue, entregar, gravar o id do recibo, e só então confirmar.

Ele também deixa explícito um detalhe que derruba jogo em produção: MarketplaceService.ProcessReceipt é um campo, não um evento. Dois scripts atribuindo esse campo não convivem — o último apaga o primeiro, em silêncio. O jogo tem um ProcessReceipt, e ele mora em um arquivo só.

Erro comum

O erro comum tem duas metades, e as duas vêm da mesma linha da documentação.

A primeira metade é não responder. A documentação descreve quatro passos: usar a API ProcessReceipt para conferir os recibos, validar cada recibo pelo id do usuário, pelo id do produto e pelo status, entregar quando o status for Open, e responder ao ProcessReceipt confirmando o recibo e validando que o item foi entregue.

Quem entrega e esquece de responder cria um recibo que nunca fecha. A Roblox continua reapresentando a compra, e o jogo continua entregando — o jogador ganha o item de novo a cada sessão, de graça. É o bug que se descobre pela economia inflacionada, não pelo log.

A segunda metade é o oposto: confirmar antes de ter certeza. Se o seu código devolve a confirmação e só depois tenta gravar que entregou, uma falha no DataStore nesse intervalo produz o pior desfecho possível — o recibo está fechado para a Roblox, e o seu jogo não tem registro de nada. O jogador pagou e não tem para onde apelar.

A ordem correta é a do artefato, e ela é conservadora de propósito: nunca confirme antes de gravar. O risco que sobra é entregar duas vezes, que é recuperável; o risco que se evita é cobrar sem entregar, que não é.

Existe ainda uma terceira forma, mais silenciosa, de perder a compra: entregar dentro de uma função que pode falhar no meio e não conferir se ela deu certo. Se a entrega depende de um leaderstats que ainda não existe, ou de uma pasta que o personagem só ganha depois de carregar, a função "roda" sem erro e não entrega nada. Por isso o artefato exige que a entrega devolva verdadeiro: sem essa confirmação, confirmar o recibo é apostar.

Vale também tratar o ProcessReceipt como o que ele é: um ponto que roda para compra feita fora do jogo também. A documentação diz que ele lê e reconhece automaticamente compra feita fora do jogo — ou seja, ele pode ser chamado para um jogador que acabou de entrar, com um recibo de dias atrás. Código que assume que a compra acabou de acontecer na sessão atual quebra exatamente aí.

Próximo passo

Depois que a entrega estiver confiável, existe uma data para colocar no calendário: a documentação marcou 30 de maio de 2026 para desativar a venda de produto entre jogos. Quem depende disso foi orientado a integrar transferência de Robux antes da data para evitar interrupção.

O teste é direto: procure no seu código qualquer PromptProductPurchase ou GetProductInfo cujo id pertença a outro jogo seu. Se não houver nenhum, a data não te afeta e é só seguir. Se houver, o trabalho existe e tem prazo — e é melhor descobrir isso lendo o próprio código do que descobrindo que a loja parou de funcionar.

Fontes

Como corrigimos erros

Artefato pronto pra usar

ProcessReceipt com entrega idempotente e recibo gravado antes de confirmar (ServerScriptService)
lua
-- ServerScriptService: o ÚNICO ProcessReceipt do jogo.
--
-- MarketplaceService.ProcessReceipt é um campo, não um evento: quem
-- escreve por último apaga o anterior. Dois scripts atribuindo ele é o
-- jeito mais comum de "a compra sumiu".
--
-- A ordem aqui é deliberada:
--   1. já entreguei este recibo?  -> confirma de novo, sem entregar 2x
--   2. entrega
--   3. GRAVA o id do recibo
--   4. só então devolve PurchaseGranted
--
-- Se o passo 3 falhar, NÃO confirmamos. A Roblox reapresenta o recibo
-- mais tarde e o jogador recebe. Confirmar antes de gravar troca
-- "entregar duas vezes" (recuperável) por "cobrar e não entregar"
-- (não recuperável).

local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local Players = game:GetService("Players")

local recibos = DataStoreService:GetDataStore("RecibosProcessados")

-- id do produto -> o que ele faz. Tudo que se vende mora aqui.
local PRODUTOS = {
	[000000] = function(player, quantidade)
		local moedas = player:FindFirstChild("leaderstats")
			and player.leaderstats:FindFirstChild("Moedas")
		if not moedas then
			return false
		end
		moedas.Value = moedas.Value + quantidade
		return true
	end,
}

local QUANTIDADE = {
	[000000] = 100,
}

local function chaveDoRecibo(info)
	return info.PlayerId .. "_" .. info.PurchaseId
end

local function processar(info)
	local player = Players:GetPlayerByUserId(info.PlayerId)
	if not player then
		-- jogador saiu: NÃO confirma. O recibo volta na próxima sessão.
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	local chave = chaveDoRecibo(info)

	-- 1. idempotência
	local jaFoi
	local okLeitura = pcall(function()
		jaFoi = recibos:GetAsync(chave)
	end)
	if not okLeitura then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end
	if jaFoi then
		return Enum.ProductPurchaseDecision.PurchaseGranted
	end

	-- 2. entrega
	local entregar = PRODUTOS[info.ProductId]
	if not entregar then
		warn("produto sem entrega registrada: " .. tostring(info.ProductId))
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	local okEntrega, entregou = pcall(entregar, player, QUANTIDADE[info.ProductId] or 1)
	if not okEntrega or not entregou then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 3. grava ANTES de confirmar
	local okGravou = pcall(function()
		recibos:SetAsync(chave, os.time())
	end)
	if not okGravou then
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end

	-- 4. confirma
	return Enum.ProductPurchaseDecision.PurchaseGranted
end

MarketplaceService.ProcessReceipt = function(info)
	local ok, decisao = pcall(processar, info)
	if not ok then
		warn("ProcessReceipt lancou: " .. tostring(decisao))
		return Enum.ProductPurchaseDecision.NotProcessedYet
	end
	return decisao
end
Neste artigo

Continue por aqui