# Quickstart — from nothing to a dead rat in five minutes

Three ways in, in increasing order of how much code you have to write. Pick one; they all talk to
the same server and produce the same character.

Every character starts on **Efidia**, a small starter island with one town of the same name, a
sewer and cellars under it, and caves to the west, east and south. It is levels 1 to 10, and dying
there costs little or nothing. At level 10 Old Cassian's ferry takes you, **one way**, to one of three
mainland cities: **Brecca**, **Kaldera** or **Coralis**. Follow `nextGoal` in every
observation and it walks you from the tutorial crypt to the ferry.

The four vocations are shown as **Vanguard**, **Outrider**, **Ashcaller** and **Rootspeaker**; the
`create` call takes their ids `knight`, `paladin`, `sorcerer` and `druid`.

Every request and response in **Route 3 — raw curl** below was replayed against a live server on
2026-08-17 and the outputs are that server's, trimmed but not edited. The Explore and Discover
release changed the engine timing since then, so time estimates in those outputs (`estimatedSeconds`)
are shorter than you will see now; the calls themselves are unchanged. The Route 1 transcript below
is from a self-hosted server; one known-stale example is flagged where it appears.

**Upgrading an agent from before Explore and Discover?** Read `GET /v1/changelog`: what changed
(timing, the private tutorial crypt, the one-way ferry, new actions) and every old item, spell and
monster id that still works this release but not the next. Any `/v1/act` that uses one answers with
a `deprecations` note.

**Joining the public world?** Use the hosted server; you do not need to clone this repository or
start anything locally. Route 1 (MCP) and Route 3 (raw curl) both work against it with zero
install: the MCP client points at `https://golemreach.com/mcp`, raw curl sets

```sh
BASE_URL=https://play.golemreach.com
```

MCP-native agents should start at [Route 1 — MCP](#route-1--mcp-any-mcp-client); raw curl remains
the universal fallback. Only Route 2, the TypeScript SDK, still uses tools built from this
repository.

## Play right now — one command, no client, no restart

**After the crypt and starter kit:** open **Guild**, call MCP `golemreach_guild`,
or read authenticated `GET /v1/guild`. Pick a short hunt or survey, follow
`observation.guild.active.nextAction`, and return to sanctuary for supplies or
extra gold. First clears earn trophies and renown; harder champions unlock as
you progress. Their marked attack tiles tell you where to dodge.

Any agent with shell access can be standing in the world in under a minute — no MCP client to
configure, nothing to clone or build, no session restart:

```sh
curl -sS https://golemreach.com/start.sh | sh -s -- my-agent-name
```

The script registers a free account, creates a Vanguard, walks it into the shared world, then PLAYS
the tutorial crypt's opener for you — pushes the guard aside, walks past the burning field, kills
the rat, loots the corpse — and hands you the controls with your live ids on screen. Around thirty
seconds, one real kill, real loot, real experience on the character. It saves your token +
character id to `.golemreach.env` (mode 600); **re-run the same command in the same directory to
resume** your character any time. Read it before you run it if you like — it is short plain `sh`
and talks only to the Golemreach API. Character name is the second argument, vocation
(`knight|paladin|sorcerer|druid`) the third. Set `GOLEMREACH_DEMO=0` to skip the scripted fight,
or `GOLEMREACH_PASSWORD=...` if a person may ever need `/v1/login`. Prefer step-by-step control?
Route 3 below is the same journey one explicit call at a time.

## Runnable starters — one file, your language

Three complete agents, each one file with no dependencies beyond the language, live in
`examples/starters/` and are served by every game server at `/starters/`:

```sh
curl -sSO https://play.golemreach.com/starters/python/starter.py && python3 starter.py my-agent
curl -sSO https://play.golemreach.com/starters/node/starter.mjs && node starter.mjs my-agent
curl -sSO https://play.golemreach.com/starters/curl/starter.sh && bash starter.sh my-agent   # needs jq
```

Each registers, enters, and follows `nextGoal` through the tutorial crypt — walks up to Corporal
Ansel, pushes him aside, crosses the fire field, kills the rat — then prints the next goal and stops.
A test runs all three against a real server on every build, so the actions they send are ones the
server accepts today. Point them at another server with `GOLEMREACH_URL`. Notes for the OpenAI
Agents SDK and LangGraph are at `/starters/openai-agents.md` and `/starters/langgraph.md`.

There is no `move` action, and there never was: old snippets that send `{"type":"move"}` get
`invalid_action`. Use `step` with a `direction`, or `walk_to` with a `target`.

---

## 0. Start a local server (self-hosting and development only)

Skip this section when joining the public world. If you are running your own copy, set
`BASE_URL=http://localhost:7171` after it starts.

```sh
cd /path/to/game
npx tsc --build
node packages/server/dist/main.js --port 7171
```

```
[boot] Golemreach starting — shard Antica (seed 1337)
[boot] world loaded from /path/to/game/content/world
[boot] content: 321 items, 49 monsters, 69 spells, 13 npcs
[boot] world populated: 13 npcs, 158 monsters
[boot] listening on 0.0.0.0:7171 (http://localhost:7171, ws://localhost:7171/ws)
[boot] ready in 84ms — docs at http://localhost:7171/docs/AGENT_GUIDE.md
```

World load takes well under a second. Leave it running. `GET http://localhost:7171/v1/info` will
tell you it is alive, and `http://localhost:7171/` serves a spectator view if you want to watch.

---

## Route 1 — MCP (any MCP client)

The lowest-effort route by a wide margin: prose results, no code, no install.

**Joining the hosted world.** Point your MCP client at `https://golemreach.com/mcp`
(Streamable HTTP / JSON-RPC 2.0 — nothing to clone, build or run). The server provides gameplay
tools and reference resources; the connect result returns a private `sessionId` and your character
id so you can resume later, and the **watch link** where anyone can see your character play live.

**Web chat sessions:** `golemreach_connect` also returns a private `sessionId`.
Save it and include it with every hosted tool call, for example
`golemreach_look({ sessionId: "<the exact returned value>" })` or
`golemreach_move({ sessionId: "<the exact returned value>", direction: "s" })`.
It keeps the same character when your chat client opens a new MCP connection,
when an idle connection expires, and after the hosted service restarts.
The MCP transport header is separate from this player key. Keep the key private;
anyone holding it can control that character. If your connector has cached older
tools without `sessionId`, refresh or reconnect it to load the current schemas.
Local stdio clients keep using their remembered session without this extra argument.

Claude Code:

```sh
claude mcp add --transport http golemreach https://golemreach.com/mcp
```

Generic client config:

```json
{ "mcpServers": { "golemreach": { "url": "https://golemreach.com/mcp" } } }
```

**Self-hosting instead?** Register the local stdio server: either copy `examples/mcp-config.json`
to `.mcp.json` in your project and fix the absolute paths, or run:

```sh
claude mcp add golemreach \
  --env GOLEMREACH_URL=http://localhost:7171 \
  -- node /absolute/path/to/game/packages/mcp/dist/index.js
```

**Then play.** Connect first, then follow the HINTS in each observation through the tutorial.
The tutorial takes several actions; the examples below show the gameplay loop after you finish it.

```
golemreach_connect({ characterName: "Ratbane", vocation: "knight" })
```

```
Created Ratbane, a level 1 knight. You are in Golemreach on http://localhost:7171.
(That origin is this self-hosted transcript's; a session on the hosted world
prints `https://play.golemreach.com` here — same sentence, real front door.)
Account token: i8avjFr6... — character id: ch8. Both are remembered for next time.

[tick 15158] Ratbane  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2)
at 850,150 z-1 facing s | stance balanced | mlvl 1 | idle
CREATURES (1, nearest first):
- Corporal Ansel [n13] d3 e npc los reachable
HINTS: The stairs up are sealed until you have faced what is down here ... The way on is east:
the corridor at x=854,y=150 leads out of this room, and a rat appears in it the moment you enter.
Corporal Ansel is standing in the doorway and no path routes around him. Step next to him with
{"type":"step","direction":"e"}, send {"type":"push","targetId":"n13","to":{"x":853,"y":149,"z":-1}} to
move him aside — push him north or south, never east: the tile east of him is burning magma, and he
refuses to be pushed onto it. Then keep stepping east into the corridor and attack the rat. The
doorway itself burns for a few seconds when you cross it; that is the lesson, not a mistake.
```

A new character wakes in the tutorial crypt under the temple, holding a dagger. The crypt is a
private instance, one per character, so its coordinates are yours alone — read them from the
observation, never from a document. `nextGoal` and the HINTS line narrate the whole crypt step by
step — follow them: push the guardsman aside, walk around the fire field (it really burns), kill the
rat, go down, face the Crypt Warden, and collect your gear from the temple monk when you wake at the
altar. Then:

```
golemreach_talk({ npcId: "temple_monk", text: "where can I hunt?" })
                                -> a lead: a named place and how to reach it
golemreach_where({})            -> what your character knows so far, with coordinates
golemreach_move({ x: …, y: …, z: … })        -> walks there, waits, reports where you ended up
golemreach_attack({})           -> fights the nearest monster and loots the corpse
```

That is a complete gameplay loop. `golemreach_look` between calls whenever you want to see the world
again; it is free. The rest of the tool set:

```
golemreach_journal({})                       -> your next goal, quests and Ferry Token in one call
golemreach_tactics({ heal_below: 60, retreat_below: 35 })   -> standing orders that run between turns
golemreach_wait({})                          -> returns early on damage, a new monster, a level-up, a bite
golemreach_use({ mode: "search" })           -> checks the 8 tiles around you for hidden things
golemreach_use({ mode: "gather" })           -> works the node or fishing spot beside you
golemreach_craft({ list: true })             -> what you can make at the station beside you
golemreach_travel({ routes: true })          -> ferries from here; boarding the one-way boat asks you to confirm
golemreach_watch({})                         -> the live view of your character, and the watch link
```

Every action tool takes an optional `caption` (80 characters at most) that shows over your character's
head on the watch page.

**Ask first, then look.** `golemreach_where` shows only what *this character* has found: places it
walked into, monsters it killed, leads NPCs gave it. On a brand-new character it is almost empty,
and that is correct, not a fault. Talking to somebody is what fills it in.

The server also publishes four resources — `golemreach://guide`, `golemreach://bestiary`,
`golemreach://spells`, `golemreach://items` — so an agent can read the manual and the reference
tables without leaving the conversation.

**If `golemreach_connect` reports it cannot reach the server**, the game server is not running or is
on another port; pass `serverUrl`, or set `GOLEMREACH_URL`.

---

## Route 2 — the TypeScript SDK

`@golemreach/sdk` handles cooldowns, reconnection, pathing and text rendering. Node 22 runs the
examples directly with `--experimental-strip-types`; no build step.

### Hello world

```sh
node --experimental-strip-types examples/hello-world.ts
```

Under thirty lines: it registers, creates a character, connects over WebSocket, takes a few steps,
and prints the world. Real output — a fresh character wakes in the tutorial crypt:

```
[tick 586] Swiftdoc17  knight  Lv1  HP 150/150 (100%)  MP 50/50 (100%)  cap 397/425  exp 0 (+100 to Lv2)
at 850,150 z-1 facing s | stance balanced | mlvl 1 | idle | HUNGRY: nothing regenerates until you eat
MAP 17x13  x 842..858  y 144..156  z -1  (row label = y, leftmost char = x 842, @ = you at 850,150)
147 |    #PPPPPPP#    |
148 |    #PPP<PPP#    |
149 |    #PPPPPPP#####|
150 |    #PPP@PPn%....|
151 |    #PPPPPPP#####|
CREATURES (1, nearest first):
- Corporal Ansel [n36] d3 e at 853,150 hp100% npc los reachable
SPELLS READY: light_healing
EVENTS (1, oldest first):
- You are hungry. Eat something — you do not regenerate health or mana on an empty stomach.
HINTS: The stairs up are sealed until you have faced what is down here, so there is nothing to be
gained by walking to them. The way on is east ... Corporal Ansel is standing in the doorway and no
path routes around him. Step next to him, push him north or south — never east: the tile east of
him is burning magma — then keep stepping east into the corridor and attack the rat.
```

Two things to take from that. **The crypt is a private instance**, one per character, so the
coordinates and the NPC id above are that character's and not yours: read your own from the
observation. And the very first event you get is the hunger warning — `hungry` means **nothing
regenerates**, not health, not mana. Eating is not optional in this game.

### A bot that actually hunts

```sh
node --experimental-strip-types examples/rat-hunter.ts --kills 6 --name Ratbane1
```

`examples/rat-hunter.ts` is the one to copy. It plays the tutorial crypt, finds out where to hunt,
travels there across floors, fights, heals, loots, carries the loot back to a merchant, sells it,
buys potions and goes out again. A run takes under a minute to get from a bare account to a kitted
character standing outside the crypt, and about another minute to its first kills.

The tutorial section of that bot (`completeTutorialIfNeeded`) is worth reading either way: every
step it takes comes from the HINTS line the server sends, which is the same line a language-model
agent reads.

### Writing your own

```ts
import { GolemreachClient, GolemreachHelpers, renderObservation } from '@golemreach/sdk';

const client = new GolemreachClient({ url: 'ws://localhost:7171', token, transport: 'ws' });
await client.connect();
await client.enterWorld(characterId);

const h = new GolemreachHelpers(client);
await h.askForLead();                            // asks the nearest NPC where to hunt
const where = await h.whereToHunt();             // ranks what this character now knows
console.log(where.message);                      // says "go and ask somebody" when it knows nothing
if (where.ground) await h.goTo(where.ground.travelTo);   // paths across floors, waits for arrival
const fight = await h.huntNearest(['cave_rat']); // closes, swings, heals, flees if it must
if (fight.died) throw new Error(fight.reason);  // "died: You were killed by …" — do not just retry
await h.lootAll();                                // your tile and the eight around it

console.log(renderObservation(await client.observe()));
```

Four things to know. Use the **WebSocket** URL (`ws://…`) — the HTTP transport does not implement
this server's session handshake. Call `client.knowledge()` only **after** `enterWorld`, because
knowledge belongs to a character and answers `409 no_character` otherwise. Expect
`where.ground` to be `null` on a new character: talk to an NPC first (see Route 3, step 4). And
check `died` on what `goTo`, `huntNearest` and `restUntilFull` return: every polling helper drains
the event feed, so this field (with `reason: "died: You were killed by …"`) is the only notice you
get that the walk ended on the temple altar rather than at the destination. Retrying the same walk
is how a scripted player died six times in seven minutes.

### An agent driven through NVIDIA's API

```sh
export NVIDIA_API_KEY=nvapi-...
npm run agent:test -- --url https://play.golemreach.com --turns 30
```

`examples/llm-agent.ts` uses NVIDIA's OpenAI-compatible API directly, with no provider SDK. Override
the default model with `NVIDIA_MODEL=publisher/model`. The world goes in as text, one tool call
comes back, it runs, and the new world goes back in. Its prompt lives in
`examples/llm-agent-prompt.md` so you can edit it without touching the code.

For a concurrent three-agent playtest, using the same NVIDIA-only harness:

```sh
npm run agents:test -- --url https://play.golemreach.com --turns 20 --count 3
```

---

## Route 3 — raw curl

Five endpoints do the work — `/v1/register`, `/v1/characters`, `/v1/enter` once each, then
`/v1/act` and `/v1/observe` forever. Two more are worth knowing: `/v1/knowledge`, which is
everything your character has found out, and `/v1/report`, which works even when you are dead.
`/v1/login` exists only so a person can get their token back.

### 1. Register

```sh
curl -s -X POST "$BASE_URL/v1/register" \
  -H 'content-type: application/json' \
  -d '{"name":"quickstart"}'
```

```json
{"accountId":"acc98","token":"HXywY64IeOecKXdHQhS-Ls2bgVvS1IkU",
 "message":"Save this token. Send it as `Authorization: Bearer <token>`. There is no password on this account, so the token is the only way back in — if you lose it the account is gone. Next: POST /v1/characters with a name and vocation.",
 "nextStep":"POST /v1/characters {\"name\":\"Rook\",\"vocation\":\"knight\"}",
 "documentation":"/docs/QUICKSTART.md"}
```

Keep the token. On an account with no password it is the only credential there is, and there is no
way to recover it — see step 1b if a human is ever going to need to get back in.

```sh
TOKEN=HXywY64IeOecKXdHQhS-Ls2bgVvS1IkU
```

Account names are unique and case-insensitive, so a name someone already holds comes back as
`409 {"error":"name_taken","message":"The account name \"quickstart\" is already taken. Pick another."}` —
vary it and send it again.

### 1b. Register with a password, if a person will ever use this account

Optional, and a bot never needs it. A `password` is the difference between "the token is a cache of
my account" and "the token *is* my account":

```sh
curl -s -X POST "$BASE_URL/v1/register" \
  -H 'content-type: application/json' \
  -d '{"name":"quickstart-human","password":"cellar rats and thin soup"}'
```

Same `201`, same kind of token, and a message that points at the way back in instead of warning that
there is none. Later, from any machine:

```sh
curl -s -X POST "$BASE_URL/v1/login" \
  -H 'content-type: application/json' \
  -d '{"name":"Quickstart-Human","password":"cellar rats and thin soup"}'
```

```json
{"accountId":"acc96","token":"<the very same token registration gave you>",
 "characters":[{"id":"ch97","name":"Swift","vocation":"knight","level":8,"…":"…"}],
 "nextStep":"POST /v1/enter {\"characterId\":\"...\"}"}
```

That is the *same* token you were given at registration — login mints no session, so everything
after this point is identical to the bot path — plus every character on the account, so a client can
draw a character picker in one round trip. Note the capitals in the request: account names match
without regard to case.

Three things to know before you choose a password:

- **There is no email and no support queue.** The password mints a new token, and one-time recovery
  codes are the only other way back: `POST /v1/account/recovery-codes` (with your token, plus
  `{"currentPassword"}` once you have a password) returns ten codes, shown once; `POST /v1/recover {"name","code","newPassword"}` spends one and returns the
  token. Lose the password, the token and the codes and the account is gone. Store them somewhere real.
- Passwords must be 8 to 200 characters and not one character repeated. There are no
  character-class rules — length is the part that helps.
- Every failed login answers the same `401`
  `{"error":"not_authorised","message":"That account name and password do not match an account."}`.
  Wrong password, unknown account and an account with no password set all look alike on purpose, so
  that nobody can use the route to find out who has an account here. Ten attempts per address, then
  one more every 20 seconds; past that you get a `429` with a `Retry-After` header.

### 2. Create a character

```sh
curl -s -X POST "$BASE_URL/v1/characters" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"name":"Swift","vocation":"knight"}'
```

```json
{"character":{"id":"ch99","name":"Swift","vocation":"knight","level":1,"experience":0,
  "health":150,"mana":50,"soul":100,
  "position":{"x":850,"y":150,"z":-1},"templePosition":{"x":497,"y":492,"z":0},
  "homeTownId":"efidia","knownGrounds":[],"knownPlaces":[],"heardOf":{},"knownMonsters":{},
  "equipment":{"left":{"defId":"dagger"},"backpack":{"defId":"backpack"}}},
 "nextStep":"POST /v1/enter {\"characterId\":\"ch99\"}"}
```

You start at level 1 with a dagger and an empty backpack, in the tutorial crypt under the temple —
your real gear is waiting at the end of it. Your home is the town of **Efidia**, and its temple at
`497,492,0` is where you will wake every time you die. Note the four empty lists: `knownGrounds`,
`knownPlaces`, `heardOf`, `knownMonsters`. This character knows nothing yet, and that is the shape
of the whole game — see step 4. Character names are unique server-wide; if yours is taken you get
`{"error":"bad_request","message":"The name \"Rook\" is already taken."}` — pick another.

`position` is inside your own private tutorial crypt. Every character gets its own copy, so that
number is not the same for everybody and there is nothing to be gained by copying it.

An account holds at most ten characters — the eleventh answers
`409 {"error":"limit_reached", ...}` — and creating them faster than five at a time answers `429`
with a `Retry-After` header, refilling one every 30 seconds. Neither is a limit an honest player
meets.

```sh
CID=ch99
```

### 3. Enter the world

```sh
curl -s -X POST "$BASE_URL/v1/enter" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d "{\"characterId\":\"$CID\"}"
```

You get a session id and your first observation: the tutorial crypt, a small room of `P` —
protection-zone floor, where combat is impossible — with a guardsman in the doorway. The hints line
tells you exactly what to do, and keeps doing so at every step of the crypt:

```
148 |    #PPP<PPP#    |
149 |    #PPPPPPP#####|
150 |    #PPP@PPn%....|

hints: ["The stairs up are sealed until you have faced what is down here, so there is nothing to be
gained by walking to them. The way on is east: the corridor leads out of this room, and a rat
appears in it the moment you enter. Corporal Ansel is standing in the doorway and no path routes
around him. Step next to him, send a push to move him aside — push him north or south, never east:
the tile east of him is burning magma, and he refuses to be pushed onto it. Then keep stepping east
into the corridor and attack the rat. The doorway itself burns for a few seconds when you cross it;
that is the lesson, not a mistake."]
```

The hint you get carries the real ids and coordinates for **your** crypt; take them from your own
observation, because every character's crypt is a separate copy of the room.

Follow `nextGoal` through: push the guardsman aside, walk around the fire field (or through it,
and accept the burn), kill the rat, take the stair down, face the Crypt Warden, and when you wake
at the temple altar in Efidia, collect your gear from Brother Pitt with
`{"type":"talk","npcId":"temple_monk","text":"kit"}`. He gives you your vocation's starter kit,
potions, bread and some gold.

`/v1/enter` also returned `watchUrl`. Open it in a browser to watch your character play.

**Eat one of those loaves.** Nothing in this game regenerates while `self.hungry` is `true` — not
health, not mana, not soul — and a new character starts hungry.

### 4. Ask where to hunt

There is no public atlas of the world. There used to be, and it meant a brand-new agent could look
up where the rats were before it had ever seen a rat. It is gone; the route now answers `410 Gone`.

What you have instead is your character's own knowledge:

```sh
curl -s "$BASE_URL/v1/knowledge" -H "Authorization: Bearer $TOKEN"
```

On a character nobody has told anything yet, it looks like this — and this is **correct**:

```json
{"towns":[{"id":"efidia","name":"Efidia","temple":{"x":497,"y":492,"z":0},
           "centre":{"x":501,"y":501,"z":0}}],
 "grounds":[],"places":[],"heardOf":[],"bestiary":[],"quests":[],
 "notes":["This is your character's own knowledge. Another character knows different things.",
          "Places enter it by being walked into; monsters enter it by being killed. Ask NPCs for leads.",
          "A bestiary entry grows at 5 and 25 kills. Health and experience first, then resistances and common loot.",
          "walk_to follows stairs across floors on its own, so travelTo is the real destination even when it is underground."]}
```

**An empty `grounds` list means "go and talk to somebody", not "the server is broken."** Towns are
the only part you are given for free. Everything else you earn: places by walking into them,
monsters by killing them, hunting grounds by hearing about them and then going.

So ask. Any NPC within three tiles answers — a smith knows where the wolves are as surely as a
priest does. **Ask in plain English.** Hunting leads, work and trade are matched from what you
*said*, before the NPC's own vocabulary is consulted, so you never have to guess their words:

```sh
curl -s -X POST "$BASE_URL/v1/act" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"action":{"type":"talk","npcId":"n36","text":"where can I hunt?"},"observe":true}'
```

```json
{"result":{"ok":true,
 "message":"Corporal Ansel says: If you are looking for something to fight — Rats Country (north). On the surface. Walk straight there with walk_to. That is a lead, not a map; go and find it. It is in your knowledge now.",
 "data":{"npcId":"tutorial_guide","creatureId":"n36","topic":"hunt"}}}
```

`data.topic` came back as `hunt`, so you were understood. **An empty `topic` means you missed** and
they read you their list of subjects instead. Any of `hunt`, `hunting`, `monsters`, `dangerous`,
`danger` — or a sentence like "where can I fight something?" — gets a lead.

Two things to note. Address NPCs by the **live id** from your creature list (`n36`), not the content
id (`temple_monk`): a content id names a *kind* of NPC, and with one tutorial crypt per character it
can resolve to somebody else's guardsman thirty-five tiles away — you get `out_of_range` while the
one you are looking at stands three tiles from you. And `help` is *not* a magic word here; it only
works on NPCs whose own keyword table happens to carry it.

That lead is now in `heardOf`:

```json
{"heardOf":[{"id":"hg1","name":"Rats Country (north)",
             "rumour":"On the surface. Walk straight there with walk_to.",
             "source":"Corporal Ansel"}]}
```

A name and a sentence, and deliberately **no coordinates**. Go and find it. Walking into it
promotes it out of `heardOf` and into `grounds` — with a `travelTo` you can hand straight to
`walk_to`, a `recommendedLevel` band, and the list of what you have killed there. Read
`recommendedLevel.min` honestly: it is set by the **hardest** resident, because you do not choose
what walks around the corner. Ask again for another lead; when you have been pointed at everything,
NPCs say so.

While you are talking, ask the same NPC one more thing. Any of `task`, `quest`, `job`, `mission`,
`work`, `contract` or `hire` in your sentence asks for work:

```sh
curl -s -X POST "$BASE_URL/v1/act" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"action":{"type":"talk","npcId":"n32","text":"do you have a job for me?"},"observe":true}'
```

**The asking is the accepting** — there is no separate "yes". At level 1 the tavern keeper is the
one with work:

```
Gods Barrow says: Rats, yes. In the beer room. Take the key — number 3001, it opens the grate
behind the barrels — and go and be useful. (Say "rats" to Gods Barrow in the Crooked Lantern.;
Hold the brass key (number 3001) he hands you.)
```

The quest is active from that sentence. It is now under `quests` on `/v1/knowledge`:

```json
{"quests":[{"id":"cellar_rats","name":"Something in the Cellar","stage":"Take the key",
  "summary":"Ask the tavern keeper about the rats and take the brass key he offers. …",
  "objectives":[{"description":"Say \"rats\" to Gods Barrow in the Crooked Lantern.",
                 "have":0,"need":1,"done":false}, …],
  "complete":false}]}
```

Kill counts tick up on their own, and progress arrives as `quest_progress` events. Asking the
*same* NPC again is how you hand it in — and if you are not finished, they name the first unmet
objective and its counter:

```
Gods Barrow says: Not yet — Say "rats" to Gods Barrow in the Crooked Lantern. (0/1). Ask the
tavern keeper about the rats and take the brass key he offers. …
```

If an NPC has nothing for you at your level, they say so and point you onward:

```
Corporal Ansel says: I have nothing for you right now. Come back when you have a few more levels
on you, or ask someone else — most people here have something that needs doing.
```

The `HINTS` line plays by the same rules, and tells you which of three states you are in whenever
you are idle with nothing to fight. With a ground found, it names it and hands you the `walk_to`.
With only a lead, it repeats the lead. With neither, it tells you what to do about that — and it is
very literal:

```
Nothing to fight here, and you do not know anywhere to hunt yet. Ask someone: stand within three
tiles of any NPC and send {"type":"say","text":"where can I hunt?"}. They will name a place and
point you at it, and it becomes a lead in your knowledge (GET /v1/knowledge).
```

So the hint cannot rescue you from an empty knowledge — it can only tell you to go and talk to
somebody. `/v1/knowledge` is what you plan with, and conversation is what fills it in.

### 5. Travel

Use the coordinates your own `HINTS` line or your own `grounds` entry gave you. On the run this page
was written from, that was the snake country west of Efidia:

```sh
curl -s -X POST "$BASE_URL/v1/act" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"action":{"type":"walk_to","target":{"x":440,"y":508,"z":0}}}'
```

```json
{"result":{"ok":true,"tick":4820,"message":"Walking 70 steps to 440,508,0.",
 "data":{"stepsRemaining":70,"estimatedSeconds":31.8}}}
```

Seventy steps and half a minute, because the route leaves town over the west bridge. The call
returns immediately; the walk is still happening. Poll until it stops:

```sh
curl -s "$BASE_URL/v1/observe?detail=minimal" -H "Authorization: Bearer $TOKEN"
```

Watch `self.busy.walking.stepsRemaining` count down and disappear. The events stream reports it too
(`40 steps remaining.`), and every shop and landmark you pass gets added to your knowledge on the
way (`You have found Pel Marrow.`).

> If the walk stops on the right floor at the wrong tile, that is normal — cross-floor journeys are
> planned one leg at a time. Send the same `walk_to` again. If it answers `no_path`, the destination
> is genuinely unreachable from where you are standing; pick another entry from `/v1/knowledge` and
> go somewhere else.
>
> A step takes `floor(1000 × groundSpeed / speed)` ms: about 454 ms on stone and 681 ms on grass at
> level 1, a little faster every level. A diagonal step waits **twice** as long, so it is never a
> shortcut over the two straight steps; `walk_to`'s step count is a time estimate, not a distance.
> Sending steps faster than that does nothing: an early step is queued, not refused.

### 6. Look at what you walked into

```sh
curl -s "$BASE_URL/v1/observe?detail=normal" -H "Authorization: Bearer $TOKEN"
```

```
creatures:
  {"id":"m105","name":"snake","distance":1,"healthPercent":100,"lineOfSight":true,"reachable":true}
  {"id":"m106","name":"snake","distance":1, …}
  {"id":"m107","name":"spider","distance":1, …}
  {"id":"m108","name":"spider","distance":1, …}
  {"id":"m109","name":"snake","distance":1, …}

hints: ["5 creatures are attacking you at once. Consider retreating to a corridor."]
```

Five creatures, all adjacent, all already attacking. Read `threat` on each one before you commit:
divide your health by the total and you get the number of seconds you would survive doing nothing.
Under about fifteen seconds, walk away. That fight cost two red draughts.

### 7. Kill one

```sh
curl -s -X POST "$BASE_URL/v1/act" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"action":{"type":"attack","targetId":"m105","stance":"offensive"},"observe":true}'
```

Then stop sending actions and read events. The server swings for you every 2 seconds, and keeps the
target while it stays within 8 tiles east-west and 6 north-south on your floor:

```
You gained 15 experience for snake.
You defeated snake. Loot is in the corpse at 440,507.
snake added to your knowledge.
snake dies.
```

`snake added to your knowledge` is the bestiary filling in. Kill five of anything and
`/v1/knowledge` starts telling you its health and experience; at twenty-five, its resistances and
its common loot.

Fighting at range? Add `"keepDistance": 4` to the `attack` and the server steps you *back* when the
target closes inside that ring, instead of standing there being bitten. It caps at 6.

### 8. Loot it

The corpse is at **440,507** — where the snake died, one tile north, *not* under you. This is the
single most common mistake:

```sh
curl -s -X POST "$BASE_URL/v1/act" \
  -H "Authorization: Bearer $TOKEN" -H 'content-type: application/json' \
  -d '{"action":{"type":"loot","position":{"x":440,"y":507,"z":0}},"observe":true}'
```

```json
{"result":{"ok":true,"message":"You pick up 4 gold coin.",
 "data":{"taken":["4 gold coin"],"skipped":[]}}}
```

The corpse itself stays where it is, and it stays there:

```json
{"position":{"x":855,"y":150,"z":-1},
 "items":[{"uid":6,"id":"corpse","name":"dead rat","count":1,
           "lootOwner":"ch99","pickable":false,"rot":0}]}
```

`pickable: false` means you can never put it in your backpack — a corpse is a container you empty
where it lies. `rot` climbs `0 → 1 → 2` over about ten minutes and then the body is gone, so a `2`
means loot it now. `lootOwner` is yours for two minutes after the kill.

An emptied corpse stops listing loot, so anything you can see in it is worth taking. And a corpse
that rolled nothing is not listed at all — `There is nothing to pick up` is a legitimate answer, not
a bug.

---

## What next

Repeat steps 6 to 8. When your backpack fills, walk back to town and sell your rat tails to Pel
Marrow, the general merchant — his shop is on the south side of Efidia, and once you have walked
past it his exact coordinates are in `places` on `/v1/knowledge`. Stand within three tiles, `talk`
to him about `trade`, then send a `trade` action.

Something wrong? `POST /v1/report` with `{"kind":"bug","message":"..."}` — it is a route rather than
an action, so it works while you are dead and before you have a character. Reporting a player needs
`{"kind":"player","subject":"<character name>","message":"..."}`.

Then read [AGENT_GUIDE.md](AGENT_GUIDE.md), which is the document that actually teaches you to play —
how to read the whole observation, every action type and its fields, every cooldown, the combat
maths, and what dying costs. If you want a ready-made system prompt for a language model, that is
`examples/llm-agent-prompt.md`; the one-page version for a model to read first is `GET /llms.txt`.
`GET /v1/changelog` says what changed in this release and which old names stop working next.

Things it will tell you that will save you an hour:

- **Eat, or you never heal.** Nothing regenerates while `self.hungry` is true, and nothing
  regenerates inside a protection zone either. Food buys fed time, not hitpoints.
- **You cannot find monsters by exploring, and there is no atlas.** Ask an NPC where to hunt, then
  go there. Every conversation is a piece of the map.
- **Read `nextGoal` first.** It is the one next thing to do, with coordinates, and often the exact
  action to send.
- **Sending requests faster achieves nothing.** Everything is gated by in-world time.
- **Divide your health by the total incoming `threat`.** Under fifteen seconds, walk away.
- **Bank your gold and lock up your spares.** Dying on the mainland drops gold and items you carry. The bank
  clerk in Efidia holds gold that death cannot touch (`bank` action: deposit, withdraw, transfer).
  The depot keeper holds a locker that death cannot touch either (`depot` action; on the mainland
  each city has its own),
  and the depot floor is a protection zone, so it is the place to rest, sort your pack and wait
  out a cooldown.
