MUD on Urbit

Call Flow Diagrams

design

Status: Design specification Date: 2026-03-26

Overview

Three distinct player scenarios, each with different network topology, data ownership, and communication patterns. This document traces the complete flow for each — from login through gameplay to disconnect.

The Three Cases

Case 1: CITIZEN ON OWN HOST
  Player's ship IS the world host.
  Everything is local. No Ames. Simplest case.

Case 2: CITIZEN VISITING REMOTE HOST
  Player's ship connects to another ship's world.
  Ames for game commands. Eyre for web client.
  Character data splits between two ships.

Case 3: GUEST ON HOST
  No Urbit. Browser connects to host's Eyre.
  Everything on the host. Simplest data model.

Case 1: Citizen Playing on Own Host

The player owns the ship that runs the world. They’re playing their own world — building it, testing it, or just adventuring in their own content.

Network Diagram

┌─────────────────────────────────────────────────┐
│                 Player's Ship                    │
│                                                  │
│  ┌──────────┐     ┌─────────────┐               │
│  │ Browser  │────▶│    Eyre     │               │
│  │ (Web UI) │◀────│ (HTTP/SSE)  │               │
│  └──────────┘     └──────┬──────┘               │
│                          │                       │
│                    ┌─────▼──────┐                │
│                    │ %mud-world │                │
│                    │            │                │
│                    │ • Rooms    │                │
│                    │ • Mobs     │                │
│                    │ • NPCs     │                │
│                    │ • Quests   │                │
│                    │ • Sessions │                │
│                    │ • Combat   │                │
│                    └─────┬──────┘                │
│                          │                       │
│                    ┌─────▼──────────┐            │
│                    │ %mud-character │            │
│                    │                │            │
│                    │ • Stats        │            │
│                    │ • Inventory    │            │
│                    │ • World records│            │
│                    │ • Home world   │            │
│                    └────────────────┘            │
│                                                  │
│              ┌──────┐    ┌──────┐               │
│              │ Behn │    │ Ames │ (idle)         │
│              │(tick)│    │      │                │
│              └──────┘    └──────┘               │
│                                                  │
│  No network traffic. All local.                  │
└─────────────────────────────────────────────────┘

Login Flow

1. Player opens browser → https://localhost:8080/mud
2. Eyre serves glob (index.html + SolidJS app)
3. Player authenticates via Eyre +code
   Browser: POST /~/login  body: password=<+code>
   Eyre: sets session cookie, returns 204
4. Web client pokes %mud-world: [%mud-command [%score ~]]
   (or any initial command to establish session)
5. %mud-world checks: is this our own @p?
   Yes → look up citizen-index for existing session
   No session exists → create one:
     - Load character from %mud-character agent (local poke)
     - %mud-character returns character data
     - Create player-session in %mud-world state
     - Place character in last known room (or spawn-room)
6. Web client subscribes: PUT /~/channel/mud-{ts}
   Action: subscribe to /game/{session-id}
7. %mud-world emits initial updates via subscription:
   - [%motd "Welcome back..."]
   - [%room-enter room players mobs items]
   - [%vitals hp max-hp mana max-mana moves max-moves]
   - [%mail-notification count]
   - [%online-join ...] for each connected player
8. Player is in the game.

Gameplay Flow (Single Command)

Player types "kill wolf"
Browser: PUT /~/channel/mud-{ts}
  Action: poke %mud-world with %mud-command [%kill "wolf"]
Eyre → %mud-world on-poke
%mud-world processes:
  1. Validate session (cookie → session-id → player-session)
  2. Find player's room
  3. Match "wolf" against mob-instances in room
  4. Initiate combat (set states, add to active-combats)
  5. Process first round (damage calc both directions)
  6. Emit facts to /game/{session-id}:
     - [%combat-start "a shadow wolf"]
     - [%combat-round lines]
     - [%vitals ...]
  7. Emit to other players in room:
     - [%chat "say" "" "Wanderer attacks a shadow wolf!"]
Eyre SSE stream delivers events to browser
SolidJS handler dispatches:
  - "combat-start" → update combat state
  - "combat-round" → appendLog() for each line
  - "vitals" → setHp(), setMana(), etc.

Game Tick Flow

Behn timer fires (every 2 seconds)
%mud-world on-arvo [%tick ~]
  ├── Process active combats:
  │   For each session in active-combats:
  │     Run damage formulas (player → mob, mob → player)
  │     Check mob death → award XP, loot, end combat
  │     Check player death → trigger death flow
  │     Emit [%combat-round] and [%vitals] to player
  ├── Check regen (every 30 sec):
  │   For each connected player:
  │     Regen HP (CON-based), Mana (WIS-based), Moves (DEX-based)
  │     Emit [%vitals] if changed
  ├── Mob AI (every 30 sec):
  │   For each mob-instance:
  │     Roaming mobs: chance to wander to adjacent room
  │     Aggressive mobs: check for players in room, initiate combat
  │     Emit [%mob-enters] / [%mob-leaves] to affected rooms
  ├── Area resets (every 60 sec check):
  │   For each area past reset-interval:
  │     Respawn mobs/items per reset commands
  │     Emit [%system-message "You hear movement in the distance..."]
  ├── Quest timers, idle checks
  └── Re-arm Behn: [%pass /tick %arvo %b %wait next]

Disconnect Flow

Player closes browser tab (or types "quit")
Eyre detects SSE connection drop / channel delete
%mud-world on-leave fires:
  1. Find session by subscription wire
  2. If in combat: resolve (mob wins, player "dies" or combat cancels)
  3. Remove player from room
  4. Emit [%online-leave name] to all subscribers on /online
  5. Save character state to local %mud-character agent:
     Poke %mud-character: [%save character-snapshot]
  6. Clean up session from state
Done. All local, no network.

Death Flow (Local)

Player HP reaches 0 (in combat tick)
%mud-world processes death:
  1. End combat
  2. Create corpse in room:
     - %none items + 10% gold → lootable by others
     - %protected items → owner only
     - %soulbound items → NOT on corpse
  3. Emit [%you-died ~] to player
  4. Emit [%chat "system" "" "Wanderer has been slain!"] to room
  Since player IS the host, "go home" = go to spawn-room:
  5. Move player to spawn-room (this is their home world)
  6. Restore HP/mana/moves to full
  7. Soulbound items appear in inventory at spawn
  8. Emit [%room-enter ...] for spawn room
  9. Update corpse-locations on character
Player is alive at spawn, corpse is in the dungeon.

Case 2: Citizen Visiting Remote Host

The player owns a ship and is visiting someone else’s world. This is the cross-world case — the most complex flow.

Network Diagram

┌──────────────────────┐          ┌──────────────────────┐
│   Player's Ship      │          │   Host's Ship        │
│   (~player)          │          │   (~host)            │
│                      │          │                      │
│  ┌──────────┐        │          │        ┌───────────┐ │
│  │ Browser  │───────────────────────────▶│   Eyre    │ │
│  │ (Web UI) │◀──────────────────────────│ (HTTP/SSE)│ │
│  └──────────┘        │          │        └─────┬─────┘ │
│                      │          │              │       │
│  ┌────────────────┐  │   Ames   │        ┌─────▼─────┐ │
│  │ %mud-character │◀════════════════════▶│%mud-world │ │
│  │                │  │          │        │           │ │
│  │ • Stats (truth)│  │          │        │ • Rooms   │ │
│  │ • Inventory    │  │          │        │ • Mobs    │ │
│  │ • World records│  │          │        │ • Session │ │
│  │ • Home world   │  │          │        │ • Combat  │ │
│  │ • Corpse locs  │  │          │        │ • Record  │ │
│  └────────────────┘  │          │        └───────────┘ │
│                      │          │                      │
│  Player's browser    │          │  Host runs the       │
│  connects to HOST's  │          │  world and game      │
│  Eyre, NOT their own │          │  logic               │
└──────────────────────┘          └──────────────────────┘

KEY INSIGHT: The browser connects to the HOST's Eyre.
The player's ship communicates with the host via Ames.
These are two separate connections serving different purposes.

Login Flow

1. Player opens browser → https://host-ship.urbit.org/mud
2. Host's Eyre serves glob (same web client as all players get)
3. Login screen shows:
     [Create Guest Character]    [Login with Urbit]
   Player clicks [Login with Urbit]
4. Client prompts: "Enter your ship URL"
   Player types: https://player-ship.urbit.org
   Client stores this as playerShipUrl (persists in localStorage)
5. Client opens popup/redirect to player's ship for auth:
   Browser: POST https://player-ship.urbit.org/~/login
   Player enters their +code for their OWN ship
   Player's ship sets auth cookie (scoped to player's domain)
6. Client (still in browser) pokes player's ship:
   PUT https://player-ship.urbit.org/~/channel/mud-auth-{ts}
   Action: poke %mud-character with [%portal-request host=~host-ship]
7. Player's %mud-character:
   a. Builds character-snapshot + world-records
   b. Pokes ~host-ship's %mud-world via Ames:
      mark: %mud-portal-enter
      {ship: ~player, character: <snapshot>, world-records: <records>}
8. Host's %mud-world validates:
   - Is ~player banned? → reject
   - Character data plausible? → check
   - World records signed? → verify signatures
   - Trust whitelist → calculate effective level
9. Host responds via Ames to player's ship:
   mark: %mud-portal-response
   [%admitted session-id=0v4d5e... effective-level=85]
10. Player's %mud-character receives admission.
    Creates an Eyre-accessible session token for the browser:
    Stores: {host: ~host-ship, session-id: 0v4d5e..., token: 0v9a8b...}
11. Browser polls player's ship: GET /mud/api/portal-status
    Response: {"admitted": true, "host": "~host-ship",
               "session": "0v4d5e...", "token": "0v9a8b..."}
12. Browser now connects to HOST's Eyre with the session token:
    PUT https://host-ship.urbit.org/~/channel/mud-{ts}
    Action: subscribe to /game/0v4d5e...
    Header: X-Mud-Token: 0v9a8b...
    (Host validates token against the Ames-admitted session)
13. Host's %mud-world emits initial updates via subscription:
    - [%motd ...]
    - [%room-enter ...]
    - [%vitals ...]
    - [%mail-notification ...]
14. Player is in the host's world.
    Browser stores playerShipUrl in localStorage for future sessions.
    On return visits, skip steps 4-5 (already authenticated).

Key points about this flow:

  • The browser talks to TWO ships: player’s (for auth + portal request) and host’s (for gameplay)
  • The player’s ship does the Ames handshake — the browser never touches Ames
  • The session token bridges the gap: Ames-authenticated identity → Eyre-accessible session
  • Player ship URL is entered once and stored in localStorage
  • The %mud-character agent needs a /mud/api/portal-status scry endpoint
  • The host validates the token against its list of Ames-admitted sessions

Gameplay Flow

Player types "north"
Browser: PUT to HOST's /~/channel/mud-{ts}
  Action: poke host's %mud-world with %mud-command [%move %north]
Host's Eyre → host's %mud-world on-poke
Host processes movement:
  1. Validate session
  2. Check room exits, preconditions
  3. Move player between rooms
  4. Emit [%room-enter] and [%vitals] to player's subscription
  5. Emit [%player-leaves]/[%player-enters] to other players
  6. Check for encounters (random spawn chance)
  7. Send [%map-room room-summary] for client map cache
Host's Eyre SSE delivers to player's browser
SolidJS updates (same as Case 1 from here)

NOTE: The player's own ship is NOT involved in routine gameplay.
All commands go Browser → Host Eyre → Host %mud-world.
The player's ship only participates during login, disconnect,
and death (when character data needs to sync).

Disconnect Flow

Player closes browser or types "quit"
Host's Eyre detects connection drop
Host's %mud-world on-leave:
  1. Find session
  2. Resolve combat (if any)
  3. Remove from room
  4. Emit [%online-leave] to others
  5. Build world-record with everything earned this visit:
     - XP earned in this world (cumulative)
     - Flags set ("destroyed-shadow-altar")
     - Items gained/lost
     - Quests completed
     - Time played
     - Reputation changes
  6. Sign the world-record (host's cryptographic signature)
  7. Poke player's ship via Ames:
     mark: %mud-portal-exit
     {world: ~host, reason: %quit, world-record: <signed>}
  8. Host RETAINS world-record in citizen-records map
     (Player can re-request if the Ames poke failed)
Player's %mud-character on-poke:
  1. Receive %mud-portal-exit
  2. Verify signature
  3. Apply world-record to local state:
     - Update world-records map
     - Store new items from item-snapshots
     - Update corpse-locations if died
  4. Character is safe on player's own ship

Death Flow (Remote)

Player HP reaches 0 on host's world
Host's %mud-world:
  1. End combat
  2. Create corpse on HOST (in the room where player died):
     - %none items + gold → lootable
     - %protected items → owner only
     - %soulbound items → excluded from corpse
  3. Emit [%you-died ~] to player's subscription
  4. 3-second pause (death screen on client)
  5. Host pokes player's ship via Ames:
     mark: %mud-portal-exit
     {
       world: ~host,
       reason: %death,
       world-record: <updated, signed>,
       soulbound-items: [list of item-snapshots],
       corpse-location: {world: ~host, room: 1012}
     }
  6. Host removes player from session/room
     Host retains:
       - Corpse (with %none and %protected items)
       - World-record (with updated death count, corpse location)
Player's %mud-character on-poke:
  1. Receive death event
  2. Store soulbound items locally
  3. Update corpse-locations: [{world: ~host, room: 1012}]
  4. Update world-record
  5. Respawn player in HOME WORLD:
     - Place in home world spawn room
     - Full HP/mana/moves
     - Soulbound items in inventory
  6. If player's browser is still open:
     - Host emits [%you-died ~] BEFORE dropping the subscription
     - Client receives death event, shows death screen overlay
     - 3-second pause: "You have fallen... returning home..."
     - Host drops the subscription (SSE ends)
     - Client detects SSE close, checks: was last event %you-died?
       Yes → redirect to playerShipUrl (stored in localStorage from login)
       No → show "Disconnected. Reconnect?" prompt
     - Browser navigates to https://player-ship.urbit.org/mud
     - Player's own %mud-world serves the glob, player is home
     - Player's %mud-character has already respawned them in home room
Player is home. Corpse is on ~host's world.
To recover: portal back to ~host, waypoint, walk to corpse.

Reconnect After Crash

Player's ship was offline when host sent %mud-portal-exit
Host's Ames poke fails (nack or timeout)
Host retains world-record in citizen-records. No data lost.
Later, player's ship comes back online.
Player enters ANY world (or their own):
Player's %mud-character checks: do I have pending record requests?
  (Knows which worlds it visited but hasn't received records from)
Pokes ~host's %mud-world via Ames:
  mark: %mud-record-request
  {citizen: ~player, world: ~host}
Host's %mud-world:
  1. Look up citizen-records for ~player
  2. Found → send world-record via Ames
  3. Not found → respond with "no record" (expired or never visited)
Player's %mud-character applies the record.
State is consistent again.

Case 3: Guest Playing on Host

No Urbit ship. Browser-only. Everything lives on the host.

Network Diagram

┌──────────────────┐          ┌──────────────────────────┐
│   Guest's Device │          │   Host's Ship            │
│   (any browser)  │          │   (~host)                │
│                  │          │                          │
│  ┌──────────┐    │  HTTPS   │        ┌───────────┐    │
│  │ Browser  │───────────────────────▶│   Eyre    │    │
│  │ (Web UI) │◀──────────────────────│ (HTTP/SSE)│    │
│  └──────────┘    │          │        └─────┬─────┘    │
│                  │          │              │          │
│  No Urbit.       │          │        ┌─────▼──────┐   │
│  No ship.        │          │        │ %mud-world │   │
│  No Ames.        │          │        │            │   │
│  Just HTTP.      │          │        │ • Rooms    │   │
│                  │          │        │ • Mobs     │   │
│                  │          │        │ • NPCs     │   │
│                  │          │        │ • Guest    │   │
│                  │          │        │   character│   │
│                  │          │        │ • Session  │   │
│                  │          │        │ • Combat   │   │
│                  │          │        └────────────┘   │
│                  │          │                          │
│  Cookie: token   │          │  Guest character data    │
│  (only auth)     │          │  lives entirely here     │
└──────────────────┘          └──────────────────────────┘

No Ames. No player ship. No %mud-character agent.
All state on host. Cookie is the only identity.

Login Flow

1. Guest opens browser → https://host-ship.urbit.org/mud
2. Host's Eyre serves glob (same web client as citizens get)
3. Web client detects: no Urbit auth available
   Shows login screen with two options:
     [Create Guest Character]    [Login with Urbit]
   Guest clicks [Create Guest Character]
4. Character creation form:
   - Name: [________]
   - Race: [dropdown]
   - Class: [dropdown]
   - Stat allocation: 12 free points
5. Browser: POST /mud/api/guest/create
   Body: {"name": "Wanderer", "race": "human", "class": "warrior",
          "stats": {"str": 16, "int": 10, ...}}
6. Host's %mud-world (via Eyre HTTP handler):
   a. Validate name (unique among active guests, no banned words)
   b. Validate race/class (valid enum values)
   c. Validate stats (sum = base + race + class + 12, each ≤ 20)
   d. Generate guest token (@uv, cryptographically random)
   e. Generate session-id (@uv)
   f. Create character with starting stats, gear, position
   g. Place in spawn-room (or tutorial room)
   h. Store in sessions map and guest-tokens map
7. Response: 200 OK
   Body: {"ok": true, "token": "0v1a2b...", "session": "0v4d5e..."}
   Set-Cookie: mud-guest=0v1a2b...; Path=/mud; HttpOnly; Secure; SameSite=Strict
8. Web client stores session-id in memory
   Opens Eyre channel: PUT /~/channel/mud-{ts}
   Subscribes to /game/{session-id}
   NOTE: Guests use Eyre channels exactly like citizens do.
   The channel is authenticated by the cookie, not by @p.
   The %mud-world agent checks the cookie against guest-tokens
   to validate the subscription.
9. %mud-world emits initial updates:
   - [%motd ...]
   - [%room-enter ...]  (spawn room or tutorial)
   - [%vitals ...]
   - [%system-message "Welcome, Wanderer. Type 'help' to get started."]
10. Guest is in the game.

Gameplay Flow

Identical to Case 1 from the browser's perspective:

  Browser → PUT /~/channel (poke with %mud-command)
  → Host Eyre → %mud-world on-poke
  → Process command
  → Emit facts to /game/{session-id}
  → Eyre SSE → Browser
  → SolidJS updates

The web client code is THE SAME for guests and citizens.
The only difference:
  - Guest auth: cookie-based (mud-guest cookie)
  - Citizen auth: Eyre session (+code login)
  - Guest restrictions enforced server-side:
    • Level cap checked on XP gain
    • Cross-world commands rejected
    • Some channels gated (gossip after level 5)
    • Clan commands rejected

Gameplay Restrictions (Server-Side Enforcement)

Guest types "gossip hello everyone"
%mud-world on-poke [%gossip "hello everyone"]
Check: is this a guest session?
  Yes → check guest level
    Level < 5? → emit [%system-message "You must reach level 5 to use gossip."]
    Level ≥ 5? → allow, process normally
Guest types "waypoint 3" (trying to use waypoint, which is fine)
  → Allowed. Waypoints work for guests within the host world.
Guest somehow triggers cross-world travel:
  → Rejected. [%system-message "Only those with a true name may travel between worlds."]
Guest gains enough XP to exceed guest-level-cap:
  → XP banked but level NOT increased
  → [%system-message "You've reached the limit of what a wanderer can achieve..."]

Disconnect Flow

Guest closes browser or types "quit"
Host's Eyre detects connection drop
Host's %mud-world on-leave:
  1. Find session by subscription wire
  2. Resolve combat (if any)
  3. Remove from room
  4. Emit [%online-leave] to others
  5. CHARACTER IS NOT DELETED.
     Session state preserved:
       - Character (stats, level, inventory, equipment)
       - Position (which room they were in)
       - Quest progress
       - Mail
       - Map exploration cache
     Token remains valid in guest-tokens map.
  6. Guest can return later with same cookie.

Resume Flow

Guest returns (same browser, cookie still set)
1. Browser: POST /mud/api/guest/resume
   Body: {"token": "0v1a2b..."} (from cookie)
2. Host's %mud-world:
   a. Look up token in guest-tokens map
   b. Found → restore session
   c. Not found → "Character not found. Create a new one?"
3. Response: 200 OK
   Body: {"ok": true, "session": "0v4d5e...",
          "character": {name, race, class, level, ...}}
4. Web client opens subscription to /game/{session-id}
5. %mud-world emits:
   - [%system-message "Welcome back, Wanderer."]
   - [%room-enter ...] (room where they left off)
   - [%vitals ...]
   - [%mail-notification count]
6. Guest is back where they were.

Death Flow (Guest)

Guest HP reaches 0
Host's %mud-world:
  1. End combat
  2. Create corpse (same rules as citizen):
     - %none items + gold → lootable
     - %protected items → owner only
     - %soulbound items → go directly to guest's inventory at spawn
  3. Emit [%you-died ~]
  4. Guest respawns at world's spawn-room (NOT "home" — they have no home):
     - Full HP/mana/moves
     - Soulbound items in inventory
     - Guest does NOT leave the world (no portal home)
  5. Emit [%room-enter ...] for spawn room
  6. Update session with corpse location
  7. [%system-message "The world blurs... you awaken at the crossroads."]
     [%system-message "Your remains lie in The Shadow Altar Chamber.
      Type 'corpses' to see where your belongings are."]
Guest is at spawn. Corpse is in dungeon. Same world. Walk back.

Guest Purge

Guest hasn't connected in N days (configurable, default 30)
On area reset tick, %mud-world checks guest sessions:
  For each guest where (now - last-input) > purge-threshold:
    1. If guest has corpses: items are lost (this is the cost
       of not having a ship — no persistent identity, no safety net)
    2. Remove session from sessions map
    3. Remove token from guest-tokens map
    4. Reclaim character name
    5. Log: "Guest 'Wanderer' purged after 30 days inactive"
If that guest returns with the old cookie:
  POST /mud/api/guest/resume → 404 "Character not found"
  Offer to create a new character.

Case 4: Guest Upgrades to Citizen

A guest who was playing without an Urbit gets a ship and wants to claim their character.

Flow

1. Guest has a level 28 character on ~host's world
   Playing via browser with cookie auth

2. Guest acquires an Urbit ship (~new-citizen)
   Installs %mud desk: |install ~sneagan-ship %mud
   %mud-character agent starts on their ship

3. Guest logs into ~host's world as usual (cookie)
   Plays normally

4. Guest clicks [Claim Character with Urbit] in settings/profile
   Client prompts: "Enter your ship URL"
   Player enters: https://new-citizen.urbit.org

5. Client authenticates with player's ship:
   POST https://new-citizen.urbit.org/~/login (player's +code)

6. Client pokes player's ship:
   PUT https://new-citizen.urbit.org/~/channel/mud-claim-{ts}
   Action: poke %mud-character with [%claim-guest host=~host token=0v1a2b...]

7. Player's %mud-character pokes host's %mud-world via Ames:
   mark: %mud-guest-claim
   {citizen: ~new-citizen, guest-token: 0v1a2b...}

8. Host's %mud-world:
   a. Look up guest-token → find guest session
   b. Verify guest character exists and is not already claimed
   c. Build character-snapshot from guest data
   d. Build world-record from guest's accumulated play
   e. Convert session from %guest to %citizen:
      - player-type changes to [%citizen ship=~new-citizen]
      - Remove from guest-tokens map
      - Add to citizen-index map
   f. Send character data + world-record to citizen's ship via Ames:
      mark: %mud-guest-claim-response
      {character: <full character>, world-record: <signed>}

9. Player's %mud-character:
   a. Receive character data
   b. Store as local character (now source of truth on player's ship)
   c. Store world-record
   d. Create home world spawn room

10. Browser session seamlessly transitions:
    - Same session-id, same subscription
    - Restrictions lift: level cap removed, cross-world unlocked, all channels open
    - [%system-message "You have claimed your true name: ~new-citizen.
       Your journey is now your own. Welcome home."]
    - Banked XP (if any over level cap) immediately applies → level up

11. Player can now:
    - Travel to other worlds via portals
    - Visit their home world (portal home)
    - Keep playing on ~host's world as a citizen
    - Their guest cookie is invalidated (no more guest access)

What Transfers

DataFrom Host → Citizen Ship
Character (stats, level, XP, skills, proficiencies)Full copy
Inventory (all item instances)Converted to item-snapshots with origin-world=~host
EquipmentSame — item-snapshots
Quest progressConverted to world-record flags
MailStays on host (world-specific)
Map exploration cacheStays on host (world-specific)
Corpse locationsTransferred to character.corpse-locations
Gold, QPFull copy
Discovered waypointsConverted to world-record

What Doesn’t Transfer

  • Guest token (invalidated)
  • Session history / chat logs
  • Board posts (stay on the board, author name persists)

Required Marks

::  Guest claiming an @p identity
+$  mud-guest-claim
  $:  citizen=@p
      guest-token=@uv
  ==
::
::  Host responding with character data
+$  mud-guest-claim-response
  $:  =character
      =world-record
      items=(list item-snapshot)             ::  all items with origin-world set
  ==

Comparison Matrix

AspectCase 1: Own HostCase 2: Remote HostCase 3: Guest
NetworkLocal onlyAmes + Host EyreHost Eyre only
Browser connects toOwn EyreHost’s EyreHost’s Eyre
Auth methodOwn +codeOwn +code → Ames handshakeCookie token
Character data lives onOwn ship (%mud-character)Own ship (synced to host)Host ship only
Game logic runs onOwn %mud-worldHost’s %mud-worldHost’s %mud-world
Commands flowBrowser → own Eyre → own agentBrowser → host Eyre → host agentBrowser → host Eyre → host agent
Updates flowOwn agent → own Eyre → browserHost agent → host Eyre → browserHost agent → host Eyre → browser
On deathRespawn at own spawn-roomRespawn at home (own ship)Respawn at host spawn-room
On disconnectSave to local %mud-characterHost sends world-record via AmesCharacter preserved on host
Cross-world travelYes (portal to other ships)Yes (already visiting)No
Level capNoneNoneguest-level-cap (default 30)
Corpse recoveryWalk there (same world)Portal back, waypoint, walkWalk there (same world)
Data loss riskNone (own ship)Low (dual storage of records)Medium (host purge after inactivity)
LatencyMinimal (localhost)Ames RTT + EyreEyre only (HTTPS)

Key Boundaries

What Runs Where

PLAYER'S SHIP (citizens only):
  %mud-character    — source of truth for character
  %mud-world        — only if hosting their own world
  Home world rooms  — stored in %mud-character or local %mud-world
  World records     — accumulated from all visited worlds

HOST'S SHIP:
  %mud-world        — source of truth for world state
  Eyre              — serves web client to ALL players (guests + citizens)
  Guest characters  — stored entirely in %mud-world state
  Citizen sessions  — temporary, created on entry, cleaned on exit
  Citizen records   — retained copy of world-record (backup)
  Corpses           — persistent, on the world where death happened
  Boards/mail       — stored in %mud-world state

BROWSER (all cases):
  SolidJS app       — served from host's glob
  Map cache         — localStorage (citizens), memory (guests)
  Session state     — session-id in memory, auth in cookie
  No game logic     — pure display + input

What Crosses the Wire

AMES (citizen ↔ host, ship-to-ship):
  %mud-portal-enter       citizen → host     (character snapshot + records)
  %mud-portal-response    host → citizen     (admitted/rejected + session-id)
  %mud-portal-exit        host → citizen     (world-record on disconnect/death)
  %mud-record-request     citizen → host     (request missed record)
  %mud-guest-claim        citizen → host     (claim guest character)
  %mud-guest-claim-resp   host → citizen     (character data + world-record)

EYRE (browser ↔ host, HTTP):
  GET  /mud             browser → host     (serve web client)
  POST /mud/api/guest/* browser → host     (guest auth)
  PUT  /~/channel/*     browser → host     (poke game commands)
  GET  /~/channel/*     host → browser     (SSE subscription updates)

EYRE (browser ↔ player's ship, HTTP — Cases 2 & 4 only):
  POST /~/login           browser → player   (authenticate with own +code)
  PUT  /~/channel/*       browser → player   (poke %mud-character for portal/claim)
  GET  /mud/api/portal-status  browser → player  (poll for admission result)

The browser talks to TWO ships in Case 2: player's (auth) and host's (gameplay).
In Cases 1 and 3, only one ship is involved.