Skip to content
Next — unreleased

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.

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

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.

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.

const patterns = await getScanPatterns(ctx);

Two projections, answering different questions:

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 retain both the exact encoded integer and its translated value. Scanning cost, bonus and the cooldown-end timestamp keep their existing representation.

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.

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.

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

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.

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.

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.

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.

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

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

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

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.

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

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.

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 signalExplicit actionExpected transition
Pending entropy, inside reveal windowplanResolveSignalContact or NoContact
Live contact, Idle Fleet inside recovery radiusplanRecoverSignalRecovered receipt
Live contact you choose to discardplanAbandonSignalAbandoned
Contact past its deadlineplanExpireSignalExpired
Pending entropy past the reveal deadlineplanForfeitScanSignalEntropyNoContact
NoContact, Recovered, Abandoned or ExpiredplanAcknowledgeScanResultEmpty, 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:

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

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.