Reference
One endpoint.
POST /api/v1/execute takes code, returns a JSON value. Everything else on
this page is detail.
Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /api/v1/execute | Run code, return a result. |
| GET | /api/v1/meta | Limits, engine version, license, sandbox properties. |
| GET | /api/v1/examples | The snippets used by the playground. |
| GET | /health | Liveness. Does not execute anything. |
| OPTIONS | * | CORS preflight, Access-Control-Max-Age: 86400. |
Request
{
"code": "return 2 + 2",
"timeout_ms": 750,
"memory_mb": 16
}
| Field | Type | Default | Rules |
|---|---|---|---|
code | string | required | 1 byte to 32 KB. A script body or an ES module. |
timeout_ms | number | 750 | Clamped to 50..1500. An upper bound: the fixed CPU budget stops a runaway loop first. |
memory_mb | number | 8 | Clamped to 4..32. QuickJS heap. |
A script body is wrapped in a function, so a top level return works. If the
code contains import or export, it runs as a module. Both shapes
are valid: export default value, or plain top level return with
the imports hoisted above it. Neither needs a wrapper of your own.
Response
{
"ok": true,
"result": 4,
"logs": [],
"meta": {
"modules": [],
"engine": "quickjs 2025-09-13 (bellard)",
"timeout_ms": 750,
"memory_mb": 8
}
}
result is whatever the code returned, serialised with
JSON.stringify inside the sandbox. A returned undefined becomes
null. Values that cannot be serialised return EXEC_ERROR.
Results above 256 KB are refused. logs holds up to 50 captured lines from
console.log, info, warn and error.
There is no server side timing field on purpose. Workers freezes its clock while a
request is executing, so any in-sandbox duration would read 0. Time the call from your
client, or read cpuTime and wallTime per event in
wrangler tail --format json. The playground on the home page shows the
client measured round trip.
Errors
Every error is JSON with ok: false, an error.code, an
error.message and a request_id. Stack traces, wasm addresses
and environment values are never returned.
| Code | HTTP | Meaning |
|---|---|---|
BAD_REQUEST | 400 | Body missing, not JSON, not an object, or code is not a string. |
PAYLOAD_TOO_LARGE | 413 | Body above 64 KB or code above 32 KB. |
MODULE_NOT_FOUND | 422 | Package could not be resolved, or is a Node builtin. |
EXEC_TIMEOUT | 408 | Instruction budget reached (about 280 ms of CPU), or the deadline passed. |
MEMORY_LIMIT | 422 | QuickJS heap ceiling reached. |
EXEC_ERROR | 422 | Your code threw, or its result cannot be serialised. |
RATE_LIMITED | 429 | Per-IP bucket empty. Honour Retry-After. |
BUSY | 503 | 4 slots busy and the queue wait expired. Honour Retry-After. |
INTERNAL | 500 | Unexpected failure. Treated as a bug, not a normal path. |
Six ways in
1. curl, arithmetic
curl -s https://axon.va-n.com/api/v1/execute \
-H 'content-type: application/json' \
-d '{"code":"return Math.round(12000 * Math.pow(1.045, 7) * 100) / 100"}'
2. curl, regex
curl -s https://axon.va-n.com/api/v1/execute \
-H 'content-type: application/json' \
-d '{"code":"const s = \"ABC-142 ABC-157\"; return [...s.matchAll(/[A-Z]{3}-\\\\d+/g)].map(m => m[0])"}'
3. JavaScript, fetch
const response = await fetch("https://axon.va-n.com/api/v1/execute", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ code: "return new Date(0).toISOString()" }),
});
const data = await response.json();
console.log(data.result);
4. Python, requests
import requests
response = requests.post(
"https://axon.va-n.com/api/v1/execute",
json={"code": "return [1, 2, 3].reduce((a, b) => a + b, 0)"},
timeout=10,
)
data = response.json()
print(data["result"])
5. A module, with lodash
{
"code": "import _ from 'lodash';\nconst rows = [{team:'core', points:12}, {team:'web', points:7}, {team:'core', points:9}];\nreturn _.chain(rows).groupBy('team').mapValues(g => _.sumBy(g, 'points')).value();",
"timeout_ms": 750
}
Imports are hoisted and the body is wrapped, so a plain top level
return next to an import is the shortest form.
export default value is accepted too. For large libraries, import the
submodule (date-fns/addDays, not the whole date-fns barrel):
a root barrel can expand to hundreds of modules, and the resolver stops at 10.
6. Business days between two dates
const end = new Date("2026-02-01");
let days = 0;
for (let d = new Date("2026-01-01"); d < end; d.setDate(d.getDate() + 1)) {
const day = d.getUTCDay();
if (day !== 0 && day !== 6) days++;
}
return days;
How the module loader works
The synchronous QuickJS build cannot await a network call from inside the sandbox, so
resolution happens first, outside, and evaluation happens second, inside. The sandbox
only ever sees a plain Map from module URL to source text.
A module specifier is turned into
https://esm.sh/<specifier>?bundle&target=es2022. The Worker follows
up to two levels of imports, refuses anything over 10 modules or 1 MB, and rejects Node
builtins before fetching. A package that cannot be resolved returns
MODULE_NOT_FOUND with the specifier in the message, never a 500.
Self-host
Node 20 or newer, a Cloudflare account on the free plan, and a zone you control. No paid bindings are used.
git clone https://github.com/naas1201/axon
cd axon
npm install
npx wrangler deploy
Edit wrangler.toml to point routes at your own domain, or remove
the block to use the *.workers.dev URL. The wasm is bundled at build time and
served from the Worker, so there is no runtime download and no extra binding.
Run npm test for the lexer and admission-control tests, and
node scripts/smoke.mjs https://your-domain for the live scenarios.
Headers
x-request-idon every response, also present asrequest_idin error bodies.cache-control: no-storeon all API responses.retry-afteron429and503.access-control-allow-origin: *, because agents run everywhere.