GetProductInfoAsync/GetItemDetailsAsync returns the wrong price in studio

When using MarketplaceService:GetProductInfoAsync() or AvatarEditorService:GetItemDetailsAsync() in studio, it returns the client’s price instead of the real item price even if it is called on the server.

There are currently no reliable ways to figure out how much a user paid for an item so this would be highly appreciated if that could be added instead of relying on this.

I can confirm that in live games it returns the intended price, this only happens in studio but this can be inconvenient for testing. The item I used is an avatar background which may be why this is happening but I cannot confirm.

Asset Id: 95681420685521
Link: Moonlight Meadow - Roblox

Visual Aids:
(studio)
20260726_171510s_fZ2TI5PqXY
(in-game)
20260726_171314s_Mlr8adD3zD

2 Likes

It would be extremely useful to be able to get the clients actual purchase amount, for both regional pricing & Plus discount.

2 Likes

Hi!
Want to make sure I get your goal. Two things sound possible:

  1. You want the base catalog price, not the personalized price a player sees with discounts or regional pricing.
  2. You want to know how much a user actually paid for the item.

Which one are you after?

Hi there, apologies for not being very clear.

I mainly wanted to know how much the user paid for the item which is currently impossible to check accurately since LastUpdated does not update with a price change on assets (besides game passes) and there is no built-in way to my knowledge to detect how much was paid per transaction. PromptBulkPurchaseFinished does include this information but it can only be called on the client not the server which makes it untrustworthy and posing a huge security risk for games that rely on it.

Until there is a way to check how much a user paid for an item, I was looking to get the base catalog price, not the personalised price with discounts and regional pricing. When this info is checked in a live game it is correct but the issue is when it is checked in studio. I am not sure when this started happening but whoever is doing the play test it seems like studio may send server requests with authentication which as far as I know is not what it should be doing.

Hi,
The price returned is the final sale price after applying all the discounts.
Can you confirm if the logged in user is the same on the Studio and in-game. Since you might be logged in with a different user on Studio who might have discounts available (such as Plus).

I am calling this on the server using the same account both in studio and in game so it shouldn’t detect any discounts or anything unless specifically called from the client.

I think developers should have access to receipt callback when any purchase is made in their game, this would be useful information.

You are right in your assumption.
In Studio, you are getting a personalized price even when calling the API from a server script. This is a pre-existing divergence between live and Studio server behavior.

On how much player paid for an item, how are you planing to use this information?

I could think of many use cases, such as a user having a UGC based game, where in-game points are awarded for purchasing items, and can then be used to purchase free UGC.

Because of the exploits that can be used to make obscenely high purchases (LastUpdated price change) , in-game currency can become inflated (and there are many cases where this bug has been used to do exactly that, such as in my game, and a friends game).

1 Like

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.

4 Likes

This never happened before to my knowledge even when discounted prices were introduced, when was this change made?

I wanted to figure out how much robux users paid for an item to compensate them with in-game currency but the reply above basically did all the extra work for me (1 robux is equivalent to 8 of my game’s currency)

2 Likes