---
name: roblox-economy-analytics
description: Use when designing or balancing an in-game economy (currencies, rewards, prices, upgrades, rebirths), tracking players with AnalyticsService (economy, funnel, onboarding, progression and custom events, custom fields, player segments), or awarding badges with BadgeService. Covers sources and sinks, inflation, pacing, the current AnalyticsService API and its limits, and AwardBadgeAsync.
---

# Economy, analytics and badges

A game economy is the flow of currencies and items between players and the game. Design it on paper, run it on
the server, and measure it with `AnalyticsService` so you can fix it with data instead of guesses.

## Economy design primer

- **Currencies.** One soft currency earned by playing (Coins). Optionally one premium currency (Gems) that is
  mostly bought with Robux and occasionally earned. More currencies confuse players and split your analytics.
- **Sources** create currency: gameplay rewards, quests, timed and daily rewards, developer products.
- **Sinks** remove it: upgrades, consumables, cosmetics, rerolls, fees, rebirths/prestige.
- **Inflation** happens when sources outgrow sinks: balances pile up, prices stop mattering and rewards feel
  worthless. Watch average wallet balance. Fix it with repeatable sinks (consumables, upgrades with rising costs),
  not by deleting currency.
- **Pacing.** Players should always see their next goal and reach the first one within minutes. Make costs grow
  faster than income per level (for example cost x1.25 per level, income x1.15), and give a reset loop (rebirth)
  with a lasting multiplier once growth slows.
- **Spending moments.** Offer products where they solve a problem the player has right now (a revive after dying,
  a boost before a hard level) and a cheap first purchase. Track them as `ContextualPurchase`.
- **Fairness.** Don't sell power over other players in competitive modes; sell speed, convenience and cosmetics.
- **Server authority.** Balances live on the server and only server code changes them. The client only displays.

## AnalyticsService

Events can only be sent from the server and only in published games (not from the client, not in Studio). Every
method takes the `Player`. Log **after** the action succeeded (after the balance changed, after the step was really
completed). Charts appear in Creator Hub > Analytics after about 24 hours; use the
**View Events** button on the Economy, Funnel or Custom page to see events in near real time.

### Economy events

```luau
local AnalyticsService = game:GetService("AnalyticsService")

-- Earned 50 coins from a level; balance is now 120
AnalyticsService:LogEconomyEvent(
	player,
	Enum.AnalyticsEconomyFlowType.Source,
	"Coins",       -- currency name
	50,            -- amount, always positive
	120,           -- balance after the transaction
	Enum.AnalyticsEconomyTransactionType.Gameplay.Name
)

-- Spent 80 coins on an item
AnalyticsService:LogEconomyEvent(
	player,
	Enum.AnalyticsEconomyFlowType.Sink,
	"Coins",
	80,
	40,
	Enum.AnalyticsEconomyTransactionType.Shop.Name,
	"DoubleJumpUpgrade", -- item SKU, optional
	{ [Enum.AnalyticsCustomFieldKeys.CustomField01.Name] = "Category - Movement" }
)
```

Transaction types: `IAP`, `Shop`, `Gameplay`, `ContextualPurchase`, `TimedReward`, `Onboarding` (use `.Name`).
Log developer product grants as `IAP` from your receipt handler, after the save.

### Funnels

```luau
-- One-time onboarding funnel (first-time user experience)
AnalyticsService:LogOnboardingFunnelStepEvent(player, 1, "Joined")
AnalyticsService:LogOnboardingFunnelStepEvent(player, 2, "Finished tutorial")

-- Recurring funnel: a new session ID each time the player opens the shop
local HttpService = game:GetService("HttpService")
local sessionId = HttpService:GenerateGUID()
AnalyticsService:LogFunnelStepEvent(player, "ShopCheckout", sessionId, 1, "Opened shop")
AnalyticsService:LogFunnelStepEvent(player, "ShopCheckout", sessionId, 2, "Viewed item")
AnalyticsService:LogFunnelStepEvent(player, "ShopCheckout", sessionId, 3, "Bought item")
```

- Only the first time a step is logged counts; logging step 3 also completes steps 1 and 2.
- Use a GUID for shop-style funnels, or a stable key such as `"{userId}-{itemId}"` for long upgrade paths. Only
  the 10 most recent session IDs per user per funnel are tracked.
- When the client reports a step, validate it on the server (step number in range, in order) before logging.

### Progression and custom events

```luau
AnalyticsService:LogProgressionStartEvent(player, "World1", 3, "Lava Cave")
AnalyticsService:LogProgressionCompleteEvent(player, "World1", 3, "Lava Cave")
AnalyticsService:LogProgressionFailEvent(player, "World1", 3, "Lava Cave")

AnalyticsService:LogCustomEvent(player, "BossDefeated")                 -- counts
AnalyticsService:LogCustomEvent(player, "MissionDurationSeconds", 120)  -- with a value
```

Progression events are recorded but don't appear in Roblox's built-in charts yet. Path names can't be empty or
contain commas, quotes or newlines. Batch high-frequency custom events (send "10 zombies killed", not ten events).

### Limits

| Limit | Value |
|---|---|
| Requests per minute (all AnalyticsService calls) | 120 + 20 x CCU |
| Custom fields per event | 3 (`CustomField01` to `03`); after 8,000 combined values, extra values show as "Other" |
| Economy currencies | 10 |
| Transaction types / item SKUs | after 20 / 100, extra values show as "Other" |
| Funnels / steps per funnel | 10 / 100 |
| Custom event names | 100 |

Put variable detail (class, weapon, level) in custom fields instead of new currency or event names.

### Player segments

`AnalyticsService:GetPlayerSegmentsAsync(player)` (server only, may yield, cached for the server session) returns
coarse buckets such as `ActivePayerStatus` (`Never`, `Lapsed`, `Casual50Percent`, `Intermediate35Percent`,
`Top15Percent`), `EngagementLevel`, `UserNewReturningStatus` and `UserAcquisitionSource`. When `HasData` is false
every bucket is `Unknown`. Use it to personalize, for example showing a starter pack to `Never` payers.

## Badges

```luau
local BadgeService = game:GetService("BadgeService")

local function awardBadge(player: Player, badgeId: number)
	local ok, owned = pcall(BadgeService.UserHasBadgeAsync, BadgeService, player.UserId, badgeId)
	if ok and not owned then
		local awarded, err = pcall(BadgeService.AwardBadgeAsync, BadgeService, player.UserId, badgeId)
		if not awarded then
			warn("Badge award failed:", err)
		end
	end
end
```

- `AwardBadgeAsync` replaces the deprecated `AwardBadge`. Server only; the player must be in the server; the badge
  must belong to this experience and be enabled (`GetBadgeInfoAsync(badgeId).IsEnabled`).
- Rate limit: 50 + 35 x players per minute. Award at milestones, not in loops.
- `CheckUserBadgesAsync(userId, badgeIds)` checks up to 10 badges in one call.
- Creating badges: 5 free per game per 24 hours (GMT), then 100 Robux each.

## Mistakes to avoid

- Logging an economy event before the purchase or reward actually succeeded, or with a negative amount.
- A new currency name or event name per item, class or level. Use SKUs and custom fields.
- Logging funnel steps straight from client requests without validation.
- Calling AnalyticsService or BadgeService from the client.
- Using deprecated `FireInGameEconomyEvent`, `FirePlayerProgressionEvent`, `FireCustomEvent` or `AwardBadge`.
- Tuning prices without looking at the Economy dashboard's sources, sinks and average balance.
- Inflation fixes that take currency away from players instead of adding sinks.
