GamepassReceipt - Secure way to purchases payments with Gamepass

🛡️GamepassReceipt 🪐

Secure way to process purchaseswith Gamepass

Get Here!

What is it?

GamepassReceipt is a secure way to purchase gamepasses, designed specifically to prevent unwanted purchases of malicious applications, ensuring secure purchases with options provided by the app itself, including a player history log and insurance for donation games, as well as a price change verifier.

How is it used?

It is currently dedicated to ServerSide-only.
This is to keep the data within your own game more secure.

Now I will show you all the options that this module can offer you:

Documentation
---[[
GamepassReceipt:PromptGamepassPurchase(player: Player, gamepassId: number): boolean

GamepassReceipt:OnGamepassPurchase(function(player: Player, gamepassInfo: GamepassInfo, isPurchased: boolean)
	if isPurchased then
		print(gamepassInfo) -- Return a gamepass info in table format | MarketplaceService:GetProductInfo(gamepassId, Enum.InfoType.GamePass)
	else
		print(player.Name .. " did not purchase gamepass with ID: " .. gamepassId)
	end
	return response
end)

GamepassReceipt:SaveGamepassArray({}) -- Saves the gamepass array to a persistent storage
		:andThen(function()
			print("Gamepass array saved successfully.")
		end)
		:andCatch(function(error)
			warn("Failed to save gamepass array: [", error .. "]")
		end)

GamepassReceipt:TrackPurchases(enable: boolean) -- Enables or disables tracking of gamepass purchases
GamepassReceipt:GetPlayerHistory(playerUserId: number) -- Returns a table of gamepass purchase history for the player
	-- Example: { {gamepassId = 123456, purchaseDate = "2023-10-01"}, ... }

GamepassReceipt.GetGamepassImage(gamepassId: number): string
	-- Returns the image URL for the gamepass with the given ID

GamepassReceipt:HasGamepass(playerUserId: number, gamepassId: number): boolean
	-- Returns true if the player has the gamepass, false otherwise

GamepassReceipt:SetAliasGamepassId(alias: string, gamepassId: number)
	-- Sets an alias for a gamepass ID, allowing you to use the alias instead of the ID in other methods

GamepassReceipt:PromptByAlias(player: Player, alias: string): boolean
	-- Prompts the player to purchase a gamepass using an alias instead of the gamepass ID

GamepassReceipt:CleanHistoryPlayer(playerUserId: number)
	-- Cleans the purchase history for the player, removing all entries

GamepassReceipt.CanChangePrice: boolean -- Allows changing gamepass prices for testing purposes
]]

Example a code:

Code Example
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local Players = game:GetService("Players")

--> -- Load the GamepassReceipt module|
local GamepassReceipt = require(ReplicatedStorage:FindFirstChild("GamepassReceipt"))

-- Configuration
GamepassReceipt.CanChangePrice = false -- Allow changing gamepass prices for testing
GamepassReceipt.DebugMode = true
GamepassReceipt.TrackingEnabled = true -- Enable tracking for gamepass purchases

local gamepassId = 1366016600
GamepassReceipt:SetAliasGamepassId("AYUDA", gamepassId) -- You can set an alias for the gamepass ID

Players.PlayerAdded:Connect(function(player)
	task.wait(2) -- Wait for the player to load fully
	print(GamepassReceipt:HasGamepass(player.UserId, gamepassId)) -- Check if the player has the gamepass

	task.wait(2)
	print(GamepassReceipt:GetPlayerHistory(player.UserId)) -- Get the player's purchase history

	task.wait(2)

	GamepassReceipt
		:SaveGamepassArray({ gamepassId }) -- Save the gamepass array for the game
		:andThen(function()
			print("Gamepass array saved successfully.")
		end)
		:andCatch(function(error)
			warn("Failed to save gamepass array: [", error .. "]")
		end)

	GamepassReceipt
		:PromptGamepassPurchase(player, gamepassId) -- Prompt the player to purchase the gamepass
		:andThen(function()
			print("Gamepass purchase prompt sent successfully.")
		end)
		:andCatch(function(error)
			warn("Failed to prompt gamepass purchase: [", error .. "]")
		end)
end)

Players.PlayerRemoving:Connect(function(player)
	GamepassReceipt:SaveHistory(player.UserId)
end)

GamepassReceipt:OnGamepassPurchase(
	function(player, gamepassId2, isPurchased, _gamepassInfo) -- Callback for gamepass purchase
		if isPurchased then
			GamepassReceipt:PromptByAlias(player, "AYUDA")
				:andThen(function()
					print(player.Name .. " successfully purchased gamepass with ID: " .. gamepassId2)

					GamepassReceipt:SaveHistory(player.UserId) -- Save the purchase history for the player
				end)
				:andCatch(function(error)
					warn("Error in prompting by alias: " .. error)
				end)

			GamepassReceipt:CleanHistoryPlayer(player.UserId) -- Clean the player's history after purchase
		else
			print(player.Name .. " cancelled the purchase of gamepass with ID: " .. gamepassId2)
		end
	end
)
	:andCatch(function(error)
		warn("Error in gamepass purchase callback: " .. error)
	end)

Contribution

Feel free to modify this code as you wish. If you would like your modified version to be published as the next update, please do not hesitate to contact me.

5 Likes

may i see the source code?

jdhdjdjdejsihsgnjjdj

So, essentially what you’re saying is, that somehow, Roblox purchase processing is not secure enough? Do you have any examples of where it’s not secure? Either way, if it’s not secure, it should be reported to Roblox directly so they can fix the issue, instead of making a third party solution.

2 Likes

Pretty sure in certain cases, exploiters can fire the purchase success signal and have the game pass benefits granted to them.

1 Like
Full Code
--!strict
local GamepassReceipt = {}
GamepassReceipt.__index = GamepassReceipt
--[[
GamepassReceipt:PromptGamepassPurchase(player: Player, gamepassId: number): boolean
GamepassReceipt:OnGamepassPurchase(function(player: Player, gamepassInfo: GamepassInfo, isPurchased: boolean)
	if isPurchased then
		print(gamepassInfo) -- Return a gamepass info in table format | MarketplaceService:GetProductInfo(gamepassId, Enum.InfoType.GamePass)
	else
		print(player.Name .. " did not purchase gamepass with ID: " .. gamepassId)
	end
	return response
end)
GamepassReceipt:SaveGamepassArray({}) -- Saves the gamepass array to a persistent storage
		:andThen(function()
			print("Gamepass array saved successfully.")
		end)
		:andCatch(function(error)
			warn("Failed to save gamepass array: [", error .. "]")
		end)
GamepassReceipt:TrackPurchases(enable: boolean) -- Enables or disables tracking of gamepass purchases
GamepassReceipt:GetPlayerHistory(playerUserId: number) -- Returns a table of gamepass purchase history for the player
	-- Example: { {gamepassId = 123456, purchaseDate = "2023-10-01"}, ... }
GamepassReceipt:HasGamepass(playerUserId: number, gamepassId: number): boolean
	-- Returns true if the player has the gamepass, false otherwise
GamepassReceipt:SetAliasGamepassId(alias: string, gamepassId: number)
	-- Sets an alias for a gamepass ID, allowing you to use the alias instead of the ID in other methods
GamepassReceipt:PromptByAlias(player: Player, alias: string): boolean
	-- Prompts the player to purchase a gamepass using an alias instead of the gamepass ID
GamepassReceipt:CleanHistoryPlayer(playerUserId: number)
	-- Cleans the purchase history for the player, removing all entries
GamepassReceipt.CanChangePrice: boolean -- Allows changing gamepass prices for testing purposes
VERSION = "1.0.0",
DESCRIPTION = "Gamepass Receipt Module",
AUTHOR = "ImNotServi",
LICENSE = "MIT",
--]]
-->> Services
local MarketplaceService = game:GetService("MarketplaceService")
local DataStoreService = game:GetService("DataStoreService")
local RunService = game:GetService("RunService")
GamepassReceipt.__Prefix = "[GPReceipt] "
GamepassReceipt.__History = {}
GamepassReceipt.__Aliases = {}
GamepassReceipt.TrackingEnabled = false
GamepassReceipt.CanChangePrice = false
GamepassReceipt.DebugMode = false
GamepassReceipt.__Profiles = {}
GamepassReceipt.__TemporalDataSaved = {}
GamepassReceipt.__StorageGamepass = {}
GamepassReceipt.__db = {}
GamepassReceipt.__DSName = "GamepassReceiptDataStore"
-->> Methods
--//Clean history player
function GamepassReceipt:CleanHistoryPlayer(playerUserId: number)
	if RunService:IsClient() then
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "CleanHistoryPlayer cannot be called from the client.")
		end
		return
	end
	if self.TrackingEnabled then
		if self.__Profiles[playerUserId] then
			self.__Profiles[playerUserId] = {}
			if self.DebugMode then
				warn(self.__Prefix or "Unknown" .. "History cleaned for player: " .. tostring(playerUserId))
			end
		end
	else
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "Tracking is not enabled. Cannot clean player history.")
		end
	end
end
--//Save data player
function GamepassReceipt:SaveHistory(playerUserId: number)
	if RunService:IsClient() then
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "SaveHistory cannot be called from the client.")
		end
		return
	end
	if self.TrackingEnabled then
		local getProfile = self.__Profiles[playerUserId]
		if getProfile == nil then
			if self.DebugMode then
				warn(self.__Prefix or "Unknown" .. "Player profile not found for saving history.")
			end
			return
		end
		if #getProfile <= 0 then
			if self.DebugMode then
				warn(self.__Prefix or "Unknown" .. "No gamepass history to save for player: " .. tostring(playerUserId))
			end
			return
		end
		local success, result = pcall(function()
			local dataStore = DataStoreService:GetDataStore(self.__DSName)
			return dataStore:SetAsync(tostring(playerUserId), getProfile)
		end)
		if not success then
			if self.DebugMode then
				warn(self.__Prefix or "Unknown" .. "Failed to save player history: " .. tostring(result))
			end
		end
	else
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "Tracking is not enabled. Cannot save player history.")
		end
	end
end
--// get history player
function GamepassReceipt:GetPlayerHistory(playerUserId: number)
	if RunService:IsClient() then
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "GetPlayerHistory cannot be called from the client.")
		end
		return {}
	end
	if not self.TrackingEnabled then
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "Tracking is not enabled. Cannot get player history.")
		end
		return {}
	end
	if GamepassReceipt.__Profiles[playerUserId] == nil then
		if GamepassReceipt.__db[playerUserId] then
			return {}
		end
		GamepassReceipt.__db[playerUserId] = true
		local success, result = pcall(function()
			local dataStore = DataStoreService:GetDataStore(self.__DSName)
			return dataStore:GetAsync(tostring(playerUserId))
		end)
		if success and result then
			GamepassReceipt.__Profiles[playerUserId] = result
			GamepassReceipt.__db[playerUserId] = false
			return result or {}
		else
			GamepassReceipt.__Profiles[playerUserId] = {}
			GamepassReceipt.__db[playerUserId] = false
			if self.DebugMode then
				warn(self.__Prefix or "Unknown" .. "Failed to get player history")
			end
			return {}
		end
	else
		return GamepassReceipt.__Profiles[playerUserId] or {}
	end
end
--// PROMPT BY ALIAS
function GamepassReceipt:HasGamepass(playerUserId: number, gamepassId: number): boolean
	local success, result = pcall(function()
		return MarketplaceService:UserOwnsGamePassAsync(playerUserId, gamepassId)
	end)
	if success then
		return result
	else
		if GamepassReceipt.DebugMode then
			warn(GamepassReceipt.__Prefix .. "Failed to check if player has gamepass ID: " .. tostring(gamepassId))
		end
		return false
	end
end
--// Prompt by alias
function GamepassReceipt:PromptByAlias(player: Player, alias: string)
	local response = {
		_success = false,
		_error = false,
		_errorMessage = "nil",
	}
	function response:andCatch(callback)
		if response._error then
			callback(self._errorMessage)
		end
		return self
	end
	function response:andThen(callback)
		if self._success then
			callback()
		end
		return self
	end
	-- Check if the alias exists
	if not self.__Aliases[alias] then
		response._error = true
		response._errorMessage = "Alias does not exist."
		return response
	end
	local gamepassId = self.__Aliases[alias]
	if GamepassReceipt.TrackingEnabled then
		if GamepassReceipt.__Profiles[player.UserId] == nil then
			response._error = true
			response._errorMessage = "Player profile not found"
			return response
		end
	end
	-- Si ya fue guardado algo antes
	if GamepassReceipt.__TemporalDataSaved[player.UserId] then
		response._error = true
		response._errorMessage = "Player already prompted or pending"
		return response -- This will prevent multiple prompts for the same player
	end
	-- Verificación y cache temporal
	if GamepassReceipt.__TemporalDataSaved[player.UserId] == nil then
		GamepassReceipt.__TemporalDataSaved[player.UserId] = {}
	end
	-- Intentar obtener la información del producto
	local success, result = pcall(function()
		return MarketplaceService:GetProductInfo(gamepassId, Enum.InfoType.GamePass)
	end)
	if success and result then
		GamepassReceipt.__TemporalDataSaved[player.UserId] = result
		MarketplaceService:PromptGamePassPurchase(player, gamepassId)
		response._success = true
		return response -- successfully prompted the player
	end
	if success and not result then
		local msg = "Gamepass with ID " .. gamepassId .. " does not exist or is invalid."
		response._error = true
		response._errorMessage = msg
		return response -- This will catch any other errors that may occur
	end
	if not success then
		local msg = "Failed to get product info for gamepass ID " .. gamepassId .. ": " .. tostring(result)
		response._error = true
		response._errorMessage = msg
		return response -- This will catch any other errors that may occur
	end
	-- Error handling
	response._error = true
	response._errorMessage = "Unknown error"
	return response
end
--// ALIAS GAMEPASS ID
function GamepassReceipt:SetAliasGamepassId(alias: string, gamepassId: number)
	local response = {
		_success = false,
		_error = "nil",
	}
	function response:andCatch(callback)
		if response._error then
			callback(response._error)
		end
		return response
	end
	function response:andThen(callback)
		if response._success then
			callback()
		end
		return response
	end
	if RunService:IsClient() then
		if self.DebugMode then
			warn(self.__Prefix or "Unknown" .. "SetAliasGamepassId cannot be called from the client.")
		end
		return response
	end
	-- check if the alias has space
	if string.find(alias, " ") then
		response._error = "Alias cannot contain spaces."
		return response
	end
	-- check if gamepass id has space
	if string.find(tostring(gamepassId), " ") then
		response._error = "Gamepass ID cannot contain spaces."
		return response
	end
	if type(alias) ~= "string" or type(gamepassId) ~= "number" then
		response._error = "Alias must be a string and gamepassId must be a number."
		return response
	end
	self.__Aliases[alias] = gamepassId
	response._success = true
	return response
end
--// Storage a new gamepass receipt
function GamepassReceipt:SaveGamepassArray(gamepassArray)
	local response = {
		_success = false,
		_error = nil,
		_errorMessage = "nil",
	}
	function response:andCatch(callback)
		if response._error then
			callback(response._errorMessage)
		end
		return response
	end
	function response:andThen(callback)
		if response._success then
			callback()
		end
		return response
	end
	if RunService:IsClient() then
		warn(self.__Prefix or "Unknown" .. "SaveGamepassArray cannot be called from the client.")
		response._success = true
		response._errorMessage = "SaveGamepassArray cannot be called from the client."
		return response
	end
	--[[
	if type(gamepassArray) ~= "table" then
		response._error = "Gamepass array must be a table."
		return response
	end
	]]
	--self.__StorageGamepass = self.__StorageGamepass or {}
	for _, gamepass in ipairs(gamepassArray) do
		if table.find(self.__StorageGamepass, gamepass) then
			response._success = true
			response._errorMessage = "Gamepass already exists in storage: " .. tostring(gamepass)
			return response
		end
	end
	-- Si llegamos aquí, todo está bien, insertamos los nuevos
	for _, gamepass in ipairs(gamepassArray) do
		table.insert(self.__StorageGamepass, gamepass)
	end
	response._success = true
	return response
end
--// Finished gamepass purchase
function GamepassReceipt:OnGamepassPurchase(callback)
	local response = {
		_catchers = {},
		_error = false,
		_errorMessage = "nil",
	}
	function response:andCatch(catchCallback)
		if typeof(catchCallback) == "function" then
			table.insert(self._catchers, catchCallback)
			if self._error then
				task.defer(catchCallback, self._errorMessage)
			end
		end
		return self
	end
	if typeof(callback) ~= "function" then
		response._error = true
		response._errorMessage = "Callback function is required and must be a function."
		for _, fn in ipairs(response._catchers) do
			fn(response._errorMessage)
		end
		return response
	end
	MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(player, gamepassId, isPurchased)
		if not isPurchased then
			callback(player, gamepassId, false, nil)
			return
		end
		local findGamepass = table.find(self.__StorageGamepass, gamepassId)
		if not findGamepass then
			local err = "Gamepass with ID " .. gamepassId or 0 .. " does not exist in storage."
			response._error = true
			response._errorMessage = err
			callback(player, gamepassId, false, nil)
			for _, fn in ipairs(response._catchers) do
				fn(err)
			end
			return
		end
		local success, gamepassInfo = pcall(function()
			return MarketplaceService:GetProductInfo(gamepassId, Enum.InfoType.GamePass)
		end)
		if success and gamepassInfo then
			if self.CanChangePrice then
				if self.DebugMode then
					warn(
						self.__Prefix
							or "Default" .. "Checking gamepass price for player: " .. player.Name
							or "Unknown" .. " | TIME: " .. os.date("%X")
					)
				end
				local TempData = GamepassReceipt.__TemporalDataSaved[player.UserId] or { PriceInRobux = 0 }
				if not TempData or TempData == nil then
					TempData = { PriceInRobux = 0 }
				end
				local PriceInRobux = TempData.PriceInRobux
				if PriceInRobux ~= gamepassInfo.PriceInRobux then
					local err = "Gamepass price has changed. Cannot proceed with purchase."
					response._error = true
					response._errorMessage = err
					callback(player, gamepassId, false, nil)
					for _, fn in ipairs(response._catchers) do
						fn(err)
					end
					return
				end
			end
			if self.TrackingEnabled then
				self.__Profiles[player.UserId] = self.__Profiles[player.UserId] or {}
				table.insert(self.__Profiles[player.UserId], {
					gamepassId = gamepassId,
					purchaseDate = os.date("%Y-%m-%d %H:%M:%S"),
				})
			end
			GamepassReceipt.__TemporalDataSaved[player.UserId] = nil
			callback(player, gamepassId, true, gamepassInfo)
			return
		else
			local err = "Failed to retrieve info for Gamepass ID: " .. gamepassId
			response._error = true
			response._errorMessage = err
			callback(player, gamepassId, false, nil)
			for _, fn in ipairs(response._catchers) do
				fn(err)
			end
		end
	end)
	return response
end
--//PROMPT PLAYER TO PURCHASE A GAMEPASS
function GamepassReceipt:PromptGamepassPurchase(player: Player, gamepassId: number)
	local response = {
		_success = false,
		_error = nil,
		_errorMessage = "nil",
	}
	function response:andCatch(callback)
		if response._error then
			callback(self._errorMessage)
		end
		return self
	end
	function response:andThen(callback)
		if self._success then
			callback()
		end
		return self
	end
	if GamepassReceipt.TrackingEnabled then
		if GamepassReceipt.__Profiles[player.UserId] == nil then
			response._errorMessage = "Player profile not found"
			return response
		end
	end
	-- Si ya fue guardado algo antes
	if GamepassReceipt.__TemporalDataSaved[player.UserId] then
		response._errorMessage = "Player already prompted or pending"
		return response -- This will prevent multiple prompts for the same player
	end
	-- Verificación y cache temporal
	if GamepassReceipt.__TemporalDataSaved[player.UserId] == nil then
		GamepassReceipt.__TemporalDataSaved[player.UserId] = {}
	end
	-- Intentar obtener la información del producto
	local success, result = pcall(function()
		return MarketplaceService:GetProductInfo(gamepassId, Enum.InfoType.GamePass)
	end)
	if success and result then
		GamepassReceipt.__TemporalDataSaved[player.UserId] = result
		MarketplaceService:PromptGamePassPurchase(player, gamepassId)
		response._success = true
		return response
	end
	if success and not result then
		local msg = "Gamepass with ID " .. gamepassId .. " does not exist or is invalid."
		response._errorMessage = msg
		return response -- This will catch any other errors that may occur
	end
	if not success then
		local msg = "Failed to get product info for gamepass ID " .. gamepassId .. ": " .. tostring(result)
		response._errorMessage = msg
		return response -- This will catch any other errors that may occur
	end
	-- Error handling
	response._errorMessage = "Unknown error"
	return response
end
return GamepassReceipt

The only case this can happen in is if the exploiter already owns the gamepass. It’s secure as long as none of your logic runs with the assumption that it’s guaranteed this is the only time the event is fired.

1 Like

Hey, thanks for responding. Let me explain:

I recently created a donation game where, unfortunately, there are many ways to bypass this gamepass system. I spoke with a guy to see how he manages to do this, and it turns out that an exploit transmits a purchase to an existing Roblox API, to which you give the value of another gamepass ID, and with an exploit, you execute the event, which allows you to purchase the gamepass without having to pay.

(If I found the video, believe me, I would show you how they do it haha, it’s similar to how they do it with devsproducts)

That’s right, that’s why I designed this system to prevent (as far as possible) any hacking of this system with that “api.”

Saving the gamepass in a table:

GamepassReceipt:SaveGamepassArray()

I really appreciate you responding to this topic. I’m currently a big fan of your Project RemotesEvents (Warp) system, haha