Skip to content
Next — unreleased

A fleet is a group of ships that move and act together. Ships do not travel individually, so the fleet is the unit almost everything else attaches to: cargo, mining, scanning, combat.

const character = await sage.characters.forProfile(profileAddress);
const fleets = await character.fleets.all();

When you already know several Fleet addresses, use getFleets to resolve them through one data-plane batch instead of issuing separate getFleet calls:

import { getFleets } from '@aephia/atlas-kit/fleets';
import type { Address, SageContext } from '@aephia/atlas-kit/client';
declare const ctx: SageContext;
declare const fleetAddresses: readonly Address[];
const fleets = await getFleets(ctx, fleetAddresses, {
commitment: 'confirmed',
});
console.log(fleets.map((fleet) => fleet.name));

The frozen result preserves input order and repeated positions. The data plane deduplicates uncached addresses, uses the provider’s batch route when available, and retains each Fleet snapshot’s own read metadata and cache identity. Every address is required: if any requested Fleet is missing or invalid, the call rejects without returning a partial result. An empty address array returns a frozen empty array without account or registry reads.

You can inspect ship configurations without owning or loading a Fleet. List the catalog, resolve one configuration by its id, or group configurations by the ship mint from the selected deployment:

import { resolveShip } from '@aephia/atlas-kit';
import {
listShipConfigurations,
listShipConfigurationsByMint,
} from '@aephia/atlas-kit/fleets';
const configurations = await listShipConfigurations(ctx);
const first = configurations[0];
if (first !== undefined) {
const configuration = await resolveShip(ctx, first.id);
const sameShip = await listShipConfigurationsByMint(ctx, configuration.mint);
console.log(configuration.stats.cargo.cargoCapacity);
console.log(configuration.stats.cargo.miningRate.value);
console.log(sameShip.map((row) => row.id));
}

A configuration’s stats describe that configuration; fleet.stats describes the Fleet’s effective stats. The mint groups configurations belonging to one ship in this Game deployment. The grouping does not choose a default configuration.

All three routes reuse the same immutable objects in a loaded ships section. Once loaded, grouping by mint adds no RPC calls or metadata requests. See Definition catalogs for preload, persistence, and section refresh behavior.

Both configuration stats and aggregate fleet.stats expose these fields as { raw: bigint, value: number }:

FieldGameplay value
misc.scanCoolDownSecondsraw / 65536, in seconds
misc.xpValueraw / 65536, configured XP
misc.scanPowerraw / 64
combat.hitChance, combat.critChance, combat.critMultiplierraw / 64
combat.stealthPower, combat.aimAbilityraw / 64
combat.fuelConsumptionRate, combat.missileDamage, combat.missileFireRateraw / 64
All eight combat.damage*Variance fieldsraw / 64

The variance fields cover Kinetic, Energy, Emp, Superchill, Shockwave, Graygoo, Heat and Bomb. Their corresponding base damage fields remain numbers.

For example, cooldown raw 47185920n means 720 seconds, XP raw 655360n means 10, and critical chance raw 96n means 1.5. Use .value for the translated units and .raw for the exact encoded integer. JSON.stringify writes raw values as decimal strings. Chance fields are not presented as percentages, and configured XP is not a promise about XP awarded by an action. These conversions match the game client’s display units, without defining its combat formulas.

0.x API correction (#575, #608): these fields previously returned the encoded number directly. Callers must now choose .value or .raw; no scalar alias or migration is provided. Persisted scalar-shaped ship stats are incompatible cache entries and reload lazily. The focused scanning projection still returns numbers at stats.cooldownSeconds and stats.power, in the correct units. Other scalar fields, such as misc.scanBonus, remain unchanged.

Use planCreateFleet to create an empty Fleet at a Starbase where your Profile already has a registered StarbasePlayer. The supported route uses an own-faction Starbase, including non-CSS Starbases, and direct Profile-key authorization. Your Profile, Character and faction membership must already exist.

The Fleet starts Docked with no ships and consumes one of your Character’s Fleet slots. Creation and importing wallet ships are separate operations; importing ships does not assign them to a Fleet.

This example is illustrative. Supply your application’s context, System address and explicit authorization; it only prepares an inspectable Plan.

import {
planCreateFleet,
type PlanAuthorization,
} from '@aephia/atlas-kit/fleets/actions';
import type { Address, SageContext } from '@aephia/atlas-kit/client';
declare const ctx: SageContext;
declare const system: Address;
declare const authorization: PlanAuthorization;
const plan = await planCreateFleet(ctx, {
authorization,
system,
label: 'Survey Team',
});
console.log(plan.describe());

The exact label, Profile and Game determine the Fleet address. Labels must fit in 64 UTF-8 bytes, which can be fewer than 64 characters. The SDK rejects blank labels, embedded NUL and invalid Unicode, and never silently truncates or normalizes accepted input. Leading and trailing whitespace is significant.

The Profile pays only the Fleet account’s remaining rent requirement while retaining its own rent reserve. Existing native funds at the derived Fleet address reduce this debit. Transaction fees are separate; creation does not top up your Profile or add a funding instruction.

A direct Profile AUTH key is supported. A SAGE-scoped manageFleet key needs drainSolVault only if the Profile must fund a shortfall. Vault withdrawal authority is a powerful permission: the SDK never grants it automatically. Certificate delegation and other faction-access routes are not supported by this planner yet.

The Plan’s persistence records a maximum observed Profile debit. Before signing, execution rereads the required accounts and rejects increased debit, insufficient funds, expired or revoked authorization, exhausted capacity, changed required relationships or a conflicting target. Additional prefunding can reduce the debit. This is an SDK freshness check, not an atomic on-chain spending limit; rent and funding can change after the check. Unrelated Game definitions and account activity do not invalidate it. Same-Character creation Plans cannot be combined; prepare another Plan after the first creation is confirmed.

Simulate before requesting signatures. Final program eligibility, including Character modifier updates, remains chain-authoritative. After confirmed execution, read the Fleet at its derived target (shown in the Plan) and refresh the Character to verify the new Fleet and slot usage. A submitted transaction alone does not prove creation.

Use planAddShipToFleet to assign ships already held in your StarbasePlayer escrow to an existing Fleet. Pass the Fleet’s address, the exact configuration ID and a positive bigint quantity. Wallet import, Fleet creation and adding crew remain separate operations.

The Fleet must be Docked at a Starbase where your Character has a current StarbasePlayer of the Fleet’s faction. That can be any Starbase level, owned by any faction. The planner follows the game’s assembly rules:

  • Each added ship must be within your Character’s maximum ship size, and your Character must hold every research tag the configuration requires. Ships already aboard are not rechecked.
  • The resulting Fleet must fit your Character’s Fleet size and the Game’s limit, counted in Fleet points: XXS 1, XS 4, Small 9, Medium 16, Large 25, Capital 36, Commander 49 and Titan 128. A Fleet holds at most one Titan, and at most two Commanders and Titans together.
  • Hulls you escrowed under any configuration of the same ship count. The game re-tags them to the configuration you add.

Two routes are not planned yet: configurations that need ship components, and Fleets built under older ship definitions (the game resynchronizes those during assembly, which can change their stats).

import {
planAddShipToFleet,
type PlanAuthorization,
} from '@aephia/atlas-kit/fleets/actions';
import type { Address, SageContext } from '@aephia/atlas-kit/client';
declare const ctx: SageContext;
declare const fleetAddress: Address;
declare const authorization: PlanAuthorization;
declare const shipId: number; // An explicitly selected supported configuration.
const plan = await planAddShipToFleet(ctx, fleetAddress, {
authorization,
shipId,
amount: 1n,
});
console.log(plan.describe());

This illustrative example prepares a Plan only. The amount must be 1n–255n whole ships and fit escrow availability. The resulting Fleet, including its existing ships, must fit both the Character’s stored Fleet size and the Game’s limit in Fleet points. Both limits are rechecked before signing; changes that leave the composition within both limits are allowed. The SDK derives the Character, System and StarbasePlayer plumbing; there are no map indices for you to resolve. It prepares one instruction and does not chunk larger requests.

A new configuration entry can grow the Fleet account, and re-tagging adds an escrow entry to your StarbasePlayer when the configuration has none. Your Profile funds the remaining rent of both while preserving its own reserve; existing native funds at the Fleet reduce the shortfall. An already-present key uses its existing storage, including when its quantity is zero. Fully consuming an escrow entry leaves that entry present at zero; it does not promise a rent refund. Transaction fees are separate.

Use direct Profile AUTH, or a manageFleet key scoped to SAGE or to the Fleet itself. A positive Profile-funded shortfall additionally requires drainSolVault for that key. That permission allows vault withdrawals and is never granted automatically. Explicit Fleet prefunding can remove the observed shortfall; the planner adds no funding step. Certificate delegation is not supported.

Plan persistence retains the exact selected intent, selected configuration hash and maximum observed Profile debit. Before signing, execution rereads the required accounts and checks support limits, relationships, entitlement, capacity, permission/expiry and current rent funding. Lower debit from prefunding is allowed; a higher debit requires replanning. This is a pre-sign check, not an atomic on-chain spending cap. Final statistics, modifier refresh and chain outcomes remain authoritative, so simulate and inspect before signing.

Two assembly Plans using the same Profile cannot be combined: independent observations do not reserve shared funds or capacity. Execute and confirm one, then prepare the next from fresh state. Support for research/component-bearing configurations, larger or mixed-size Fleets and other sequence/access routes is tracked in #517.

Use planLoadFleetCrew and planUnloadFleetCrew to move an explicit number of ordinary crew between a directly owned Docked Fleet and its StarbasePlayer. The System and StarbasePlayer are derived from the Fleet; neither operation imports crew from a wallet or assigns them to crafting.

Illustrative example (supply your context, Fleet address and direct Profile authorization):

import {
planLoadFleetCrew,
planUnloadFleetCrew,
} from '@aephia/atlas-kit/fleets/actions';
// Choose the action appropriate to the freshly read state.
const unload = await planUnloadFleetCrew(ctx, fleetAddress, {
count: 1,
authorization,
});
console.log(unload.describe());
// After separately executing and confirming an unload, refetch and prepare
// a new load Plan when you want the crew aboard again.
const load = await planLoadFleetCrew(ctx, fleetAddress, {
count: 1,
authorization,
});
console.log(load.describe());

Inspect and simulate each Plan before explicit execution. Transfers stand alone; confirm and refresh before preparing another transfer or movement action. Counts are positive whole numbers from 1 to 65,535. Loading checks available Starbase crew and Fleet capacity; unloading checks the number aboard. Relevant state is checked again before signing, although another transaction can still change it afterward.

Existing reads tell you what is available: fleet.crewCount, fleet.stats.misc.requiredCrew, fleet.stats.misc.passengerCapacity, and the StarbasePlayer’s totalCrew, busyCrew and availableCrew. Maximum aboard crew is required crew plus passenger capacity. Busy Starbase crew cannot be loaded.

Unloading everyone while docked is allowed. The Plan warns when the remaining crew are below departure requirements. Put enough crew back aboard before undocking; docking does not itself assign them to crafting, and this planner does not start a crafting job.

This route supports ordinary, non-rented crew with no Character roster page zero and no populated Fleet crew binding. Individual crew, skills, bonuses, renting and delegated authority are outside its scope. A stale Fleet ship-definition sequence requires synchronization through a supported game route and a fresh Plan. No roster accounts or funding transfers are created implicitly.

Use planDepositShipsToGame from @aephia/atlas-kit/fleets/actions to bring wallet ship tokens into the game. Start with an existing Profile, Character and Starbase Player at a permitted Central Space Station (CSS); see Preparing wallet imports for missing prerequisites. Importing credits that player’s ship escrow. It does not create a Fleet or add ships to one.

New import Plans record the maximum observed Profile debit for player storage. Before signing, the SDK recalculates the actual shortfall from fresh balances and Rent data, retains the Profile’s own reserve, and sums permitted mixed imports, Fleet creation and assembly debits for that Profile. Target prefunding can lower the debit; a larger shortfall requires replanning. Existing inventory and authorization checks still apply. These checks do not reserve funds on-chain.

Imports of different configurations into the same Starbase Player require separate confirmed Plans, so they cannot each reuse the same prefunding.

Choose a configuration explicitly from this deployment’s ship catalog. Its mint must match the wallet tokens. The SDK does not guess a default from a name or ID. Unsigned PTR checks accepted both a default and a same-mint non-default configuration, even when the latter lists research and component requirements. That proves import behavior on the captured deployment, not eligibility for later assembly or upgrades. Simulate the actual Plan against current state.

import { planDepositShipsToGame } from '@aephia/atlas-kit/fleets/actions';
import type { PlanAuthorization } from '@aephia/atlas-kit/fleets/actions';
import type { Address, SageContext } from '@aephia/atlas-kit/client';
import type { StarbasePlayerSnapshot } from '@aephia/atlas-kit/starbases';
declare const ctx: SageContext;
declare const starbase: StarbasePlayerSnapshot;
declare const authorization: PlanAuthorization;
declare const owner: Address;
declare const shipId: number;
declare const payer: Address;
const plan = await planDepositShipsToGame(ctx, starbase, {
shipId,
amount: 1n,
owner,
authorization,
escrowAccount: { mode: 'create-if-missing', payer },
});
console.log(plan.describe());

owner signs for the wallet tokens; authorization identifies the Profile key that consents to the game import. This route requires a direct Profile AUTH key; delegated Profile authorization is not supported. Both roles must sign when their addresses differ. The source defaults to the owner’s associated token account (ATA); source may select an existing original Token account with the same owner and mint. Amounts are positive raw bigint quantities, and the supported ship mint has zero decimals. Token-2022, delegated token spending and multisig are outside this route.

The destination is the Character’s ATA for that mint. It must already exist unless escrowAccount explicitly permits its creation. When creation is needed, the Plan includes an idempotent ATA instruction and the named payer’s signature and storage funding. If the ATA exists and is valid, no creation payer is needed.

Storage has two separate costs. Adding a previously absent configuration entry to shipsInEscrow grows the Starbase Player account, paid from the Profile’s excess native balance. Increasing an existing entry does not grow storage. Creating a missing ATA uses the explicit payer instead. The planner checks observed funding using current Rent data; transaction fees are additional, and concurrent spending can still change what is available before execution.

Review the Plan, simulate and execute once, then verify all three results using fresh reads at or after confirmation:

  • the source wallet token balance decreased by amount;
  • the Character’s token escrow balance increased by amount;
  • the entry with id === shipId in the Starbase Player’s shipsInEscrow increased by amount (quantityRaw retains the exact bigint quantity).

The escrow ATA can back several configurations sharing one mint, so its balance alone does not establish a particular configuration entitlement. Confirmed execution invalidates affected cached reads; it does not automatically prove these expected deltas. If submission has an unknown outcome, inspect the signature and accounts before another attempt. Never retry an import blindly.

New import Plans tolerate unrelated Game definition changes and incoming token receipts. They still require a valid Game, the same complete selected ship configuration, valid token identities and enough source tokens. The Mint is read fresh and must retain original Token ownership, non-executable 82-byte initialized layout, valid optional authority tags, and zero decimals. Supply and mint/freeze authority changes do not invalidate a newly prepared import Plan; final transfer behavior remains chain-authoritative. Profile, Character and player-Starbase observations remain guarded; refresh and replan when relevant state changes.

An import token owner that was absent during planning may remain absent or receive native funding as an empty, non-executable System account before execution. The balance is unrestricted by this guard; other owners or account data are rejected. Owners already present keep their existing owner/data checks. This does not fund the owner, create escrow, or change authorization requirements.

Fleet state is a union, not a status string

Section titled “Fleet state is a union, not a status string”

This is the single most important thing to understand about fleets. A fleet’s state is a discriminated union, and what data is available depends on which variant you have:

if (fleet.state.kind === 'docked') {
const system = await getStarSystem(ctx, fleet.state.system.address);
}

The variants are idle, docked, mining, warp, subwarp, respawn, and claimStakeTransfer.

The reason this matters: a fleet’s location is not a field. There is no fleet.system. Where a fleet is depends on what it is doing — a docked fleet is at a starbase, a warping fleet is between two points with an arrival time, a mining fleet is at a resource. Flattening that into one field would mean inventing a value for cases where it does not exist.

So the pattern is always: narrow on kind first, then read what that variant offers. TypeScript enforces this — reaching for state.system without narrowing is a compile error, not a runtime surprise.

const inventory = await fleet.inventory.get();
const mining = await fleet.mining.get(); // undefined when not mining

Note that mining.get() returns undefined rather than throwing when the fleet is not mining. That is a legitimate state, not an error.

Fleet names are strings, already trimmed. On chain they are fixed-width byte arrays padded with zeros. The SDK decodes and trims them, so fleet.name is "Dread Kraken", not a padded buffer.

An empty fleet list is a valid answer. A character with no fleets returns []. Do not treat it as an error.

NPC-owned fleets decode with faction: 0. Their identity comes from the owning profile, not the faction field.

Movement timings are derived, not stored. Arrival times come from the fleet’s state plus the Game account’s movement rules. Where the SDK cannot verify the arithmetic against the current program version, it declines to guess rather than reporting a number that might be wrong.

  • fleets — every export in this entry
  • fleets/actions — ship import and Fleet action planners
  • cargo — what a fleet is carrying
  • world — where a fleet’s state points
  • Warp & subwarp — plan and execute an explicit move

Recovery is an explicit choice that can lose cargo or other assets. A zero-Fuel Idle Fleet can start timed recovery to its faction CSS, provided the Character has sufficient ATLAS for its ship recovery fees. Profile funds may also pay for Fleet storage growth. The SDK does not deposit funds or initiate recovery when movement fails.

Illustrative, non-signing example (supply your context and addresses):

import {
planFleetStartRespawn,
planFleetCompleteRespawn,
} from '@aephia/atlas-kit/fleets/actions';
declare const factionCss: Address;
const options = {
authorization: { profile, authority, keyIndex: 0 },
system: factionCss,
};
const start = await planFleetStartRespawn(ctx, fleetAddress, options);
console.log(start.describe());
// Separately, after recovery was confirmed and its on-chain timer elapsed:
const complete = await planFleetCompleteRespawn(ctx, fleetAddress, options);
console.log(complete.describe());

Start and completion are separate transactions. There is no fixed timer or paid bypass here. A direct Profile AUTH key or SAGE respawn key is required; a SAGE key also needs drainSolVault when the Profile must fund storage growth. That permission can drain the Profile vault; explicitly prefunding the Fleet avoids that requirement when no growth debit remains.

Inspect and simulate each Plan before signing. The observed ATLAS fee and storage debit are checked again before signing, but transaction races remain possible. Do not assume ships, crew, HP or cargo survive unchanged. The measured PTR recovery returned the same low-risk, empty-cargo Fleet to its CSS with its ship, crew and HP unchanged. Fuel remained zero: refuel before another journey. A subsequent near-CSS test reports one Food preserved through recovery, with ship, crew and HP unchanged. That single item and location do not establish general cargo preservation or outcomes in other regions. Refresh after confirmed execution and establish Docked at the intended CSS before claiming recovery is complete. Completion uses the explicit docking instruction, not the game’s separate disbanding recovery UI route.

Use discoverFleetAddresses when you need every Fleet address for the context’s configured Game, regardless of owner:

import { createSageContext, readMeta } from '@aephia/atlas-kit/client';
import { discoverFleetAddresses } from '@aephia/atlas-kit/fleets';
const ctx = createSageContext({ cluster: 'zink-ptr', rpc });
const fleetAddresses = await discoverFleetAddresses(ctx);
console.log(fleetAddresses, readMeta(fleetAddresses)?.strategy);

The frozen result contains Kit addresses only. It is different from the internal decoded account envelopes used during validation and from hydrated Fleet snapshots returned by getFleet or getFleetsByOwner.

Address-only output does not mean header-only network traffic. The current data plane downloads complete Fleet account bytes and verifies the program owner, discriminator, layout, configured Game and canonical PDA before exposing an address. It does not load the Game registry or resolve ship and cargo definitions. A configured indexer supplies candidate addresses, but the SDK still directly reads and validates every unique non-missing candidate. Without an indexer, discovery uses only the stable discriminator and Game filters; it does not add a state, tail, data-size or owner filter.