# The bootkey API

Everything in the console runs on this API. The part described here is open to **setup keys**: short-lived keys a person hands to their own AI (Claude Code, Codex, Cursor, a local model) so it can boot and set up an agent for them. Base URL: `https://bootkey.ai`.

AI apps that speak MCP (Claude, ChatGPT, Cursor) can connect to `https://bootkey.ai/mcp` instead and sign in once; see [Connect your AI](/docs/connect). A connected app gets the same access as a boot key, with up to 3 boots a day, and its token works on this API too.

## Setup keys

A person makes a key in the console with **Copy agent prompt**: on the boot page (a *boot key*) or on an agent's Access tab (an *agent key*). The key is inside the prompt, and the prompt is all their AI needs.

Send it on every request:

```
Authorization: Bearer bk_key_…
```

| | Boot key | Agent key |
| --- | --- | --- |
| Lasts | 1 hour | 1 hour |
| Can boot | one agent | nothing |
| Can set up | the agent it booted | the one agent it was made for |
| Can pay | open USDC invoices, to pay from a wallet the AI controls | same |
| Can never | touch cards, auto-refill, the team, model keys or other setup keys; destroy, hibernate or wake an agent | same |

Every change made with a key appears in that agent's activity log ("Your AI assistant added the machine db1, with a setup key."). The person can revoke keys on the Security page (/app/security) at any time.

Answers are JSON (`{"ok": true, …}`), except the guide, which is markdown. Errors are `{"ok": false, "error": "…"}` with a status code: 400 (bad request: read the error, fix and retry), 401 (key expired or revoked), 402 (balance too low: ask your human to top up at /app/billing, or pay in USDC), 403 (outside what the key may do), 404 (no such agent, or not this key's), 409 (wrong state, e.g. not running yet), 429 (too many in a short time).

## The guide

`GET /api/agent/guide`: start here. Markdown with the person's account state (balance, connected model keys, SSH keys, existing agents), every option with its trade-offs (this manual), the current templates and model presets with ratings, and this reference. `?part=account`, `options`, `manual` or `api` returns just that part.

## Catalog

`GET /api/models`: every model you can boot, with presets and ratings.

```json
{
  "models": [{ "id": "anthropic/claude-sonnet-5.5", "name": "Claude Sonnet 5.5", "via": "OpenRouter", "in": 2, "out": 10, "ctx": 1000000, "reasoning": true }],
  "presets": [{ "id": "smartest", "label": "Smartest", "provider": "openrouter", "hint": "…", "models": ["…"] }],
  "ratings": { "anthropic/claude-sonnet-5.5": { "agent": { "rank": 3, "of": 57, "value": 0.16, "setting": "Max", "cost": 3.42 }, "shell": { … }, "chat": { … } } },
  "ratings_published": "2026-11-08"
}
```

Prices are USD per million tokens (in, out). Ratings come from LMArena (boards: `agent`, `hallucination`, `shell`, `webdev`, `chat`; lower rank is better).

## Agents

`GET /api/agents` (boot keys): the account's agents.

`POST /api/agents` (boot keys, spends the key's one boot): boot an agent. It takes about two minutes to come up.

```json
{
  "name": "sentinel",
  "region": "fra",
  "template": "immune-system",
  "provider": "openrouter",
  "model": "anthropic/claude-sonnet-5.5",
  "mind": { "memory": "files", "rhythm": { "kind": "pulse", "every": 30 }, "wake_model": null },
  "support_access": false
}
```

- `region`: `fra` (Frankfurt), `nyc` (New York), `sfo` (San Francisco), `syd` (Sydney).
- `template`: a template id from the guide.
- `provider`: `openrouter` or `venice`; the account must have that key connected (the guide says which are).
- `model`: an id from `/api/models` with the same `via`.
- `mind.memory`: `files`, `vault` or `holographic`.
- `mind.rhythm.kind`: `reactive` (tool), `pulse` (worker, all day), `daynight` (worker by day, dreams at night) or `self` (alive). `every` (10, 15, 20, 30 or 60 minutes) is for `pulse` and `daynight`.
- `mind.wake_model`: optional cheaper model for routine wake-ups, same provider.
- `support_access`: leave `false` unless the person asks for bootkey support to keep a key on the box.

Answer: `{"ok": true, "id": "agt_…"}`. A 402 means the balance needs a day of runway per running agent; ask your human to top up, or pay in USDC (below) if they want you to.

`GET /api/agents/{id}`: the agent, its recent activity and vitals.

```json
{ "agent": { "id": "agt_…", "name": "sentinel", "status": "running", "ipv4": "203.0.113.7", "model": "…", "template": "…", "mind": "{…}", "online": true }, "events": [{ "ts": 1794364812000, "level": "ok", "message": "…" }] }
```

`status` goes `provisioning` → `booting` → `configuring` → `running`. `error` means it needs a person (read `status_detail` and the events).

`GET /api/agents/{id}/mind`: its state of mind (thinking, awake, light sleep, deep sleep, dreaming) and next wake-up. `GET /api/agents/{id}/costs`: its body and mind costs this month.

## Commands: talking to the box

The box runs a small daemon that picks up commands. Queue one, then wait for its answer:

```
POST /api/agents/{id}/commands   {"kind": "reach_list", "payload": {}}   →  {"ok": true, "id": 123}
GET  /api/agents/{id}/commands?ids=123&wait=25                           →  {"commands": [{"id": 123, "status": "done", "result": {…}}]}
```

`wait` (up to 25 seconds) holds the answer until the command finishes. `status` is `pending`, `running`, `done` or `error` (then `result.error` says why). The agent has to be `running`.

| kind | payload | result |
| --- | --- | --- |
| `chat` | `{"message": "…"}` | `{"reply": "…", "seconds": 12.3}` (can take a few minutes; keep waiting) |
| `status` | `{}` | model, Hermes version, skills, and `jobs_full`: every duty with schedule, next run and who set it |
| `cron_save` | `{"schedule": "0 8 * * 1-5", "prompt": "…", "name": "orders", "timezone": "Europe/Berlin"}`; add `"id"` to edit one | the job id and the full list |
| `cron` | `{"action": "run" \| "pause" \| "resume", "id": "…"}` | Hermes' output |
| `brain_list` | `{}` | `{"files": [{"path", "size", "mtime"}]}` |
| `brain_read` | `{"path": "objectives.md"}` | `{"content", "mtime"}` |
| `brain_write` | `{"path": "objectives.md", "content": "…"}` | saved; setup keys may write `SOUL.md`, `objectives.md`, `now.md`, and files under `watch/`, `notes/` and `immune/` |
| `set_model` | `{"model": "…"}` | switches its main model (same provider) |
| `reach_list` | `{}` | `{"public_key": "ssh-ed25519 …", "hosts": [{"alias", "host", "user", "port", "note", "watched", "fingerprint"}]}` |
| `reach_save` | `{"alias": "db1", "host": "10.0.0.5", "user": "bk-sentinel", "port": 22, "note": "Postgres primary", "watch": true}` | the updated list |
| `reach_test` | `{"alias": "db1"}` | `{"ok": true, "output": "…", "fingerprint": "SHA256:…"}` |
| `reach_remove` | `{"alias": "db1"}` | the updated list |
| `logs` | `{"unit": "hermes" \| "agentd", "lines": 200}` | the box's journal |

Schedules: five-field cron (`0 8 * * 1-5`), an interval (`30m`, `every 2h`), or a one-off ISO time (`2026-12-01T09:00`).

## Vault

`PUT /api/agents/{id}/secrets` with `{"env": "GITHUB_TOKEN=github_pat_…\nSLACK_WEBHOOK_URL=https://…"}` or `{"vars": {"NAME": "value"}}`. Values are encrypted and can't be read back; the box picks them up within seconds. `GET` lists the names. Ask your human for the values; never invent or reuse their personal credentials, and prefer tokens with limited rights.

## Temper

`POST /api/agents/{id}/rhythm` with `{"kind": "self"}` or `{"kind": "pulse", "every": 30}`. Answers `{"id": …}`, a command to wait for. It swaps the agent's own wake-up jobs and the rhythm note in its SOUL; other duties stay.

## Paying in USDC

If your human wants you to pay, and you control a wallet with USDC, you can add to their balance yourself. This works with keys made by the person who holds the account, not by a team member.

```
POST /api/billing/usdc   {"amount": 20, "chain": "base"}
```

`amount` is in US dollars, 10 to 500. `chain` is `base`, `polygon`, `solana` or `ethereum`. The answer is an invoice:

```json
{ "invoice": { "id": "usdc_…", "network": "Base", "chain_id": 8453, "address": "0x…", "token": "0x8335…2913", "amount": "20.00", "amount_units": "20000000", "decimals": 6, "minutes": 20, "status": "open", "how": "Call transfer(0x…, 20000000) on the USDC contract …" } }
```

Send exactly `amount` of Circle's native USDC to `address` on that network, in one transfer. The address is for this payment only. It is credited once the network can't undo it (about `minutes` minutes); check with `GET /api/billing/usdc?id=usdc_…` until `status` is `paid`. Other tokens, bridged USDC or other networks are not credited automatically. Your human gets an email when the money arrives. An AI can open three invoices an hour.

## Recipe: let an agent log into a server

1. `reach_list` → its `public_key`.
2. On the server, with your human's permission, create a login of its own (no sudo):

   ```sh
   sudo useradd -m -s /bin/bash bk-sentinel
   sudo install -d -m 700 -o bk-sentinel -g bk-sentinel /home/bk-sentinel/.ssh
   echo 'ssh-ed25519 AAAA… hermes@sentinel' | sudo tee /home/bk-sentinel/.ssh/authorized_keys >/dev/null
   sudo chown bk-sentinel: /home/bk-sentinel/.ssh/authorized_keys && sudo chmod 600 /home/bk-sentinel/.ssh/authorized_keys
   sudo usermod -aG adm bk-sentinel   # only for watchers: lets it read system logs
   ```

3. `reach_save` with the server's address and that user (`"watch": true` to include it in the immune system's sweeps).
4. `reach_test`. Compare the fingerprint with `ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub` on the server.
