# Server instance

## overview.md
```yaml
id: server-instance
title: Server instance
area: ops
summary: A working, running server instance — hosting, deploy, and environments for whatever server-architecture lands on.
repo: land-of-lor
depends_on: ["server-architecture"]
```

## What this is

Getting an actual instance of the game server running somewhere reachable: hosting choice, deployment process, and environment separation (at minimum, a dev/staging environment distinct from whatever eventually serves real players).

## Why it exists

`server-architecture` designs what the server *is*; this epic makes one *exist and run*. Given the December 2026 release target, there needs to be a real, iterable server environment well before launch — not just a local dev loop — so that client work (whichever engine is chosen) has something live to talk to.

## Scope

- Hosting provider/platform choice — **decided, see below.**
- Deployment pipeline (how a change to the server code gets from commit to running instance) — a manual `git pull` + `systemctl restart` workflow exists (deliverable #4 §7); automating provisioning itself is this epic's live task.
- Environment strategy (dev/staging/production, or some collapsed version of that appropriate to project size) — still genuinely a single environment; not revisited by this pass.
- Basic operability: logs, restarts, how Kris (or an agent) checks if it's up — covered in deliverable #4 §9.

## Decided (2026-08-02 architecture conversation, written up as deliverable #4)

**Hosting is chosen: a single DigitalOcean droplet.** Ubuntu 24.04 LTS, Sydney region (`syd1` — nearest DO region to Brisbane), 2 GB RAM / 1 vCPU regular droplet. Godot 4 headless world-server processes and a director service run on it under systemd, Postgres alongside, Caddy in front for TLS and WebSocket reverse-proxying. The complete, exact-commands runbook is [`deliverables/004-server-setup-guide.md`](../../deliverables/004-server-setup-guide.md) — that document *is* this epic's hosting decision, not a separate summary of it.

**What's not yet decided:** the director service's own runtime (Go vs. Node/TS, leaning Node) — tracked as `server-architecture`'s one remaining open question, not this epic's. This epic can provision the box and run everything deliverable #4 describes regardless of which way that lands.

## Done: provisioned via the DigitalOcean API (2026-08-02)

Kris wanted the droplet stood up by an agent calling the DO API directly — not by hand — using deliverable #4 as the executable spec. `server-instance/001` did exactly that: `lor-game-server-01` (`209.38.29.22`, `syd1`, 2 GB/1 vCPU) is live, hardened, and running Postgres, Godot 4.7.1 headless, Caddy, and systemd-managed `lor-director`/`lor-world@1` units, verified end-to-end with a real WebSocket connection through Caddy to the world instance. Full provisioning record, deviations from the guide as written, and follow-ups (DO project assignment, git deploy credentials on the box, TLS once a domain exists) are in `server-instance/001`'s dated note, written up for external reading as [`deliverables/005-server-instance-live-and-how-to-deploy.md`](../../deliverables/005-server-instance-live-and-how-to-deploy.md).

## DNS (2026-08-04)

The game server has a hostname: **`lor-server.cocreations.com.au` → A `209.38.29.22`** (TTL 200). It's a dev/server hostname only — it deliberately does *not* preempt `naming-and-brand`'s still-open public-domain question (Q002).

The `cocreations.com.au` zone is **not** managed at the domain's registrar — its nameservers are iFastNet's (`ns101/ns201.ifastnet.com`), and the zone is edited in Kris's byethost cPanel: `drawonthe.net:2083` → Zone Editor → `cocreations.com.au`. Kris added this record by hand on 2026-08-04; any future record changes happen there.

This hostname is what lets Caddy hold a real Let's Encrypt certificate, which the Godot web client requires (web exports refuse to run outside a Secure Context — the plain-HTTP-on-bare-IP `/play/` link failed in a real browser for exactly this reason).

## Non-goals

- Not the server's internal architecture — that's `server-architecture`.
- Not load/scale planning for a hypothetical large player base — appropriate scope here is "a small number of players can connect and it works," matching the project's current stage.
- Not the staged multiplayer PoC implementation itself (director code, the Godot world server, the client) — this epic is the box it all runs on, not the software that runs on it.

## Relevant docs

- `deliverables/004-server-setup-guide.md` — the actual, executable hosting decision: exact commands for every layer (droplet, hardening, Godot headless, Postgres, Caddy, systemd, deploy, verification).
- `deliverables/005-server-instance-live-and-how-to-deploy.md` — the server as actually built: what's running, how to deploy to it, day-to-day ops, and open follow-ups.
- `server-architecture/overview.md` — the architecture decisions this hosting setup serves; its one open question (director runtime) doesn't block this epic.
- `server-instance/questions/001-droplet-provisioning-credentials.md` — exactly what Kris needs to supply before `server-instance/001` can run.

## status.md
```yaml
updated: 2026-08-04
parked: false
tasks: {"backlog":0,"needs-input":0,"ready":0,"doing":0,"review":0,"done":1}
open_questions: []
tag: done
next: Droplet live and now behind HTTPS at lor-server.cocreations.com.au (Let's Encrypt via Caddy, wss for director and game channels, verified 9/9 externally). Remaining follow-up cleanup — DO project assignment, git deploy credentials on the box — none of it blocking.
```

## Task summary

0 ready · 0 backlog · 0 needs-input · 0 doing · 0 review · **1 done** (1 total) — every task in this epic is done, so it's tagged `done`, not `blocked`. `server-instance/001` — provisioning the droplet via the DigitalOcean API and running deliverable #4's setup — is complete. `lor-game-server-01` (`209.38.29.22`, syd1, 2 GB/1 vCPU) is live: hardened, Postgres and Godot 4.7.1 installed, Caddy fronting it, `lor-director` and `lor-world@1` running under systemd, and a real WebSocket connection through Caddy to the world instance confirmed working. Written up for external reading as [`deliverables/005-server-instance-live-and-how-to-deploy.md`](../../deliverables/005-server-instance-live-and-how-to-deploy.md).

## Open questions

None. `server-instance/Q001` is answered (2026-08-02) — see its file for the two scope gaps that surfaced in practice (no `ssh_key:create`, no `project:read`), both worked around or flagged as follow-ups rather than blockers.

## Next

Nothing blocking. **TLS is done (2026-08-04):** Kris pointed `lor-server.cocreations.com.au` at the box (see overview's DNS section), and the same-day HTTPS wiring session (brief + outcome: `land-of-lor/pocs/multiplayer-poc/handoff/https-wiring-brief.md`) added the Caddy site block with an automatic Let's Encrypt cert, `wss://` for both the director (`/director/`) and the dynamic game channel (`/game/<port>` path proxy), verified 9/9 externally over TLS (`director/cli-test-client/verify-live-droplet-wss.ts`) with the plain-HTTP path regression-checked 9/9 too. Remaining follow-ups from `server-instance/001`'s note and deliverable #5: move the droplet into a proper DO project, register the read-only deploy key on GitHub (until then deploys use the git-bundle method), and replace the stub Godot project at `/opt/lor/game-server` with real server code once it exists. None of these block anything.

## tasks/ (1)

### server-instance/001 — Provision the droplet via the DigitalOcean API and run deliverable #4's setup
```yaml
id: server-instance/001
title: Provision the droplet via the DigitalOcean API and run deliverable #4's setup
epic: server-instance
state: done
priority: 1
blocked_by: []
estimate: M
created: 2026-08-05
updated: 2026-08-02
claimed_by: null
claimed_at: null
delivers: []
review_artifact: null
```

## What to do

`server-architecture` has landed its runtime decisions and `deliverables/004-server-setup-guide.md` already exists as an exact, step-by-step runbook. This task is executing that runbook via the DigitalOcean API instead of by hand:

1. Using the credentials Kris supplies (see `server-instance/Q001` — do not proceed without them, and do not use anyone's personal DO account/API token or personal SSH key for this), call the DigitalOcean API to provision a droplet matching deliverable #4 §1: region `syd1`, Ubuntu 24.04 LTS (`ubuntu-24-04-x64`), the confirmed size (2 GB/1 vCPU regular unless Q001's answer says otherwise), with the dedicated runner SSH keypair attached at creation.
2. Run deliverable #4's setup against the new droplet end-to-end: §2 (hardening), §3 (Godot headless — verify the x86_64 binary, per the guide's own recommendation), §4 (Postgres + role/database), §5 (Caddy — pick 5.2a or 5.2b depending on whether a domain is available yet), §6 (systemd units for director + a world instance), §7 (clone the `land-of-lor` repo to `/opt/lor/game-server`).
3. Run deliverable #4 §8's full verification checklist, ending in a successful WebSocket connection through Caddy to a world instance.
4. Record what was actually provisioned — droplet ID, IP, region/size actually used, any point where this deviated from deliverable #4 as written — as a dated note appended to this task file. "It worked" alone is not sufficient; the next session needs to be able to find the box.

## Definition of done

A running droplet, reachable exactly as deliverable #4's verification checklist describes, provisioned via the DO API rather than by hand, with its real identifying details (droplet ID/IP, actual final config) recorded back in this task file.

## Where the work lands

The droplet itself and the `land-of-lor` repo clone on it (`/opt/lor/game-server`); the record of what was provisioned lands back in this workspace repo, in this task file.

## Docs to read first

`deliverables/004-server-setup-guide.md` in full — it is the spec this task executes, not background reading. `server-instance/questions/001-droplet-provisioning-credentials.md` for exactly what must exist before this task can start at all.

## Blocked by

`server-instance/Q001` — real DO API credentials, a dedicated runner SSH keypair, and confirmation of region/size are all prerequisites an agent cannot supply for itself. This task stays `needs-input` until Kris answers that question.

## 2026-08-02 — attempted pickup, still blocked

A session was pointed at this task on the premise that Q001 was fully answered. Checked `/workspace/lor/.secrets/` before creating anything billable, per this task's own instructions not to proceed without real credentials:

- SSH keypair (`lor_runner_key` / `lor_runner_key.pub`) — present, correct (ed25519, `lor-server-runner` comment). Private key permissions were `644`; corrected to `600` in place.
- DO API token (`do_api_token`) — **not present**, and not found anywhere else in the workspace or environment.

No droplet, DO project, tag, or API call was made — nothing billable exists as a result of this session. Full detail in `server-instance/Q001`'s "Progress check, 2026-08-02" note; state stays `needs-input` pending the token.

## 2026-08-02 — provisioned and verified end-to-end

Kris placed the DO API token; re-verified it (format, permissions, and a live `region:read` call) before creating anything. Full run against `deliverables/004-server-setup-guide.md`, executed via the DO API and SSH (no console clicks). **Done.**

### What's actually running

- **Droplet:** `lor-game-server-01`, ID **`589446588`**, IP **`209.38.29.22`**, region `syd1`, size `s-1vcpu-2gb` (2 GB/1 vCPU/50 GB, ~US$12/mo), image `ubuntu-24-04-x64`, tag `lor-game-server`.
- **Access:** `ssh lor@209.38.29.22` with the runner key; `root` login and all password auth are disabled (`PermitRootLogin no`, `PasswordAuthentication no`, confirmed by a refused root-login attempt). `lor` has passwordless sudo (`/etc/sudoers.d/90-lor-nopasswd`) — needed since this session drives the box entirely over non-interactive SSH with no way to type a sudo password; flagging as an operational choice, not literally spelled out in deliverable #4.
- **Firewall:** `ufw` active, only OpenSSH/80/443 allowed (confirmed via `ufw status`). `fail2ban` and `unattended-upgrades` both active and enabled.
- **Godot:** 4.7.1 headless (`godot4 --headless --version` → `4.7.1.stable.official.a13da4feb`), x86_64 binary at `/usr/local/bin/godot4`.
- **Postgres:** role + database `lor_game`, password generated with `secrets.token_urlsafe(24)` and written only to `/etc/lor/game.env` (`600`, owned `lor:lor`) — never appeared in any command line, log, or repo file. Connection re-verified (`\conninfo` from the `lor_game` role over TCP to `127.0.0.1`).
- **Caddy:** 2.11.4, `/etc/caddy/Caddyfile` using **§5.2b (bare IP)** — no domain exists for this project yet, so this is plain `http://209.38.29.22`, not TLS. `curl -I http://209.38.29.22` → `200`.
- **systemd:** `lor-director.service` and `lor-world@1.service` both `enabled` and `active (running)`.
- **Repo:** `land-of-lor` cloned to `/opt/lor/game-server`, `origin` correctly pointing at `https://github.com/krisrandall/land-of-lor.git`.

### Deviations from deliverable #4 as written (all judgment calls made in-session, none needed to block on Kris)

1. **SSH key delivery.** The API token is deliberately scoped `ssh_key:read` only (per Q001), so the runner key couldn't be registered as a DO SSH-key object and attached via the droplet API's `ssh_keys` field (which only accepts pre-registered IDs/fingerprints). Used cloud-init `user_data` to inject the key directly instead — a standard DO-documented pattern for this exact situation.
2. **First droplet attempt (ID `589445718`) was destroyed and recreated.** DigitalOcean's own droplet-init flow expires the root password unless a droplet API `ssh_keys` entry was used at creation; since that wasn't available (see #1), the first droplet locked out with "password expired, no TTY to change it" even though pubkey auth succeeded. Fixed by adding `chage -d $(date +%Y-%m-%d) root` / `chage -M -1 root` to cloud-init `runcmd` and recreating. The first droplet existed for under 2 minutes before deletion — DO bills per-second, so cost impact is negligible, but if Kris sees two droplet-hour line items on the invoice, that's why.
3. **No domain yet**, so §5.2b (bare IP, plain `http://`) was used rather than a domain-backed TLS config. Chosen over the self-signed-TLS alternative the guide also offers, since this is explicitly a dev-stage box for a small number of trusted testers per the epic's own non-goals.
4. **`land-of-lor` has no server-side Godot project yet** (still docs/ideation stage — confirmed by checking the repo directly, contrary to this task's framing that assumed a runnable server existed). Cloned the real repo to `/opt/lor/game-server` exactly as specified, then added a minimal, explicitly-labeled stub (`project.godot`, `main.gd`, `main.tscn` — a `WebSocketMultiplayerPeer` listener, nothing more) directly on the droplet, **not committed to git** (excluded via `.git/info/exclude`, not the repo's tracked `.gitignore`). This is what made §6-§8 genuinely verifiable rather than just "the systemd unit exists." **Follow-up:** delete these three files once real world-server code lands; if real files of the same name arrive via `git pull`, that pull will need manual reconciliation rather than a clean fast-forward.
5. **Repo clone used a git bundle, not droplet-side GitHub auth.** `land-of-lor` is private and no GitHub credential was ever placed on the droplet (out of scope for this task, and not something to hand to a box without being asked). Bundled this container's already-authenticated clone and transferred it via SFTP, then repointed `origin` at the real URL. **Follow-up:** the §7 "ship a change" workflow (`git pull --ff-only` on the box) won't work until Kris sets up real git credentials there — a deploy key scoped read-only to `land-of-lor` is the natural choice, not yet done.
6. **Droplet not moved into a `lor-game-server` DO project.** The token has `project:update` but not `project:read`/list, so there was no way to discover the project's ID via API — confirmed even `/v2/projects/default` 403s. The droplet sits in DO's Default project. Recorded as a gap in `server-instance/Q001`'s answered note too.

### §8 verification checklist — all passed

1. `ssh lor@209.38.29.22` succeeds; `ssh root@209.38.29.22` refused. ✅
2. `ufw status`: only OpenSSH/80/443 `ALLOW`. ✅
3. `godot4 --headless --version` → `4.7.1.stable.official.a13da4feb`. ✅
4. `postgresql` active. ✅
5. `psql -h 127.0.0.1 -U lor_game -d lor_game` connects, `\conninfo` confirms. ✅
6. `caddy` active; `curl -I http://209.38.29.22` → `200`. ✅
7. `lor-director` and `lor-world@1` both `active (running)`. ✅
8. **End-to-end WebSocket check:** opened `ws://209.38.29.22/world/1/` from outside the droplet using Python's `websockets` library — connection reached `OPEN` state through Caddy's reverse proxy to the Godot world instance. ✅

### Follow-ups for Kris (not blockers, just real gaps)

- Move the droplet into a `lor-game-server` DO project by hand (console), or grant the token `project:read` for a future session to do it via API.
- Set up real git access on the droplet for `/opt/lor/game-server` (deploy key recommended) before relying on the `git pull --ff-only` deploy workflow.
- No domain is pointed at `209.38.29.22` yet — when one exists, switch the Caddyfile to §5.2a for automatic TLS.
- Confirm the ~US$12/mo billing alert mentioned in Q001 is actually set (this session couldn't verify it — no `billing` scope, by design).
- The stub Godot project at `/opt/lor/game-server` needs to be swapped for real server code, and the director's `run.sh` placeholder needs the real runtime once `server-architecture`'s open question lands.

## questions/ (1)

### server-instance/Q001 — Credentials and confirmation needed before automated droplet provisioning
```yaml
id: server-instance/Q001
title: Credentials and confirmation needed before automated droplet provisioning
epic: server-instance
blocks: ["server-instance/001"]
status: answered
raised: 2026-08-02
answered: 2026-08-02T00:00:00.000Z
decision_ref: null
```

## The question

An agent can execute `server-instance/001` (provision the droplet via the DigitalOcean API and run deliverable #4's setup) end-to-end — but not with Kris's own account, own API access, or own SSH key. This question is the checklist of exactly what Kris needs to do first, worked out so he doesn't have to research any of it himself.

## Options

- **Do the checklist below, exactly as scoped.** Unblocks `server-instance/001` immediately; the token and key can be rotated or revoked independently of Kris's own credentials at any time without touching anything else on his account.
- **Provide a broader, unscoped ("Full Access") token and/or Kris's own personal SSH key, to save a step.** Faster to produce, but loses the blast-radius containment scoping gives — a bug in a future task, or a task doing something unintended, could then touch anything in Kris's DO account or anything his personal key can reach, not just this one droplet.
- **Skip automation for now; provision by hand using deliverable #4 directly.** No credentials handed to an agent at all. Available as a fallback at any time — deliverable #4 was written to be followed by a human exactly as well as by an agent — but doesn't get the DO-API automation Kris asked for.

## Recommendation

**Do the checklist below.** It's scoped, cheap, and takes maybe ten minutes total — there's no real argument for handing over broader access than this task actually needs.

### 1. Create a separate DigitalOcean project for containment

In the DO control panel: **Projects → New Project.** Name it something like `lor-game-server`. This is a DO "project" (their resource-grouping concept — a different thing entirely from this repo's own `epics/` board, and from the AI Task Runner platform's own "project" concept), and its only job is to visibly bound "everything this game server touches" to one place in the console, separate from anything else on the account.

### 2. Generate a dedicated SSH keypair for the runner — not your personal key

On your own machine (not the droplet, not this container):

```bash
ssh-keygen -t ed25519 -f ~/lor_runner_key -C "lor-server-runner" -N ""
```

This produces `lor_runner_key` (private) and `lor_runner_key.pub` (public). Keep this keypair separate from whatever key you personally use to SSH anywhere else — it exists so an agent's access to the droplet is its own thing, revocable on its own, never tangled up with your own SSH access.

### 3. Generate a custom-scoped API token

**Verified against DigitalOcean's live scopes documentation (`docs.digitalocean.com/reference/api/scopes/`) on 2026-08-02** — the scope names below are the actual current granular scope strings, not guessed:

In the control panel: **Account (bottom-left) → API → Tokens tab → Generate New Token.** Give it a name (`lor-server-runner`), pick an expiration (a fixed expiration — e.g. 90 days — is safer than "no expiry" for a token with real provisioning power; you'll regenerate it if the work is still ongoing when it lapses), then choose **Custom Scopes** (not "Read Only," not "Full Access") and select exactly:

| Resource | Scopes | Why |
|---|---|---|
| `droplet` | `create`, `read`, `update`, `delete` | Provision, inspect, and tear down the droplet. `update` is also what droplet *actions* (power off/on, reboot, **and snapshot** — see below) run under; not the same as `admin` (needed only for restore/rebuild/password-reset, which this task doesn't do — leave `admin` off). |
| `ssh_key` | `read` (add `create` only if the runner will upload its own key via the API rather than you pasting it into the control panel yourself) | Attach the runner keypair from step 2 to the new droplet at creation. |
| `image` | `read`, `create` | `read` to look up the Ubuntu 24.04 LTS image slug; **`create` is required for the snapshot action** in step 9 below — DigitalOcean's droplet snapshots are internally a type of image resource, so `snapshot:*` alone does not cover taking one (a real, non-obvious gotcha this task's earlier draft got wrong by assuming a `snapshot:create` scope exists — it doesn't). |
| `region`, `size` | `read` | Confirm the region/size slugs before provisioning (see step 4). |
| `tag` | `create`, `read` | Tag the droplet (e.g. `lor-game-server`) for identification — cheap, worth doing. |
| `project` | `update` (this alias also grants `project:assign_resource`, the specific permission actually needed — `read` alone is not enough to place the droplet in your new project) | Move the newly created droplet into the `lor-game-server` project from step 1. |
| `snapshot` | `read`, `delete` | List and manage existing snapshots (deliverable #4 §9's "snapshot before anything risky" advice). There's no `snapshot:create` — see the `image:create` note above for what actually creates one. |
| `firewall` | *(leave off unless you want it)* | Only needed if you want a DigitalOcean **cloud** firewall in addition to `ufw` on the box itself — deliverable #4's guide uses `ufw` alone, which is sufficient. Add `firewall:create/read/update` later if you decide you want the extra layer; not needed to start. |
| `domain` | *(leave off unless you have a domain yet)* | Only needed if you want DNS records automated via the API for Caddy's TLS (deliverable #4 §5.2a) rather than adding the `A` record by hand at your registrar, which is what the guide assumes. Add `domain:create/read/update` if/when that's the plan. |

**Explicitly leave off:** `billing`, `account` write access, `spaces` (object storage), `kubernetes`, `database` (this setup self-hosts Postgres on the droplet — deliverable #4 §4 — not DO's managed database product), and `load_balancer`. None of these are touched by anything in deliverable #4 or `server-instance/001`.

The token's secret value is shown exactly once, immediately after clicking **Generate Token** — copy it straight into step 5 below; DigitalOcean cannot show it to you again.

### 4. Confirm region and droplet size

Deliverable #4 recommends **Sydney (`syd1`)** — the only Australian DigitalOcean region, and the obvious lowest-latency choice from Brisbane — and a **2 GB RAM / 1 vCPU regular droplet**. If that's right, no action needed beyond this confirmation; if you want something different, say so instead of the agent guessing.

### 5. Set a billing alert

In the control panel: **Billing → Billing Alerts** (or **Account → Billing**, depending on current DO nav — look for "alert" specifically). Set a threshold comfortably above the droplet's expected ~US$12/month (deliverable #4 §9) — this is a tripwire against "an agent left something bigger than intended running," not a budget plan; DO's per-second billing with a monthly cap already makes a true runaway unlikely, but the alert costs nothing and catches the case the cap doesn't (e.g. accidentally provisioning a larger size than intended).

### 6. Decide where credentials live on the runner, and place them there

**Recommendation, not yet a repo convention — first time this has come up:** a gitignored `/workspace/lor/.secrets/` directory (already added to this repo's `.gitignore` in this same session, so it's ready to use and will never accidentally get committed). Place:

- `/workspace/lor/.secrets/do_api_token` — the token from step 3, file contents only, nothing else.
- `/workspace/lor/.secrets/lor_runner_key` and `/workspace/lor/.secrets/lor_runner_key.pub` — the keypair from step 2.

Then lock down permissions so only the owning user can read them:

```bash
chmod 600 /workspace/lor/.secrets/do_api_token /workspace/lor/.secrets/lor_runner_key
chmod 644 /workspace/lor/.secrets/lor_runner_key.pub
```

This directory persists across tasks in this project (per this platform's own persistence guarantee for the project directory) without ever being committed to git or served by the public site. If you'd rather these lived somewhere else entirely, say so — this is a recommendation because nothing established it before, not a fact being reported.

### 7. Hand off

Once steps 1-6 are done, tell whichever session picks up `server-instance/001` that this question is answered — at minimum "Q001 done, credentials are in place." That session should then update this file's `status` to `answered` and set `answered:` to that date, and move `server-instance/001` from `needs-input` to `ready`.

## Progress check, 2026-08-02

A session was asked to execute `server-instance/001`, on the premise that this question was fully resolved. Verifying `/workspace/lor/.secrets/` before touching anything billable found it **partially** done:

- **Present and correct:** the dedicated SSH keypair (step 2) — `lor_runner_key` / `lor_runner_key.pub`, ed25519, commented `lor-server-runner` as specified. Its private-key permissions were `644` on disk (should never be group/world-readable); fixed to `600` in place, no content changes.
- **Missing:** `/workspace/lor/.secrets/do_api_token` (step 3) does not exist. No DO API token was found anywhere else searched (env vars, home directory, rest of the workspace).
- **Not independently verifiable from the repo:** step 4's region/size confirmation and step 5's billing alert are Kris-side actions with no file artifact to check — taking their completion on trust is fine, but they're moot until the token itself shows up.

Per `server-instance/001`'s own instructions, provisioning does not proceed without the token — nothing billable was created. **Status stays `open`.** Once `do_api_token` is placed at the path above (contents only, matching step 6's layout), the next session can re-check and proceed straight to provisioning.

## Answered, 2026-08-02

Kris placed `/workspace/lor/.secrets/do_api_token`. Verified present, correctly formatted (`dop_v1_` prefix), permissions fixed to `600`, and validated against the live DO API (`region:read` on `/v2/regions` returned `200`; the scopes table's deliberate omission of `account` read was confirmed harmless — `/v2/account` correctly 403s, `/v2/regions` doesn't need it). `server-instance/001` then ran end-to-end — see its task file for the full provisioning record. Two scope gaps surfaced in practice, both non-blocking, recorded here for whoever revisits scoping:

- **No `ssh_key:create`** (as scoped, deliberately) meant the runner key couldn't be registered as a DO SSH-key object, so the droplet API's `ssh_keys` field (which only accepts pre-registered key IDs/fingerprints) couldn't be used. Worked around with cloud-init `user_data` injecting the key directly — a standard, DO-documented pattern for exactly this case, no scope change needed.
- **No `project:read`** meant the droplet could not be looked up or assigned into a `lor-game-server` DO project via API — even `/v2/projects/default` 403s without read access. The droplet sits in DO's Default project. Either Kris moves it manually in the console (a few seconds), or a future session gets `project:read` added to close this gap for good.

## deliverables (2)

- #5 — lor-game-server-01 is live: server notes and how to deploy to it (2026-08-02) — /deliverables/5
- #4 — The server instance — a droplet setup guide (2026-08-02) — /deliverables/4