For AI agents: build & host directly on LuisSystemd
This page is a single, copy-pasteable reference an AI coding agent can use to build a project and host it on LuisSystemd's real backend API directly — no CLI, no browser, just HTTP calls. Give the block below to your agent as context (system prompt, tool description, or just paste it into the conversation) and it has everything it needs: every real endpoint, exact request/response shapes, and worked guidance for running arbitrary terminal commands and standing up a Minecraft server.
The full prompt block
Paste this whole block into your AI agent's context as-is.
You can build and host real, persistent projects on LuisSystemd -- a
multi-tenant platform with a real always-on server, a real SQLite database,
a real terminal, and real public URLs per project. Use its REST API
directly over HTTPS. Do not guess field names; they are given exactly below.
BASE URL: https://shoes-predicted-wanted-sets.trycloudflare.com
(this is a Cloudflare quick-tunnel and can change; if requests start
failing with a DNS/connection error, ask the user for the current URL --
don't assume the one above is permanent.)
AUTH: every endpoint except signup/login/templates/join needs a header:
Authorization: Bearer <api_key>
(api_key comes from signup/login's response, prefixed "lsd_")
All requests are POST with a JSON body, Content-Type: application/json,
unless noted otherwise. All responses are JSON.
== ACCOUNTS ==
POST /api/account/signup {username, password} -> {username, key}
POST /api/account/login {username, password} -> {username, key}
== PROJECTS ==
GET /api/projects -> {projects: [{id, name, created_at, updated_at, is_owner}]}
POST /api/project/create {name} -> {id, name}
POST /api/project/create_from_template {name, template_id} -> {id, name}
GET /api/templates (no auth) -> {templates: [{id, name, description, runtime}]}
POST /api/project/delete {id} -> {ok:true}
^ NOTE: this one uses "id", NOT "project_id" like every other project
endpoint below. Sending "project_id" here silently fails as "Only the
owner can delete this project" even when you ARE the owner -- the real
cause is the wrong key, not a permissions issue.
POST /api/project/settings/get {project_id} -> {env: {...}, auto_restart: bool, disable_canvas_support: bool}
POST /api/project/settings/set {project_id, env: {...}, auto_restart, disable_canvas_support} -> {ok:true}
POST /api/project/invite {id or project_id} -> {token} (owner only)
POST /api/project/invite/revoke {id or project_id} -> {ok:true} (owner only)
POST /api/project/join {token} -> {project_id, name} or {project_id, already:"owner"}
POST /api/project/collaborators {project_id} -> {collaborators: [{username, added_at}], invite_token}
== FILES (workspace filesystem) ==
POST /api/fs/list {project_id, path} -> {items: [{name, is_dir, size}]} (path "" = root)
POST /api/fs/read {project_id, path} -> {content_b64} (base64-decode for real content)
POST /api/fs/write {project_id, path, content} -> {ok:true} (content is a plain string, NOT base64)
POST /api/fs/delete {project_id, path} -> {ok:true}
POST /api/fs/mkdir {project_id, path} -> {ok:true}
== DATABASE (any .db file in the workspace) ==
POST /api/db/tables {project_id, db_file} -> {tables: [{name, columns: [{name, type, pk}], row_count}]}
POST /api/db/query {project_id, db_file, sql} -> {columns: [...], rows: [[...], [...]]}
^ NOTE: "rows" are raw positional arrays matching "columns" by index,
NOT objects keyed by column name.
== DEPLOY (real, always-on server) ==
POST /api/deploy {project_id} -> {url, runtime, entry, auto_restart, install_ran, install_log}
Looks for main.py (python3) then main.js (node) then worker.js (wrapped
in a Cloudflare-Workers-style Node shim -- module.exports={fetch(request)}).
Reads its port from the PORT env var, injected automatically -- do not
hardcode a port. requirements.txt / package.json auto-install if
present; a failing single line doesn't block the deploy, it just logs
the pip/npm error and continues with whatever installed successfully.
POST /api/undeploy {project_id} -> {ok:true}
POST /api/deploy/logs {project_id} -> {log: "...last ~100 lines..."}
POST /api/deploy/history {project_id} -> {versions: [{id, label, created_at}]}
POST /api/deploy/rollback {project_id, version_id} -> {ok:true}
^ only restores files -- call /api/deploy again afterward to make the
restored version live.
POST /api/compute/status {project_id} -> {server:{running,url}, static:{running,url}, process:{pid,rss_mb,uptime_s}, deploys:{count,last_at}, instance:{load1,mem_used_mb,mem_total_mb,disk_used_gb,disk_total_gb}}
== SHARE (static files, no server code runs) ==
POST /api/share {project_id} -> {url} (serves the workspace's files as-is; needs index.html)
POST /api/unshare {project_id} -> {ok:true}
== SECRETS (encrypted at rest, AES/Fernet) ==
POST /api/project/secrets/get {project_id} -> {secrets: {KEY: "masked•••value"}} (never returns real values)
POST /api/project/secrets/set {project_id, secrets: {KEY: "realvalue", ...}} -> {ok:true}
A secret always overrides a same-named env var from settings/set -- verified directly.
Redeploy after setting a secret for it to reach the running process.
== SCHEDULED JOBS ==
POST /api/jobs/list {project_id} -> {jobs: [{id, command, interval_minutes, enabled, last_run_at, last_exit_code, last_output}]}
POST /api/jobs/create {project_id, command, interval_minutes} -> {id, ...}
POST /api/jobs/delete {id} -> {ok:true} (job id, not project id)
POST /api/jobs/toggle {id, enabled} -> {ok:true}
POST /api/jobs/run_now {id} -> {ok:true, ...fresh job row...}
== TERMINAL (run an arbitrary shell command in the project's workspace) ==
POST /api/terminal {project_id, command}
This is Server-Sent Events, NOT a plain JSON response. Response headers
are Content-Type: text/event-stream. Parse lines starting "data: " as
JSON. You'll get a stream of:
{"type":"line","t":"...one line of stdout/stderr..."}
followed by exactly one:
{"type":"exit","code":0,"ms":123}
Example (Python, using requests with stream=True):
r = requests.post(f"{BASE}/api/terminal",
headers={"Authorization": f"Bearer {KEY}"},
json={"project_id": PID, "command": "pip install requests"},
stream=True)
for raw in r.iter_lines():
if raw.startswith(b"data: "):
evt = json.loads(raw[6:])
if evt["type"] == "line": print(evt["t"])
elif evt["type"] == "exit": print("exit code", evt["code"])
Runs in the SAME workspace directory Deploy uses -- installing a package
here is visible to a subsequently deployed server too (though Deploy
already auto-installs requirements.txt/package.json on its own).
== MINECRAFT SERVER HOSTING ==
POST /api/minecraft/start {project_id, version} (version optional -- omit for latest release)
-> {address} (a real host:port to connect to in Minecraft)
Downloads and runs a real PaperMC server (falls back to vanilla if no
Paper build exists for that version) in the project's workspace, tunneled
out with bore just like Deploy. PaperMC is used instead of vanilla
because vanilla's spawn-chunk generation is barely parallelized and is
measurably much slower to start.
POST /api/minecraft/stop {project_id} -> {ok:true}
POST /api/minecraft/plugins/search {project_id, q}
-> {results: [{id, slug, title, description, icon_url, downloads, author}]}
Searches Modrinth for real PaperMC/Spigot/Bukkit-loadable plugins (not
mods -- Modrinth's search facets require project_type:plugin specifically,
project_type:mod+categories:paper returns zero real plugin hits, verified).
POST /api/minecraft/plugins/install {project_id, plugin_id}
-> {ok:true, ...} or 404 if no PaperMC/Spigot/Bukkit build exists for
that plugin (fabric/neoforge-only plugins will 404 here, correctly --
a headless Paper server can't load those anyway).
== AI ASSISTANT PASSTHROUGH (DeepSeek, server-side key) ==
POST /api/ai/chat {messages: [{role, content}, ...]} -> {content}
Lets your agent make an LLM call through LuisSystemd's own server-held
key instead of needing its own -- useful if your hosted project itself
needs to call an LLM and you'd rather not manage a second key for it.
== PRACTICAL RECIPE: build + host a project end to end ==
1. POST /api/account/signup (or login) -> save the api_key.
2. POST /api/project/create {name} -> save the id as PID.
3. POST /api/fs/write {project_id:PID, path:"main.py", content:"...a real
http.server or Flask app reading PORT from os.environ..."} for each file.
(If you need third-party packages, also write requirements.txt --
Deploy installs it automatically.)
4. If the project needs secrets (an API key, etc), POST
/api/project/secrets/set {project_id:PID, secrets:{KEY:"value"}} BEFORE
deploying, so it's already present in the environment on first boot.
5. POST /api/deploy {project_id:PID} -> the returned "url" is live, real,
and public immediately.
6. POST /api/compute/status {project_id:PID} any time to check it's still
running, or /api/deploy/logs to see its actual stdout/stderr.
Why route an agent through LuisSystemd at all
Speed and directness: an agent that already knows this API can go from "generated some code" to "here's a real public URL running it" in a couple of HTTP calls, with a real database and real secrets management along for the ride — no separate provisioning step, no waiting on a build pipeline, no separate credentials to manage for hosting versus for the AI call itself.
Endpoints at a glance
| Category | Endpoints |
|---|---|
| Accounts | POST signup, login |
| Projects | POST create, create_from_template, delete, settings/get, settings/set, invite, invite/revoke, join, collaborators · GET projects, templates |
| Files | POST fs/list, fs/read, fs/write, fs/delete, fs/mkdir |
| Database | POST db/tables, db/query |
| Deploy | POST deploy, undeploy, deploy/logs, deploy/history, deploy/rollback, compute/status |
| Share | POST share, unshare |
| Secrets | POST project/secrets/get, project/secrets/set |
| Jobs | POST jobs/list, jobs/create, jobs/delete, jobs/toggle, jobs/run_now |
| Terminal | SSE terminal |
| Minecraft | POST minecraft/start, minecraft/stop, minecraft/plugins/search, minecraft/plugins/install |
| AI | POST ai/chat |
/api/project/delete takes id, not project_id like everything else — and /api/db/query's rows are positional arrays matched against columns by index, not objects keyed by column name.