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.

Every endpoint below was verified directly against the live server source, not guessed — request/response field names are exact, including the couple of real inconsistencies (noted inline) worth knowing about before an agent hits them blind.

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

CategoryEndpoints
AccountsPOST signup, login
ProjectsPOST create, create_from_template, delete, settings/get, settings/set, invite, invite/revoke, join, collaborators · GET projects, templates
FilesPOST fs/list, fs/read, fs/write, fs/delete, fs/mkdir
DatabasePOST db/tables, db/query
DeployPOST deploy, undeploy, deploy/logs, deploy/history, deploy/rollback, compute/status
SharePOST share, unshare
SecretsPOST project/secrets/get, project/secrets/set
JobsPOST jobs/list, jobs/create, jobs/delete, jobs/toggle, jobs/run_now
TerminalSSE terminal
MinecraftPOST minecraft/start, minecraft/stop, minecraft/plugins/search, minecraft/plugins/install
AIPOST ai/chat
Two real quirks worth an agent knowing up front (both confirmed by direct testing, not speculation): /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.

Full human docs →