Scanning
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';Current SDK scope
Section titled “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
Section titled “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.
const patterns = await getScanPatterns(ctx);State is per fleet, and per character
Section titled “State is per fleet, and per character”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.
Cooldowns are derived
Section titled “Cooldowns are derived”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.
Gotchas
Section titled “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
Section titled “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):
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
Section titled “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.
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
Section titled “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
Section titled “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):
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
Section titled “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):
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
Section titled “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):
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
Section titled “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:
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.
Reference
Section titled “Reference”scanning— reads and pure helpersscanning/actions— explicit scanning lifecycle planningfleets— the fleet doing the scanning