# Fleets
Documentation: next (source: develop).

Install the preview: `pnpm add @aephia/atlas-kit@next`. These docs follow develop and may include changes not yet published to npm.

> Inspect Fleet state, create and assemble Fleets, import ships, and plan movement.

Markdown source of https://develop.atlas-kit-docs.pages.dev/guides/fleets/ — see https://develop.atlas-kit-docs.pages.dev/ai/ for the full machine-readable surface.

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.

:::game[Ships operate as fleets]
Ships take part in SAGE as fleets. A fleet can contain many ships, but even one
ship becomes a one-ship fleet before it starts travelling or working. The
fleet moves, carries supplies, mines, scans, and fights as one group. Which
ships you put together determines how much the fleet can carry and how well it
can perform those jobs.
:::

## Listing a character's fleets

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

> **Runnable example — Read a fleet.** Walk from a profile to its character, then to the fleets that character owns. Run it in the browser at https://develop.atlas-kit-docs.pages.dev/guides/fleets/.

## Reading known Fleets in one batch

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

```ts
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.

## Ship configuration catalog

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:

```ts
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](/concepts/definitions/) for preload, persistence, and
section refresh behavior.

### Fixed-point stat units

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

| Field                                                                          | Gameplay value               |
| ------------------------------------------------------------------------------ | ---------------------------- |
| `misc.scanCoolDownSeconds`                                                     | `raw / 65536`, in seconds    |
| `misc.xpValue`                                                                 | `raw / 65536`, configured XP |
| `misc.scanPower`                                                               | `raw / 64`                   |
| `combat.hitChance`, `combat.critChance`, `combat.critMultiplier`               | `raw / 64`                   |
| `combat.stealthPower`, `combat.aimAbility`                                     | `raw / 64`                   |
| `combat.fuelConsumptionRate`, `combat.missileDamage`, `combat.missileFireRate` | `raw / 64`                   |
| All eight `combat.damage*Variance` fields                                      | `raw / 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.

## Creating an empty Fleet

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](#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.

```ts
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.

## Adding ships to a Fleet

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).

```ts
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](https://github.com/Aephia/atlas-kit/issues/517).

## Loading and unloading crew

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):

```ts
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.

## Importing wallet ships

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](/guides/import-onboarding/) 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.

```ts
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](/guides/moving-a-fleet/#sign-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

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:

```ts
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.

:::game[Warp trades fuel for time]
Fleets can travel by warp or subwarp. Warp gets a fleet to its destination
much faster, but it costs more fuel and the fleet can only warp so far before
it must wait. Subwarp is the slower, cheaper option when time matters less than
fuel. The ships in the fleet decide its actual speed, fuel use, warp range, and
waiting time.
:::

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.

## What hangs off a fleet

```ts
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.

> **Runnable example — Read a fleet's cargo.** Follow a relationship one step further: from a fleet to what it is carrying. Run it in the browser at https://develop.atlas-kit-docs.pages.dev/guides/fleets/.

## Gotchas

**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.

## Reference

- [`fleets`](/reference/fleets/) — every export in this entry
- [`fleets/actions`](/reference/fleets/actions/) — ship import and Fleet action planners
- [`cargo`](/guides/cargo/) — what a fleet is carrying
- [`world`](/guides/world/) — where a fleet's state points
- [Warp & subwarp](/guides/moving-a-fleet/) — plan and execute an explicit move

## Recovering a stranded Fleet

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):

```ts
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](https://github.com/Aephia/atlas-kit/pull/647#issuecomment-5752952117)
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.

## Discovering every Fleet address in a Game

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

```ts
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.
