> ## Documentation Index
> Fetch the complete documentation index at: https://sdk-guide.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Recovery Phrase

> The account seed your wallets derive from: derivation paths, state and errors

> [!NOTE]
>
> **Your integration never establishes the personal phrase.** An account cannot be issued an API key
> until it has registered, set a wallet password and created or imported a recovery phrase in the
> Trustodian app, so the seed already exists by the time the SDK runs. This page covers what that
> means for you: the derivation paths, reading the state, and the errors.

> [!NOTE]
>
> An account whose root is a hardware wallet has no recovery phrase. Its wallets derive on the device,
> and `establishRecoveryPhrase` throws `HardwareAccountRefusedError` (`SEED_ROOT_IS_HARDWARE`). See
> [Hardware Wallets](https://sdk-guide.mintlify.site/sdk-guide/hardware-wallets.md).

## What the account seed is

Wallets on a Trustodian account are not independent keys. The account has one BIP39 recovery phrase behind it, the account seed, and the SDK derives every wallet from that seed on a standard path, at its own HD index per chain.

There is one secret to protect for the whole account, not one per wallet. The seed is split 2-of-3 and rebuilt from the wallet password when a key is needed. The wallets themselves are shareless: they carry `derivationSource: 'seed'`, a `derivationPath` and a per-chain `index`, nothing is written to your share store for them, and a signing key is re-derived on demand and wiped when the call returns.

The recovery phrase is the last resort. If it and the wallet password are both lost, the wallets are gone.

> [!NOTE]
>
> The phrase is portable. Derivation uses the standard paths, so the same phrase imported into
> MetaMask, Phantom, Trust, Eternl, Xaman, Sparrow or TronLink produces the same addresses. A phrase
> set up in the Trustodian web app can also carry a BIP39 passphrase. The SDK derives with it, and an
> external wallet needs the same passphrase to produce the same addresses.

> [!WARNING]
>
> The backend never sees the phrase or the raw entropy. Neither Trustodian nor you can reset it. If
> the user loses the phrase **and** the wallet password, seed wallets are gone.

## Lifecycle

```mermaid theme={null}
flowchart LR
    A[App onboarding: phrase created or imported, verified] --> B[API key issued]
    B --> C[Your integration]
    C -->|createWallet| D[Seed wallets, HD-derived]
```

`getRecoveryPhraseStatus()` returns `{ established, verified }`; `hasRecoveryPhrase()` is the boolean shortcut. Both report on the account the API key resolves to.

## Establishing, verification and import

The account holder generates or imports the phrase once and completes a one-time verification
step, both during app onboarding. Attempting to establish a second phrase returns
`SeedAlreadyEstablishedError`.

Use `getRecoveryPhraseStatus()` to assert the state if you need to.

## Derivation paths

Wallets are derived on the standard path for each chain, at a per-chain index.

| Chain | Path | Address format |
| :- | :- | :- |
| EVM | `m/44'/60'/0'/0/{index}` | Checksummed hex |
| TRON | `m/44'/195'/0'/0/{index}` | Base58 (from the hex address) |
| XRP | `m/44'/144'/0'/0/{index}` | Classic address |
| BTC (mainnet) | `m/84'/0'/0'/0/{index}` | BIP84 native segwit P2WPKH |
| BTC testnet (`BTC_T3`) | `m/84'/1'/0'/0/{index}` | BIP84 native segwit P2WPKH |
| SOL | `m/44'/501'/{index}'/0'` | Base58 public key, SLIP-0010 ed25519 (all-hardened) |
| ADA | `m/1852'/1815'/{index}'/0/0` | CIP-1852 Icarus **base** address (payment + stake) |

> [!WARNING]
>
> **ADA varies the account, not the address leaf.** Each Trustodian ADA wallet is an independent
> CIP-1852 account with its own stake key, so an external restore has to scan *accounts* for ADA and
> *addresses* for the other chains. ADA wallet #1 and above only appear in wallets that enumerate
> accounts, such as Eternl. Icarus master-key scheme only: a Ledger-scheme wallet derives different
> ADA addresses from the same phrase.

## Creating seed wallets

Once a phrase is established, `createWallet` routes to the seed path on its own. You do not pass a mode:

```typescript theme={null}
await sdk.unlockWalletSession(userRef, walletPassword);

const { wallets } = await sdk.createWallet({
  chain: EChainType.SOL,
  userRef,
  password: walletPassword,
});
```

The SDK rebuilds the account seed from the wallet password, takes the next free index for the chain, derives the key and registers the resulting address. The key and the seed are wiped when the call returns, and nothing is written to your share store.

### Signing with a seed wallet

Signing re-derives the key from the account seed instead of reading shares. The address check still applies: a derived key that does not reproduce the wallet address raises an `ADDRESS_MISMATCH` error and never signs.

A wallet always signs with the phrase it was created from, personal or organization. The SDK never falls back to another phrase.

> [!NOTE]
>
> Because it rebuilds the seed, signing a seed wallet needs the wallet password unless
> `seedEntropyCache` is on. With no password and no cache the SDK fails closed rather than deriving
> blind. See [the cache trade-off](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#what-is-kept-in-memory-and-for-how-long).

## Organization recovery phrase

An organization can have a recovery phrase of its own, separate from its Master's personal phrase.
It belongs to the Master's account and opens with the same wallet password. Organization wallets
derive from it; personal wallets keep deriving from the personal phrase.

### Reading the state

`getSeedStatus()` returns the state of both phrases:

```typescript theme={null}
const status = await sdk.getSeedStatus();

type AccountSeedStatus = {
  personal: SeedScopeStatus;
  organization:
    | (SeedScopeStatus & {
        organizationId: string;
        availability: 'available' | 'personal_seed';
        canLinkPersonalSeed?: boolean;
        archivedWalletCount?: number;
      })
    | null;
};

type SeedScopeStatus = {
  seedId: string | null;
  status: 'active' | 'archived' | 'lost_access' | 'awaiting_phrase' | null;
  verified: boolean;
};
```

`organization` is `null` for anyone who is not the Master of an organization. The method returns
`null` when the status is not available.

| `availability` | Meaning |
| :- | :- |
| `available` | The organization uses a phrase of its own. `seedId` is `null` until the Master creates it |
| `personal_seed` | The Master linked the organization to their personal phrase, so organization wallets derive from it |

| `status` | Meaning |
| :- | :- |
| `active` | The current phrase |
| `archived` | Replaced by a newer phrase. Its wallets can still send |
| `lost_access` | The wallet password that sealed it is gone |
| `awaiting_phrase` | Still current, but locked until the phrase is entered again in the Trustodian web app, for example after the wallet password was reset |

### Creating it

The Master creates the organization's phrase with `establishRecoveryPhrase`:

```typescript theme={null}
const { mnemonic, verified } = await sdk.establishRecoveryPhrase({
  userRef,
  password: walletPassword,
  ownerKind: 'organization',
});

// Show `mnemonic` to the Master once. After they have written it down:
if (!verified) {
  await sdk.confirmRecoveryPhraseVerified(userRef, { ownerKind: 'organization' });
}
```

An organization phrase is always generated. Passing `mnemonic` throws `InvalidInputError`.

> [!WARNING]
>
> When the organization's existing wallets were created from the Master's personal phrase
> (`availability` is `personal_seed`, or `orgSeedOffersPersonalLink(status.organization)` returns
> `true`), creating the organization's phrase moves those wallets to the [Archive](#the-archive)
> and cannot be undone. The SDK refuses with `OrgSeedChoiceRequiredError` unless you pass
> `acknowledgeArchive: true`. Pass it only after the Master has seen that consequence and confirmed
> it.

### The Archive

Once the organization has its own phrase, the organization wallets created from the personal phrase
form its Archive. They keep working: they can still send, receive and sign. New organization vaults
accept only wallets of the organization's phrase, and offering a wallet from the Archive as an owner
throws `OrgWalletRequiresOrgSeedError`.

The Archive is not `accessState: 'archived'`. Its wallets derive from the Master's current personal
phrase and stay `active`; `archived` marks wallets of a phrase that was replaced, which can no longer
sign vault orders. See [Wallet generations](https://sdk-guide.mintlify.site/sdk-guide/wallets.md#wallet-generations).

An organization wallet is in the Archive when `availability` is `available` and the wallet has
`derivationSource: 'seed'` with `seedOwnerKind: 'personal'`.

### What stays in the web app

Linking an organization to the personal phrase, entering an `awaiting_phrase` phrase again,
replacing, recovering and revealing a phrase are available only in the Trustodian web app. The SDK
never does any of them, and never creates a phrase you did not ask for. The typed errors below say
which web app action is needed.

## Revealing the phrase

> [!WARNING]
>
> As of SDK v3.0.0, re-displaying an already-established recovery phrase is no longer available
> over the API-key/SDK transport. `sdk.revealRecoveryPhrase(...)` throws `PermissionDeniedError`
> immediately. The phrase is shown once, at establish time, in the Trustodian app; re-displaying it
> afterward is available only in the first-party Trustodian web app.

## Errors

| Error | When | What to do |
| :- | :- | :- |
| `RecoveryPhraseRequiredError` | The account has no recovery phrase | It never completed onboarding. Send the account holder back to the app rather than trying to establish one from your integration |
| `RecoveryPhraseUnverifiedError` | The phrase exists but was never verified | The account holder completes the verification step in the app. Re-establishing is not the fix |
| `SeedAlreadyEstablishedError` | A phrase already exists | The normal state for any account with an API key. For `ownerKind: 'organization'`, the organization already has its phrase |
| `SeedAwaitingPhraseError` / `RecoveryPhraseReentryRequiredError` | The phrase is `awaiting_phrase`. `ownerKind` says which one when known | The phrase has to be entered again in the web app. Retrying or unlocking does not help |
| `OrgRecoveryPhraseRequiredError` | The organization has no phrase of its own yet | The Master creates it with `establishRecoveryPhrase({ ownerKind: 'organization' })` or in the web app |
| `OrgSeedChoiceRequiredError` | The Master has not yet chosen between the personal phrase and a new organization phrase, or creating a new one would move wallets to the Archive | The Master chooses in the web app, or you pass `acknowledgeArchive: true` after showing the consequence |
| `OrgRecoveryPhraseUnverifiedError` | The organization's phrase was never confirmed | The Master confirms the backup, then `confirmRecoveryPhraseVerified(userRef, { ownerKind: 'organization' })` |
| `OrgSeedMasterOnlyError` | Someone other than the Master tried to use or change the organization's phrase | Do not retry |
| `SeedEchoMismatchError` | The server did not confirm which phrase it answered for | Nothing was signed or derived from the answer. From `establishRecoveryPhrase` it means the phrase was saved but not confirmed for the requested owner, and the phrase is not returned: have the user check their phrases in the web app before relying on it |

Full reference: [Errors](https://sdk-guide.mintlify.site/sdk-guide/errors.md).

```typescript theme={null}
import {
  RecoveryPhraseRequiredError,
  RecoveryPhraseUnverifiedError,
} from 'vaults-multichain-sdk';

try {
  await sdk.createWallet({ chain: EChainType.EVM, userRef, password });
} catch (error) {
  if (error instanceof RecoveryPhraseRequiredError) {
    return sendUserBackToAppOnboarding();
  }
  if (error instanceof RecoveryPhraseUnverifiedError) {
    return sendUserBackToAppOnboarding();
  }
  throw error;
}
```

## Handling rules

> **Never persist the phrase**
>
> Not in your database, not in a cache, not in a session store, not in a queue message. The one
> legitimate destination is the end user's own eyes, once, at establish time.

> **Never log it, and audit your serializers**
>
> Structured loggers, APM tools and error trackers serialize objects wholesale. Make sure the
> variable holding `mnemonic` is never inside anything that gets serialized, including an error's
> `details`.

> **Show it once, and say so**
>
> The phrase is shown once, at onboarding. If the user did not write it down, the only way back is
> a gated, audited reveal in the Trustodian web app — your integration cannot retrieve it. Tell them
> that on the screen where you show it.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.