Everything you need to protect a Roblox script and hand out access. If you can paste one line, you can ship.
Every project runs one of three modes. You can switch any time; the same encryption applies to all of them.
| Mode | Key required | Best for |
|---|---|---|
| Keyless | No | Public / free tools you still want VM-protected. |
| Free key | Yes (free) | Whitelisting users without charging. |
| Paid key | Yes (you sell) | Selling access with expiring, per-buyer keys. |
This is the only thing your users ever touch. Everything else, authentication, decryption, sessions and unloading, is handled for you.
loadstring(game:HttpGet("https://www.xauth.live/loader/<project>"))()
local KEY = "paste-key-here" loadstring(game:HttpGet("https://www.xauth.live/loader/<project>"))(KEY)
Keys are random and unguessable. Each key can carry:
Create, ban and delete keys from the dashboard Keys tab, or through your Discord bot.
A key binds to the first machine that uses it. If a legitimate user changes hardware, they can request a reset. Requests appear in the dashboard HWID requests tab, where you approve or deny each one. Approving clears the bound device so the key can bind again.
Every run opens a short lived session that keeps itself alive with periodic heartbeats. If the heartbeats stop, the session expires on its own. You can also kick any active session from the dashboard, the running script unloads cleanly on its next heartbeat.
Run one bot for every server you sell in. Each server links to your account with your API key, then buyers redeem keys and pull their loader from Discord. When a buyer redeems, their Discord identity (username and avatar) is attached to the key and shown in your dashboard, so you always know who owns what.
Open your dashboard, go to Overview, and generate an API key under the API key card. It starts with xauth_ and is shown once, so copy it. It only ever grants access to your own projects; rotating or revoking it instantly cuts off any bot still using the old value.
cd bot npm init -y npm i discord.js DISCORD_TOKEN=... DISCORD_CLIENT_ID=... API_BASE=https://www.xauth.live node discord-bot.mjs
The bot token is the only secret in the environment. Each server stores your API key locally in bot-config.json.
Manager commands need Discord Manage Server or the role you set with /config:
Buyer commands (anyone): /redeem, /getscript, /resethwid, /stats. Redeeming grants the buyer role if you set one.
Let users earn a timed key by completing ad-link steps (Linkvertise, work.ink, lootlabs, rinku). Open a project in the dashboard, scroll to Free key (ad-links), set the key duration and cooldown, and add checkpoints.
Users open your public key page (/getkey/<project>), complete each step, and claim a key. A session cookie, a minimum step time, and a per-IP cooldown stop casual farming.
Linkvertise anti-bypass (recommended). Without it, a patient user can skip the ad and still advance. To verify completions for real: in your Linkvertise publisher settings turn on Anti-Bypass and copy your token, then paste it into the checkpoint's Linkvertise anti-bypass token field. Linkvertise then appends a one-time hash when a user finishes the link, and the server checks it with your token before advancing. Checkpoints with a token set show verified; without one they show timing only. The token is stored write-only and never shown back.
Macros are the Xauth globals and helpers you can use inside a keyed script once it authenticates. If you are coming from Luarmor, the LRM_* names work as aliases, so you can paste an existing script with no changes.
| Macro | What it does |
|---|---|
| XA_SEND_WEBHOOK(url, {…}) | Fire a Discord webhook server-side. The URL and template are stripped out at upload, so a logger never sees them. Details → |
| XA_SANITIZE(value, regex) | Validate a client value against your regex before it reaches the webhook, so it can't be spoofed. Used inside XA_SEND_WEBHOOK. Details → |
| XA_SendWebhook(name, {…}) | Fire a named webhook you registered in the Webhooks tab, without putting the URL in your source. Details → |
| XA_IsUserPremium | true once authenticated. |
| XA_LinkedDiscordID | The buyer's linked Discord id, or "Not linked". |
| XA_ScriptName | The project name. |
| XA_ScriptVersion | Bumps every time you re-upload the script. |
| XA_TotalExecutions | Total runs by this key. |
| XA_UserNote | The key's note, or "Not specified". |
| XA_SecondsLeft | Seconds until the key expires, or math.huge for lifetime. |
The variables are set the moment the key is verified, before your code runs, so you can read them anywhere. The full detail for each is below: Runtime variables, Init script, and Server webhooks.
The XA_* names above are set by the loader once a key checks out. These are different: XS_* macros are ones you write directly into your own script before you upload it. The obfuscator recognizes them at build time and expands them as part of protecting your script. There is no setting to turn on, it activates the moment your source actually calls one.
| Macro | What it does |
|---|---|
| XS_ENCSTR("text") | Rebuilds a plain ASCII string (up to around 160 characters) so the literal never sits in the output as itself. Use it for text only your code should ever compare against. |
| XS_ENCNUM(1234) | Same idea for a whole number literal. Useful for a magic id or threshold you don't want appearing as itself in the build. |
| XS_OBFUSCATED | Reads true once your script has actually been through protection, and reads as nothing in your own raw source, so you can gate debug-only code without a flag to remember to flip. |
| XS_CRASH() | Deliberately stops execution where it's called. Nothing after it runs. Put it behind a check you only expect a tampered copy to trip. |
| XS_SECRET_EQ("expected", value) | Compares value against a hidden literal without the literal ever appearing in plaintext, and without leaking how much of it matched. Use it anywhere you'd otherwise write value == "some hardcoded string" for something sensitive. |
| XS_LINE | The source line number at the call site, fixed at build time. Useful for your own error messages or tamper logging. |
| XS_ENCFUNC(fn, "key", token) | Encrypts one function's body so it never appears in the build. It only runs if token, something only known at runtime (a validated key, a linked id), turns out to be correct. A function that reads a variable from its surrounding scope can't be wrapped this way and is left as is. |
| XS_PRECHECK(fn, expected) | Runs fn() once at startup and gives you a plain true or false for whether the result matched expected, without expected ever sitting in the build as plaintext. Use the result as a startup check, or fold it into your own logic. |
| XS_OMIT(fn) / XS_NO_VIRTUALIZE(fn) | Two names for the same thing: runs this one function at native speed instead of inside the protected runtime. Trades protection on that function for top performance, so use it only on hot, non sensitive code like a per frame callback. |
| XS_OMIT_HOT(fn) | The same speed trade as XS_OMIT, but for a function that gets called extremely often. Values it reads from the rest of your script are refreshed on a bounded schedule instead of on every single call. |
| XS_STACKVM(fn) | Builds this one function down a different internal path than the rest of your script, so a tool built to read one doesn't carry over to the other. Only works on a function that captures nothing from its surrounding scope. |
| XS_STACKALLOC(n) | Reserves a fixed block of n slots for a function's own data without using a real table. |
local secret = XS_ENCSTR("do-not-leak-me") local payout = XS_ENCFUNC(function() return calculateReward() end, "build-key", XA_LinkedDiscordID) if XS_OBFUSCATED then if not XS_SECRET_EQ("expected-token", tamperCheck()) then XS_CRASH() end end
After a keyed script authenticates, these globals are available inside it:
| Variable | Value |
|---|---|
| XA_IsUserPremium | true when authenticated |
| XA_LinkedDiscordID | linked Discord id, or "Not linked" |
| XA_ScriptName | the project name |
| XA_ScriptVersion | changes when you re-upload the script |
| XA_TotalExecutions | total runs by this key |
| XA_UserNote | the key's note, or "Not specified" |
| XA_SecondsLeft | seconds until expiry, or math.huge for lifetime |
print(XA_ScriptName .. " loaded for " .. XA_LinkedDiscordID)
The init script is Lua the loader runs the moment a key is verified, before your protected code, with every runtime variable above already set. Set it under Projects, your project, Init script. You can change it any time without re-uploading or re-obfuscating your main script, which makes it the right place for setup, environment checks, or a one-time telemetry ping.
It runs over the authenticated channel, so it is never delivered to anyone without a valid key. If it fails to compile or errors, the loader stops before your payload runs and the user is told why.
-- runs before the main script, sees XA_* and XA_SendWebhook if XA_LinkedDiscordID == "Not linked" then XA_SendWebhook("alert", { msg = "a user ran without a linked Discord" }) end if XA_SecondsLeft < 86400 then print("[heads up] your key expires within a day") end
A normal Discord webhook fired from a script can be seen and nuked by an HTTP logger. Xauth fires it server-side instead, so a logger only ever sees a request to our API, never your webhook URL or payload.
Write the webhook straight in your script with XA_SEND_WEBHOOK. The URL and the whole template are pulled out at upload and stored on our server, so they never ship to the client. The call takes a constant URL and a constant table (do not pass variables directly). Wrap any client value in XA_SANITIZE(value, regex) so the server validates it against your regex (JS regex, no slashes or anchors) and nobody can spoof it.
if bounty > 45000 then XA_SEND_WEBHOOK("https://discord.com/api/webhooks/…", { username = "Bounty Watch", embeds = {{ title = "High bounty!", color = 16711680, description = "Bounty: " .. XA_SANITIZE(bounty, "[0-9]{1,6}"), fields = { { name = "Player", value = XA_SANITIZE(plrName, "[a-zA-Z0-9_]{3,40}"), inline = true }, { name = "Caught by", value = "<@%DISCORD_ID%>", inline = true } } }} }) end
Server-only variables are filled by us and can't be spoofed: %DISCORD_ID%, %CLIENT_IP%, %COUNTRY_CODE%, %USER_KEY%, %USER_NOTE%. Migrating from Luarmor? LRM_SEND_WEBHOOK and LRM_SANITIZE work as aliases, no changes needed.
Prefer to keep the URL out of your source entirely? Register a named webhook in the project (Webhooks tab) and call it by name. XA_SendWebhook is a global the loader sets for you:
XA_SendWebhook("alert", { msg = "high bounty!" })
Keyed scripts already kick an invalid key on their own. If you would rather check a key first and show a message instead of kicking, use the key-check library.
local api = loadstring(game:HttpGet("https://www.xauth.live/library.lua"))() api.script_id = "your-project-id" local status = api.check_key(script_key) if status.code == "KEY_VALID" then api.load_script() else print(status.message) end
Status codes: KEY_VALID, KEY_EXPIRED, KEY_BANNED, KEY_HWID_LOCKED, KEY_INCORRECT, KEY_INVALID. On valid it also returns data with the expiry, note, and total executions.
At a high level, Xauth never ships anything usable to disk:
The practical result: a copied file is inert, a shared key fails on the second machine, and a revoked key or kicked session stops working right away.
Source Locker. Your original source is kept only so we can re-build it and so you can recover it if you lose it. It is encrypted at rest with AES-256-GCM, so a stolen database or backup reveals nothing without the separate server key. Projects with a stored source show "encrypted at rest" under the script.
Most people never need the API, the dashboard and Discord bot cover day-to-day work. For automation, an authenticated management API lets you script the same actions:
Management calls require your account credentials and are rate limited. The runtime endpoints your loader uses are handled automatically and aren't meant to be called by hand.
Obfuscate a script straight from your own tooling instead of the dashboard. Send us plain Lua, get the protected build back. This is for automation at build time (a CI step, a build script, your own bot). The result is a standalone build: no license loader and no key gate, exactly like the Obfuscator tab produces, so you ship the file yourself.
POST https://www.xauth.live/api/v1/obfuscate with your key as a Bearer token. Grab the key from Settings in the dashboard.
curl -X POST https://www.xauth.live/api/v1/obfuscate \
-H "Authorization: Bearer xauth_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{ "source": "print(\"hello\")", "preset": "hybrid" }'
Presets: hybrid (Recommended, default, full VM protection + engine folds), heavy (Strong, adds return blocking and error isolation), max (Maximum, table dispatch and every hardening layer). Native, dumpable tiers are not offered here, every preset runs your script fully inside the VM.
{ "ok": true, "script": "…protected lua…", "bytes": 48213, "preset": "hybrid", "protected": true }
On an error you get { "ok": false, "error": "…" } with a 4xx status (400 bad input, 401 bad key, 429 too fast, 503 obfuscator offline).
Standalone builds are not key bound (a key would have to ship inside the file to run, which protects nothing). For a leaked file to be useless without a key, use a Xauth project: the loader hands each authenticated user their key at runtime, so the key is never in the file.
const res = await fetch("https://www.xauth.live/api/v1/obfuscate", { method: "POST", headers: { "Authorization": "Bearer " + process.env.XAUTH_KEY, "Content-Type": "application/json" }, body: JSON.stringify({ source: mySource, preset: "hybrid" }) }); const data = await res.json(); if (data.ok) fs.writeFileSync("build/protected.lua", data.script);
A build saved through the API is kept in your Source Locker too, so you can recover the original later.
No. The file is encrypted and the decrypt key is never in it. Without an authenticated request to your project, it won't run.
The key binds to the first machine. The second machine fails. If they genuinely changed hardware, they request an HWID reset for your approval.
No. Environment anomalies are reported to you, not auto-enforced on the device, so a false positive never bricks a real user.
No. Failed auth returns nothing to run and the loader exits cleanly. On success it unloads itself when the session ends.