Script History

Session
7
Scripts
6
Warnings
7
Refined
ModuleScript2 refinements
2026-05-26 14:32

Currency System — DataStore Integration

Complete server-authoritative currency system with DataStore v2 persistence, leaderboard sync, and anti-exploit guards.

1-- CurrencyService.lua (ModuleScript → ReplicatedStorage/Modules)
2-- Kings Hub Generated | 2026-05-26
3-- ⚠️ Server-authoritative: all currency mutations happen server-side only
4 
5local DataStoreService = game:GetService(string">"DataStoreService")
6local Players = game:GetService(string">"Players")
7local RunService = game:GetService(string">"RunService")
8 
9local CurrencyStore = DataStoreService:GetDataStore(string">"CurrencyStore_v2")
10 
11local CurrencyService = {}
12CurrencyService.__index = CurrencyService
13 
14-- Cache player data in-memory to reduce DataStore calls
15local playerCache: {[number]: {coins: number, gems: number}} = {}
16local SAVE_INTERVAL = 60 -- seconds between auto-saves
17local MAX_COINS = 9_999_999
18local MAX_GEMS = 99_999
19 
20-- Initialize player data on join
21local function initPlayer(player: Player)
22 local userId = player.UserId
23 local success, data = pcall(function()
24 return CurrencyStore:GetAsync(string">"player_" .. userId)
25 end)
26 
27 if success and data then
28 playerCache[userId] = data
29 else
30 -- Default values for new players
31 playerCache[userId] = { coins = 100, gems = 0 }
32 if not success then
33 warn(string">"[CurrencyService] DataStore load failed for", player.Name, data)
34 end
35 end
36 
37 -- Sync leaderboard stats
38 local leaderstats = Instance.new(string">"Folder")
39 leaderstats.Name = string">"leaderstats"
40 leaderstats.Parent = player
41 
42 local coinsValue = Instance.new(string">"IntValue")
43 coinsValue.Name = string">"Coins"
44 coinsValue.Value = playerCache[userId].coins
45 coinsValue.Parent = leaderstats
46 
47 local gemsValue = Instance.new(string">"IntValue")
48 gemsValue.Name = string">"Gems"
49 gemsValue.Value = playerCache[userId].gems
50 gemsValue.Parent = leaderstats
51end
52 
53-- Save player data (call on leave + auto-save)
54local function savePlayer(player: Player)
55 local userId = player.UserId
56 local data = playerCache[userId]
57 if not data then return end
58 
59 local success, err = pcall(function()
60 CurrencyStore:SetAsync(string">"player_" .. userId, data)
61 end)
62 
63 if not success then
64 warn(string">"[CurrencyService] DataStore save failed for", player.Name, err)
65 end
66end
67 
68-- Public API: Add currency (server-side only)
69function CurrencyService:AddCoins(player: Player, amount: number): boolean
70 assert(RunService:IsServer(), string">"AddCoins must be called from the server")
71 assert(type(amount) == string">"number" and amount > 0, string">"Amount must be a positive number")
72 
73 local userId = player.UserId
74 local cache = playerCache[userId]
75 if not cache then return false end
76 
77 local newTotal = math.min(cache.coins + amount, MAX_COINS)
78 cache.coins = newTotal
79 
80 -- Update leaderboard stat
81 local leaderstats = player:FindFirstChild(string">"leaderstats")
82 if leaderstats then
83 local coinsValue = leaderstats:FindFirstChild(string">"Coins")
84 if coinsValue then coinsValue.Value = newTotal end
85 end
86 
87 return true
88end
89 
90-- Public API: Deduct currency with validation
91function CurrencyService:SpendCoins(player: Player, amount: number): boolean
92 assert(RunService:IsServer(), string">"SpendCoins must be called from the server")
93 assert(type(amount) == string">"number" and amount > 0, string">"Amount must be a positive number")
94 
95 local userId = player.UserId
96 local cache = playerCache[userId]
97 if not cache then return false end
98 
99 if cache.coins < amount then
100 return false -- Insufficient funds
101 end
102 
103 cache.coins = cache.coins - amount
104 
105 local leaderstats = player:FindFirstChild(string">"leaderstats")
106 if leaderstats then
107 local coinsValue = leaderstats:FindFirstChild(string">"Coins")
108 if coinsValue then coinsValue.Value = cache.coins end
109 end
110 
111 return true
112end
113 
114-- Public API: Get current balance
115function CurrencyService:GetBalance(player: Player): {coins: number, gems: number}
116 local cache = playerCache[player.UserId]
117 return cache or { coins = 0, gems = 0 }
118end
119 
120-- Lifecycle hooks
121Players.PlayerAdded:Connect(initPlayer)
122Players.PlayerRemoving:Connect(function(player)
123 savePlayer(player)
124 playerCache[player.UserId] = nil
125end)
126 
127-- Auto-save loop using task.delay (not deprecated delay())
128task.spawn(function()
129 while true do
130 task.wait(SAVE_INTERVAL)
131 for _, player in ipairs(Players:GetPlayers()) do
132 savePlayer(player)
133 end
134 end
135end)
136 
137return CurrencyService
Server-Authoritative

All currency mutations are server-side. RemoteEvents should validate on server before calling CurrencyService methods.

DataStore Rate LimitLine 87

Auto-save every 60s. For 100+ player servers, consider staggering saves with task.delay(i * 0.5).

Placement in Studio
ReplicatedStorage > Modules > CurrencyService