Documentation

Xauth docs

Everything you need to protect a Roblox script and hand out access. If you can paste one line, you can ship.

Quickstart

  1. Create an account and verify your email.
  2. Create a project in the dashboard and pick a mode (keyless, free key, or paid key).
  3. Upload your script. Xauth obfuscates it and binds it to a runtime key automatically.
  4. Copy your loader line from the project page.
  5. Hand out keys (for free/paid modes), from the dashboard or your Discord bot.
Your loader URL uses your project ID, e.g. https://www.xauth.live/loader/<project>. Set your public domain later, snippets update automatically.

Project modes

Every project runs one of three modes. You can switch any time; the same encryption applies to all of them.

ModeKey requiredBest for
KeylessNoPublic / free tools you still want VM-protected.
Free keyYes (free)Whitelisting users without charging.
Paid keyYes (you sell)Selling access with expiring, per-buyer keys.

The loader line

This is the only thing your users ever touch. Everything else, authentication, decryption, sessions and unloading, is handled for you.

Keyless

loadstring(game:HttpGet("https://www.xauth.live/loader/<project>"))()

Free key / paid key

local KEY = "paste-key-here"
loadstring(game:HttpGet("https://www.xauth.live/loader/<project>"))(KEY)
Only run the loader inside a Roblox executor. Opening the URL in a browser returns a blocked page, that's expected.

Keys

Keys are random and unguessable. Each key can carry:

  • Device limit, how many machines it may bind to (default 1).
  • Expiry, optional; the key stops working after it.
  • Note, a label for your own records (buyer name, order ID, etc.).
  • Status, active or banned. Banning takes effect on the next request.

Create, ban and delete keys from the dashboard Keys tab, or through your Discord bot.

HWID resets

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.

Sessions & kicks

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.

Discord bot

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.

1. Get your API key

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.

2. Run the bot

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.

3. Link a server and go

Manager commands need Discord Manage Server or the role you set with /config:

  • /login paste your API key to link the server, then /project to pick the product.
  • /setscript upload a file to replace the project script (obfuscated server side).
  • /config set the manager and buyer roles.
  • /generate mint keys, /whitelist add|remove bind or revoke a member's key.
  • /panel post a message with Redeem key, Get script, and My key buttons.

Buyer commands (anyone): /redeem, /getscript, /resethwid, /stats. Redeeming grants the buyer role if you set one.

Treat the API key like a password. Anyone holding it can manage your projects and keys. Revoke it from the dashboard if it leaks.

Free key (ad-links)

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.

  1. Create the link in your provider dashboard.
  2. Set that link's destination/target to the checkpoint's Dynamic URL (shown in the panel).
  3. Paste the provider link back into the checkpoint as the Short URL.

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

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.

MacroWhat 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_IsUserPremiumtrue once authenticated.
XA_LinkedDiscordIDThe buyer's linked Discord id, or "Not linked".
XA_ScriptNameThe project name.
XA_ScriptVersionBumps every time you re-upload the script.
XA_TotalExecutionsTotal runs by this key.
XA_UserNoteThe key's note, or "Not specified".
XA_SecondsLeftSeconds 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.

Source macros

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.

MacroWhat 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_OBFUSCATEDReads 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_LINEThe 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
These work in every project mode, and in standalone builds from the Obfuscator tab or the API. If one is called with the wrong kind of argument, for example a variable where a fixed value is required, it is left alone rather than shipped broken, and a note is added to your build log.

Runtime variables

After a keyed script authenticates, these globals are available inside it:

VariableValue
XA_IsUserPremiumtrue when authenticated
XA_LinkedDiscordIDlinked Discord id, or "Not linked"
XA_ScriptNamethe project name
XA_ScriptVersionchanges when you re-upload the script
XA_TotalExecutionstotal runs by this key
XA_UserNotethe key's note, or "Not specified"
XA_SecondsLeftseconds until expiry, or math.huge for lifetime
print(XA_ScriptName .. " loaded for " .. XA_LinkedDiscordID)

Init script

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

Protected webhooks

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.

Inline macro (recommended)

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.

Named template (alternative)

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!" })
Only Discord webhook URLs are allowed, and there is a per-key rate limit. Always inform users if you log their IP.

Key-check library

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.

How protection works

At a high level, Xauth never ships anything usable to disk:

  • Your script is compiled into our virtual machine and encrypted.
  • The key that unlocks it lives on the server and is only released to a request that has already authenticated.
  • Delivery happens over an authenticated session and is verified end-to-end.
  • The running payload checks that its session is still valid and stops if it isn't.

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.

We intentionally don't publish the internals of the handshake, encryption scheme or payload format. Keeping those private is part of the protection.

API overview

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:

  • Create and list projects, upload scripts, rotate settings.
  • Create, list, ban and delete keys.
  • List and resolve HWID reset requests.
  • Read audit and execution history.
  • Obfuscate scripts from your own build tooling (see Obfuscation API).

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.

Obfuscation API

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.

Your API key is a secret. Use it from a machine you control (build server, bot host). Never paste it into a script your buyers receive, or they get your key and can obfuscate on your account.

Endpoint

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" }'

Body

  • source (required) the plain Lua to protect, up to 2 MB.
  • preset (optional) one of the ids below. Defaults to hybrid.
  • brand (optional) the name shown in the kick message if the build is run somewhere it should not be.

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.

Response

{ "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.

Node example

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.

FAQ

Can someone just re-host my leaked file?

No. The file is encrypted and the decrypt key is never in it. Without an authenticated request to your project, it won't run.

What if a buyer shares their key?

The key binds to the first machine. The second machine fails. If they genuinely changed hardware, they request an HWID reset for your approval.

Will legit users get false-banned?

No. Environment anomalies are reported to you, not auto-enforced on the device, so a false positive never bricks a real user.

Does my script hang if auth fails?

No. Failed auth returns nothing to run and the loader exits cleanly. On success it unloads itself when the session ends.

Ready to lock your script?

Create a project and copy your loader line.

Get started