Starmap
The world entry covers the map: star systems and the bodies inside them. It is shared account state that fleets move through and starbases sit in. Even when topology changes rarely, these are observed snapshots with account freshness rules, not permanently valid definitions.
const system = await sage.systems.byId(systemId);const bodies = await system.celestialBodies.all();Systems by id or by address
Section titled “Systems by id or by address”Systems have a numeric id as well as an account address, and you can read by either:
const byId = await sage.systems.byId(3);const byAddress = await sage.systems.get(systemAddress);The id is the friendlier handle when you already know which system you mean. The address is what other accounts reference, so it is what you will have when arriving from a fleet’s state.
Getting from a fleet to its system
Section titled “Getting from a fleet to its system”A fleet’s location lives inside its state, so narrow first:
if (fleet.state.kind === 'docked') { const system = await sage.systems.get(fleet.state.system.address);}See fleets for why location depends on Fleet state instead of being one flat field.
Warp lanes
Section titled “Warp lanes”Systems are not floating islands — the starmap is a network, and warp lanes are its links. Every system lists its own:
const neighbours = system.connections.map((connection) => connection.systemId);Each connection also carries the lane’s toll — an ATLAS cost per starbase
level (connection.costs.level1Atlas through level5Atlas, plus the CSS
tier) — so “what does this jump cost” is a read, not a guess.
Whether a lane is usable is a faction question: both ends must be held by the same faction, and a system’s controller is on its shared starbase data:
const controller = system.starbase?.owner; // 'mud' | 'oni' | 'ustur' | …Travelling a lane is a fleet move — planFleetWarpLane in
Warp & subwarp — and the fee a character actually
pays can shrink with Council Rank research.
Bodies: planets and asteroids
Section titled “Bodies: planets and asteroids”Celestial bodies are the things inside a system worth interacting with:
const planets = await system.planets.all();const asteroids = await system.asteroids.all();Both are projections over the same underlying body accounts, filtered to the kind you asked for. Asteroids are where mining happens; planets are where claim stakes go.
Star presentation metadata
Section titled “Star presentation metadata”The inspected C4 client generates star visuals procedurally. In the
deployed game client,
getVisualStarCount and createStarVisualProfile use system identity and
gameplay properties to seed visual generation, including visual companion
stars. This is evidence for the inspected rendering path, not a universal
statement about on-chain storage or a supported rendering contract.
Separately, the official map editor authors star type, display name, and
scale in exported JSON. The initial #368 audit checked that producer on
2026-09-09, using code rather than treating the reported 2026-06-09 export
as a chain schema. Editor authorship alone does not establish on-chain absence.
The editor’s
STAR_TYPES table
explicitly lists 20 numeric IDs, 0–19, with display labels. Its
system operations
include a random generator that chooses a table entry, constructs a star name
from the system name and type label, and assigns a rounded random scale. The
editing controls can change the star’s name, type, and scale. The
import/export code
converts a legacy singular star into a stars array and exports the system
records in mapData as JSON.
This is not a stable SDK input contract: a separate addStar path in the
same producer still creates a string type ("G") with size and color rather
than scale. Do not assume that every editor record follows the numeric table,
that a system always has exactly one visual star, or that scale is a physical
radius. The producer code establishes authorship, not uniform export validation.
The checked @staratlas/dev-sage@0.52.0 StarSystem codec has Game and system
identity, a system name, region, coordinates, sequence metadata, connections,
body addresses and nested Starbase data, but no star visual fields.
StarSystem.name names the system, not a separately decoded visual star.
The pinned CelestialBodyType is Planet | Asteroid, and the generated
account/type audit found no separate Star account or owning star-visual field.
That does not prove universal on-chain absence or rule out future bindings.
Fresh reads of three StarSystem accounts and their 17 bodies consumed all bytes
and re-encoded identically. This checks those layouts; it cannot exclude
additional semantics in existing fields or data stored in other accounts.
Keep editor visuals in an application-owned overlay if your UI needs them.
Verify a deployment-specific mapping from exported systems to C4 system
identities, and manage the overlay’s version and refresh separately from SDK
account caching. This audit establishes neither that mapping nor a supported
live metadata endpoint or schema version. The SDK does not fetch the editor
export or add these fields to StarSystemSnapshot. Any later proven on-chain
source needs a separate implementation issue with its owning codec,
Game/system relationship and captured or live account verification.
See the dated star provenance record for inspection anchors and source hashes. Body presentation and Region colors remain separate research subjects.
Body presentation metadata
Section titled “Body presentation metadata”Base planet categories have an on-chain subtype representation. The
Planet.providedTags set within the generated CelestialBody account contains
numeric tags that the
deployed C4 game client
maps to planet categories through PLANET_TYPE_TAG_NAMES and
getPlanetSubtypeLabel. The Planet | Asteroid account variant is a separate
classification. Checking only field names in @staratlas/dev-sage@0.52.0
missed the meaning of these existing numeric values; the original #369
editor-only conclusion is superseded by this evidence.
The exact editor field remains distinct: its
PLANET_TYPES table
defines 0–31 faction/category IDs; the older 2026-06-09 export reported
0–23. The table repeats eight categories for ONI, MUD, USTUR, and Neutral.
The observed type % 8 grouping is not an SDK parsing contract. We have not
verified a direct chain field or deployment-specific mapping for that full
editor ID; do not reconstruct it from planet subtype and current ownership.
For orbit, angle, and scale, two producers were inspected:
- The editor’s body creation code generates spacing, random angles and type-based scales; its export code serializes those presentation values as JSON.
- The C4 client uses
getBodyOrbitIndexto parse a-P<number>suffix in on-chain body names, with a fallback. Its inspected detail renderer computes orbital spacing from sorted body index and rendering constants.createPlanetuses seeded randomness and subtype-dependent sizes for visual angle and size. This is a frontend convention, not authoritative orbital data or a guaranteed body-name format. It does not demonstrate consumption of the editor export.
Raw PTR account checks found no unread suffix in the checked layouts. That does not prove that no other on-chain source exists or exclude further semantics in numeric fields. No editor dataset, orbital field, name-parsing rule, or renderer is added to SDK snapshots.
The corrected chain audit records the capture counts, fresh read slots, frontend source hash, and limits; the editor provenance record preserves the original producer evidence. Both source URLs and deployment semantics can change.
Planet subtypes and building your map
Section titled “Planet subtypes and building your map”The SDK translates the verified category tags to planet.details.subtype:
terrestrial, volcanic, barren, gas-giant, ice-giant, dark, or
oceanic. The original numeric providedTags remain available for gameplay
rules and future interpretation.
Runnable with the sage client from Your first read:
const bodies = await sage.celestialBodies.all();for (const body of bodies) { if (body.kind === 'planet') { console.log(body.name, body.details.subtype ?? 'Unclassified'); }}The value is undefined when category tags are absent, conflict, or include
category tag 3 (the frontend’s Asteroid Belt entry, not a supported planet
subtype). Unrelated unknown tags are ignored. This conservative projection does
not guess from a display name or convert a Planet account to an Asteroid.
Subtype uses the same body account cache and freshness as other planet fields. It requires no extra RPC request, Game definition load, or separate catalog cache. This follows #398’s account-projection approach; #372’s Game section registry is not involved. A future deployment may change tag semantics; the mapping is pinned to the evidence recorded above, not guaranteed by the raw u16 tag codec alone.
For an application map, use system coordinates for system positions, account addresses for identity, and body names, subtypes, and resources for labels and styling. Choose visual spacing, sizes, and animation in your application. The SDK does not promise the current game’s exact rendering or authoritative per-body orbital parameters. If you use an editor export instead, verify its deployment mapping and version that presentation dataset separately.
Discovering bodies across the galaxy
Section titled “Discovering bodies across the galaxy”For a starmap or resource survey covering every system, use the galaxy-wide
collection. This example runs with the sage client created in
Your first read:
const bodies = await sage.celestialBodies.all();console.log(bodies.length, bodies[0]?.name);The functional equivalent uses a ctx context from createSageContext and
returns loaded snapshots without the root client’s relation methods. See
How it thinks for the two API styles:
import { readMeta } from '@aephia/atlas-kit/client';import { getAllCelestialBodies } from '@aephia/atlas-kit/world';
const bodies = await getAllCelestialBodies(ctx, { commitment: 'confirmed' });console.log(bodies.length, readMeta(bodies));This discovers Celestial Bodies belonging to the context’s Game in one
operation, without first listing systems or requesting each system’s bodies.
A configured indexer takes precedence; otherwise the SDK uses targeted RPC
discovery. Indexer candidates still require authoritative reads and validation,
so one discovery operation does not guarantee one network request for every
provider. readMeta reports the selected strategy and available observation
metadata. Reading the bodies does not load Game definition catalogs.
The result is one materialized, readonly array. The context’s
maxDiscoveryResults ceiling defaults to 10,000 entries per RPC or indexer
response. An oversized response rejects with RESOURCE_LIMIT_EXCEEDED; it is
not silently truncated or split into pages. Raising this context option allows
a larger response and increases potential transfer, decoding, and memory costs.
Prefer system.celestialBodies.all() when you only need one system.
Galaxy-wide discovery and direct body reads share the same address-keyed cache.
Resource projections therefore use the same validated body snapshots and
freshness rules described below. Repeating discovery can still perform network
work; cached bodies are not a permanent catalog of galaxy membership. The
finder accepts read options such as { refresh: true } and
{ policy: 'no-store' }, while choosing its own filters and discovery strategy.
Resource values and freshness
Section titled “Resource values and freshness”Resource entries live inside each body snapshot:
const bodies = await system.celestialBodies.all();for (const body of bodies) { if (body.kind === 'planet') { for (const resource of body.details.resources) { console.log(resource.cargoId, resource.richness.value); } } else if (body.kind === 'asteroid') { for (const resource of body.details.resources) { console.log(resource.cargoId, resource.amountMined, resource.miners); } }}Planets expose resource richness. Asteroids expose richness plus amountMined
and miners, which can change with mining activity. Resolving a cargoId to a
cargo definition is a separate catalog read that may
load Game configuration.
These projections reuse the body account’s cache and observation. Use account
read options such as { refresh: true } when you need a new observation.
systemSequenceId identifies a system generation; it is not a verified revision
counter for every change to an individual body’s mining state. A stable
sequence or apparently stable richness does not justify keeping the entire
body forever. See Caching and provenance.
Starbases appear in two places
Section titled “Starbases appear in two places”Shared starbase data — where it is, what level it is — belongs to the world. A player’s own state at that starbase is separate and lives in starbases.
const bases = await system.playerStarbases.all();The split exists because the two have different lifetimes and different readers: the starbase itself is shared infrastructure, while your cargo sitting in it is yours.
Gotchas
Section titled “Gotchas”systems.all() discovers systems, not every body in the galaxy. It uses
configured discovery and can materialize a large result. Prefer known system
reads when they answer your question. Use sage.celestialBodies.all() for bodies
across the galaxy or system.celestialBodies.all() for one system. Definition
preload does not perform either discovery operation.
Bodies are not evenly distributed. A system may have no asteroids, or many. Write for both.
Coordinates are game-space, not screen-space. StarSystem coordinates locate
systems in the galaxy; they do not supply a body’s editor orbit or angle.