Why closing an invite prompt shouldn't pay a reward
GameInvitePromptClosed and ShareSheetClosed fire whether or not anything was sent. How to reward invites when the friend actually joins instead.
What it looks like
The game pays players for inviting or sharing, but pays when the prompt closes rather than when a friend actually joins. The reward hangs off SocialService.ShareSheetClosed, a client's GameInvitePromptClosed handler that fires a remote, or a remote that reports how many friends were invited.
SocialService.ShareSheetClosed:Connect(function(player) profile.Coins += 50 end)
What an exploiter could do
They open the invite prompt and close it straight away, as often as the game allows, and collect the reward each time without inviting anyone. Where the client reports a friend count, they send the largest count the server accepts. A reward meant to grow the game becomes free currency, and none of it brings in a single player.
How to fix it
- Don't reward opening or closing an invite or share prompt. Show the prompt, and leave it at that.
- Pay for invites when the invited friend joins: read
ReferredByPlayerIdon the server inPlayers.PlayerAdded, pay the inviter and the invitee once per invitee, and save that the referral was paid. - If the game wants a small thank-you for using the prompt at all, make it a one-off per account, saved, with no currency attached.
Vulnerable example
local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local ServerScriptService = game:GetService("ServerScriptService") local SocialService = game:GetService("SocialService") local PlayerData = require(ServerScriptService.PlayerData) local shareReward = 50 local lastShared: { [Player]: number } = {} ReplicatedStorage.Remotes.ShareGame.OnServerEvent:Connect(function(player: Player) local ok = pcall(SocialService.PromptLinkSharingAsync, SocialService, player) if not ok then warn("Couldn't open the share sheet") end end) SocialService.ShareSheetClosed:Connect(function(player: Player) local now = os.clock() if now - (lastShared[player] or -60) < 60 then return end lastShared[player] = now local profile = PlayerData.get(player) if profile then profile.Coins += shareReward end end) Players.PlayerRemoving:Connect(function(player) lastShared[player] = nil end)
Fixed example
local DataStoreService = game:GetService("DataStoreService") local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local ServerScriptService = game:GetService("ServerScriptService") local SocialService = game:GetService("SocialService") local PlayerData = require(ServerScriptService.PlayerData) local referrals = DataStoreService:GetDataStore("Referrals") local inviterReward = 50 local lastPrompted: { [Player]: number } = {} ReplicatedStorage.Remotes.ShareGame.OnServerEvent:Connect(function(player: Player) local now = os.clock() if now - (lastPrompted[player] or -10) < 10 then return end lastPrompted[player] = now local ok = pcall(SocialService.PromptLinkSharingAsync, SocialService, player) if not ok then warn("Couldn't open the share sheet") end end) Players.PlayerAdded:Connect(function(player) local referrerId: unknown = (player:GetJoinData() :: any).ReferredByPlayerId if typeof(referrerId) ~= "number" or referrerId <= 0 or referrerId == player.UserId then return end local ok, first = pcall(function() local fresh = false referrals:UpdateAsync(tostring(player.UserId), function(paidTo: number?): number? fresh = paidTo == nil if paidTo then return nil end return referrerId end) return fresh end) if not ok or not first then return end local inviter = Players:GetPlayerByUserId(referrerId) local profile = inviter and PlayerData.get(inviter) if profile then profile.Coins += inviterReward end end) Players.PlayerRemoving:Connect(function(player) lastPrompted[player] = nil end)
How to spot this in your own scripts, and the cases that only look like it, are part of the Luaudit check your AI runs.