Sign in with Google, confirm your Roblox username, and you'll get a short-lived access key for the hub. It's re-checked every 30 seconds while you play, so a leaked or revoked key stops working immediately — not just the next time you join.
Secure access session
Get your key
Verified per Google account. Revoked instantly if abused.
Checking key status...
1
Sign in with Google
Ties the key to a real account instead of just a device.
2
Confirm your Roblox username
Used to tag the key and check it against the blacklist.
3
Get your key
Copy it into the loader script in Studio or your executor.
Google-verified
One active key per Google account, enforced against the database.
Auto-expiring
Every key carries its own expiry — nothing to renew by hand.
Live re-checks
The hub re-validates your key every 30 seconds while it runs.
Instant revoke
An admin can kill one key — or every active key — at once.
Admin Authentication
✕
Signed in as . This account is on the admin allowlist.
0
TOTAL
0
ACTIVE
0
INACTIVE
Paste into your Roblox script / executor:
local HttpService = game:GetService("HttpService")
local KeyInput = _G.UserKey
-- ⬇️ Raw link ng script hub mo
local HUB_URL = "https://xaimm2.vercel.app"
local BASE_URL = "https://keysystem-1f090-default-rtdb.asia-southeast1.firebasedatabase.app"
local DB_URL = BASE_URL .. "/keys/" .. tostring(KeyInput) .. ".json"
local ANNOUNCEMENT_URL = BASE_URL .. "/announcement.json"
local lastSeenAnnouncement = nil
local function showAnnouncementBanner(text)
local player = game:GetService("Players").LocalPlayer
if not player then return end
local existing = player:FindFirstChild("PlayerGui") and player.PlayerGui:FindFirstChild("KeySystemAnnouncement")
if existing then existing:Destroy() end
local gui = Instance.new("ScreenGui")
gui.Name = "KeySystemAnnouncement"
gui.ResetOnSpawn = false
gui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 340, 0, 0)
frame.AutomaticSize = Enum.AutomaticSize.Y
frame.Position = UDim2.new(0.5, -170, 0, -100)
frame.BackgroundColor3 = Color3.fromRGB(19, 19, 24)
frame.BorderSizePixel = 0
frame.Parent = gui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 14)
corner.Parent = frame
local stroke = Instance.new("UIStroke")
stroke.Color = Color3.fromRGB(36, 36, 44)
stroke.Thickness = 1
stroke.Parent = frame
local topBar = Instance.new("Frame")
topBar.Size = UDim2.new(1, 0, 0, 6)
topBar.BackgroundColor3 = Color3.fromRGB(99, 102, 241)
topBar.BorderSizePixel = 0
topBar.Parent = frame
local topCorner = Instance.new("UICorner")
topCorner.CornerRadius = UDim.new(0, 14)
topCorner.Parent = topBar
local label = Instance.new("TextLabel")
label.Size = UDim2.new(1, -28, 0, 0)
label.AutomaticSize = Enum.AutomaticSize.Y
label.Position = UDim2.new(0, 14, 0, 16)
label.BackgroundTransparency = 1
label.Text = "ANNOUNCEMENT\n\n" .. text
label.TextColor3 = Color3.fromRGB(242, 242, 245)
label.TextWrapped = true
label.TextXAlignment = Enum.TextXAlignment.Left
label.Font = Enum.Font.GothamMedium
label.TextSize = 14
label.RichText = true
label.Parent = frame
local padding = Instance.new("UIPadding")
padding.PaddingBottom = UDim.new(0, 16)
padding.Parent = frame
gui.Parent = player:WaitForChild("PlayerGui")
frame:TweenPosition(UDim2.new(0.5, -170, 0, 20), "Out", "Quad", 0.35, true)
task.delay(8, function()
if frame and frame.Parent then
frame:TweenPosition(UDim2.new(0.5, -170, 0, -200), "In", "Quad", 0.3, true, function()
gui:Destroy()
end)
end
end)
end
local function checkAnnouncement()
local ok, resp = pcall(function() return game:HttpGet(ANNOUNCEMENT_URL) end)
if ok and resp and resp ~= "null" then
local ok2, text = pcall(function() return HttpService:JSONDecode(resp) end)
if ok2 and type(text) == "string" and text ~= "" and text ~= lastSeenAnnouncement then
lastSeenAnnouncement = text
showAnnouncementBanner(text)
end
end
end
-- Do NOT trust os.time() here — it's the player's own device clock and can be
-- changed freely, which lets anyone bypass expiry by winding their clock back.
--
-- We also do NOT use Firebase's write-then-read ".sv timestamp" trick anymore,
-- because it needs HttpService:RequestAsync with a PUT method, which many
-- executors either block or silently fail on. When that happens, this line
-- would end up reading back an OLD leftover timestamp instead of a fresh one —
-- and an already-expired key would keep validating as "still valid" forever,
-- because it was being compared against stale time.
--
-- Instead we fetch real-world time from a public time API using ONLY
-- game:HttpGet (a plain GET) — the same call already used to fetch key data
-- above, so if that works, this works too. It always returns a fresh
-- timestamp, so there's nothing to go stale.
local function getRealTime()
local ok, resp = pcall(function()
return game:HttpGet("https://worldtimeapi.org/api/timezone/Etc/UTC")
end)
if ok and resp then
local ok2, data = pcall(function() return HttpService:JSONDecode(resp) end)
if ok2 and data and type(data.unixtime) == "number" then
return data.unixtime * 1000
end
end
-- Fallback time source in case the primary API is down or blocked.
local ok3, resp2 = pcall(function()
return game:HttpGet("https://timeapi.io/api/time/current/zone?timeZone=UTC")
end)
if ok3 and resp2 then
local ok4, data2 = pcall(function() return HttpService:JSONDecode(resp2) end)
if ok4 and data2 and type(data2.dateTime) == "string" then
local y, mo, d, h, mi, s = data2.dateTime:match("(%d+)-(%d+)-(%d+)T(%d+):(%d+):([%d%.]+)")
if y then
return os.time({
year = tonumber(y), month = tonumber(mo), day = tonumber(d),
hour = tonumber(h), min = tonumber(mi), sec = math.floor(tonumber(s))
}) * 1000
end
end
end
return nil
end
local success, response = pcall(function()
return game:HttpGet(DB_URL)
end)
-- IMPORTANT: the hub only ever loads INSIDE the passing branch below.
-- Nothing runs if the key is missing, revoked, or expired — the script
-- just warns and stops (via `return`) instead of falling through.
if not (success and response ~= "null") then
warn("Invalid Key!")
return
end
local data = HttpService:JSONDecode(response)
local currentTime = getRealTime()
if not currentTime then
warn("Could not verify current time. Try again.")
return
end
if not (data.active and type(data.expiresAt) == "number" and currentTime < data.expiresAt) then
warn("Key is Expired or Revoked!")
return
end
-- 🔒 One key, one player, one device. The key is bound to whichever Roblox
-- account AND device redeem it first — UserId can't be spoofed the way a
-- username can, and GetClientId() is the closest thing Roblox exposes to a
-- stable per-installation device fingerprint (it's tied to the Roblox client
-- install on that machine, not the account, so it doesn't change if someone
-- logs into a different account on the SAME device — but it does change if
-- the same account is used from a DIFFERENT device). Together, this stops a
-- leaked key string alone from being usable by anyone else, anywhere else.
local LocalPlayer = game:GetService("Players").LocalPlayer
local myUserId = tostring(LocalPlayer.UserId)
local myUsername = LocalPlayer.Name
local hwidOk, myHwid = pcall(function()
return tostring(game:GetService("RbxAnalyticsService"):GetClientId())
end)
if not hwidOk or not myHwid or myHwid == "" then
myHwid = "" -- couldn't read a device id here — falls back to account-only locking below
end
if data.claimedUserId and data.claimedUserId ~= "" then
if tostring(data.claimedUserId) ~= myUserId then
warn("This key is already locked to another player and cannot be reused.")
return
end
if data.claimedHwid and data.claimedHwid ~= "" and myHwid ~= "" and tostring(data.claimedHwid) ~= myHwid then
warn("This key is locked to a different device and cannot be used here.")
return
end
else
-- Not claimed yet — claim it for this player+device now.
--
-- IMPORTANT: a lot of executors silently mishandle HTTP methods other
-- than GET/POST — PATCH (and even PUT, see the getRealTime() note above)
-- can fail outright or, worse, report a fake "success" without the
-- server actually applying the write. That's what was causing keys to
-- show "Not claimed yet" in the dashboard even after someone genuinely
-- used them. To make this work everywhere:
-- 1) Try POST with an X-HTTP-Method-Override: PATCH header — Firebase's
-- REST API explicitly supports this for clients that only speak
-- GET/POST, and POST is by far the most widely supported verb.
-- 2) If that doesn't clearly succeed, fall back to a plain PUT of the
-- FULL key record (merging the claim fields into the `data` we
-- already fetched) — the same method the web dashboard itself uses
-- for every write, so it's known to work against this database.
local function attemptClaim()
local ok1, resp1 = pcall(function()
return HttpService:RequestAsync({
Url = BASE_URL .. "/keys/" .. tostring(KeyInput) .. ".json",
Method = "POST",
Headers = {
["Content-Type"] = "application/json",
["X-HTTP-Method-Override"] = "PATCH"
},
Body = HttpService:JSONEncode({
claimedUserId = myUserId,
claimedRobloxName = myUsername,
claimedHwid = myHwid
})
})
end)
if ok1 and resp1 and (resp1.Success or (resp1.StatusCode and resp1.StatusCode < 300)) then
return true
end
data.claimedUserId = myUserId
data.claimedRobloxName = myUsername
data.claimedHwid = myHwid
local ok2, resp2 = pcall(function()
return HttpService:RequestAsync({
Url = BASE_URL .. "/keys/" .. tostring(KeyInput) .. ".json",
Method = "PUT",
Headers = { ["Content-Type"] = "application/json" },
Body = HttpService:JSONEncode(data)
})
end)
return ok2 and resp2 and (resp2.Success or (resp2.StatusCode and resp2.StatusCode < 300))
end
if not attemptClaim() then
warn("Could not lock this key to your account/device. Try again.")
return
end
end
print("Key Validated! Tagged user: " .. tostring(data.note))
-- ✅ Key passed every check above — safe to load the hub now.
loadstring(game:HttpGet(HUB_URL))()
checkAnnouncement()
-- ⚠️ IMPORTANT: the check above only runs ONCE, at the exact moment this
-- script executes. If your key expires WHILE the hub is still running
-- (mid-session), nothing catches that on its own — the hub just keeps
-- working until you rejoin. This watchdog fixes that by re-checking the
-- key in the background every 30 seconds, and kicking you out of the game
-- the moment it's no longer valid.
task.spawn(function()
while true do
task.wait(30)
checkAnnouncement()
local ok, resp = pcall(function() return game:HttpGet(DB_URL) end)
local stillValid = false
if ok and resp and resp ~= "null" then
local ok2, freshData = pcall(function() return HttpService:JSONDecode(resp) end)
local freshTime = getRealTime()
if ok2 and freshTime and freshData.active
and type(freshData.expiresAt) == "number"
and freshTime < freshData.expiresAt
and (not freshData.claimedUserId or tostring(freshData.claimedUserId) == myUserId)
and (not freshData.claimedHwid or freshData.claimedHwid == "" or myHwid == "" or tostring(freshData.claimedHwid) == myHwid) then
stillValid = true
end
end
if not stillValid then
warn("Key expired, revoked, or locked to another player/device — disconnecting.")
local Players = game:GetService("Players")
local ok3 = pcall(function() Players.LocalPlayer:Kick("Key expired or revoked.") end)
if not ok3 then
pcall(function() game:GetService("TeleportService"):Teleport(0) end)
end
break
end
end
end)