Bit-Me app API & the BMR1 / BME1 maps

The /bitme/* endpoints are a plain-JSON polling API built for the Bit-Me mobile companion app — no SpacetimeDB protocol, no authentication. Six GETs — plus one optional WebSocket, the live resource change stream — cover the whole app: name resolution, a ~1 Hz activity snapshot, a packed binary resource map centered on the player (the BMR1 format, documented below), the same window anchored at arbitrary world coordinates, a matching elevation plane (the BME1 format), and the per-region dictionary needed to decode BMR1.

The seven endpoints

EndpointReturnsCadence
GET /bitme/resolve?name= Exact-match (case-insensitive) player name → entity_id, identity, home region (name, host, module), live signed_in. Covers all 25 regions. This is a lookup, not a search — use GET /player?name= for substring search. Once at onboarding
GET /bitme/session/<entity_id> One snapshot for the activity screens: position, current claim, stamina, live buffs, action lifecycle, action-target health, watched spawns (citric detection). ~1 Hz while open
GET /bitme/session/<entity_id>/resources Packed 400×400 map centered on the player — binary BMR1 (application/octet-stream, 320,024 bytes), not JSON. A merged view: every resource type in the region, plus player paving and a water layer. Only while the map screen is open
GET /bitme/session/<entity_id>/resources/ws The one WebSocket: a live change stream over the same 400×400 window. After a subscribed ack, every in-window resource/paving tile change arrives as a small binary BMD1 delta frame; the server follows the player's anchor server-side. Read-only; the client keeps convergence by re-fetching the BMR1 window (details below). One per player, while the map screen is open
GET /bitme/world/<x>/<z>/resources The same BMR1 window anchored at explicit world tile coordinates — the region is derived from the coordinates, so the map can pan anywhere without following a player. No player involved, so no session keep-alive. Map panning / arbitrary views
GET /bitme/world/<x>/<z>/elevation Packed super-hex terrain plane covering the same 400×400 footprint — binary BME1, 134–137 × 136 super-hexes (145,818–149,082 bytes; the odd-r shear makes the width vary with the window's row alignment): current and original (pre-terraform) elevation, water level, and water body type per super-hex. With the map; refetch when generation changes
GET /bitme/region/<region>/resource-dictionary Tile-word index → resource_id + name + gamedata + harvestable flag, for expanding BMR1 payloads client-side. Once per region; refetch when dict_version changes

Conventions

ConventionDetail
Entity ids JSON strings — upstream ids are u64 and exceed JS Number.MAX_SAFE_INTEGER. Never parse them as numbers.
Positions world_x/world_z are float world units (one tile = 1.0); tile_x/tile_z are integer odd-r tiles — the same coordinate space as the /roads endpoints.
Clocks *_ms fields are unix milliseconds; buff start_timestamp/duration are unix seconds; last_decrease_at is RFC 3339.
Nulls & errors Present-but-null means unknown/not applicable, never an error. 400 for bad input, 404 with {"found": false, …} for misses. Every response carries Cache-Control: no-store; CORS is *.

Coordinates: world ↔ tiles ↔ in-game N/E

Three coordinate systems show up across the relay APIs. They agree on axis directions — x grows east, z grows north — and differ only in scale and origin:

SystemShapeWhere you meet it
World x/z Float world units (upstream milli-units ÷ 1000; one tile = 1.0) session.position.world_x/world_z, building & player lookups
Tile tile_x/tile_z Integer odd-r small-hex coordinates (floor(world)) session.position.tile_x/tile_z, BMR1 window origins, all /roads/* endpoints, location_state
In-game N/E Integer super-hex coordinates — the game's terrain lattice: a true hex grid at 3× the tile scale (odd-r; odd super rows sheared one tile east) The in-game map/HUD, hexite deposit claim names (N: 5593, E: 9647), /deposits north/east

Conversions (the same formulas the game's own claim names use):

world (float) │ tile_x = floor(world_x) tile_z = floor(world_z) ▼ tile (odd-r small hex) │ to axial: q = x − (z − (z&1))/2 r = z │ round each component to the nearest third: (v+1) / 3 floored │ back to odd-r at super scale: E = Q + (N − (N&1))/2 ▼ in-game N/E (super hex) │ inverse (super → its CENTER tile): x = 3E + (N&1) z = 3N

Notes:

Converting in practice (Python)

import json, math, struct, urllib.request

BASE = "https://relay.bitcraftsync.app"

def world_to_tile(wx, wz):
    return math.floor(wx), math.floor(wz)

def tile_to_ne(x, z):
    q = x - (z - (z & 1)) // 2           # odd-r tile → axial
    Q, N = (q + 1) // 3, (z + 1) // 3      # round each to nearest third
    return N, Q + (N - (N & 1)) // 2     # axial → odd-r super (north, east)

def ne_to_tile(north, east):
    return east * 3 + (north & 1), north * 3   # super's center tile

# session snapshot → HUD-style coordinates
snap = json.load(urllib.request.urlopen(
    f"{BASE}/bitme/session/504403158290646123", timeout=30))
pos = snap["position"]
x, z = world_to_tile(pos["world_x"], pos["world_z"])
north, east = tile_to_ne(x, z)
print(f'tile ({x}, {z})  →  in-game N {north}, E {east}')

# hexite deposit name → super center tile for /roads or a BMR1 window check
x, z = ne_to_tile(5593, 9647)              # → (28942, 16779), region 14

# BMR1 window: word (lx, lz) → world tile → HUD coords
raw = urllib.request.urlopen(
    f"{BASE}/bitme/session/{snap['player_entity_id']}/resources",
    timeout=30).read()
_, _, width, ox, oz, _, _ = struct.unpack_from("<4sHhiiII", raw, 0)
tiles = struct.unpack_from(f"<{width*width}H", raw, 24)
i = next(i for i, t in enumerate(tiles) if t & 0x3FF)
lx, lz = i % width, i // width
north, east = tile_to_ne(ox + lx, oz + lz)
print(f"first resource at world tile ({ox + lx}, {oz + lz})"
      f" → N {north}, E {east}")

Session snapshot in one look

Top-level fields of GET /bitme/session/<id> — full field-by-field reference with real payloads lives in crates/relay-cache/BITME-API.md in the relay repo:

FieldMeaningGotcha
position Last known world position + destination + dimension. Row persists after logout — judge freshness by age_ms and signed_in. dimension > 1 is a building interior.
claim Claim under the player's tile (name, owner, neutral). null in wilderness or interiors.
stamina current/max + last_decrease_at. Regen is not simulated server-side; project it client-side from the passive rate anchored at last_decrease_at.
buffs Live buffs only (placeholders filtered). Expired entries can linger until the next upstream flush — check the countdown, not just presence.
actions Action lifecycle per layer (Base, UpperBody): type, start, duration, ends_at_ms, result. Rows persist after completion. In progress iff server_time_ms < ends_at_ms.
target The acted-on entity, enriched: name, health/max, respawn gamedata, growth_ends_at_ms. health is tracked server-side only while a session polls — expect null on the first poll after targeting.
activity_spawns Watched spawns in the player's claim/wilderness: destroy-yield chains + the three Citric Giant bushes. The citric-detection signal: alert when an entry with a citric resource_id appears. Entries expire after 30 min or on upstream delete.

The BMR1 resource window

GET /bitme/session/<id>/resources serves a 400×400 window out of the region's dense tile map — one u16 per odd-r tile across the whole 7680×7680 region, merging three layers:

The player's tile sits at relative (200, 200); the window covers world tiles origin … origin+399 per axis.

Header (24 bytes, little-endian)

OffsetTypeMeaning
0–3[u8; 4]Magic BMR1
4–5u16Format version (1)
6–7u16Window width in tiles (400)
8–11i32origin_world_x — odd-r world tile of relative column 0
12–15i32origin_world_z — odd-r world tile of relative row 0
16–19u32Region id
20–23u32dict_version — pairs the payload with a resource-dictionary

Tile word (u16 LE)

Every bit is now allocated:

BitsMeaning
0–9Dictionary index (0 = empty tile; capacity 1023). With bit 14 clear this is a resource index; with bit 14 set it is a paving index — the two live in separate dictionary namespaces, so equal indices never collide.
10Origin flag — this tile is the resource's anchor tile (resources only; never set on paving)
11–13direction_index (0–5), repeated on every tile of the resource footprint (resources only)
14Paving flag — the tile is player-paved; bits 0–9 hold a paving index (dictionary entries with "paving": true)
15Water flag — the tile's terrain sits below its water level (terrain super-hex resolution: a water super wets its 7-tile flower, and corner tiles — where three supers meet — are water when any of the three is). Rides on every word, including otherwise-empty tiles (0x8000 alone = water, nothing on it).

Two rendering styles fall out of this layout. Per-tile: paint every occupied tile from the dictionary — no footprint math needed, the server already stamped each tile of every multi-hex shape. Per-instance: render one icon per resource by anchoring on tiles with the origin flag set and rotating the resource's resource_desc.footprint by direction; adjacent same-type resources are disambiguated by their origin tiles. Natural layer order for a map: water (bit 15) as the background, paving (bit 14) as the underlay, resources on top. A ready-made hex-grid viewer that does exactly this lives in the relay repo at crates/relay-cache/debug/window.html.

The BME1 elevation window

GET /bitme/world/<x>/<z>/elevation serves the terrain half of the same footprint: the super-hexes covering the 400×400 window anchored at world tile (x, z). One u64 per super-hex, sliced straight from the resident terrain grid. Super-hexes are the game's terrain lattice — a true hex grid at 3× the tile scale, addressed in odd-r offset coordinates the same way small tiles are, not a rectangular 3×3 blocking of tile indices:

The cell layout is the roads terrain cell unchanged — see the region map format page for the full terrain layer:

Cell bits (u64 LE)Field
0–15elevation (i16 stored as u16)
16–31original_elevation (i16) — pre-terraform
32–47water_level (i16)
48–55water_body_type (u8)
56–63unused (zero)

BME1 header (26 bytes, little-endian)

OffsetTypeMeaning
0–3[u8; 4]Magic BME1
4–5u16Format version (2)
6–7u16width — super-hex columns (134–137)
8–9u16height — super-hex rows (136)
10–13i32origin_center_world_x — world tile of cell (0, 0)'s center tile
14–17i32origin_center_world_z
18–21u32Region id
22–25u32generation — grid update counter; terraform bumps it

Decoding pairs naturally with a BMR1 fetch at the same anchor:

# terrain plane for the same anchor as a BMR1 window
raw = urllib.request.urlopen(
    f"{BASE}/bitme/world/{x}/{z}/elevation", timeout=30).read()
magic, version, width, height, cx, cz, region, generation = struct.unpack_from(
    "<4sHHHiiII", raw, 0)
assert magic == b"BME1" and version == 2 \
    and len(raw) == 26 + width * height * 8

cells = struct.unpack_from(f"<{width * height}Q", raw, 26)

def elevation(cell):                     # i16 stored as u16
    return struct.unpack("<h", struct.pack("<H", cell & 0xFFFF))[0]

# header origin is cell (0, 0)'s CENTER tile → super offset
X0 = (cx - (cz & 1)) // 3
Z0 = cz // 3

def tile_to_super(x, z):                 # world tile → super offset
    q = x - (z - (z & 1)) // 2           # odd-r → axial
    Q, N = (q + 1) // 3, (z + 1) // 3    # round to nearest thirds
    return Q + (N - (N & 1)) // 2, N    # axial → odd-r super

def cell_at(tile_x, tile_z):             # world tile → covering cell
    sx, sz = tile_to_super(tile_x, tile_z)
    return cells[(sz - Z0) * width + (sx - X0)]

hi = max(elevation(c) for c in cells)
print(f"region {region}: highest elevation {hi}, generation {generation}")

Resource dictionary

The 10-bit tile index is assigned in first-sighting order and is stable per deploy but not across deploys. Resource and paving entries use separate index namespaces — a tile word's bit 14 says which list an index refers to, so equal indices never collide. dict_version covers both. Fetch the dictionary once per region and refetch it when a window header's dict_version changes:

GET /bitme/region/7/resource-dictionary

{
  "region": 7,
  "ready": true,
  "dict_version": 4018046328,
  "entries": [
    {
      "index": 1,
      "resource_id": 815748648,
      "name": "Rough Quarry Rock",
      "harvestable": false,
      "max_health": 5000,
      "despawn_time_secs": 0.0,
      "respawn_time_secs": 0.0,
      "paving": false
    },
    {
      "index": 1,
      "paving_type_id": 59838,
      "name": "Cobblestone Road",
      "paving": true
    }
  ]
}
FieldMeaning
index The 10-bit value carried in tile words (index 0 is the empty sentinel and is not listed).
paving / paving_type_id true on paving entries, which carry the raw paving_type_id instead of resource_id (names from the global paving_tile_desc catalog).
name / max_health / … resource_desc gamedata on resource entries (null if the desc row is missing).
harvestable The old roads-side forestry/mining/clay/sand classification — use it to filter the map client-side; the window includes all resource types.

The resources/ws change stream (BMD1)

GET /bitme/session/<entity_id>/resources/ws (a WebSocket — wss://relay.bitcraftsync.app/…) pushes every resource/paving tile change whose world tile falls inside the player's 400×400 window. The server keeps only the window coordinates per listener — no window copy, no diffing — and the client owns convergence: initial state, reconnect recovery, and resync recovery are all "re-fetch the BMR1 window". Frames are never silently dropped to a live socket; backpressure closes the connection instead, so the delivered stream is totally ordered. The stream is read-only (client→server frames are ignored; WS pings are answered).

Pre-upgrade errors are byte-identical to the HTTP window's (the table below), plus 503 when the fleet-wide stream cap — 2048 connections, shared with the dim-buildings stream — is reached. One stream per player; heartbeats every 5 s also refresh the session TTL.

Server → client frames:

FramePayloadMeaning
text (first) {"type":"subscribed", "player_entity_id":"…", "region":14, "anchor":{"x":7690,"z":7700}, "width":400, "dict_version":123456789} Ack after the upgrade. Note region and anchor; pair dict_version with the dictionary.
binary BMD1 — one per applied upstream batch, only in-window changed tiles (layout below) Apply each entry onto your window: word → tile at (x, z) when in bounds. Tiles are world odd-r coordinates, not window-relative. A 0 word (or water-bit-only word) means the tile emptied.
text {"type":"resync","reason":"reseed"|"live"} The region grid was replaced (upstream disconnect) or finished re-seeding. Refetch the BMR1 window; refetch the dictionary too if the next frame's dict_version differs from the snapshot header's.
text {"type":"moved","region":7,"anchor":{…}} The player crossed into another region and the stream re-anchored there. Refetch the BMR1 window (different area of the world).
text {"type":"gone","player_entity_id":"…"} No live position in any mirrored region for ~60 s (logged out, or a deploy/reseed outlasting the window). The server closes right after.
text {"ts":1753296000123} Heartbeat every 5 s. Treat ~15 s without a frame as dead.

BMD1 frame layout

OffsetTypeMeaning
0–3[u8; 4]Magic BMD1
4–5u16Format version (1)
6–9u32Region id
10–13u32dict_version — pair with /resource-dictionary
14–15u16Entry count n (batches larger than 65535 tiles split into consecutive frames)
16+n × 10 bytesi32 world_x, i32 world_z, u16 tile word (the same bit layout as BMR1, water bit included)

Duplicate tiles within a frame never occur; writes within one batch collapse to the final word. While walking, keep the movement-triggered BMR1 refetch — the server follows the anchor at ~1 s granularity, so trailing-edge events for a few tiles around a fast-moving player are intentionally trimmed until the next full fetch.

Consuming the stream (Python)

# pip install websocket-client        (stdlib has no WebSocket client)
import json, struct, urllib.request, websocket

BASE = "https://relay.bitcraftsync.app"
PLAYER = "504403158290646123"

# 1. fetch the BMR1 window first — connect order is safe either way:
#    replaying an already-applied tile word is idempotent, and frames
#    that race the fetch carry the same final word.
raw = urllib.request.urlopen(
    f"{BASE}/bitme/session/{PLAYER}/resources", timeout=30).read()

# 2. open the stream; on "subscribed" note region/anchor
ws = websocket.create_connection(
    f"wss://relay.bitcraftsync.app/bitme/session/{PLAYER}/resources/ws")
ack = json.loads(ws.recv())
assert ack["type"] == "subscribed", ack

def refetch_window():                    # 4. the universal recovery move
    return urllib.request.urlopen(
        f"{BASE}/bitme/session/{PLAYER}/resources", timeout=30).read()

while True:
    try:
        op, data = ws.recv_data()
    except websocket.WebSocketException:          # closed, any reason
        raw = refetch_window(); break              # …then reconnect with backoff

    if op == websocket.ABNF.OPCODE_TEXT:
        msg = json.loads(data)
        if msg.get("type") in ("resync", "moved"):
            raw = refetch_window(); break          # + dictionary on dict_version change
        # {"type":"gone", …} → server closes right after; {"ts":…} = heartbeat
    elif op == websocket.ABNF.OPCODE_BINARY:
        # 3. apply each BMD1 entry onto the window
        magic, ver, region, dict_version, n = struct.unpack_from(
            "<4sHIIH", data, 0)
        assert magic == b"BMD1" and ver == 1
        for x, z, word in struct.iter_unpack("<iiH", data[16:16 + n * 10]):
            apply_word(x, z, word)   # word → tile at (x, z) when in bounds;
                                     # 0 (or water-only) = tile emptied

Decoding a window (Python)

import json, struct, urllib.request

BASE = "https://relay.bitcraftsync.app"
PLAYER = "504403158290646123"          # entity_id string from /bitme/resolve

# --- fetch the window (binary!) -------------------------------------------
raw = urllib.request.urlopen(
    f"{BASE}/bitme/session/{PLAYER}/resources", timeout=30).read()

magic, version, width, ox, oz, region, dict_version = struct.unpack_from(
    "<4sHhiiII", raw, 0)
assert magic == b"BMR1" and version == 1 and len(raw) == 24 + width*width*2

tiles = struct.unpack_from(f"<{width*width}H", raw, 24)

def word(t):
    return {
        "index": t & 0x3FF,                # bits 0-9
        "origin": bool(t & 0x400),         # bit  10
        "direction": (t >> 11) & 0b111,   # bits 11-13
        "paving": bool(t & 0x4000),         # bit  14
        "water": bool(t & 0x8000),          # bit  15
    }

# --- resolve names via the dictionary (refetch on dict_version change) -----
d = json.load(urllib.request.urlopen(
    f"{BASE}/bitme/region/{region}/resource-dictionary", timeout=30))
assert d["dict_version"] == dict_version
# separate namespaces: (paving?, index) → entry
names = {(e["paving"], e["index"]): e for e in d["entries"]}

# --- classify every tile: water / paving / resource -------------------------
water = paving = resource = 0
for t in tiles:
    w = word(t)
    if w["water"]: water += 1
    if w["paving"]: paving += 1
    elif t & 0x3FF and not w["paving"]: resource += 1
print(f"water tiles: {water}, paved: {paving}, resource tiles: {resource}")

# --- list resource instances (one per origin tile; paving has no origin) ----
for i, t in enumerate(tiles):
    w = word(t)
    if w["origin"]:
        lx, lz = i % width, i // width
        e = names[(False, w["index"])]
        print(f'{e.get("name", "?")} dir={w["direction"]} '
              f'at world tile ({ox + lx}, {oz + lz})')

# paving underlay: tiles where word["paving"] is set —
#   names[(True, w["index"])]["name"] gives e.g. "Cobblestone Road"

With curl:

curl -s 'https://relay.bitcraftsync.app/bitme/resolve?name=whisper' | jq .
curl -s "https://relay.bitcraftsync.app/bitme/session/$ENTITY_ID" | jq .
curl -s "https://relay.bitcraftsync.app/bitme/session/$ENTITY_ID/resources" -o window.bmr1
ls -l window.bmr1   # 320024 bytes
curl -s "https://relay.bitcraftsync.app/bitme/world/$X/$Z/resources" -o pan.bmr1
curl -s "https://relay.bitcraftsync.app/bitme/world/$X/$Z/elevation" -o pan.bme1
ls -l pan.bme1      # 145818–149082 bytes (width varies with alignment)
curl -s "https://relay.bitcraftsync.app/bitme/region/$REGION/resource-dictionary" | jq .
# the change stream is a WebSocket — try it with any WS client, e.g.:
#   websocat wss://relay.bitcraftsync.app/bitme/session/$ENTITY_ID/resources/ws
# (first frame: the {"type":"subscribed",…} ack; Ctrl-C to leave)

Window error modes

StatusMeaning
200320,024-byte BMR1 payload.
202 Empty body — the region grid is still seeding (right after a deploy). Retry with backoff.
400 {"error": "entity_id must be a u64"}; the world windows instead say {"error": "x and z must be i32 world tile coordinates"}.
404 {"found": false, …} — player not present in any mirrored region, not on the overworld (interiors have no tile), or the region has no resource map. A player resolves globally even when their region is not mirrored, but a session requires presence in a mirrored one. The world windows instead 404 with {"found": false, "x": …, "z": …} when the coordinates fall outside the 5×5 region grid.
503Roads cache not enabled (not the case in production).
Bit-Me app │ 1. resolve?name=whisper → entity_id, region 7, host/module │ 2. session/<id> @ ~1 Hz → position, claim, stamina, buffs, │ actions, target, activity_spawns │ 3. region/7/resource-dictionary → index → resource_id + names ▼ session/<id>/resources → 320,024-byte BMR1 window │ header: magic, width 400, origin (world tiles), region, dict_version └─ 160,000 u16 words: 10-bit index │ origin │ 3-bit direction expand via the dictionary; refetch it when dict_version moves session/<id>/resources/ws → live BMD1 deltas over the same window │ text: subscribed → binary: BMD1 batches → resync / moved / gone / ts └─ per changed tile: i32 world x, i32 world z, u16 word (same bit layout) recovery is always: refetch the BMR1 window, then reconnect world/<x>/<z>/resources → same BMR1 window at any world tile world/<x>/<z>/elevation → BME1 plane over the same │ footprint: 134–137 × 136 super-hexes (hex lattice at 3× tile scale) │ header v2: magic, width, height, center-tile origin, region, generation └─ 18,224–18,632 u64 cells: elevation │ original │ water level │ body type refetch when generation moves (terraform)