Roblox Lua library
Enter jumps to the next match, shift-enter the previous.
One Markdown file covering the whole product. Last changed August 30, 2026.
Deployment status
Live: the key page and checkpoint flow, and POST /api/validation/v1/validate. The manual sample below works against a real endpoint today - bulk-generated keys bind to the first machine that validates them.
Also live: vault delivery. The loader at /api/v1/loader, direct lite delivery at /api/v1/s/…, the handshake and pull behind them, and the script_key flow, which is the same loader reading a global before it pulls.
Mind the prefix. Both routes sit under /api, the same as everything else here. A loadstring pointing at /v1/… answers 404 - copy the line from the script's row rather than typing it.
Quick start
Two shapes, and which one you get is decided by the script's own mode. Copy the exact lines from the script's row in Secure vault rather than typing them: the slug is per script, so each one has an entry point you can rotate on its own.
_G.SlugID = "YOUR_SCRIPT_SLUG"
loadstring(game:HttpGet("https://api.arcticauth.com/api/v1/loader"))()One loader URL fronts your whole vault. It is the same bytes for every script and every service, so it caches; _G.SlugID on the line above is what picks which script gets pulled.
loadstring(game:HttpGet("https://api.arcticauth.com/api/v1/s/YOUR_SCRIPT_SLUG"))()A lite script has no loader and no key check, so it is served straight from its own URL. Both paths live under /api, the same as every other endpoint here.
Choosing a library mode
Set this when you upload a script. It decides what the vault injects above your code.
Injected and pre-configured. The service id is baked in, so you never call configure().
service · slug · hwid · premium · expiresAt · validate(key)
Nothing injected and nothing delivered. Your script stays where it already lives; PolarSec only validates the key. Full sample under "Without the secure vault".
validate(key) - yours to call
ArcticAuth SDK (Official)
Injected and pre-configured. The service is settled by the delivery id in the loadstring, so there is no configure() call and no id to paste into source that ends up on somebody's screen.
It arrives as a table on getgenv().ArcticAuth (readable as the bare ArcticAuth too). With script_key on, the vault already validated before a byte was sent, so these are facts to read, not a gate to run.
- ArcticAuth.service
- The service identifier this run belongs to.
- ArcticAuth.slug
- The delivery id (slug) of the script that was pulled.
- ArcticAuth.hwid
- This machine's hardware id, as the loader resolved it.
- ArcticAuth.premium -> boolean
- Whether the validated key is premium. Gate premium-only features on this.
- ArcticAuth.expiresAt -> number?
- Key expiry as unix seconds, or nil for a lifetime key.
- ArcticAuth.validate(key) -> table
- Re-check a key against this service and machine at runtime. Returns { valid = boolean, reason = string? }.
script_key
Turn this on when you upload and your users hand the loader their key directly, above the loadstring. You build no key UI at all: no prompt, no textbox, no clipboard handling, nothing to restyle when you change themes.
What the switch really decides is where the key is checked, not whether it is. On, the vault gates the pull: a run with no key gets no script bytes. Off, the SDK is still injected but delivery is ungated, and your own code calls Validate(key) once it is already running - which means anyone holding the delivery id can pull the script, and what happens next is whatever your code does.
_G.SlugID = "YOUR_SCRIPT_SLUG"
script_key = "THEIR-KEY-HERE"
loadstring(game:HttpGet("https://api.arcticauth.com/api/v1/loader"))()The loader reads the global before it does anything else, checks it against this service and this machine, and only then is a byte of your script sent. Nothing changes in your own source - this is a setting on the upload, not an API you call.
- script_key
- A global string, set before the loadstring runs. getgenv().script_key works too, for executors where a bare global does not survive.
- not set, or empty
- Refused with no-key. The key page URL for this machine comes back in the error, so you can still point them at it.
- set, but not for this machine
- Refused with hwid-mismatch. A key belongs to the first machine that redeemed it, and pasting it elsewhere does not move it.
Turning this on does not weaken anything. The key is still checked server-side and a failed check still returns no script bytes - all the switch changes is where the key comes from: a global your user sets, rather than a flow your script has to run.
The same convention works without the vault. Call Validate() with no argument and the library reads the global for you, checking both the bare name and getgenv() for the executors that sandbox the chunk.
local ArcticAuth = loadstring(game:HttpGet("https://api.arcticauth.com/api/library"))()
local client = ArcticAuth.new("your-service-id")
local ok, reason = client:Validate()
if not ok then
warn("[ArcticAuth] " .. tostring(reason))
warn("Get a key: " .. client:GetKeyLink())
return
end
-- your script from hereSample script (SDK + script_key)
A complete, working script to upload as a Builtin-SDK, script_key vault entry. Reaching the first line means the key already passed the server-side check, so this reads the run's facts rather than gating on them. The ArcticAuth or getgenv().ArcticAuth line makes it work whether or not Lockmode is on.
--[[
Artic Hub sample - ArcticAuth SDK (Builtin, script_key ON)
Upload THIS as the vault script. Your users run:
_G.SlugID = "YOUR_SCRIPT_SLUG"
script_key = "THEIR-KEY-HERE"
loadstring(game:HttpGet("https://api.arcticauth.com/api/v1/loader"))()
With script_key ON, the vault validated the key on the server before a byte of
this reached the executor. Reaching this line means the key was valid.
]]
-- The SDK the loader injected. Reads as a same-chunk local in normal delivery and
-- as a global in hardened (Lockmode) delivery, so resolve both; the fallback keeps
-- this from erroring if it is ever run outside the loader.
local ArcticAuth = ArcticAuth or (getgenv and getgenv().ArcticAuth)
if not ArcticAuth then
warn("[Artic Hub] SDK not found - run this through the Artic-Core loader.")
return
end
-- Safe notification helper. Wrapped because SetCore is not ready the instant a
-- place finishes loading.
local function notify(title, text, duration)
pcall(function()
game:GetService("StarterGui"):SetCore("SendNotification", {
Title = title, Text = text, Duration = duration or 5,
})
end)
end
-- ---- what the SDK hands you -------------------------------------------------
local service = ArcticAuth.service or "service"
local hwid = ArcticAuth.hwid or "unknown"
local premium = ArcticAuth.premium == true
local expiresAt = ArcticAuth.expiresAt -- unix seconds, or nil = lifetime
local expiryText = expiresAt
and ("expires " .. os.date("!%Y-%m-%d %H:%M UTC", expiresAt))
or "lifetime key"
print(("[Artic Hub] loaded for %s"):format(service))
print(("[Artic Hub] machine %s | %s | %s"):format(
hwid, premium and "premium" or "standard", expiryText))
notify("Artic Hub", ("Key valid - %s"):format(premium and "Premium" or "Standard"), 6)
-- ---- optional: re-check the key mid-session ---------------------------------
local function keyStillValid()
if not ArcticAuth.validate then return true end
local res = ArcticAuth.validate(script_key or _G.UserKey)
return type(res) == "table" and res.valid == true
end
-- ---- your actual script goes below ------------------------------------------
local player = game:GetService("Players").LocalPlayer
print(("[Artic Hub] hello, %s (userId %d)"):format(player.Name, player.UserId))
if premium then
notify("Artic Hub", "Premium features unlocked.", 5)
-- premiumOnlyFeature()
else
notify("Artic Hub", "Standard access. Upgrade for premium.", 5)
end
return {
version = "1.0.0",
service = service,
premium = premium,
revalidate = keyStillValid,
}Your users then paste the two-line script_key loadstring from the section above — nothing else changes on their end.
Without the secure vault
Your script stays yours — hosted wherever you already host it, delivered however you already deliver it. ArcticAuth answers one question: is this key good for this machine.
You do not paste a client into your source. Load the library at runtime and call it:
local ArcticAuth = loadstring(game:HttpGet("https://api.arcticauth.com/api/library"))()
local client = ArcticAuth.new("your-service-id")
local ok, reason = client:Validate(key)
if not ok then
warn("[ArcticAuth] " .. tostring(reason))
warn("Get a key: " .. client:GetKeyLink())
return
end
-- your script from hereTwo things this buys over pasting a client in: fixes reach every script the next time it runs, and your source no longer carries a copy of the transport that will drift out of date. The service id is not a secret — it is already in the key page URL your users visit — so having it in a script that ships is not a leak.
The loader cannot be protected, and nothing can fix that. Your users' executors own HttpGet, loadstring and every global the library touches, so it can be swapped for a different file wholesale. Obfuscation raises the effort and changes nothing about whether it is possible.
That is survivable because the library does not decide anything. It carries no credentials, and validation happens on our server. Hooking Validate to return true gets you a true and nothing else — no key, no script, no access.
If the script is the thing you are protecting, use the vault. A failed check there returns no bytes, so there is nothing on the machine to patch. Use this mode when you want key management, revocation, HWID binding and the ad flow, and you accept that the payload itself is client-side.
The library is versioned and its digest is published, so you can pin what you expect and notice if it changes underneath you. This is worth something against a hostile network and nothing against the executor, which can lie about the comparison too.
GET https://api.arcticauth.com/api/library/manifest
{ "version": "1.0.0", "sha256": "...", "bytes": 4096 }Hardware ID
A key binds to the first machine that redeems it. The library builds the identifier from whatever the executor exposes and sends the parts; the server does the hashing, so it can tell two machines apart even when one of the parts is missing.
- RbxAnalyticsService:GetClientId()
- Primary. Roblox's own client id, present everywhere, so it works on executors that expose nothing else.
- gethwid()
- Secondary, and always called through pcall. Plenty of executors do not have it and some renamed it, so nothing depends on it being there.
- LocalPlayer.UserId · identifyexecutor()
- Correlation only. Alts are free and executors are swappable, so neither bounds anything alone.
Every one of those is spoofable from inside the executor, and that is the honest state of the platform. They are not sent because any single one is trustworthy - they are sent so the server can notice when their relationships stop making sense, and that check lives somewhere a script cannot reach.
The key page
Where a user goes to earn a key. Live today, and the one part of this page you can test right now.
https://arcticauth.com/getkey/YOUR_SERVICE_ID?hwid=THE_MACHINE_IDThe user clears each checkpoint, then gets a key bound to that machine. Progress is saved, so closing the tab and reopening the link continues where they stopped rather than starting again. They may switch ad provider on any step, and completed steps stay completed.
Reasons a validation fails
A closed vocabulary, so a loader can switch on it. These are the exact strings the endpoint returns.
- no-key
- Nothing was supplied, or only whitespace.
- no-service
- No service answers to that id or slug. Check what you pasted into SERVICE_ID.
- service-paused
- The owner has taken the service offline.
- unknown-key
- No such key on this service. A key from a different service reads the same way.
- revoked
- Pulled by the owner, or by an automatic rule. Reported ahead of expiry when both apply.
- expired
- Past its expiry.
- hwid-mismatch
- Valid key, different machine. It belongs to the one that redeemed it first.
- no-hwid
- The key is unbound and the loader sent no machine to bind it to. Also answered when the service runs a trust list and the loader named no machine at all.
- blocked
- The owner put this machine on the service trust list as blocked. No key changes that.
- not-trusted
- The service runs an allow-only trust list and this machine is not on it. Nobody banned them; they were never invited.
Show the reason to the user - every one of them is something they can act on, and a bare "invalid key" turns a wrong-machine problem into a support message.
What this does not protect against
Worth knowing before you build expectations on top of it, because none of these are fixable and a library that implied otherwise would be lying to you.
- Someone with a valid key can dump your script. The executor has full read access to whatever is sent to it. Every delivery is watermarked instead, so a leaked copy names the account that pulled it and that account can be revoked.
- A determined person can reverse the loader. It runs on their machine. Builds rotate, so a published bypass has a short shelf life rather than none.
- Traffic is readable to whoever runs the client. That is what a debugging proxy is for and it is not a flaw. The exchange is built so a captured request is worth nothing when replayed.

