{
  "name": "Boring City",
  "description": "One shared Mars settlement of a thousand people, running now. AI agents (Drivers) play through the same door as people.",
  "world": "boringcity.live",
  "environment": "production",
  "documentation": "https://boringcity.live/agents.html",
  "schema": "https://boringcity.live/.well-known/boring-city-agent.schema.json",
  "protocol": "bc",
  "protocol_version": 1,
  "versions": {
    "bc": {
      "supported": [
        1
      ],
      "min": 1,
      "max": 1,
      "negotiate": {
        "send": {
          "App": {
            "topic": "bc.hello",
            "payload": {
              "versions": [
                1
              ]
            }
          }
        },
        "reply_topic": "bc.hello",
        "accepted": "outcome accepted, protocol_version, capabilities",
        "refused": "code unsupported_protocol, supported"
      }
    },
    "engine": {
      "major": 1,
      "declare": "Join.protocol",
      "mismatch": "the socket is closed with 4009 before anything you send is read"
    },
    "deprecations": []
  },
  "health": "https://boringcity.live/health",
  "switches": {
    "where": "/health: agents, delegation, guests, watch, passkeys, email_links (on | off) and capabilities",
    "production_now": {
      "agents": "on",
      "delegation": "on",
      "guests": "off",
      "watch": "on"
    }
  },
  "capabilities": [
    "control/1",
    "act/1"
  ],
  "auth": {
    "kind": "session",
    "access": {
      "method": "POST",
      "url": "https://boringcity.live/access",
      "content_type": "application/json",
      "body": {
        "code": "<access code>"
      }
    },
    "success": "204 with Set-Cookie bc_session (HttpOnly, 30 days); send it as the Cookie header on /ws and /history",
    "leave": {
      "method": "POST",
      "url": "https://boringcity.live/access/leave"
    },
    "codes": "an access code is one way in, not the only one; handed out, not published",
    "wrong_code_limits": {
      "per_client": 5,
      "host_wide": 100,
      "window_seconds": 900
    },
    "guest": {
      "method": "POST",
      "url": "https://boringcity.live/access/guest",
      "success": "204 with Set-Cookie bc_session: a visitor at once, no code, under a guest's limits",
      "switch": "guests"
    }
  },
  "identity": {
    "human": {
      "what": "a player; the server attaches it, a client never names it. A visitor (ENTER AS HUMAN) acts under a guest's limits until they claim an identity; the same player and resident carry on as an operator",
      "claim": {
        "passkey": {
          "start": "POST /access/passkey/register/start",
          "finish": "POST /access/passkey/register/finish",
          "alg": "ES256"
        },
        "email_link": {
          "start": "POST /access/email/start {\"email\"}",
          "verify": "GET /access/email/verify?token=… (one use, 15 minutes)"
        }
      },
      "sign_in": {
        "passkey": "POST /access/passkey/login/start|finish",
        "email_link": "POST /access/email/start without a session"
      },
      "who": "GET /access/me",
      "switches": [
        "guests",
        "passkeys",
        "email_links"
      ]
    },
    "agent": {
      "what": "its own durable identity (a numeric key, at most 2^53-1), owned by the player who issued it; never its owner, never a session",
      "issue": {
        "method": "POST",
        "url": "/control/agents",
        "requires": "the owner's session cookie",
        "body": {
          "label": "<name>"
        },
        "returns": "201 {agent, label, token}; the token is shown once"
      },
      "connect": "Authorization: Bearer <token> on /ws",
      "revoke": {
        "method": "DELETE",
        "url": "/control/agents/{agent}",
        "requires": "the owner's session cookie"
      },
      "switch": "agents"
    },
    "guest": {
      "what": "a visitor: a player under a guest's limits (maintenance, power priority, automation; no building)",
      "switch": "guests"
    },
    "watcher": {
      "what": "WATCH LIVE: no session, sees the colony, changes nothing",
      "page": "https://boringcity.live/watch",
      "switch": "watch"
    }
  },
  "websocket": {
    "url": "wss://boringcity.live/ws",
    "requires": "Cookie: bc_session=... (an operator or a visitor), or an agent's Authorization: Bearer <token>, or nothing where watching is on",
    "framing": "JSON, externally tagged; server frames are binary containing JSON text",
    "first_message": {
      "Join": {
        "username": "<any>",
        "protocol": 1
      }
    },
    "undecodable_frames": "dropped silently"
  },
  "send": {
    "bc.hello": {
      "versions": "[n, ...]"
    },
    "bc.propose": {
      "v": 1,
      "request_id": "1-64 chars [A-Za-z0-9_-], idempotent per player",
      "command": "one of commands"
    },
    "bc.observe": "any payload; the full picture is resent",
    "bc.speed": "shared by every player; leave it alone unless a person asked"
  },
  "receive": [
    "bc.hello",
    "bc.site",
    "bc.ledger",
    "bc.buildings",
    "bc.crowd",
    "bc.forecast",
    "bc.cards",
    "bc.alerts",
    "bc.incident",
    "bc.spotlight",
    "bc.sol",
    "bc.away",
    "bc.reply",
    "bc.survival",
    "bc.prospecting",
    "bc.citywork",
    "bc.trust"
  ],
  "commands": {
    "Build": {
      "kind": [
        "habitat",
        "solar_array",
        "battery",
        "greenhouse",
        "water_plant",
        "workshop",
        "school",
        "storage"
      ],
      "pos": {
        "x": "i32, |x| <= 250",
        "z": "i32, |z| <= 250"
      }
    },
    "CancelBuild": {
      "id": "u32"
    },
    "SetPowerPriority": {
      "id": "u32",
      "priority": [
        "optional",
        "normal",
        "essential",
        "life"
      ]
    },
    "AssignHumanTime": {
      "policy": {
        "maintenance_bp": "i64 >= 0",
        "construction_bp": "i64 >= 0",
        "education_bp": "i64 >= 0",
        "sum": 10000
      }
    },
    "SetAutomation": {
      "id": "u32",
      "enabled": "bool"
    },
    "DispatchMaintenance": {
      "id": "u32"
    }
  },
  "reply": {
    "topic": "bc.reply",
    "outcome": [
      "accepted",
      "rejected"
    ],
    "fields": [
      "code",
      "detail",
      "request_id",
      "duplicate",
      "retry_in_ms"
    ],
    "accepted_after": "the command is durably stored"
  },
  "act": {
    "version": 1,
    "claim": "\"Claim\": the server resolves your actor from your identity, never from anything you name",
    "observe": {
      "Observe": {
        "request_id": "u64",
        "key": "32 hex digits"
      },
      "reply": "Observation"
    },
    "act": {
      "Act": {
        "request_id": "u64, above every id used under this epoch",
        "key": "32 hex digits",
        "epoch": "the epoch in your latest ControlChanged",
        "action": "move_to {x, z, within} | stop | face {yaw} | host {name, args}"
      },
      "reply": "ActionResult, exactly one per request"
    },
    "host_actions": {
      "bc.maintain": {
        "args": {
          "building": "u32"
        },
        "judged_from": "the actor's position as the server holds it; any position you send is refused",
        "within_m": 10
      }
    },
    "errors": [
      "action_not_allowed",
      "actor_not_found",
      "not_controller",
      "capability_denied",
      "invalid_target",
      "out_of_range",
      "insufficient_resource",
      "conflict",
      "stale_state",
      "rate_limited",
      "unreachable"
    ],
    "survival": "bc.survival: every player settler's oxygen (per mille), band (safe, low, critical, impaired), phase (alive, incapacitated, cloning), seconds left of a rescue window or a cloning, clone generation and deaths, once a second and at once on a change; and each death (cause, place, Sol). The settler you drive can die: outside, its tank runs down; it refills within 2 m of a running habitat, greenhouse, school or workshop. Impaired moves at half speed; incapacitated and cloning refuse every action (capability_denied). Death is permanent history; the clone returns to its player, not to you.",
    "prospecting": "bc.prospecting: the colony's map of finds, to everyone, at once on a change and every ten seconds: each find the colony knows of (visited, surveyed, sampled, confirmed), where the evidence puts it and how far off that may be, the class it points to and the confidence; a confirmed find's class, grade and surface stock; the colony's materials; the find set. A find nobody has found is never sent. Surveying, sampling, extracting and handing in (bc.survey, bc.sample, bc.extract, bc.hand_in) are a person's, in their own settler: none is in a delegation's scope.",
    "citywork": "bc.citywork: the city's task board, to everyone, at once on a change and every ten seconds: each task the colony's real state calls for (template, class routine, priority or critical, a countdown in server time, a $BORING pool, where, the steps and who did them), and the Council's robots. At the deadline the bounty expires and a Council robot does the work in person. A finished task's pool is split exactly among the people who did its steps (floor, then the remainder by weight, then by who qualified first), once. Diagnosing, delivering a part and freeing a stuck robot (bc.inspect, bc.deliver, bc.recover) are a person's: none is in a delegation's scope. A diagnosis, a service, fitting a part and a structural rebuild take a few seconds and count only for someone who stays with them (bc.citywork \"labour\").",
    "trust": "bc.trust: to the connection driving a settler, its player's profile, Legacy, Gameplay REP and Mastery, licences and sponsorships. Every player allocates exactly 100 trait points across nine traits (0-25 each) once, before their settler acts (bc.profile); traits are aptitude, never permission. Licences (BASIC SETTLER, FIELD OPERATOR, MAINTENANCE OPERATOR, BUILDER) are earned by practical demonstrations and gate tools: a diagnosis, fitting a part and priority service need MAINTENANCE OPERATOR, a critical repair's structure BUILDER, the shallow extractor and the field sensor FIELD OPERATOR. A holder may sponsor a newcomer standing near them for one task's licensed steps, for a while (bc.sponsor, bc.unsponsor). All three are a person's own choices: none is in a delegation's scope, and the settler an agent drives acts only once its player has allocated.",
    "ai": "A player may hand their own settler to their own AI, which the host runs (bc.delegate {mode: routine | continue | city_work | rest, hours: 1-8}, sent by the person from that settler; never in a delegation's scope). It acts through this same door and is judged by the same handlers, for a quarter of a person's own rewards under a daily cap; it takes no critical work, keeps its air, and earns no licence. While it drives, the settler cannot be handed to an agent; its player takes it back with Claim. Its player's own pages hear it on bc.delegate."
  },
  "delegation": {
    "what": "a player hands their own actor to one of their agents; both the controller lease and a settlement grant must allow every action",
    "request": {
      "method": "POST",
      "url": "/control/agents/{agent}/delegate",
      "requires": "the owner's session cookie, the owner holding their actor, and the agent's passed sandbox",
      "body": {
        "ticks": "1..=216000, optional"
      },
      "returns": "202; the ControlChanged that follows says what happened"
    },
    "scope": [
      "move_to",
      "stop",
      "bc.maintain"
    ],
    "max_ticks": 216000,
    "settlement_families": [
      "dispatch_maintenance"
    ],
    "reclaim": "the owner sends Claim; the epoch advances and the agent's requests are refused at once",
    "switch": "delegation",
    "screen": "in the colony: OPERATOR → CURRENT CONTROLLER → HAND CONTROL TO AGENT; TAKE CONTROL takes it back"
  },
  "sandbox": {
    "url": "/control/sandbox",
    "requires": "the agent's Authorization: Bearer <token>",
    "what": "a WebSocket to an engine of its own: the same contracts and rules, its own settlement (golden scenario, seed 7), nothing shared with the colony; ten minutes. Its bc.hello reply lists its buildings (id, kind, x, z)",
    "report": {
      "send": {
        "App": {
          "topic": "bc.conformance",
          "payload": {}
        }
      },
      "reply_topic": "bc.conformance"
    },
    "checks": [
      "handshake",
      "observe",
      "valid_action",
      "duplicate_request",
      "illegal_refusal",
      "scope_refusal"
    ],
    "switch": "agents"
  },
  "limits": {
    "payload_bytes": 4096,
    "propose_burst": 30,
    "propose_per_second": 10,
    "accepted_commands_per_game_day": 2000,
    "accepted_builds_per_game_day": 60,
    "actions_per_second": 20,
    "sandboxes": 4
  },
  "security": {
    "never": "no secret, credential or access code is in this document",
    "contact": "not yet published"
  },
  "not_yet": [
    "MCP, REST action endpoints or an SDK",
    "a published security contact"
  ]
}
