Introduction
AegisLoader protects your Roblox Luau and delivers it per run. Your script ships on a custom engine — renamed, string-encrypted, and virtualized — so the original source never leaves readable, and each download is armed and licensed on its own.
How protection works
When you upload a script it runs through four transforms before it ships:
- Rename — every local, global and field becomes a meaningless token.
- String encryption — string constants are sealed and decoded at runtime, never shipped in the clear.
- Number rewriting — numeric literals are re-spelled into equivalent, unreadable forms.
- Virtualization — your logic is translated into instructions for an engine shipped inside your build, so it never exists as readable Luau.
The first three are near-free at runtime. Virtualization is the strongest layer — and the heaviest to run, which is why hot code gets its own directive.
Directives
A directive is a one-line marker you place in your own source to tune how a piece of it is protected. Two rules hold for every directive:
- It runs raw — with its one-line stub each marker is a harmless no-op (an identity passthrough, or for resume a plain
false), so your script still runs correctly when executed unprotected. See Shimming. - The
AEGIS_prefix is reserved. Don't name your own locals or globalsAEGIS_*; the obfuscator reads those names and rewrites them.
AEGIS_FASTLANE
Run a hot function at native speed while keeping it protected.
AEGIS_FAULT
Brick the build cleanly when your own tamper check trips.
AEGIS_SEAL
Force a chosen secret out of the shipped payload into the vault.
AEGIS_RESUME
Returning users skip the key UI — resume from a sealed credential, no raw key on disk.
More directives land here as they ship. Each gets its own page with the exact rules and its raw-mode stub.
Integrating
Free, Paid and Ad builds ship a complete loader line — you paste one line and you're done. If you run Custom mode, your own UI fetches and applies the key; the Integration API documents those endpoints.
AEGIS_FASTLANEAEGIS_FASTLANE(fn) → fn
Run your hottest functions at native Luau speed — without taking protection off them.
Virtualized code is slow on a hot path: every step is fetched and decoded one at a time, which is invisible once a frame and a freeze sixty times a frame. AEGIS_FASTLANE marks a single function to build on the light tier — it runs as native Luau, but is still renamed and string-encrypted.
Tiers
Virtualized
- Runs on the engine
- Opcodes gone — no native Luau
- Strongest; slowest per call
Native
- Runs as native Luau
- Still renamed + string-encrypted
- Native speed; for hot paths
Using it
Add the one-line identity stub at the top of your script so it also runs un-obfuscated, then wrap any hot function literal:
local function AEGIS_FASTLANE(f) return f end -- stub: identity when run raw
-- a per-frame loop — keep it off the heavy tier
local render = AEGIS_FASTLANE(function(dt)
esp:redraw(); tracers:update(dt)
end)
RunService.RenderStepped:Connect(render)
-- a metamethod fires on every index
proxy.__index = AEGIS_FASTLANE(function(self, key) return resolve(self, key) end)
The wrapper is an expression that returns the function, so use it anywhere a function goes — assign it, connect it inline, store it, or return it.
When to use it
RenderStepped/Heartbeat/Steppedhandlers and their helpers.__index/__namecallmetamethod hooks.- Tight no-yield loops — per-frame math, per-tick scans.
Rule of thumb: more than a handful of times per frame → light tier. Once at startup or on a click → leave it virtualized. Mark as few functions as you can; each one is moved off the strongest tier.
Common mistakes
- Passing a named reference —
AEGIS_FASTLANE(myFn)does nothing. Wrap the function literal in place. - Forgetting to use the return value — the marker returns the function; assign or connect what it gives back.
- Reusing the reserved name for your own variable.
Rules
| Argument | Exactly one function literal written in place. A named reference (AEGIS_FASTLANE(myFn)) does nothing. |
|---|---|
| Returns | The same function, unchanged — the marker is identity at runtime. |
| Form | A bare call, AEGIS_FASTLANE(fn). An indexed call (t.AEGIS_FASTLANE(fn)) is not the directive. |
| Kept | Rename, string encryption, number rewriting. |
| Skipped | Virtualization, control-flow flattening, junk insertion. |
| No marker | Your build is byte-for-byte identical to one built without the feature. |
AEGIS_FAULTAEGIS_FAULT()
A kill-switch. When your own tamper check decides something is wrong, call it and the protected build cleanly bricks itself for the rest of the run.
AEGIS_FAULT() is the reaction, not the detector — you write the check, it delivers the response. When reached, it scrubs the current VM frame, trips a session flag, and raises an error. From that point every protected function in the run returns nothing, so the script can't limp on in a tampered state.
Using it
Add the one-line stub so the raw script still runs, then call it as a bare statement inside your own detection:
local function AEGIS_FAULT() end -- stub: no-op when run raw
-- your own checks decide WHEN to brick
if isHooked(game.HttpGet) or detectedDumper() then
AEGIS_FAULT()
end
When to use it
- Anti-tamper tripwire — a checksum of a critical function, or a honeypot value, failed.
- Hook / dump-tool detection — a sensitive function was hooked, or a known logger is present.
- Environment refusal — wrong executor, a sandbox, Studio, or an unexpected
PlaceId. - Your own blacklist gate, on top of the platform's licensing.
Rules
| Form | A bare statement, AEGIS_FAULT(), with no arguments. Using it in an expression (return AEGIS_FAULT()), with arguments, or as call sugar fails the build — never a silent no-op. |
|---|---|
| Effect | Scrubs the VM frame, sets a session flag, scrubs the live key, and errors. Every later protected call in the run then halts too. |
| Scope | The current run only — it's in-memory, so re-executing the script starts clean. For a persistent, cross-run ban use your key system's revocation, not this. |
| pcall | Call it where the error can unwind. A fault buried inside a pcall your script survives won't stop that frame (the session flag still halts the next protected call). |
| No marker | Your build is byte-for-byte identical to one built without the feature. |
AEGIS_SEALAEGIS_SEAL(value) → value
Force a specific string or number out of the shipped payload and into the loader-delivered vault — even when the automatic vault's budget would otherwise leave it in the clear.
AegisLoader already pulls string and number constants into a server-delivered table automatically, so they read nil in an offline dump. That table has a size budget, though — on a large script the budget fills and the overflow literals ship raw. AEGIS_SEAL guarantees a chosen value (a webhook, a token) is vaulted regardless, so it's never baked into the build.
Using it
Add the identity stub, then wrap a single literal — assign it to one local and reference that:
local function AEGIS_SEAL(v) return v end -- stub: identity when run raw
local WEBHOOK = AEGIS_SEAL("https://discord.com/api/webhooks/...")
Rules
| Argument | Exactly one string or number literal, written in place. A variable or expression (AEGIS_SEAL(x), AEGIS_SEAL(a..b)) fails the build. |
|---|---|
| Returns | The value, unchanged — identity at runtime. |
| Effect | The literal becomes a vault lookup; it is not in the shipped build and reads nil on an offline dump. |
| Declines | Best-effort: if the enclosing function is at Luau's local cap, SEAL ships the value raw rather than risk the build — it never bricks. |
| No marker | Your build is byte-for-byte identical to one built without the feature. |
AEGIS_RESUMEAEGIS_RESUME(authorityId, loaderUrl) → boolean
A returning user whose key is still valid skips the key UI entirely — the product loader resumes from a sealed credential, with no re-validate round trips and no raw key left on disk. First-time and invalid users fall straight through to your normal UI.
On a successful unlock the server hands back an opaque resume credential — bound to the device and the key's own expiry — and the loader stores it in place of the raw key. On the next launch AEGIS_RESUME replays that credential: the server unseals it, recovers the key, and runs the exact same live key check — so a returning user loads in one step instead of the keysource+validate round trips.
Using it
Add the stub, then call it at the top of your key script and return when it resumes — otherwise fall through to your own UI in the same script:
local function AEGIS_RESUME(id, url) return false end -- stub: no-op when run raw
if AEGIS_RESUME(CONFIG.Id, LOADER_URL) then return end -- returning user resumed → done
-- ...your normal key UI runs here (prompt / ad gate / validate).
-- On a successful validate, persist the returned credential instead of the raw key:
-- local res = validateKey(key) -- { ok, rt = "ar1..." }
-- if res and res.ok and res.rt then
-- writefile("AegisLoaderCache/aegis_resume_"..CONFIG.Id..".bin", res.rt)
-- end
AEGIS_RESUME only activates when the script it's in is itself protected with AegisLoader. If you host your key UI raw, the stub makes it a harmless no-op (returns false, your UI runs as normal) — to get resume without protecting the key UI, paste the full snippet from the Integration API instead.Rules
| Arguments | authorityId — your key authority (the key-system id for a shared connected script, otherwise the project id). loaderUrl — your product loader URL. Exactly two; any other form fails the build. |
|---|---|
| Returns | true when it resumed — you should return. false when there's nothing to resume — fall through to your key UI. |
| Effect | A returning user skips the keysource+validate round trips, and the raw key is never written to disk — only the sealed credential is. |
| Requires | The key script must be protected with AegisLoader for the directive to expand (otherwise the stub no-ops). Resume is available platform-wide — there's nothing to switch on. |
| Security | Revocation, expiry, ban and HWID stay live — the credential is a key-carrier, never an authorization. |
| No marker | Your build is byte-for-byte identical to one built without the feature. |
rt field, the credential format, and a drop-in for key scripts you don't protect — are in the Integration API.Shimming
Keep your script runnable un-obfuscated. A directive is a real name the obfuscator reads, so define a one-line stub and the raw file still runs in Studio.
When you protect a script, the obfuscator recognizes each directive and strips it — AEGIS_FASTLANE(fn) becomes just fn, and so on. But the raw file you test with has no such step: calling AEGIS_FASTLANE there would error with attempt to call a nil value. A shim is a passthrough definition that makes the directive a no-op when run raw.
The pattern
Every directive has a one-line identity stub. Define the ones you use at the top of your script:
-- raw-mode stubs — harmless, stripped when protected
local function AEGIS_FASTLANE(f) return f end
local function AEGIS_FAULT() end
local function AEGIS_SEAL(v) return v end
local function AEGIS_RESUME(id, url) return false end
Each directive's page lists its own stub. The rule is always the same: the stub returns its value unchanged (or does nothing), so your logic is identical whether or not the build is protected.
Reserved names
The AEGIS_ prefix is reserved for directives. Don't name your own locals, globals or fields AEGIS_* — the obfuscator reads those names, and a collision will be rewritten or rejected. Keep your stubs the only AEGIS_ identifiers in the file.
Performance
Protection has a runtime cost, and most of it is avoidable. Where it matters is the heavy tier on hot code — and that's exactly what the directives are for.
Where the cost is
Rename, string encryption and number rewriting are near-free — they change how your code reads, not how fast it runs. Virtualization is the heavy layer: your logic runs on an engine, so one native operation becomes several engine steps. On code that runs once, you never feel it. On code that runs sixty times a frame, you do.
Keep hot code native
Mark genuinely hot functions with AEGIS_FASTLANE — they run as native Luau while staying renamed and string-encrypted. Reach for it on:
RenderStepped/Heartbeat/Steppedloops and the helpers they call every frame.__index/__namecallhooks, which fire on every access.- Tight per-tick math and scans.
Mark as few functions as possible — each one leaves the strongest tier. Protect everything by default; fast-lane only what you've measured to be hot.
Optimize before you protect
Because one source operation can expand into many engine steps, sloppy code is far more expensive protected than raw. Cache lookups and cut redundant work first:
-- before: four chained lookups, every frame
a.b.c.d.x = 1
a.b.c.d.y = 2
-- after: resolve once
local t = a.b.c.d
t.x = 1
t.y = 2
Fewer instructions is always cheaper — and the win is larger once the code is on the engine.
Compatibility & limits
What changes once a script is protected. None of this affects normal game code — it matters only if you introspect your own functions or depend on exact runtime details.
The debug library
Protected functions are not native Luau closures, so the debug library sees the engine, not your code. debug.getinfo, debug.getlocal, debug.getupvalue and debug.traceback return engine internals or nothing useful for your own protected functions. Don't build logic on introspecting your obfuscated code.
Errors & line numbers
The original local, upvalue and function names are stripped and can't be recovered, so they won't appear in error messages. Line numbers and error text from protected code won't match the raw file — don't match on exact error strings. Errors still propagate and pcall still catches them.
Very large functions
Luau caps a single function at 200 locals/registers. A function already near that ceiling may be kept native rather than virtualized (never mis-compiled, never dropped). If you have one enormous function, splitting it into smaller ones protects it better and runs faster.
Executors & environment
- Builds run on mainstream executors. The loader probes for what it needs and fails safe if something is missing — it needs
game:HttpGet,loadstring, and standardcrypt/base64 functions. loadstring(string.dump(f))on a protected function won't reproduce it — its bytecode is not your function's.- Tricks that depend on a native call-stack level (
getfenv/setfenvby level) behave differently through the engine.
debug introspection, exact stack levels), wrap it in AEGIS_FASTLANE — it stays renamed and string-encrypted but runs as real Luau.Integration API
For Custom mode: your own in-game UI fetches and applies the key, then runs the loader. These are AegisLoader's licensing endpoints — public, called from your script, no API key.
https://api.aegisloader.com. Replace <id> with your project or key-system id, and compute hwid with RbxAnalyticsService:GetClientId(). Free, Paid and Ad builds don't need any of this — they ship a complete loader line.The getgenv().Key convention
A Custom-mode loader arms by reading getgenv().Key. Set it to the player's key, then run the loader line — nothing else to wire:
getgenv().Key = key
loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()
The key stays set for the session, so several Custom scripts sharing one key system each read the same getgenv().Key. You don't send the HWID to the loader — it computes its own.
Endpoints
| Endpoint | Returns |
|---|---|
GET /loader/<id> | The build. loadstring + run it. |
GET /api/validate/<id>?key=&hwid= | { ok, mode, expires, reason } — check a key for a device. Doesn't bind it. |
GET /api/keysource/<id> | { ok, shared, key_system, mode } — whether a script uses its key system's key or its own. |
GET /gate/<id>=<token> | The ad-unlock page. Open it in a browser. |
GET /gate/<id>=<token>/wait | {} while pending, then { key } once earned. |
Validate a key
Check a key before you run the loader. ok is the verdict; reason explains a failure (expired, banned, hwid_mismatch, not_found).
local Http = game:GetService("HttpService")
local hwid = game:GetService("RbxAnalyticsService"):GetClientId()
local url = "https://api.aegisloader.com/api/validate/<id>?key="..key.."&hwid="..hwid
local ok, res = pcall(function() return Http:JSONDecode(game:HttpGet(url)) end)
if ok and res.ok then
getgenv().Key = key
loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()
else
warn("key rejected: "..tostring(res and res.reason))
end
Ad unlock
Generate a token, send the player to the gate page, and poll until they've earned a key. The /wait call long-polls — it returns {} on timeout, so just call it again.
local Http = game:GetService("HttpService")
local token = Http:GenerateGUID(false)
setclipboard("https://api.aegisloader.com/gate/<id>="..token) -- player opens it
while true do
task.wait(3)
local r = Http:JSONDecode(game:HttpGet("https://api.aegisloader.com/gate/<id>="..token.."/wait"))
if r.key then
getgenv().Key = r.key
loadstring(game:HttpGet("https://api.aegisloader.com/loader/<id>"))()
break
end
end
Key source
If your script is attached to a key system, /api/keysource tells you whether it validates against the key system (shared: true) or its own licenses (shared: false) — so one UI can drive several scripts and gate each against the right id.
pcall. HttpGet throws on a dropped request; never let a blip brick a paying player. Treat a failed validate as "try again," not "rejected."