# You
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 wallets, Profiles, Characters, and permissions; plan Profile creation and Character registration.

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

Almost every read starts by working out **who** you are asking about. That chain
has three links, and one of them is harder than it looks.

```text
Wallet ──▶ Profile ──▶ Character
```

A **wallet** is a Solana account someone controls. A profile
is their on-chain identity across Star Atlas. A character
is that profile's presence inside SAGE specifically — the
thing that owns fleets and accrues progress.

## Getting a character

If you already have a profile address, one call gets you the character:

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

That reads an existing Character; it does not register one. Use the separate
`planRegisterCharacter` from `@aephia/atlas-kit/identity/actions` to prepare
explicit registration with a funder. Current Game-2 registration derives and
creates the canonical Character and Career XP budget in the same instruction;
the Game-owned XP configuration must already exist. The explicit on-curve
funder is the sole instruction signer; Profile authority is not inferred from it.

Registration creates Character v0 and reconciled Career XP budget v1 (marker 1).
The qualified initializer allocates 415 Character bytes and 280 budget bytes,
then unlocks only research node 0 at level 1. Final Character storage is
`423 + 2*T + 2*C + 10*M` bytes, where T/C/M count that node's modifier research
tags, cargo categories and rare-mineral entries. The planner derives collection
widths using the pinned codecs and rejects a missing starting node.

Absent targets and empty, non-executable System-owned pre-funded targets work;
already initialized Characters or orphaned budgets are rejected, never repaired.
For current rent minima Rc/Rb and target balances c/b, upfront storage is
`max(Rc-c,0) + max(Rb-b,0)`; net storage debit is `(Rc-c) + max(Rb-b,0)`.
Character excess is refunded **after** budget funding; budget excess remains.
A future refund cannot pay an earlier allocation. The Plan describes both
creation targets, their funding source, the debit and any refund explicitly.

JSON retains finite initialization, identity, pre-funding and storage-ceiling
intent. Execution rechecks fresh Game 2, node initialization, Profile/XP identity,
target state, current Rent and funding before any signer. Meaningful changes
require a fresh Plan; unrelated Game definitions and mutable XP configuration
economics are not equality-frozen. Saved registrations missing this intent must
be recreated, not migrated.

Transaction fees are separate at planning. Execution queries `getFeeForMessage`
through `SageWriteRpc` for the assembled transaction, including any priority-fee
policy. Shared funders pay that applicable fee once and must cover ordered
upfront debits before refunds; final empty-System balances must be zero or
rent-exempt. Distinct registrations may compose, duplicate targets may not.
Unqualified instructions referencing a registration's funder or creation targets
must execute separately before replanning.

The [#621 qualification](https://github.com/Aephia/atlas-kit/issues/621#issuecomment-5743640905)
distinguishes authentic unsigned PTR observations from synthetic local controls.
Fresh checks are not atomic on-chain assertions: state, rent and fees can race
execution (including a wallet refreshing the blockhash). Simulate before signing
and read back both confirmed accounts; submission alone is not proof of creation.
No top-up, XP-config creation, legacy fallback, live signing or submission is
implicit in planning.

Most guides start
from an already registered Profile. For a new player, see
[Preparing wallet imports](/guides/import-onboarding/) for the distinct
Character, faction, and local Starbase prerequisites.

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

## The part that surprises people

**You cannot calculate a profile address from a wallet address.**

Many Solana accounts have a derivable address computed from
known inputs, so you can work them out offline. Profiles do not: a profile is created
by its owner, and its address is assigned at creation. Nothing about the wallet
predicts it.

:::game[A wallet is not a player Profile]
Your wallet is the key you use to approve actions. Your Player Profile is your
identity inside Star Atlas, and you create it separately. A Profile can be
linked to more than one wallet, so a wallet address alone does not tell the
game which Profile you mean. An app therefore needs the Profile address or a
service that already knows which wallets belong to which Profile.
:::

So going wallet → profile is a _search_, not a calculation, and the SDK makes
you choose how to search rather than guessing:

```ts
const sage = createSageClient({
  cluster: 'zink-ptr',
  rpc,
  discovery: { walletProfiles },
});

const wallet = sage.wallets.get(walletAddress);
const profiles = await wallet.profiles.all({ strategy: 'provider' });
```

`walletProfiles` is a provider you supply — a lookup table you maintain, an
indexer, or anything else that can answer "which profiles belong to this
wallet". Without one, the `provider` strategy has no way to answer and says so.

If you already know the addresses, skip the provider entirely and pass them:

```ts
const profiles = await wallet.profiles.all({
  strategy: 'known-addresses',
  addresses: [profileAddress],
});
```

:::caution[This is the most common place to get stuck]
If you are building something where users arrive with a wallet address and
nothing else, solve this first. It shapes everything downstream, and it has no
free answer — you need either a known mapping or an indexer.

If you control the profile addresses you care about, skip the whole problem and
start from `sage.characters.forProfile()`.
:::

## Creating a Profile

For a new identity, use `planCreateProfile` from
`@aephia/atlas-kit/identity/actions`. Supply a fresh signer-created Profile
address, an explicit funder, initial scoped keys, and a key threshold. The SDK
checks that the target is absent; it never derives it from the wallet or creates
it during a read.

The threshold controls protected Profile updates, not every gameplay action.
Profile AUTH keys must be nonexpiring and all sign creation, even when the
threshold is lower than their count. The new Profile signer authorizes creation;
its address gains ongoing authority only through a stored Profile key entry.
Keep that signer available if the same address is also an AUTH key.
Key permissions belong to their program
scope; the same numeric bit in another scope is not Profile AUTH.
See [planning Profile creation](/guides/import-onboarding/#plan-profile-creation)
for a minimal explicit example, signer roles, and confirmation checks.

## Inspecting permissions and keys

Permission masks stay exact bigint values. Scope is explicit when composing
names or interpreting meaning; raw mask union and containment need no scope.
`composeProfilePermissions(scope, names)` creates a mask from known names;
`unionProfilePermissionMasks(masks)` combines raw masks without losing
unrecognized bits. `inspectProfilePermissions(scope, raw)` explains recognized
names and retains `unrecognizedBitsRaw`. These helpers live in `identity` and
return immutable values. Inspection objects provide `toJSON()` with bigint values
as decimal strings; composed masks themselves remain raw bigint values.
The Profile permission vocabulary uses `PROFILE_PERMISSION_SCOPE`; the 43 pinned
C4 SAGE names use `SAGE_PERMISSION_SCOPE`. In SAGE scope, bit zero is
`manageGame`, never Profile `authenticate`. Only `drainSolVault` (bit 63) is
recognized in arbitrary other scopes. No helper adds default or all-permissions authority.

`containsProfilePermissions(availableRaw, requiredRaw)` is numerical bit
containment. A `true` result does not establish what unknown bits mean or prove
authorization. `inspectProfileKeys` checks direct entries against an explicit
signer, scope, mask, and evaluation time. It returns all entries with their
original indices and diagnostic reasons, so your application can explain each
candidate before the caller chooses one.

This illustrative example examines the direct Profile AUTH path accepted by
initial faction enrollment. Supply a loaded Profile, a caller-selected key
index, an explicit evaluation timestamp in Unix seconds, and the declared
application context and addresses. Inspecting keys does not select an index or
sign a transaction.

```ts
import {
  composeProfilePermissions,
  inspectProfileKeys,
  PROFILE_PERMISSION_SCOPE,
  type ProfileSnapshot,
} from '@aephia/atlas-kit/identity';
import { planChooseFaction } from '@aephia/atlas-kit/factions/actions';
import type { Address, SageContext } from '@aephia/atlas-kit/client';

declare const ctx: SageContext;
declare const profile: ProfileSnapshot;
declare const authority: Address;
declare const at: bigint;
declare const chosenKeyIndex: number;

const candidates = inspectProfileKeys(profile, {
  signer: authority,
  scope: PROFILE_PERMISSION_SCOPE,
  permissionsRaw: composeProfilePermissions(PROFILE_PERMISSION_SCOPE, [
    'authenticate',
  ]),
  at,
});
console.log(candidates);
const chosen = candidates.find(({ keyIndex }) => keyIndex === chosenKeyIndex);
if (chosen?.status === 'match') {
  const plan = await planChooseFaction(ctx, {
    faction: 'ustur',
    authorization: {
      profile: profile.address,
      authority,
      keyIndex: chosen.keyIndex,
    },
  });
  console.log(plan.describe());
}
```

For `at`, prefer the bank Clock sysvar's `unixTimestamp` obtained through your
application's RPC/data layer. A local value such as
`BigInt(Math.floor(Date.now() / 1000))` is approximate and can disagree with chain
time near expiry. The helper does not fetch Clock; action planners still check
fresh chain time. Load the Profile with `getProfile`; malformed snapshot input
produces a typed error directing you to that read.

Inspection performs no network reads, refresh, or wall-clock lookup. Negative raw
expiry values mean permanent. A finite expiry is valid at exactly `at` and
expired when it is less than `at`. A candidate
is a `mismatch` for address, scope, expiry, or missing permission bits, with
those reasons retained in `diagnostics`. If required bits have unrecognized
meanings and the candidate otherwise fits, its status is `indeterminate`, even
when numerical containment succeeds. Extra available unknown bits remain
visible. Profile AUTH is reported separately and does not override an unrelated
scope requirement. Role chains and certificates require their own validation;
these helpers do not evaluate delegated authorization.

The example matches an explicit Profile AUTH requirement. The faction planner
independently applies its own verified AUTH policy; the helper does not apply a
universal AUTH override. The selected snapshot match is advisory. The planner
still reads fresh state and checks its action-specific requirements and funding; inspect
and simulate its Plan before explicit execution. Protected Profile-update
thresholds do not become gameplay-wide multisig rules.

## Inspecting SAGE gameplay permission names

Use `SAGE_PERMISSION_SCOPE` for named C4 gameplay flags such as `moveFleet`
(bit 10) and `doMining` (bit 20). Profile snapshots translate those known scoped
bits into the same `ProfilePermission` names used by composition and inspection.
This illustrative example inspects an explicit requirement on a loaded Profile;
it displays every candidate and selects none.

```ts
import {
  composeProfilePermissions,
  inspectProfileKeys,
  inspectProfilePermissions,
  SAGE_PERMISSION_SCOPE,
  type ProfileSnapshot,
} from '@aephia/atlas-kit/identity';
import type { Address } from '@aephia/atlas-kit/client';

declare const profile: ProfileSnapshot;
declare const authority: Address;
declare const at: bigint;

const permissionsRaw = composeProfilePermissions(SAGE_PERMISSION_SCOPE, [
  'moveFleet',
  'doMining',
]);
console.log(inspectProfilePermissions(SAGE_PERMISSION_SCOPE, permissionsRaw));
const candidates = inspectProfileKeys(profile, {
  signer: authority,
  scope: SAGE_PERMISSION_SCOPE,
  permissionsRaw,
  at,
});
console.log(candidates);
```

A direct key can now `match` these recognized SAGE requirements when its
address, scope, expiry, and bits fit. This does not establish the complete
permission requirements for moving or mining, or prove that either action will
succeed. Permission names describe the pinned binding's bit meanings, not a
recipe mapping actions to masks. The [scoped vocabulary audit](https://github.com/Aephia/atlas-kit/blob/develop/docs/evidence/sage-permissions-468.md)
records each name, bit, and pinned source. The administrative names likewise do not
introduce SDK administration APIs or new account types.

A [dated unsigned PTR experiment](https://github.com/Aephia/atlas-kit/blob/develop/docs/evidence/sage-permissions-479.md)
found that `moveFleet` alone for C4 `startSubwarp`, and `doMining` alone for
`startMiningAsteroid`, passed the initial permission check but failed a later
Profile vault call. Adding `drainSolVault` allowed those measured calls, which
transferred native funds from the Profile vault into the Fleet account. This is evidence for
those fixtures, not a universal minimum mask. Vault authority is a separate
power: do not add it automatically or treat a snapshot `match` as proof that an
action will succeed.

A bot key with `drainSolVault` is a vault-withdrawal key, not merely a movement
or mining key. The pinned Profile instruction takes a caller-selected recipient;
the permission is not limited to funding that bot's Fleet. Treat the Profile's
available vault balance as exposed if that key is compromised, and avoid
keeping unrelated funds there. Never add bit 63 automatically to make a failed
gameplay simulation pass.

A [prefunding follow-up](https://github.com/Aephia/atlas-kit/blob/develop/docs/evidence/fleet-prefunding-488.md)
confirmed that these two action-only keys succeed when an explicit simulated
transfer covers the Fleet’s allocation shortfall first: 612,480 lamports for
subwarp and 292,320 for mining in that fixture. Neither target then called the
Profile vault. Those amounts are measurements, not SDK defaults or reusable
rent quotes; another Fleet or later allocation can need different funding.
The same experiment confirmed a direct one-lamport vault withdrawal with a
SAGE-scoped `drainSolVault` key to a recipient other than the Fleet. Prefer
explicit funding with narrow action authority where simulation verifies it,
without assuming that prefunding removes every future funding requirement.

Bits 43 through 62 remain unrecognized in SAGE scope. A requirement using those
bits is still `indeterminate` when otherwise consistent, while raw inspection
retains their values. No Profile AUTH override, role-chain resolution,
certificate validation, or automatic key selection is added. An existing
planner retains its own fresh checks; inspect and simulate its Plan before
explicit execution.

## What a character gives you

The character is the hub. Almost every gameplay read hangs off it:

```ts
const fleets = await character.fleets.all();
const stakes = await character.claimStakes.all();
const bases = await character.starbases.all();
```

Each of those is a separate network read, which is why each is a method rather
than a property. See [how Atlas Kit reads the
game](/start-here/how-it-thinks/) for why that distinction is consistent across
the whole API.

## Gotchas

**A profile with no character is possible.** Someone can hold a Star Atlas
profile without having entered SAGE. `forProfile` will tell you rather than
inventing an empty character.

**A wallet can hold more than one profile.** `wallet.profiles.all()` returns an
array for that reason. Do not assume the first is the one you want.

**Progression is already on the Character.** Its XP, stored levels, spent
research points, Renown and acquired nodes need no Game read. Use the optional
progression read to join sequence-matched XP definitions.
[Council Rank](/guides/council-rank/) shows the point arithmetic and read paths.

## Reference

- [`identity`](/reference/identity/) — every export in this entry
- [`fleets`](/guides/fleets/) — the usual next step
