# Scanning
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.

> Scan patterns, Fleet signals, policies, regional loot and cooldowns.

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

Scanning is how fleets survey space for things worth finding. Like combat, it is
a capability-only entry, imported directly rather than reached through the root
client.

```ts
import {
  getFleetScanningState,
  getScanPatterns,
} from '@aephia/atlas-kit/scanning';
```

:::game[Scanning looks for contacts]
Scanning lets a fleet search its surroundings for useful finds. The scan pattern,
its policy and the region's loot profile together shape the scan. Detection is
followed by resolution: the result may be a contact or no contact. Recovering a
contact is a separate action, not an automatic reward from starting a scan.
:::

## Current SDK scope

This entry reads ScanPattern definitions, Fleet/Character projections, Fleet
signals, pattern policies and regional loot profiles. The separate
`scanning/actions` entry provides explicit signal setup, including Docked Fleets,
detection for an Idle Fleet with an existing Empty signal, and explicit reveal,
recovery and result-handling actions. Pure helpers provide labelled loot-selection
estimates. These do not establish full eligibility for an action or guarantee
random rewards.

## Patterns are the catalog

A scan pattern is a definition: what a kind of scan does and what it costs.
Patterns are shared definitions. Reuse them through the context cache, and
refresh when you need current configuration.

```ts
const patterns = await getScanPatterns(ctx);
```

> **Runnable example — Read scanning patterns and cooldown.** The scan pattern catalog, one fleet scanning state, and its derived cooldown. Run it in the browser at https://develop.atlas-kit-docs.pages.dev/guides/scanning/.

## State is per fleet, and per character

Two projections, answering different questions:

```ts
const fleetState = await getFleetScanningState(ctx, fleetAddress);
const characterState = await getCharacterScanningState(ctx, profileAddress);
```

The fleet's state is about this fleet's scanning. The character's is the wider
picture across what they own — and note it starts from the **profile**
address, not the character's: the SDK derives the character from the profile
for you.

`fleetState.stats.cooldownSeconds` and `.power` are numeric gameplay values.
The corresponding [Fleet and configuration stats](/guides/fleets/#fixed-point-stat-units)
retain both the exact encoded integer and its translated value. Scanning cost,
bonus and the cooldown-end timestamp keep their existing representation.

## Cooldowns are derived

```ts
const cooldown = deriveScanCooldownState(fleetState, nowUnixSeconds);
```

Note that this is a **plain function, not a read**. Cooldown is computed from
the Fleet cooldown-end timestamp and the time you supply — there is no account to
fetch that says "ready in 40 seconds".

That is a recurring shape in this SDK: anything time-dependent is derived at the
moment you ask, because storing it would mean storing something that is wrong a
second later.

:::game[One fleet cannot scan continuously]
After a scan, the fleet must wait before it can scan again. Different fleets
wait for different amounts of time because their ships and scanning abilities
are not the same. The chosen scan pattern can also make the wait longer or
shorter. This gives players a reason to build specialised scanning fleets and
to choose carefully when and where to use them.
:::

## Gotchas

**A cooldown is only as fresh as its inputs.** If you derive from a cached
snapshot, you are computing against that snapshot's age. Re-read before
deriving if precision matters.

**Not composed into the root client.** Import from `@aephia/atlas-kit/scanning`.

**A Fleet that has never scanned still has scanning stats.** The Fleet read
returns its stats and cooldown-end timestamp; derive readiness from that timestamp.

## Read the current signal and configuration

Each read derives one account for the configured Game. The `maybeGet` forms
return `undefined` when it has not been created; malformed accounts still throw.
The corresponding `get` forms require the account to exist. These reads share
the context cache and accept normal `ReadOptions`, including `refresh`,
commitment and cancellation. `readMeta(snapshot)` reports the source slot and
derived strategy. They do not read or create the Fleet itself.

Illustrative example (supply your context, Fleet address and IDs):

```ts
import {
  maybeGetScanSignalForFleet,
  getScanPatternPolicy,
  getScanLootProfile,
} from '@aephia/atlas-kit/scanning';

const signal = await maybeGetScanSignalForFleet(ctx, fleetAddress);
const policy = await getScanPatternPolicy(ctx, 2);
const regionalLoot = await getScanLootProfile(ctx, 3);
console.log(signal?.status, policy.strategy, regionalLoot.loot.length);
```

A signal has one of seven statuses: `empty`, `pending-entropy`, `contact`,
`no-contact`, `recovered`, `abandoned` or `expired`. `attempt` is present only
once the signal is nonempty. `contact` describes an available target and cargo;
`receipt` records recovered cargo amounts; `noContact` records its XP outcome
without inventing cargo. `ended` retains expiry and terminal timestamps for
abandoned/expired signals. Empty signals deliberately omit stale attempt fields,
even if their sequence is nonzero.

Coordinates and target radius are map units with exact signed I8F56 `raw`
integers retained beside `value`. Timestamps are named `…AtUnixSeconds` (or
`cooldownEndsAtUnixSeconds`) and use bigint; reveal/commit slots remain bigint
slot numbers. Raw cargo quantities and XP are exact bigint. Basis points use
10,000 for 100%; regional risk multipliers may exceed 10,000. Loot weights are
relative integers, not probabilities. All nested values are immutable and
`JSON.stringify` serializes bigint as decimal strings.

The snapshots expose configuration fingerprints, supported pattern IDs, allowed
focus categories and value-budget inputs for the probability helpers below.
Quantity modes and effective scan-power encoding remain omitted: their meaning
is not established well enough to offer quantity estimates. Use the bindings
entry if those raw fields are needed.

## Estimate regional loot chances

`deriveScanLootOdds` estimates which resource a scan might find. It first
excludes rows that do not match the pattern, maximum rarity, value budget,
focus or unlocked research, then divides each resource's eligible weight by
the total eligible weight. Duplicate rows for the same cargo are combined.

- **Conditional probability** means the resource's share **given a contact**.
- **Per-scan probability** includes the chance of finding nothing.
- Probabilities are numbers from **0 to 1**. Multiply by 100 for a percentage.

Every result is labelled `client-derived-estimate`. The official client provides
the eligibility rules and weighted category shares, but the program's random
draw has not been independently reproduced. These are estimates, not exact
program probabilities, promises of cargo recovery, or quantity forecasts.

Illustrative example using loaded player/region state and an explicit chain
time. Get region requirements from `getTerritoryRegions` in `factions`; a
Character's unlocked tags are `character.modifiers.values.researchTags`.

```ts
import {
  getScanPatternPolicy,
  getScanLootProfile,
  maybeGetScanSignalForFleet,
  deriveScanExhaustion,
  deriveScanLootOdds,
} from '@aephia/atlas-kit/scanning';

const policy = await getScanPatternPolicy(ctx, patternId);
const loot = await getScanLootProfile(ctx, region.id);
const signal = await maybeGetScanSignalForFleet(ctx, fleetAddress);
const exhaustion = deriveScanExhaustion(policy, signal, {
  regionId: region.id,
  unixSeconds: chainUnixSeconds,
});
const odds = deriveScanLootOdds(policy, loot, {
  focus: 'any',
  unlockedResearchTagIds: character.modifiers.values.researchTags,
  requiredRegionResearchTagIds: region.scanningResearchRequirementTagIds,
  exhaustionBasisPoints: exhaustion.exhaustionBasisPoints,
});
if (odds.status === 'available') {
  for (const resource of odds.resources) {
    console.log(resource.cargo.name, resource.perScanProbability * 100);
  }
} else {
  console.log(odds.reasons);
}
```

Pass known research sets explicitly: an empty array means known empty, not
unknown. Likewise, supplying zero exhaustion explicitly assumes none; use the
history helper with a known-present or known-absent signal and chain time to
calculate it. Repeated recoveries in the same region reduce contact chance;
configured decay intervals remove effective recoveries over time. Do not use
an unrelated Fleet's signal for that history.

Results retain eligible rows, excluded row indexes with reasons, total weight,
resource totals and configuration provenance. Inactive/locked configuration,
an unsupported focus, or an empty/zero-weight pool returns `unavailable` without
inventing probabilities. A valid zero contact chance is different: conditional
shares are still available, while all per-scan probabilities are zero.
The estimate assumes an otherwise valid detection; it does not check Fleet
state, authority, inventory or cooldown. Reads use the ordinary context cache;
use `{ refresh: true }` when a new decision needs fresh configuration. The
pure helpers themselves perform no reads.

### A scan already in progress

For a `pending-entropy` signal, use
`deriveCommittedScanLootOdds(signal, policy, lootProfile)` with the pattern and
region recorded in `signal.attempt`. It uses the scan's committed eligible rows
and contact chance. Both configuration revisions **and fingerprints** must
match; refreshing into a changed table does not make the old scan compatible.
Today's research or exhaustion must not replace the committed values.
A resolved or terminal scan has a known result and is not a new forecast.

As an example from the September 22 PTR configuration, region 1's Broad Spectrum
pool has 33 eligible rows. With no exhaustion, the estimated Fuel chance is
**6.53% given a contact**, or **5.55% per scan** after the 85% contact chance.
These are dated examples, not constants applications should embed.

## Signal timing and recovery distance

`deriveScanSignalTiming(signal, { slot, unixSeconds })` is a pure projection:
pass your explicit bigint chain clock values, without a context or network call.
It reports remaining recorded cooldown, pending entropy and Contact expiry.
The observed client opens the reveal window at `commitSlot + 10` (inclusive)
and closes it 256 slots later (exclusive). Contact expiry includes equality:
`unixSeconds >= expiresAtUnixSeconds`. These are client timing observations,
not proof that an instruction will succeed. In particular, do not use cooldown
as acknowledgement eligibility: the current optional-Fleet abandonment route
can shorten it.

`deriveScanRecoveryRange(signal, location)` compares exact I8F56 raw coordinates
against the Contact radius. The boundary is included; floating-point `value`
fields are ignored. Supply the current position in the same raw I8F56 units.
`FleetSnapshot.location` contains display numbers, not this raw-coordinate shape;
do not round-trip those numbers to invent exact bits, or substitute the original
scan-attempt location for the Fleet’s current position. Its squared raw outputs
use scale `2^112`. Geometry does not
check expiry, ownership, capacity or recovery permission. Without a Contact it
returns `kind: 'no-contact'`; absent/Empty/terminal signals have no entropy or
Contact-expiry result. Invalid negative slots/radii or missing required active
state fields throw `INVALID_DERIVATION_INPUT` with a refetch/input remedy.

Illustrative example (supply a fresh signal and current exact coordinates):

```ts
import {
  deriveScanSignalTiming,
  deriveScanRecoveryRange,
  type ScanSignalSnapshot,
  type ScanCoordinates,
} from '@aephia/atlas-kit/scanning';

declare const signal: ScanSignalSnapshot | undefined;
declare const location: ScanCoordinates;
const timing = deriveScanSignalTiming(signal, {
  slot: 54_000_000n,
  unixSeconds: 1_790_000_000n,
});
const range = deriveScanRecoveryRange(signal, location);
console.log(timing.entropy?.phase, range.kind);
```

## Explicit signal setup

`planOpenScanSignal` prepares one inert Plan for an absent canonical Game + Fleet
signal. Pass an explicit Fleet address and the directly owning Profile's
address-only authority/key index (0–65535). Docked Fleets are supported; setup
has no Idle requirement. An existing signal is a refetch/reuse error, never an
implicit no-op or replacement. Certificates and substitute PDA/metas are rejected.

Illustrative example (supply your context, addresses and fee payer):

```ts
import { planOpenScanSignal } from '@aephia/atlas-kit/scanning/actions';
import { simulatePlan } from '@aephia/atlas-kit/planning';

const setup = await planOpenScanSignal(ctx, fleetAddress, {
  authorization: {
    profile: profileAddress,
    authority: authorityAddress,
    keyIndex: 0,
  },
});
console.log(setup.describe());
// Requires an explicitly configured write transport, but no signer.
const simulation = await simulatePlan(ctx, setup, {
  feePayer: feePayerAddress,
});
```

The Profile pays the signal's current calculated rent while retaining its own
storage reserve. Transaction fees belong to the separate fee payer. The SDK
never deposits funds implicitly or hard-codes a measured rent amount. Funding and the approved debit ceiling are rechecked
before signing, including combined Profile obligations; this is not an atomic
on-chain spending cap.

The Plan describes its program, signer, writable Profile/signal, preconditions
and expected Empty sequence-zero result. It validates fresh Fleet, Profile and
canonical owner/controller Character relationships, the selected direct key,
expiry, relevant scope and absence. Saved Plans preserve finite safeguards and
reject changed authorization, identity, absence or insufficient funding before
signing. Unrelated Game definitions do not stale setup. Recreate/refetch instead
of editing unsupported or altered saved Plans.

Instruction permission acceptance remains chain-authoritative. The qualified
Profile-program-scoped key had bits 0–11 and 63; restricted-key least privilege
is **not qualified**, and legacy SCAN bit 38 is not established as sufficient.
Simulate before explicit execution. Planning never signs or submits. Detection,
implicit open-before-detect, cargo funding and all later signal lifecycle
operations remain outside this action.

## Plan signal detection

`planDetectSignal` prepares one inspectable detection Plan. Choose the pattern
and focus explicitly; the SDK refreshes the Fleet, signal, authority and relevant
configuration before planning. Detection requires a directly owned, currently
Idle Fleet, an existing Empty signal, elapsed cooldown and the selected pattern's
cargo costs aboard the Fleet. Its stored ship-definition sequence must match the
current Game: if it is stale, synchronize the Fleet through a supported game
action and replan. This SDK restriction avoids quoting scan costs from outdated
stats; it does not claim the program itself rejects every stale Fleet.

Illustrative example (supply your context, addresses and selected pattern):

```ts
import { planDetectSignal } from '@aephia/atlas-kit/scanning/actions';
import { simulatePlan } from '@aephia/atlas-kit/planning';

const plan = await planDetectSignal(ctx, fleetAddress, {
  patternId: selectedPatternId,
  focus: 'any',
  authorization: {
    profile: profileAddress,
    authority: authorityAddress,
    keyIndex: 0,
  },
});
console.log(plan.describe());
const simulation = await simulatePlan(ctx, plan, {
  feePayer: authorityAddress,
});
console.log(simulation);
```

Focus is `any`, `industrial`, `technology`, `tactical` or `exotic`; named focus
choices must be enabled by the selected policy. Pattern IDs and cargo costs
come from current configuration, not a fixed SDK list. Amounts remain exact raw
units. Review the Plan's required resources and simulate before explicitly
executing it with your signer.

**Setup is a separate transaction.** If the signal is absent, create it through
`planOpenScanSignal`, confirm that transaction and refetch before
planning detection. A PendingEntropy, Contact or terminal signal is not Empty;
finish its lifecycle and refetch. This planner never creates a signal, deposits
or loads cargo, undocks, settles movement, or resolves the scan automatically.
Resolution, recovery and acknowledgement are separate lifecycle actions.

The saved Plan binds the selected pattern/focus, signal sequence and configuration
revisions. Execution refreshes the relevant identities, Idle/Empty state,
cooldown and resources before signing. Changed intent or stale state requires a
fresh Plan; unrelated inventory and shared-account activity do not intentionally
invalidate it. These checks cannot eliminate races after the final read, and
simulation does not guarantee a particular random reward.

The qualified PTR path uses the owner's broad Player Profile key. Key identity,
expiry and scope are checked, but instruction permission acceptance remains
chain-authoritative; a restricted-key minimum mask has not been qualified.
Delegated certificates and moving-Fleet detection are outside this planner's
scope. Cooldown checking follows the current source contract; its rejection was
not separately measured in the live qualification.

## Reveal, recover and clear a result

Every lifecycle action prepares an inert Plan. Inspect it, simulate, execute
explicitly and wait for confirmed success. Then refresh the signal before
preparing the next action: resolution, recovery and acknowledgement each advance
its sequence. These completion Plans must stand alone; no automatic polling,
travel, retry or follow-up transaction is included.

| Current signal                                  | Explicit action                | Expected transition                              |
| ----------------------------------------------- | ------------------------------ | ------------------------------------------------ |
| Pending entropy, inside reveal window           | `planResolveSignal`            | Contact or NoContact                             |
| Live contact, Idle Fleet inside recovery radius | `planRecoverSignal`            | Recovered receipt                                |
| Live contact you choose to discard              | `planAbandonSignal`            | Abandoned                                        |
| Contact past its deadline                       | `planExpireSignal`             | Expired                                          |
| Pending entropy past the reveal deadline        | `planForfeitScanSignalEntropy` | NoContact                                        |
| NoContact, Recovered, Abandoned or Expired      | `planAcknowledgeScanResult`    | Empty, ready for reuse subject to Fleet cooldown |

Resolution, expiry and entropy forfeiture need no gameplay Profile authority;
executing their transactions still needs an explicit fee payer. Recovery,
abandonment and acknowledgement require direct Profile authorization. Delegated
controllers and restricted-key minimum permissions are not qualified.

Permissionless completion Plans record the canonical SAGE program address as
their guard authority with key index `0`. Consumers can compare that value
through the root package without importing raw bindings or internal modules:

```ts
import { SAGE_PROGRAM_ADDRESS } from '@aephia/atlas-kit';
import { planResolveSignal } from '@aephia/atlas-kit/scanning/actions';

const reveal = await planResolveSignal(ctx, fleetAddress);
const guard = reveal
  .toJSON()
  .preconditions.find(
    (precondition) => precondition.kind === 'scan-completion',
  );
const usesSageProgram = guard?.authority === SAGE_PROGRAM_ADDRESS;
```

This is an inspection value, not a signer, permission grant or configurable
program override. Profile-authorized recovery, abandonment and acknowledgement
continue to use their direct Profile authority checks.

Illustrative example (existing context and Fleet address):

```ts
import {
  planResolveSignal,
  planRecoverSignal,
  planAcknowledgeScanResult,
} from '@aephia/atlas-kit/scanning/actions';

// Prepare only the action appropriate to the freshly read signal.
const reveal = await planResolveSignal(ctx, fleetAddress);
console.log(reveal.describe());

// After a separately confirmed reveal yields a Contact, travel and settle
// separately. Prepare recovery once the Idle Fleet is in range.
const recover = await planRecoverSignal(ctx, fleetAddress, { authorization });
console.log(recover.describe());

// After separately confirming recovery, inspect its receipt before clearing it.
const acknowledge = await planAcknowledgeScanResult(ctx, fleetAddress, {
  authorization,
});
console.log(acknowledge.describe());
```

Recovery consumes the stored contact, not a new roll from the regional loot
pool. Its nominal cargo and XP are not promises: inspect the refreshed
`signal.receipt` for accepted cargo, clipping and XP actually awarded. Capacity
and final outcomes remain chain-authoritative. The SDK supports Idle recovery;
this is not a claim that the program rejects every moving Fleet.

Acknowledgement retains the funded scanning account and clears its result. It
supplies the Fleet for the current abandoned-result cooldown behavior; it does
not impose the older website's extra acknowledgement wait. Read the new Fleet
cooldown before detecting again. Expired contacts must use expiry rather than
abandonment. An overdue unresolved attempt can be forfeited explicitly instead
of leaving the signal stuck.

The planners check fresh signal identity, sequence, state and applicable timing,
range, direct authority, committed configuration and current XP accounts. They
repeat those checks before signing. Slot-window projections do not guarantee
entropy availability at execution; simulate and handle a typed failure. Witness
refresh and closing a signal are outside these planners.

Qualification combines two finalized on-chain reveal/recovery histories with
correlated retained receipts and unsigned terminal/range controls. It does not
claim historical pre/post cargo snapshots, successful partial-capacity recovery,
or every moving/delegated route.

## Reference

- [`scanning`](/reference/scanning/) — reads and pure helpers
- [`scanning/actions`](/reference/scanning/actions/) — explicit scanning lifecycle planning
- [`fleets`](/guides/fleets/) — the fleet doing the scanning
