API
This is the Axle site API — the HTTP surface behind the obfuscator and dashboard. It is not the compiler manual; it documents what this web service accepts and returns, plus the preset, profile, macro, and plugin behavior those fields control.
API
Obfuscate
POST/api/obfuscate
Send JSON. Every protection defaults to off when the field is false or missing. Sign in and the browser sends your session cookie automatically. A script or another server sends Authorization: Bearer axle_… with the key from the dashboard. A free key can obfuscate 100 times a day; the count resets at 00:00 UTC. The account must have a verified email — see Rate limits.
{
"source": "print(1)",
"preset": "SWIFT",
"profile": "sunc-full",
"generation": 1,
"compress": "never",
"strings": false,
"numbers": false,
"stripNames": false,
"mba": "off",
"inline": false,
"unroll": false,
"fastRuntime": false,
"macros": false,
"antiTamper": false,
"watermark": false,
"watermarkId": "",
"licenseKey": false,
"license": "",
"serverLock": false,
"host": "",
"nonce": "",
"job": "",
"precheck": "",
"precheckFrom": "place",
"encfunc": ""
}
curl -X POST https://axletool.app/api/obfuscate \
-H "authorization: Bearer axle_your_key" \
-H "content-type: application/json" \
-d '{"source":"print(1)","preset":"HARDENED","token":"your-16-byte-min-token"}'
Request fields
Fields map 1:1 onto axle.toml — see Presets and the compiler's config.mdx for the deeper rules each one triggers.
Core
| Field | Values |
|---|---|
source | Lua or Luau text. Required. 512 KiB max. |
preset | SWIFT, BALANCED, HARDENED. Default SWIFT. See Presets. |
profile | sunc-full, unc-lite, roblox-studio. Default sunc-full. See Profiles. |
generation | Integer, 1 to 65535. 1 keeps the current layout. 2 and up remixes fuse cuts, emit order, and operand packing — bump it after a lift. |
compress | never, auto, loadstring. loadstring is a compile error on roblox-studio. Security builds should use never. |
Protection
| Field | Values |
|---|---|
strings | Boolean. Encrypt string constants through the recipe KDF. Default false, except presets turn it on — see Presets. |
numbers | Boolean. Encrypt number constants. HARDENED turns this on by default. |
stripNames | Boolean. Removes local names from the emit without changing behavior. |
mba | off, light, on. light rewrites integer-looking + on native spice; on also rewrites -. Loop-index and {0, 1, -1, 2}-only operands are skipped. |
inline | Boolean. Inlines small non-recursive callees (cost ≤ 32) that are a single block and return. BALANCED and HARDENED turn this on even when the field is omitted. |
unroll | Boolean. Unrolls numeric for loops with constant bounds and a trip count of 1–8. Same default-on presets as inline. |
fastRuntime | Boolean. Keeps small hot kernels native instead of virtualizing them (cost.hot_loop_native). On by default on every preset; turning this off puts hot loops on the VM, which measured ~89s virtualized vs 0.003s native on the reference hot-loop fixture. |
macros | Boolean. Expands AXL_*, LPH_*, and MS_* calls into Axle actions. Off leaves those calls as ordinary globals; an unknown AXL_* name is then not an error. See Macros & attributes. |
antiTamper | Boolean. Weaves the _t tripwire check. Applied only on HARDENED; SWIFT and BALANCED accept the field and ignore it. See Plugins. |
Watermark, license, lock
| Field | Values |
|---|---|
watermark | Boolean. Writes a build-identifying token into the emit. Does not change runtime behavior. |
watermarkId | String, optional. ASCII letters, digits, . _ - :, 32 bytes max. Used as the token when set; otherwise the token composes from account/build info or falls back to the seed. |
licenseKey | Boolean. Requires license. The emit checks _lk (or _G._lk) against it at runtime — no network call. |
license | String. Required when licenseKey is true. Goes through the string-encrypt pool and mixes into the HARDENED constant KDF. |
serverLock | Boolean. Requires host. The emit compares tostring(game.PlaceId) and tostring(game.JobId) to it. |
host | String. Required when serverLock is true. The PlaceId or JobId to lock to. Stored encrypted; field names stay plaintext so the host can read them. |
Bind (HARDENED)
Empty means off. HARDENED still mints a build secret (_ak) even with every bind field empty — a token of at least 16 bytes is required to compile HARDENED at all.
| Field | Values |
|---|---|
nonce | Optional second secret, 16+ characters. Read at runtime as the chunk's second argument, or _ax. |
job | Optional, 16+ characters. Locks the emit to one JobId; a copy run elsewhere decrypts as noise. |
encfunc | Optional, 16–256 bytes. The chunk's third argument at runtime. Never written into the emit itself. |
precheck | Expected tostring of PlaceId, or of the HWID when precheckFrom is hwid. A wrong value at runtime leaves constants unreadable rather than erroring loudly. |
precheckFrom | place or hwid. Empty defaults to place when precheck is set and the source has no AXL_PRECHECK. |
HARDENED compiles need a token of 16 bytes or more — pass it as the Authorization key's paired token field in the dashboard key flow, or via axle.toml/--token when compiling directly with the CLI. It is never written into the output file; the chunk reads it as its first runtime argument, falling back to a global named _ak.
Result
A 200 response is JSON with these fields, plus the usual usage counters.
| Field | Contents |
|---|---|
lua | The obfuscated script. |
presetprofilegeneration | What the build actually used, after defaults and attribute overlays resolved. |
seed | The seed behind this layout. Reusing the same seed, source, and config is byte-identical. |
token | HARDENED only. The first line of lua sets _ak to this token. |
usedlimitremainingresetsAt | Today's quota after this call, echoing the same shape as GET /api/me. |
Errors
Errors are JSON: {"error": "message"}. A 429 also carries the current used / limit / remaining / resetsAt.
| Status | Meaning |
|---|---|
| 400 | Bad body, or the compiler rejected the script (profile-gate failure, bad config value, etc.). |
| 401 | Not signed in and no valid axle_… key sent. |
| 403 | Signed in or keyed, but the account's email is not verified yet — code: "EMAIL_NOT_VERIFIED". Verify from the dashboard, then retry. |
| 413 | Script over 512 KiB. |
| 429 | The daily limit is spent. Resets at 00:00 UTC. |
| 502 | The compiler did not answer. The call did not consume your quota. |
| 503 | The site is not connected to the compiler backend right now. |
Rate limits & email verification
Every account gets 100 obfuscations a day, counted in UTC and shared between the dashboard, the API key, and the signed-in browser session — it is one bucket per account, not per credential. The counter resets at 00:00 UTC; GET /api/me and every obfuscate response report resetsAt as an ISO timestamp.
New accounts must verify their email before /api/obfuscate will run a job, whether the call is authenticated by session cookie or by axle_… key. An unverified account gets 403 with code: "EMAIL_NOT_VERIFIED" even if a key was already issued. Changing the account email re-locks it until the new address is verified. Resend the link from the dashboard; sign-up and sign-in both trigger one automatically.
Account & auth
/api/auth/* is Better Auth (email + password, session cookies). The account routes below are specific to this site and sit beside it.
GET
/api/me- Returns the signed-in account:
email,name,image,emailVerified, whether a key exists (hasKey,prefix), and today'sused/limit/remaining/resetsAt. 401 when signed out. POST
/api/keys- Creates the account's key, or rotates it when the body is
{"rotate":true}. The full key string is only ever present in this response — the dashboard andGET /api/meonly ever see the prefix. GET
/api/keys- Returns
hasKey,prefix, andcreatedAtfor the current key, without the full secret. DELETE
/api/keys- Deletes the current key. 404 if there isn't one.
POST
/api/verify/send- Resends the verification email to the signed-in account. Rate-limited to one send per minute; returns
{"ok": true, "emailVerified": true}immediately if the account is already verified. POST
/api/avatar- Uploads a profile photo (JPEG/PNG/WebP/GIF body, 256 KiB max after client-side crop). Returns the new
imageURL, versioned so it can be cached indefinitely. DELETE
/api/avatar- Removes the profile photo.
Compiler reference
How preset, profile, macros, and plugin fields actually change the emit. This mirrors the compiler's own docs — useful when a result looks different than you expected.
Presets
preset picks the encrypt pad and how Axle assigns native, cps, reduce, and vm per function. Default is SWIFT. HARDENED is the security tier — see the HARDENED notes in Macros & attributes and the bind fields above.
| Preset | native | cps | reduce | vm | Encrypt |
|---|---|---|---|---|---|
SWIFT | Default, including hot loops | Cold code + sensitive functions | Off | Off | Strings. Names stripped. |
BALANCED | Hot loops | Everything else | Off | Off | Strings. Numbers outside small loop-safe constants. |
HARDENED | Small hot kernels only, plus frames the VM can't express | Fallback when vm isn't listed | ≤ 24 blocks when vm is off | Default for the rest | Strings + numbers. Bind fields fold into the key. |
Preset-level defaults for the protection fields, before your request overlays them:
| Preset | strings | numbers | stripNames | inline / unroll |
|---|---|---|---|---|
SWIFT | true | false | true | off |
BALANCED | true | false | true | on |
HARDENED | true | true | true | on |
native output still strips names and encrypts strings by default — it is never plaintext Lua. antiTamper only weaves on HARDENED; SWIFT and BALANCED silently ignore the field.
Profiles
profile sets the host gate — what syntax and APIs the emit is allowed to assume. Default is sunc-full. Runtime emit never calls debug.*, getfenv, hookfunction, or loadstring; compress: "loadstring" is a wrapper around the emit, not a call inside it.
| Profile | Hosts |
|---|---|
sunc-full | Volt, Potassium, Wave, SirHurt, Cosmic, Real, Madium, Delta, MacSploit, Opiumware, Isaeva |
unc-lite | Solara, Xeno |
roblox-studio | ModuleScript / LocalScript source, run inside Studio itself |
sunc-full | unc-lite | roblox-studio | |
|---|---|---|---|
| Syntax after canonicalize | Lua 5.1-safe | Lua 5.1 only | Luau, types stripped |
bit32 | Use host | Bundled polyfill if used | Use host |
buffer | Allowed | Compile error | Allowed |
compress: "auto" | Wraps if ≥15% smaller | Never wraps | Never wraps |
compress: "loadstring" | Wraps | Wraps | Compile error |
| Control flow | TCO | Trampoline fold | TCO |
Profile-gate failures are hard 400 errors — a buffer call on unc-lite, or compress: "loadstring" on roblox-studio, fails the request rather than silently degrading.
Macros & attributes
Compile-time only — macros are fully erased from the emit, and attributes just set fields that this API's JSON body can also set directly. Both are read from the script's source text, so they travel with the script rather than the request.
Attributes
--!axl lines in the first 32 lines of source. An unknown key is a compile error. These are shorthand for the matching JSON field and are overlaid after the request body and before macros resolve, so a script can tighten a build the request left loose.
--!axl profile=sunc-full
--!axl preset=HARDENED
--!axl vm=auto
--!axl encrypt=str
--!axl inline
--!axl unroll
| Key | Values |
|---|---|
profile | sunc-full, unc-lite, roblox-studio |
preset | SWIFT, BALANCED, HARDENED |
vm | auto, none, cps, reduce, vm — forces that backend; none forces native |
encrypt | none, str, num, both — shorthand for the strings / numbers pair |
inline | flag, no value |
unroll | flag, no value |
Macros
Prefix is AXL_. Enabled by the macros request field — with it false, these calls are left untouched as ordinary global function calls rather than expanded, and an unrecognized AXL_* name is not an error.
| Macro | Effect |
|---|---|
AXL_OBFUSCATED() | Folds to true. |
AXL_ENCSTR(s) | s must be a string literal. Forces the recipe/XOR encrypt path for it; plaintext is never emitted. |
AXL_ENCNUM(n) | n must be a number literal. Marks it encrypted even when the numbers field is false. |
AXL_ENCBUF(s) | String literal → buffer.fromstring of a recipe-encrypted payload. Illegal on unc-lite. |
AXL_ENCFUNC(fn) | Forces vm and recipe-encrypts every string/number constant in that function and its nested closures. |
AXL_NO_UPVALUES(fn) | Isolates captured upvalues — copies them at closure creation instead of sharing. |
AXL_ONCE(expr) | First evaluation is stored; later reads return the cached value. |
AXL_NO_VIRTUALIZE(fn) | Forces native. fn is a function expression or a local function name. |
AXL_VIRTUALIZE(fn) | Forces cps, or vm on HARDENED when "vm" is in the backend list. |
AXL_SENSITIVE(fn) | Zeroes the heat weight so the backend selector virtualizes it unless also marked NO_VIRTUALIZE. |
AXL_NATIVE(expr) | Emits expr with no call-wrapping. |
AXL_CRASH() | Smashes a negative-index table, then spins on error("e…") with a per-build hex token. The emit never contains the literal string error("axle"). |
AXL_PRECHECK(pred) | A constant true folds into bind; false fails the compile. A value (game.PlaceId, a call) is mixed into both bind words — compile with precheck set to that value's tostring. |
AXL_SECURE_CALL(fn)AXL_SECURE_CALLBACK(fn) | Same as AXL_ENCFUNC. |
AXL_REWRITE(expr) | Identity — exists only so competitor aliases below compile unchanged. |
AXL_PROFILE() | Compile error unless the build is run with debug macros enabled, in which case it folds to the profile string. |
Luraph and MoonSec aliases
These names compile to the matching Axle action — they are not no-ops, and the emit does not fingerprint as either competitor.
| Alias | Axle |
|---|---|
LPH_ENCSTRLPH_STRENCMS_ENCSTRMS_STRENC | AXL_ENCSTR |
LPH_ENCNUMMS_ENCNUMMS_NUMENC | AXL_ENCNUM |
LPH_ENCBUFMS_ENCBUFMS_BUFENC | AXL_ENCBUF |
LPH_ENCFUNCMS_ENCFUNCMS_FUNCENCLPH_SECURE_CALLMS_SECURE_CALLLPH_SECURE_CALLBACKMS_SECURE_CALLBACK | AXL_ENCFUNC |
LPH_NO_VIRTUALIZELPH_JITMS_NO_VIRTUALIZEMS_JIT | AXL_NO_VIRTUALIZE |
LPH_NO_UPVALUESMS_NO_UPVALUES | AXL_NO_UPVALUES |
LPH_CRASHMS_CRASH | AXL_CRASH |
LPH_PRECHECKMS_PRECHECK | AXL_PRECHECK |
LPH_OBFUSCATEDMS_OBFUSCATED | AXL_OBFUSCATED |
LPH_REWRITELPH_HOOK_FIXMS_REWRITEMS_HOOK_FIX | AXL_REWRITE |
LPH_VIRTUALIZEMS_VIRTUALIZE | AXL_VIRTUALIZE |
LPH_SENSITIVEMS_SENSITIVE | AXL_SENSITIVE |
There is no --!lph attribute form — use --!axl even when the macro calls themselves are the LPH_* names.
Plugins
Watermark, license lock, server lock, and the HARDENED anti-tamper check. They weave into the emit itself — not a file-prefix if block you can delete — and none of them call out over the network. Guard failure strings go through the same encrypted pool as the rest of the script, so grepping the output won't find a plaintext failure message.
watermark / watermarkId
license → licenseKey / license
server lock → serverLock / host
anti-tamper → antiTamper (HARDENED only)
Watermark
When on, emit includes local _w = "<token>" inside the IIFE (or at the top of the chunk on roblox-studio, which has no IIFE). Token order: watermarkId if set; else a composed customer.script.build.generation.seedhex string when any of those account fields are set; else 16 hex digits of the seed. The same seed and config always produce the same token — it identifies a leak, it does not stop one.
License lock
Requires license. The guard passes when rawget(_G, "_lk") or a local _lk equals that string. The license literal is run through the string-encrypt pool; a mismatch calls error with an encrypted message. Set _G._lk (or _lk) on the host before the chunk runs — there is no HTTP round trip. On HARDENED the license hash also mixes into the constant KDF, so stripping the check leaves constants undecipherable rather than just skipping a check.
Server lock
Requires host. The guard compares tostring(game.PlaceId) and tostring(game.JobId) against it. Both field names stay plaintext so the host engine can read them; the comparison string itself is encrypted. Mismatch calls error with an encrypted message, and on HARDENED the host hash mixes into the constant KDF the same way the license does.
Anti-tamper
File-overlay only on SWIFT and BALANCED — those presets accept antiTamper: true and silently ignore it, no error. On HARDENED, the guard also checks rawget(_G, "_t"): nil or 0 passes, any other planted value fails closed. That probe is mixed into the same constant KDF as the license and lock hashes and as any AXL_PRECHECK result — deleting the bind if leaves strings and numbers undecipherable rather than just removing a check. Emit never calls debug.*, getfenv, hookfunction, or string.dump; AXL_CRASH() smashes a negative-index table and then spins on a mixed error("e…") token rather than erroring cleanly.