Mainly for secure server side rewards, spending milestones, special effects, and leaderboards based on how much Robux a player actually spent.
There is currently no fully reliable way for an experience to know the actual price paid for a Marketplace purchase. This is a Roblox side limitation, and developers cannot make systems like this completely secure without receiving the final transaction information directly from Roblox.
MarketplaceService.PromptPurchaseFinished only returns the player, asset ID, and whether the purchase was completed.
MarketplaceService.PromptPurchaseFinished:Connect(function(
player,
assetId,
wasPurchased
)
print(player, assetId, wasPurchased)
end)
The same issue exists with MarketplaceService.PromptBundlePurchaseFinished and MarketplaceService.PromptGamePassPurchaseFinished.
MarketplaceService.PromptBundlePurchaseFinished:Connect(function(
player,
bundleId,
wasPurchased
)
print(player, bundleId, wasPurchased)
end)
MarketplaceService.PromptGamePassPurchaseFinished:Connect(function(
player,
gamePassId,
wasPurchased
)
print(player, gamePassId, wasPurchased)
end)
These events confirm that something was purchased, but they do not tell us the original price, the personalized price shown to that player, which discounts were applied, or the amount of Robux the player actually spent.
This is important for experiences that give rewards based on purchase amounts, track spending leaderboards, run special effects or events when a certain amount is spent, or give different rewards at certain spending milestones.
A good example is Avatar Stores.
Avatar Stores lets players purchase avatar items from creator stores. The more Robux someone spends, the more Cloutbux they receive. Cloutbux is an in game currency that can be used for things such as nameplates and stores.
For that system to be secure, the server needs to know exactly how much Robux was spent during the purchase. There are also plans for leaderboards showing how much Robux players have spent and how much creators have earned through purchases, but those cannot be made fully secure right now because there is no trusted serverside transaction record containing the actual amount paid.
The only option developers currently have for normal asset purchases is calling MarketplaceService.GetProductInfoAsync and using the price returned from that. The problem is that this returns product information, not information from the transaction that just happened, and it can return cached or outdated pricing information.
For example, someone could price an item at 5 billion Robux and have GetProductInfoAsync called while it is at that price. They could then change the item to 5 Robux and have somebody purchase it.
PromptPurchaseFinished would confirm that the purchase was successful, but it would not tell the server that the player only paid 5 Robux. If the old product information is still cached, the game could reward the player as if they spent 5 billion Robux even though they only spent 5.
This could be intentionally abused, but it could also happen completely legitimately when a creator changes an item’s price and the cached information has not updated yet. This means even developers who validate everything properly on the server can still have their systems break due to information they cannot control.
Using an external API after the purchase is not completely reliable either. The price could change before the external server checks it, and the current price of an item still does not prove how much the player paid during that specific transaction. It also does not prove which regional pricing or Plus discounts were applied.
The issue becomes even worse with MarketplaceService.PromptBulkPurchaseFinished.
That event returns the player, the overall prompt status, and a results dictionary containing RobuxSpent and the purchase status of each item.
{
RobuxSpent = 1025,
Items = {
{
type = Enum.MarketplaceProductType.AvatarAsset,
id = "23852835234",
status = Enum.MarketplaceItemPurchaseStatus.Success,
},
{
type = Enum.MarketplaceProductType.AvatarBundle,
id = "182",
status = Enum.MarketplaceItemPurchaseStatus.Success,
},
}
}
The problem is that the result received by the client cannot be trusted for validation.
Developers have to send the results to the server through a RemoteEvent and then attempt to validate everything themselves. The server cannot trust the client supplied RobuxSpent, item IDs, or purchase statuses because an exploiter could modify any of those values before sending them.
Roblox recommends checking ownership with MarketplaceService.PlayerOwnsAssetAsync and MarketplaceService.PlayerOwnsBundleAsync, but those methods only prove that the player currently owns the item.
They do not prove that the item came from that specific bulk purchase, when the item was purchased, how much the player paid, which discounts were applied, or whether the player already owned the item before the prompt was shown.
Checking the price of every item with GetProductInfoAsync brings back the same cached price issue, so there is still no completely secure way for developers to validate the amount spent during a bulk purchase.
Ideally, Roblox would add a trusted server side handler or callback for Marketplace transactions. It does not specifically have to use MarketplaceService.BindReceiptHandler. Any new server side event or handler would work as long as the information comes directly from Roblox and represents the completed transaction.
A normal purchase could theoretically provide something like this.
{
PurchaseId = "purchase_abc123",
PlayerId = 123456789,
ProductType = Enum.MarketplaceProductType.AvatarAsset,
ProductId = 23852835234,
OriginalPriceInRobux = 900,
UserBasePriceInRobux = 850,
PricePaidInRobux = 800,
Discounts = {
{
Type = "RegionalPricing",
AmountInRobux = 50,
},
{
Type = "PlusDiscount",
AmountInRobux = 50,
},
}
}
In this example, the original price was 900 Robux, the player had a personalized base price of 850 Robux, another 50 Robux discount was applied, and the player actually paid 800 Robux.
For a bulk purchase, the same information could be returned for every item.
{
PurchaseId = "bulk_purchase_abc123",
PlayerId = 123456789,
RobuxSpent = 1025,
Items = {
{
ProductType = Enum.MarketplaceProductType.AvatarAsset,
ProductId = 23852835234,
Status = Enum.MarketplaceItemPurchaseStatus.Success,
OriginalPriceInRobux = 900,
UserBasePriceInRobux = 850,
PricePaidInRobux = 800,
Discounts = {
{
Type = "RegionalPricing",
AmountInRobux = 50,
},
{
Type = "PlusDiscount",
AmountInRobux = 50,
},
},
},
{
ProductType = Enum.MarketplaceProductType.AvatarBundle,
ProductId = 182,
Status = Enum.MarketplaceItemPurchaseStatus.Success,
OriginalPriceInRobux = 250,
UserBasePriceInRobux = 250,
PricePaidInRobux = 225,
Discounts = {
{
Type = "PlusDiscount",
AmountInRobux = 25,
},
},
},
}
}
These examples do not have to be used exactly as written. The field names and structure can be completely different. They are only examples of the information that would be useful.
The important part is that developers receive trusted information about the completed transaction, including the original price, the personalized price, the final amount paid, the discounts applied, the product that was purchased, and a unique transaction ID.
Developer products are more secure because MarketplaceService.ProcessReceipt gives the server a trusted receipt containing information such as PurchaseId, PlayerId, ProductId, and CurrencySpent.
However, even developer product receipts do not provide the original listed price or which discounts were applied. CurrencySpent gives developers the final amount charged, which is already much better than the current asset, bundle, game pass, and bulk purchase events, but a full price breakdown would still be useful there as well.
The main issue is that there is currently no fully reliable way to know the original price, which discounts were applied, and how much Robux the player actually spent on Marketplace assets, bundles, game passes, and bulk purchases.
This can be exploited intentionally, but it can also break experiences through completely legitimate price changes and cached data. Roblox already knows the exact information used to complete the transaction, so having that information available through a trusted server side callback would solve the problem.