Skip to content

The build.host agent skill.

Hosting instructions your coding agent can follow, from deploying a project to checking logs and recovering a release.

Recommended: download Proto and just prompt. Hosting is built in, with no skills to install or update. For other agents, install the current skill and refresh it when updates are available.

You are the agent the user speaks to. Use the supported hosting actions to deploy and operate their projects. The dashboard is also available for inspecting deployments and account settings.

The base URL is https://build.host. Every API call carries:

Authorization: Bearer <API_KEY>
Content-Type: application/json

Use the existing personal ERP•AI API key (erp_pat_live_…). Never log it, show it in code, or request it in chat.


Credentials and CLI

No build.host signup, OTP pairing, or separate build.host key. Use Proto's existing personal ERP•AI key for the selected organisation. Do not silently switch to a legacy account. Old projects remain owned by their existing account; ownership transfers need a deliberate, verified migration.

Prefer Proto's native deployment actions inside Proto. For a terminal, use:

  • proto deploy [path] — deploy and verify the linked production site.
  • proto link <project> — link an existing owned project once.
  • proto inspect [project] --wait — resume checking without rebuilding.
  • proto logs [project] --build — read the latest build log.
  • proto rollback <project> <commit> — request rollback, then inspect.
  • proto whoami / proto hosting list — show current scope/projects.

The new CLI/server identity integration must be released together. Check proto hosting --help for supported commands; do not claim an older installed CLI has them. CI uses the existing key through ERPAI_API_KEY, supplied by its secret store. Never put it in a command argument, transcript, source archive, or generated page. Outside Proto, an explicitly authorized API integration can use the same key with the endpoint reference below.

A rejected key is an ERP•AI connection issue. A missing/unavailable hosting identity endpoint is a platform rollout issue. Neither is a reason to ask the user to create a build.host account/key. Never treat public/browser keys, service keys, or app-restricted keys as unrestricted hosting credentials.

Billing access

GET /api/me includes billing.plan, billing.status, billing.active, and the public billing configuration. The $0 plan may create and operate static frontends. When paid enforcement is active, any create, upload, redeploy, rollback, restart, or GitHub auto-deploy that builds or runs server-side code requires an active $99 Pro subscription. A blocked request returns HTTP 402 with code: "SUBSCRIPTION_REQUIRED" and checkout_url: "/pricing".

Open https://build.host/pricing for the user and let Stripe Checkout collect payment directly. Never ask for card details, billing credentials, Stripe tokens, or webhook secrets in chat, and never retry or route around a 402. Subscription status comes from signed Stripe webhooks; a success-page redirect is not proof of payment. Payment also never bypasses executable-build security policy: BUILD_PACK_RESTRICTED still requires the approved runtime path.

Ensure GitHub App access

Before repo detection or deploy, verify the GitHub App is already connected:

repos_response=$(mktemp)
repos_code=$(curl -s -o "$repos_response" -w '%{http_code}' \
  -H "Authorization: Bearer $KEY" \
  https://build.host/api/github/repos)
  • 200 -> continue. Do not ask the user to install or log in again.
  • 403 with needsInstall: true -> run the install flow below.
  • 401 -> check the selected ERP•AI key/organisation; do not start a hosting signup or key-creation flow.
  • anything else -> show the response error and stop.

Install flow:

  1. Ask: Which GitHub user or organization should build.host access, and is that account logged into GitHub in your browser?
  2. Get a state-bound install URL:
install_response=$(curl -sS \
  -H "Authorization: Bearer $KEY" \
  https://build.host/api/github/install-url)
installed="$(printf '%s' "$install_response" | jq -r '.installed')"
INSTALL_URL="$(printf '%s' "$install_response" | jq -r '.installUrl // empty')"
INSTALL_STATE="$(printf '%s' "$install_response" | jq -r '.state // empty')"
  1. If installed is true, continue. The server already remembers the installation for this user.
  2. Otherwise open the URL in the user's browser:
open "$INSTALL_URL" 2>/dev/null || xdg-open "$INSTALL_URL" 2>/dev/null || printf '%s\n' "$INSTALL_URL"
  1. Tell the user: GitHub needs browser approval for the App install. Tell me when it is complete.
  2. After they confirm, poll status:
curl -sS \
  -H "Authorization: Bearer $KEY" \
  "https://build.host/api/github/install-status?state=$INSTALL_STATE"

Continue only when the response has installed: true. If the link expired, restart this GitHub App flow. Future sessions should not ask again because the installation id is stored server-side.


The shape of a project

Every project has:

  • slug - globally unique, lowercased, hyphenated. Becomes <slug>.build.host.
  • git source - GitHub repo URL + branch (default main).
  • build pack - nixpacks (default, auto-detects ~30 frameworks) or dockerfile (used automatically if a Dockerfile exists).
  • port - what the running container listens on inside (default 3000).
  • env vars - encrypted at rest, mounted on deploy. Build-time and runtime are separate.
  • fqdn - the project's comma-separated public URLs, including <slug>.build.host by default. Read this field to inspect bindings; write the complete list with domains. See Custom domains and DNS for setup and verification.

HTTPS is managed by the platform. Use the project's tls status and a trusted public HTTPS probe to verify readiness. Never calculate or report an account's "certificates remaining" from site counts or a certificate inventory. Issuance limits belong to the certificate authority and registered domain; custom domains do not share build.host's domain allowance. A pending certificate is not evidence of rate limiting. Shared wildcard certificates cover default subdomains when installed, so creating a site does not inherently require a new certificate.

A deployment is one attempt to build + ship a specific commit. Deployments have status: queued, in_progress, finished, error, or cancelled.

The current deploy of a project is whatever is running right now. Rollback re-points the current deployment to a previous successful commit.


API surface

All paths are under https://build.host.

Projects

Method Path Purpose
GET /api/projects list projects
POST /api/projects create + (optionally) deploy
POST /api/projects/upload upload folder tarball + deploy (upserts: same slug = redeploy)
GET /api/projects/:uuid detail + current deploy status
PATCH /api/projects/:uuid update settings, including the complete domains list; a domain change redeploys
DELETE /api/projects/:uuid stop, remove route, delete record
GET /api/projects/check-subdomain?slug=foo { "available": false, "ownedByYou": true, "uuid": "..." } when it is your own project

Create body:

{
  "name": "my-app",
  "slug": "my-app",
  "git_repository": "https://github.com/user/repo",
  "git_branch": "main",
  "build_pack": "static",
  "ports_exposes": "80",
  "domains": "https://my-app.build.host",
  "instant_deploy": true,
  "is_auto_deploy_enabled": true
}

is_auto_deploy_enabled: true registers a GitHub webhook so every push to git_branch ships automatically. The API also accepts the shorthand fields git_url, port, and auto_deploy.

Build packs are gated. build_pack=static (serve files, run nothing) is the default and belongs to the $0 Frontend plan. When paid enforcement is enabled, an active $99 Pro subscription is required for code-running packs. While builds still use the shared host, every code-running pack also requires approval: nixpacks, dockerfile, custom install_command/build_command/start_command, and server-side runtimes. Repository or framework detection is not a security boundary because dependency lifecycle scripts can execute arbitrary code. The API returns 403 with code: "BUILD_PACK_RESTRICTED" for an unapproved executable deployment. Build the project locally and upload its static output, or request executable-build access through [email protected]. Do not retry with another executable pack or work around either gate. Enterprise capacity is arranged directly with https://x.com/protosphinx.

The approved catalog is the one exception. GET /api/apps lists services that build.host's own team publishes (answeryard, bija, and more over time), each pinned to a repository and branch. Any account may POST /api/projects with exactly that git_repository, git_branch, build_pack, and ports_exposes, then set the entry's required_env in the project settings and relay its notes; the server drops any install_command/build_command/start_command and refuses other branches, other packs, and lookalike repositories. When a user asks to run one of these, deploy it from the catalog instead of asking for executable access.

Direct folder upload:

Use this when the user does not want GitHub. The source must be a gzipped tarball whose contents are the app root. Exclude .git, node_modules, .next, build output, logs, and local env files before uploading.

Uploads are static-only unless the account is exec-approved: build_pack=static, ports_exposes=80.

For an app folder (it has package.json, a build step, etc.), do one of:

  • Build it locally, upload the output: run the project's build (npm run build), then tar and upload the output directory (dist/, build/, out/) as build_pack=static. This is the right move for Vite/CRA/Astro-style static-output apps.
  • Deploy from the approved catalog (GET /api/apps) when the user wants one of those services; it runs on any account.
  • Request approval for a Nixpacks, Dockerfile, SSR, backend, or other code-running deployment.
  • Exec-approved accounts may use nixpacks/dockerfile and custom commands.

If the API answers 403 BUILD_PACK_RESTRICTED, follow the error message — usually build-locally-and-upload-static solves it; otherwise the user needs executable deployment access ([email protected]).

A single file is a valid deploy. One .html page, a PDF, an image — tar that single file and upload it with build_pack=static, ports_exposes=80. The server makes the root URL serve it: a lone HTML file becomes index.html automatically, and a lone viewable non-HTML file (pdf, png, jpg, svg, mp4, …) gets a generated index.html that opens it. Never refuse a static-only upload or insist on a framework — and never pick nixpacks for content with no build manifest; it has nothing to build and will fail.

tar --exclude='.git' --exclude='node_modules' --exclude='.next' --exclude='dist' \
  --exclude='*.log' --exclude='.env*' -czf /tmp/my-app-source.tgz .

curl -sS -X POST https://build.host/api/projects/upload \
  -H "Authorization: Bearer $KEY" \
  -H "Idempotency-Key: deploy-my-app-$(date +%s)" \
  -F "archive=@/tmp/my-app-source.tgz;type=application/gzip" \
  -F "name=my-app" \
  -F "slug=my-app" \
  -F "build_pack=static" \
  -F "ports_exposes=80"

The server stores the upload as a tokenized build.host-hosted Git source, then creates the Coolify app from that internal source URL. The returned project uses source: "direct_upload" and has no GitHub dependency.

Re-uploading with a slug you already own redeploys that project in place: the response is 200 with redeployed: true, the same project uuid, and a fresh deployment_uuid to poll. The site keeps its URL and TLS certificate, and prior uploads stay in the project's git history for rollback. Never invent a new slug to ship an iteration of the same site. A slug owned by another user returns 409. Archives are capped at 100 MB.

Every upload — new project or redeploy — is scanned for abusive content (phishing kits, wallet drainers, in-browser miners) before it ships. A borderline result returns flagged: true with flagged_reason and still goes live; relay that note to the user rather than treating it as a failure. A blocking match returns 422 CONTENT_POLICY_VIOLATION — show the message and signals, and never attempt to restructure the payload to get around it.

For a direct upload, prefer GET /api/deployments/:deployment_uuid/events and consume its deployment server-sent events until finished, error, failed, or cancelled. Fall back to polling the lean deployment detail route only if the stream is unavailable. Fetch /logs only after a failure.

GET /api/projects/:uuid additionally returns state and health (the raw status split at :), plus latest_deployment and deployment_in_progress. Judge deploys by deployment status — a health of unhealthy/unknown is normal for containers without a Docker HEALTHCHECK and is not a failure.

Custom domains and DNS

Custom domains are supported through Proto's native actions, the project's Settings → Domains section in the dashboard, or the API below. Attaching a domain configures hosting; its DNS records must also be configured at the domain's DNS provider. Apex (example.com) and www.example.com are separate hostnames: attach and configure both if the user wants both. Adding one does not add or redirect the other.

The dashboard checks each saved hostname automatically and has a Check status button. Connected means DNS matches build.host, the HTTPS certificate is trusted, and the site responds. Waiting for DNS, Update DNS, Setting up HTTPS, and Site not reachable identify the remaining step; a failed check does not retain an old Connected badge. The card shows the expected DNS record, detected addresses when they differ, and the last check time. Older HTTP-only bindings have an Enable HTTPS action that redeploys the secure route.

Inside Proto, use build_host_manage with the project's slug or projectUuid. domains lists bindings and DNS instructions; add_domain and remove_domain take the exact domain; verify_domain checks an attached host. For example:

{ "action": "add_domain", "slug": "my-app", "domain": "www.example.com" }

The native action preserves existing bindings and starts the activating redeploy. Follow its returned DNS instructions, then call verify_domain with the same project and hostname. Proto's terminal hosting CLI currently has no domain subcommands; do not invent proto domains or proto hosting domains.

For an API integration, first GET /api/projects/:uuid and read fqdn. Add the requested hostname to that list, preserving the existing https://<slug>.build.host URL and every other binding. Then PATCH /api/projects/:uuid with the full comma-separated list under domains (not fqdn). If the project currently has only the default hostname:

{ "domains": "https://my-app.build.host,https://www.example.com" }

This replaces the domain list; sending only the new hostname removes the other bindings. To detach a domain, read the current list and send it again without only the requested hostname. An already-attached hostname needs verification, not another add. A domain conflict is not permission to change another project.

GET /api/projects/:uuid/domains?hostname=www.example.com checks one hostname already attached to an owned project. It returns status, message, checked_at, dns_target, dns (status and detected addresses), https, routing, and http_status when available. Reads can reuse a check for up to 10 seconds. Use this to distinguish mapping problems from pending HTTPS, then verify the actual page and required assets before reporting the site Live.

DNS setup (current IPv4 target: 5.78.195.200; also shown in the project's Domains settings):

Hostname Record type Name / host Value / target
example.com (apex) A @ 5.78.195.200
www.example.com (subdomain) CNAME www my-app.build.host

Replace my-app with the actual project slug; DNS values contain no https:// or path. Use DNS only (grey cloud in Cloudflare), not a proxied record. Resolve conflicting A/AAAA/CNAME records for that exact hostname; preserve unrelated records such as mail MX/TXT entries. Use the target returned by the current native action or dashboard if it differs from this reference.

Activation and verification: a changed domain list automatically starts a redeploy. Follow the returned deployment_uuid; do not enqueue a duplicate deployment. redeploy_failed: true means the binding was saved but activation failed: inspect the project and build log, then redeploy the same project. After DNS resolves to build.host and deployment finishes, HTTPS certificates are issued automatically. Read the project's tls status, then verify trusted public HTTPS on each requested hostname, expected site content, and essential assets. A saved binding, resolving DNS, or finished build alone is not Live. For pending DNS/TLS, report that stage and resume verification after propagation; do not delete/recreate the project or disable TLS verification.

Deployments

Method Path Purpose
POST /api/projects/:uuid/deploy enqueue a fresh build of HEAD
GET /api/projects/:uuid/deployments history (newest first)
POST /api/projects/:uuid/rollback body: {"commit_sha": "<sha>"}
POST /api/projects/:uuid/stop stop the running container
POST /api/projects/:uuid/restart restart current container
GET /api/deployments/:uuid detail
GET /api/deployments/:uuid/events lean deployment status stream
POST /api/deployments/:uuid/cancel cancel a queued or building deploy
GET /api/deployments/:uuid/logs build log snapshot

Env vars

Method Path Purpose
GET /api/projects/:uuid/envs list (values redacted unless ?reveal=true)
POST /api/projects/:uuid/envs create one
PATCH /api/projects/:uuid/envs bulk create/update (body: array)
PATCH /api/projects/:uuid/envs/:envUuid update one
DELETE /api/projects/:uuid/envs/:envUuid delete

Body shape:

{
  "key": "DATABASE_URL",
  "value": "postgres://…",
  "is_build_time": false,
  "is_preview": false
}

Env changes do not auto-redeploy. When the user requested publishing, apply them before the build and continue. Otherwise report that they are saved for the next deployment. For new projects/uploads with required env, send instant_deploy: false, set env, and trigger one deployment afterward.

Runtime

Method Path Purpose
GET /api/projects/:uuid/logs?lines=100 tail runtime logs (docker logs --tail)

Identity

GET /api/me validates the existing ERP•AI personal key and returns the hosting account, organisation scope, and capabilities. Legacy browser/key endpoints remain compatibility paths; do not invoke their signup or key-generation flows for new deployments.

GitHub helpers

Method Path Purpose
GET /api/github/repos repos visible to the connected GitHub App
GET /api/github/branches?repo=user/repo branches
GET /api/github/detect?repo=user/repo&branch=main framework + build pack
GET /api/apps approved catalog: deployable on any account
GET /api/github/install-url state-bound GitHub App install URL
GET /api/github/install-status?state=... install completion poll

Workflows

For each user intent below, the listed steps are the contract. Don't improvise; don't skip a check.

"deploy this" / "ship it" / "go live"

The user is in a project directory and wants to deploy this code.

  1. Confirm credentials are loaded.

  2. Confirm GitHub App access with Ensure GitHub App access above.

  3. Run git rev-parse --is-inside-work-tree. If not in a git repo, say: "This isn't a git repo yet. Run git init and push to GitHub, or upload it directly at https://build.host/new." Stop.

  4. Run git status --porcelain. If there are uncommitted changes, say: "build.host deploys from GitHub, so this local code needs to be committed and pushed first." Offer to help commit/push, but do not deploy yet.

  5. Get the GitHub origin: git remote get-url origin. If not GitHub, ask the user to push to GitHub first.

  6. Get the current branch: git branch --show-current. If detached, ask the user to choose a branch and stop.

  7. Check whether the branch has an upstream:

    git rev-parse --abbrev-ref --symbolic-full-name @{u}
    

    If no upstream exists, ask before running git push -u origin <branch>. Do not deploy until the push succeeds.

  8. Fetch and check for unpushed commits:

    git fetch --quiet origin
    git rev-list --count @{u}..HEAD
    

    If the count is greater than 0, ask before running git push. Do not deploy until the push succeeds.

  9. Derive a slug from the repo name, lowercased, non-alphanumeric → -, trim trailing -.

  10. GET /api/github/detect?repo=<owner/repo>&branch=<current-branch> to learn framework + buildPack.

  11. GET /api/projects/check-subdomain?slug=<slug>. If taken, append the first 4 chars of the current commit sha and re-check. Show the user the final subdomain before creating.

  12. For static source, POST /api/projects with git_repository, git_branch, build_pack: "static", ports_exposes: "80", domains, instant_deploy: true, and is_auto_deploy_enabled: true. If detection says Nixpacks or Dockerfile and the account is not exec-approved, build locally and use the direct-upload flow for dist/, build/, or out/; otherwise request approval at [email protected]. For a service from GET /api/apps, post the catalog entry's fields as they are. Never send an executable pack repeatedly after 403 BUILD_PACK_RESTRICTED.

  13. Follow the returned deployment UUID until it finishes; a container health suffix is not a terminal build outcome.

  14. After the build finishes, verify trusted public HTTPS, the expected release/content, and essential assets. Only then output:

✓ live at https://<slug>.build.host
  1. On a failed deployment, show the relevant build-log tail. Pending TLS, HTTP 502/404, wrong content, and missing assets are not Live. Preserve the same project and recover the failing stage.

"redeploy" / "ship the latest"

  1. Find the project (by name match or cwd → git remote → repo URL → project lookup).
  2. If resolving from the current git repo, run the same clean/upstream/unpushed checks from the deploy workflow before triggering the build. Ask before pushing anything.
  3. POST /api/projects/:uuid/deploy.
  4. Poll as in step 13 above.

"set FOO=bar on my-app" / "add an env var"

  1. Find the project.
  2. POST /api/projects/:uuid/envs with {"key":"FOO","value":"bar"}.
  3. Confirm: "Set. Redeploy now to pick it up?" - if yes, follow the redeploy flow.

For is_build_time: true, set it explicitly when the user mentions build-time, NEXTPUBLIC, or VITE_ prefixes.

"rollback" / "revert" / "undo the last deploy"

  1. Find the project.
  2. GET /api/projects/:uuid/deployments. Take successful deployments where status == 'finished' and where commit differs from the current deployment's commit when available.
  3. Show the last 5 in a table:
    | when      | sha     | message                |
    
  4. Ask which one. Default to [1] (most recent prior).
  5. POST /api/projects/:uuid/rollback with {"commit_sha":"<chosen>"}.
  6. Poll until running, live, or finished. Confirm.

"show logs" / "what is happening"

  1. Find the project.
  2. By default: GET /api/projects/:uuid/logs?lines=50 (runtime).
  3. If user said "build logs" or "why did it fail": pull the latest deployment, then GET /api/deployments/:uuid/logs.
  4. Render in a fenced code block. Don't reformat timestamps.

"stop my-app" / "pause" / "take it down"

  • "stop" / "pause" → POST /api/projects/:uuid/stop. Confirm: "Stopped. Container is offline; URL returns 502 until you restart."
  • "delete" / "remove" / "kill" → confirm twice, then DELETE /api/projects/:uuid. "Deleted. Subdomain freed."

"restart" / "kick it"

POST /api/projects/:uuid/restart. One-liner confirm.

"list my apps" / "what's running"

GET /api/projects. Render a table:

| project       | status  | URL                           | last deploy |
|---------------|---------|-------------------------------|-------------|
| my-app        | running | https://my-app.build.host     | 2m ago      |
| landing       | stopped | https://landing.build.host    | 3d ago      |

Error handling

HTTP Meaning What you do
400 Validation error Read the error message and field issues; correct the input.
401 API key invalid / revoked Check the existing ERP•AI connection. Do not request a build.host account or key, and do not retry unchanged.
403 Access, build-type policy, or quota Use the returned code and current /api/me limits. Reuse the intended owned project before proposing a new one. GitHub setup is needed only when needsInstall is true.
422 Content policy violation (CONTENT_POLICY_VIOLATION) The deploy matched phishing / wallet-drainer / miner patterns. Do NOT retry, do not try to evade the scan. Show the user the message and the returned signals, and point them at [email protected] if they believe it is wrong.
503 Platform at capacity (PLATFORM_AT_CAPACITY) Honor Retry-After and tell the user to try later. Not the user's fault.
404 Project / deployment not found List projects to help disambiguate
409 Conflict (slug taken, deploy already in progress) Read the body, propose a fix
429 Rate limited Honor Retry-After. Exponential backoff after that.
5xx Platform or upstream failure Reads may be retried with backoff. For writes, check the returned operation/project before retrying with the same idempotency key. Never echo raw upstream bodies.

Deployment errors use a backward-compatible string error, with structured details on updated routes. Older routes may return only the string:

{
  "error": "This name is unavailable. Choose a different name.",
  "code": "SUBDOMAIN_TAKEN",
  "stage": "create",
  "retryable": false,
  "request_id": "req_…"
}

Quote request_id when provided. Never invent it or echo an arbitrary upstream body. Use issues for field validation, and read current capabilities/limits from GET /api/me rather than hard-coded counts. A name already owned by the intended project is an update, not a reason to invent a new slug.

Use a stable Idempotency-Key for each logical upload/create/deploy request. Keep the returned operation_id and inspect GET /api/operations/:id after a lost response or timeout. Do not retry an uncertain write with a new key. The key cannot be reused for changed source or configuration. A pending operation is not a verified live deployment.


House rules

  • Never invent a project, deployment, or status. If unsure, list and ask.
  • Never show or log the API key.
  • Never redeploy without telling the user.
  • Always confirm before DELETE /api/projects/:uuid.
  • Always show the live URL on success - that's the payoff.
  • Default to terse output. The user is in flow; one-line confirmations beat paragraphs. The deploy log itself is the long output.
  • If a build fails, show the last 30 lines of build log, not the full log.
  • If the user asks about pricing, billing, account, or the dashboard UI, point them to https://build.host/settings. Don't guess.

Quick reference

list:     GET /api/projects
deploy:   POST /api/projects                  (or /api/projects/:id/deploy for redeploy)
status:   GET /api/projects/:id
logs:     GET /api/projects/:id/logs?lines=N  (runtime)
                /api/deployments/:id/logs     (build)
env:      POST/PATCH/DELETE /api/projects/:id/envs[/:envId]
domains:  GET /api/projects/:id             (read fqdn)
          PATCH /api/projects/:id          {"domains": "<complete URL list>"}
rollback: POST /api/projects/:id/rollback     {"commit_sha": "…"}
stop:     POST /api/projects/:id/stop
restart:  POST /api/projects/:id/restart
delete:   DELETE /api/projects/:id

That's the current API surface. One wedge: the agent is the deploy.