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.

JSON · request body
{
  "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": ""
}
bash · with a key
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

FieldValues
sourceLua or Luau text. Required. 512 KiB max.
presetSWIFT, BALANCED, HARDENED. Default SWIFT. See Presets.
profilesunc-full, unc-lite, roblox-studio. Default sunc-full. See Profiles.
generationInteger, 1 to 65535. 1 keeps the current layout. 2 and up remixes fuse cuts, emit order, and operand packing — bump it after a lift.
compressnever, auto, loadstring. loadstring is a compile error on roblox-studio. Security builds should use never.

Protection

FieldValues
stringsBoolean. Encrypt string constants through the recipe KDF. Default false, except presets turn it on — see Presets.
numbersBoolean. Encrypt number constants. HARDENED turns this on by default.
stripNamesBoolean. Removes local names from the emit without changing behavior.
mbaoff, light, on. light rewrites integer-looking + on native spice; on also rewrites -. Loop-index and {0, 1, -1, 2}-only operands are skipped.
inlineBoolean. 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.
unrollBoolean. Unrolls numeric for loops with constant bounds and a trip count of 1–8. Same default-on presets as inline.
fastRuntimeBoolean. 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.
macrosBoolean. 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.
antiTamperBoolean. Weaves the _t tripwire check. Applied only on HARDENED; SWIFT and BALANCED accept the field and ignore it. See Plugins.

Watermark, license, lock

FieldValues
watermarkBoolean. Writes a build-identifying token into the emit. Does not change runtime behavior.
watermarkIdString, 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.
licenseKeyBoolean. Requires license. The emit checks _lk (or _G._lk) against it at runtime — no network call.
licenseString. Required when licenseKey is true. Goes through the string-encrypt pool and mixes into the HARDENED constant KDF.
serverLockBoolean. Requires host. The emit compares tostring(game.PlaceId) and tostring(game.JobId) to it.
hostString. 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.

FieldValues
nonceOptional second secret, 16+ characters. Read at runtime as the chunk's second argument, or _ax.
jobOptional, 16+ characters. Locks the emit to one JobId; a copy run elsewhere decrypts as noise.
encfuncOptional, 16–256 bytes. The chunk's third argument at runtime. Never written into the emit itself.
precheckExpected tostring of PlaceId, or of the HWID when precheckFrom is hwid. A wrong value at runtime leaves constants unreadable rather than erroring loudly.
precheckFromplace 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.

FieldContents
luaThe obfuscated script.
presetprofilegenerationWhat the build actually used, after defaults and attribute overlays resolved.
seedThe seed behind this layout. Reusing the same seed, source, and config is byte-identical.
tokenHARDENED only. The first line of lua sets _ak to this token.
usedlimitremainingresetsAtToday'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.

StatusMeaning
400Bad body, or the compiler rejected the script (profile-gate failure, bad config value, etc.).
401Not signed in and no valid axle_… key sent.
403Signed in or keyed, but the account's email is not verified yet — code: "EMAIL_NOT_VERIFIED". Verify from the dashboard, then retry.
413Script over 512 KiB.
429The daily limit is spent. Resets at 00:00 UTC.
502The compiler did not answer. The call did not consume your quota.
503The 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's used / 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 and GET /api/me only ever see the prefix.

GET/api/keys

Returns hasKey, prefix, and createdAt for 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 image URL, 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.

PresetnativecpsreducevmEncrypt
SWIFTDefault, including hot loopsCold code + sensitive functionsOffOffStrings. Names stripped.
BALANCEDHot loopsEverything elseOffOffStrings. Numbers outside small loop-safe constants.
HARDENEDSmall hot kernels only, plus frames the VM can't expressFallback when vm isn't listed≤ 24 blocks when vm is offDefault for the restStrings + numbers. Bind fields fold into the key.

Preset-level defaults for the protection fields, before your request overlays them:

PresetstringsnumbersstripNamesinline / unroll
SWIFTtruefalsetrueoff
BALANCEDtruefalsetrueon
HARDENEDtruetruetrueon

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.

ProfileHosts
sunc-fullVolt, Potassium, Wave, SirHurt, Cosmic, Real, Madium, Delta, MacSploit, Opiumware, Isaeva
unc-liteSolara, Xeno
roblox-studioModuleScript / LocalScript source, run inside Studio itself
sunc-fullunc-literoblox-studio
Syntax after canonicalizeLua 5.1-safeLua 5.1 onlyLuau, types stripped
bit32Use hostBundled polyfill if usedUse host
bufferAllowedCompile errorAllowed
compress: "auto"Wraps if ≥15% smallerNever wrapsNever wraps
compress: "loadstring"WrapsWrapsCompile error
Control flowTCOTrampoline foldTCO

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.

lua · attribute lines
--!axl profile=sunc-full
--!axl preset=HARDENED
--!axl vm=auto
--!axl encrypt=str
--!axl inline
--!axl unroll
KeyValues
profilesunc-full, unc-lite, roblox-studio
presetSWIFT, BALANCED, HARDENED
vmauto, none, cps, reduce, vm — forces that backend; none forces native
encryptnone, str, num, both — shorthand for the strings / numbers pair
inlineflag, no value
unrollflag, 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.

MacroEffect
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.

AliasAxle
LPH_ENCSTRLPH_STRENCMS_ENCSTRMS_STRENCAXL_ENCSTR
LPH_ENCNUMMS_ENCNUMMS_NUMENCAXL_ENCNUM
LPH_ENCBUFMS_ENCBUFMS_BUFENCAXL_ENCBUF
LPH_ENCFUNCMS_ENCFUNCMS_FUNCENCLPH_SECURE_CALLMS_SECURE_CALLLPH_SECURE_CALLBACKMS_SECURE_CALLBACKAXL_ENCFUNC
LPH_NO_VIRTUALIZELPH_JITMS_NO_VIRTUALIZEMS_JITAXL_NO_VIRTUALIZE
LPH_NO_UPVALUESMS_NO_UPVALUESAXL_NO_UPVALUES
LPH_CRASHMS_CRASHAXL_CRASH
LPH_PRECHECKMS_PRECHECKAXL_PRECHECK
LPH_OBFUSCATEDMS_OBFUSCATEDAXL_OBFUSCATED
LPH_REWRITELPH_HOOK_FIXMS_REWRITEMS_HOOK_FIXAXL_REWRITE
LPH_VIRTUALIZEMS_VIRTUALIZEAXL_VIRTUALIZE
LPH_SENSITIVEMS_SENSITIVEAXL_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 → 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.