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

# FAQ

> Answers to the questions integrators hit first

## Setup and access

> **What do I install, and what do I import?**
>
> The package is `vaults-multichain-sdk`. The client class is `TrustodianSDK`.
>
> ```typescript theme={null}
> import { TrustodianSDK, InMemoryShareStore, EChainType } from 'vaults-multichain-sdk';
>
> const sdk = new TrustodianSDK({
>   apiKey: process.env.TRUSTODIAN_API_KEY!,
>   network: 'testnet',
>   shareStore: new InMemoryShareStore(),
> });
> ```
>
> `apiKey`, `network` and `shareStore` are all mandatory, and the constructor throws if any is
> missing. Everything else has a default. See [Quickstart](https://sdk-guide.mintlify.site/sdk-guide/quickstart.md).

> **How do API keys and permissions work?**
>
> Every request carries the key in the `x-api-key` header; the server resolves it to a user and,
> if the key belongs to an organization, to a role and member id. You never pass a user id or an
> org id yourself.
>
> A key holds a set of permissions drawn from `view_balances`, `view_transaction_history`,
> `wallet_management`, `vault_management`, `transaction_management`, `team_management`,
> `policy_management`, `address_book_management` and `audit_log_access`. Each operation declares which
> ones they accept, so a key missing the right permission gets a `403` (surfaced as
> `PermissionDeniedError`) even though the key itself is valid. Keys can also carry an IP
> whitelist. See [Authentication](https://sdk-guide.mintlify.site/sdk-guide/authentication.md).

> **Does the SDK run in a browser?**
>
> No. It is a server-side Node.js library, and it should stay that way, for two reasons:
>
> 1. Using it means holding an API key and handling users' wallet passwords. Both belong on your
>    backend and nowhere else.
> 2. It is built for Node and depends on APIs a browser bundle does not supply.
>
> Put the SDK behind your own API and let your frontend talk to that.

> **Can the SDK connect a Ledger or Trezor?**
>
> No. The SDK never talks to a device. It prepares each step, your frontend runs it on the user's
> device, and the SDK verifies the answer before it sends anything. See
> [Hardware Wallets](https://sdk-guide.mintlify.site/sdk-guide/hardware-wallets.md).

> **Does a hardware wallet need the wallet password?**
>
> No. The key stays on the device, and nothing is written to your `shareStore` for it. The
> exceptions are activating a Solana Vault, which also takes `userRef` and `walletPassword` because
> the Vault activation key is under the wallet password, and executing or activating a Vault under
> [strict authentication](https://sdk-guide.mintlify.site/sdk-guide/strict-authentication.md#hardware-wallets).

> **Can I set up the hardware root through the SDK?**
>
> No. The account holder connects the hardware wallet as the account root in the Trustodian app.
> The SDK never connects, replaces or changes the root.

> **What is userRef?**
>
> The Trustodian user id that the API key resolves to. Read it with `await sdk.getSdkUserId()`.
> It is the key under which device shares are filed
> in your `shareStore`, and the handle every session and crypto call takes. Use the same value
> consistently; a mismatched `userRef` looks to the SDK like a different user with no shares.

> **Can I call the REST API directly instead?**
>
> Only for reads, and only where the SDK has no method for what you need. Anything that touches
> key material must go through the SDK: the share, password and seed routes are primitives it
> drives, and calling them by hand means reimplementing key derivation, share reconstruction and
> per-chain signing. Treat the raw API as a gap-filler, not as a second path running alongside
> the SDK.

## Secrets: recovery phrase vs wallet password

> [!WARNING]
>
> These are two different secrets with two different jobs. Mixing them up is the single most common
> integration mistake.

> **What is the wallet password?**
>
> A user-chosen secret that unlocks the session. It is never sent to the server: the SDK turns it
> into two keys locally, one that decrypts the server share and one that decrypts the recovery
> share.
>
> It is set during app onboarding, before an API key can be issued, so your integration only
> unlocks each session with `unlockWalletSession(userRef, password)`. The session lasts
> `keyTtlMs`, 5 minutes by default, and can be dropped early with `lockWalletSession(userRef)`.
> Rotate the password with `changeWalletPassword(userRef, { currentPassword, newPassword })`,
> which re-encrypts every share. See [Key management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md).

> **What is the account recovery phrase?**
>
> A 12-word BIP-39 phrase that is the root of the account's wallets, not a password reset. It
> is created or imported during app onboarding, before an API key can be issued, so your
> integration never establishes one. `createWallet` derives wallets from that existing seed rather than
> generating standalone keys. The phrase is shown to its owner once, at establish time.
>
> What your integration uses: `getRecoveryPhraseStatus` to read the state. Re-displaying an
> already-established phrase is not available over the API-key/SDK transport (as of SDK v3.0.0,
> `revealRecoveryPhrase` throws `PermissionDeniedError`); that is a first-party Trustodian web app
> operation only. See [Recovery phrase](https://sdk-guide.mintlify.site/sdk-guide/recovery-phrase.md).

> **Why does every crypto call still want the password if the session is unlocked?**
>
> Unlocking a session only derives the key that decrypts the server share. Anything that also
> touches the recovery share, which is every create, import, recover, rotate and seed
> operation, needs the second key, and that one is never stored: it is re-derived from the
> password on each call. See [Key management](https://sdk-guide.mintlify.site/sdk-guide/key-management.md#the-wallet-password).

> **A user forgot the wallet password. Can the recovery phrase rescue them?**
>
> No. There is no method on the SDK that resets a wallet password without the current one:
> `changeWalletPassword` requires `currentPassword`. Reading the phrase back would not help
> either, because it is gated *behind* the wallet password rather than the other way round — and
> as of SDK v3.0.0 it is not reachable from your integration at all, only from the Trustodian web
> app. Every stored share is encrypted under keys derived from that password, and Trustodian
> cannot derive them for you.
>
> What you can actually do:
>
> * `getWalletPasswordHint()` returns the hint the user set when they chose the password. Prompt
>   for a hint at setup so this is worth something.
> * `getEncTokenStatus()` tells you whether a password was ever configured for the account.
>
> Design your onboarding around this: capture a hint, and make the recovery phrase backup step
> non-skippable.

> **Can Trustodian spend my users' funds?**
>
> No. Two of three shares are needed to reconstruct a key. The device share lives in your
> `shareStore`; the other two are stored server-side but encrypted under keys derived from the
> user's wallet password, which the server never sees. Holding the ciphertext without the password
> rebuilds nothing.

## Shares, devices and sessions

> **Why a share store instead of Trustodian storing everything?**
>
> Because a party holding two of three shares is custodial, and the device share has to live
> outside Trustodian for the model to hold. The web app keeps it in browser storage; your
> backend keeps it in whatever `shareStore` you implement.

> **Is InMemoryShareStore fine in production?**
>
> No. `InMemoryShareStore` is a `Map`: it evaporates on restart and is not shared between
> processes. In production, implement `IntegratorShareStore` against durable, encrypted storage:
>
> ```typescript theme={null}
> export interface IntegratorShareStore {
>   save(record: DeviceShareRecord): Promise<void>;
>   load(userRef: string, walletId: string): Promise<DeviceShareRecord | null>;
>   findByAddress(
>     userRef: string,
>     chainType: string,
>     address: string,
>   ): Promise<DeviceShareRecord | null>;
>   delete(userRef: string, walletId: string): Promise<void>;
> }
> ```
>
> A `DeviceShareRecord` holds `userRef`, `walletId`, `address`, `chainType`, `deviceShareHex`,
> `version` and `oprfKeyVersion`. The last two count different things: `version` is the share's own
> rotation lineage, `oprfKeyVersion` is which encryption key wrapped it. A store that copies fields
> by hand has to carry both, or a rotated share reads as stale and gets rebuilt on every call.
> `deviceShareHex` is key material, so encrypt it at rest and keep it out of logs. XRP
> addresses are matched case-insensitively in `findByAddress`; mirror that if your backing store is
> case-sensitive.

> **A new server instance has no device share. What happens?**
>
> Nothing is lost. The device share is the recoverable one: with the wallet password you can
> rebuild it from the server and recovery shares.
>
> * `ensureWalletDeviceShare(userRef, { address, chainType, password })` puts one wallet's share
>   back into your store.
> * `recoverWallet(userRef, params)` rebuilds a single wallet.
> * `recoverWalletsBatch(userRef, params)` rebuilds many at once.
> * `ensureWalletReadyForCrypto(userRef, { address, chainType, walletPassword })` checks one wallet
>   and silently recovers it if needed.
>
> Losing the device share is survivable, including losing your whole share store: as long as the
> wallet password is known, `recoverWalletsBatch` rebuilds every device share. Losing the share
> store *and* the wallet password together is unrecoverable for an imported wallet. Wallets derived
> from the seed can still be rebuilt from the [recovery phrase](https://sdk-guide.mintlify.site/sdk-guide/recovery-phrase.md).

> **What does an ADDRESS\_MISMATCH error mean?**
>
> The key rebuilt from the available shares does not derive the address the wallet claims,
> almost always because of a stale device share. What you actually catch is a plain error
> carrying `code: 'ADDRESS_MISMATCH'`, not the exported `KeyMismatchError` class, whose
> `KEY_MISMATCH` code is reserved and not thrown in practice.
>
> The fix is to rebuild the device share, and the SDK will do it for you: pass `walletPassword` to
> the crypto call so it can recover in place, or call `ensureWalletReadyForCrypto` /
> `recoverWallet` first. See [Errors](https://sdk-guide.mintlify.site/sdk-guide/errors.md).

> **The same user also uses the Trustodian web app. Do the shares collide?**
>
> No, they are kept apart on purpose. Your integration and the web app hold independent sets of
> encrypted shares for the same wallets, and each side sets its own up on first use, which the SDK
> does for you. The wallet password itself is per account, and `changeWalletPassword` re-encrypts
> every side.

## Networks, chains and fees

> **How do I switch between testnet and mainnet?**
>
> Through `network: 'mainnet' | 'testnet'` at construction. It selects two things at once: the
> default `apiBaseUrl` and the whole default RPC set. Chain identifiers differ too: `EChainType`
> stays the same but the chain id changes (`"1"` vs `"11155111"`, `sol_mainnet` vs `sol_devnet`,
> `tron_mainnet` vs `tron_shasta`, `ada_mainnet` vs `ada_preprod`, `xrp_mainnet` vs
> `xrp_testnet`). Bitcoin is the one case where the chain *type* differs: `BTC` on mainnet,
> `BTC_T3` on testnet.
>
> Build one SDK instance per network rather than trying to flip a live one.

> **Can I use my own RPC endpoints?**
>
> Yes. `rpcOverrides` is a flat map keyed by chain id, merged over the defaults for the selected
> network. EVM chains are keyed by their
> numeric id as a string; the other chains use their named id. There is no nesting and no `evm`
> sub-object:
>
> ```typescript theme={null}
> const sdk = new TrustodianSDK({
>   apiKey: process.env.TRUSTODIAN_API_KEY!,
>   network: 'mainnet',
>   shareStore,
>   rpcOverrides: {
>     '1': 'https://your-eth-node.example',
>     '8453': 'https://your-base-node.example',
>     sol_mainnet: 'https://your-solana-node.example',
>   },
> });
> ```
>
> The shipped defaults are public endpoints: fine for trying things out, not for production
> throughput.
>
> Cardano has no default endpoint at all, but that is not a gap you have to fill: the SDK
> never resolves an RPC endpoint for ADA. Cardano UTXO sourcing and fee estimation go through the
> Trustodian backend instead. Set an ADA override only if your own code reads the SDK's RPC config
> for Cardano.

> **Which chains are supported, and with which EVM chain IDs?**
>
> `EChainType` has seven members: `EVM`, `SOL`, `TRON`, `BTC`, `BTC_T3`, `ADA`, `XRP`. On EVM the
> SDK ships names and RPC defaults for Ethereum, Polygon, Base, Arbitrum One, Optimism, BNB Smart
> Chain and Avalanche C-Chain, on both networks. The chain ids are listed in
> [Supported chains](https://sdk-guide.mintlify.site/sdk-guide/introduction.md#evm-chain-ids).
>
> Passing an EVM chain id with no configured RPC throws
> `No RPC endpoint configured for EVM chain ID <id>`. Add it through `rpcOverrides`.

> **How are fees paid, per chain?**
>
> Always in the chain's native asset, out of the account that broadcasts. The SDK's own table of
> native units and the minimum balance it expects a sender to hold:
>
> | Chain | Native asset | Decimals | Minimum balance for a transaction |
> | - | - | - | - |
> | EVM | ETH (chain-native gas token) | 18 | 0.001 |
> | SOL | SOL | 9 | 0.01 |
> | TRON | TRX | 6 | 1 |
> | BTC / BTC\_T3 | BTC | 8 | 0.0001 |
> | ADA | ADA | 6 | 1 |
> | XRP | XRP | 6 | 1 |
>
> For multisig, the fee comes from the executing owner's personal wallet rather than the vault,
> which is why `activateMultisig` and `executeOrder` take `feeQuote` (and `gasLimit` / `gasPrice`
> on EVM). See [Multisig fees](https://sdk-guide.mintlify.site/sdk-guide/multisig-fees.md).
>
> To put a number in front of a user, use `estimateFee`, a chain RPC, or `preflightOrder`.

## Transactions, orders and scope

> **Why does an order need both signing and executing?**
>
> Because they are different acts by potentially different people. A multisig vault has owners and
> a threshold. `signOrder` rebuilds one owner's key and adds that owner's approval to a
> pending order; it broadcasts nothing. Once enough owners have signed to meet the threshold,
> `executeOrder` assembles the collected signatures into a transaction and broadcasts it,
> paying the network fee from the executor's wallet.
>
> That is also why they are two separate calls, and why a
> single-owner flow still makes both: one signature is simply enough to reach the threshold.
> `rejectOrder` is the third outcome. See [Orders](https://sdk-guide.mintlify.site/sdk-guide/orders.md).

> **When do I pass walletScope: 'organization'?**
>
> Whenever the wallet or vault being operated on is owned by the organization rather than by the
> individual. `walletScope` is `'personal' | 'organization'` and defaults to personal; it routes
> the call to the organization-owned resource and tells the share layer which partition to read.
>
> The scope of the *listing* method and the scope you pass to the *action* must agree: a wallet
> returned by `listOrgWallets` / `listMyOrgWallets` must be acted on with
> `walletScope: 'organization'`. Getting this wrong reads the wrong share set and fails, usually
> as a 403 or a key mismatch. See [Organization](https://sdk-guide.mintlify.site/sdk-guide/organization.md) and
> [Organization wallets](https://sdk-guide.mintlify.site/sdk-guide/organization-wallets.md).

> **How do I test a transfer safely before sending it?**
>
> Four layers, cheapest first:
>
> 1. `validateAddressForChain` / `assertValidAddressForChain` catch a malformed recipient locally,
>    with no network call. See [Transactions](https://sdk-guide.mintlify.site/sdk-guide/transactions.md).
> 2. `preflightSend` / `preflightOrder` check the whole thing before any key is touched. See
>    [Preflight](https://sdk-guide.mintlify.site/sdk-guide/preflight.md).
> 3. Run against `network: 'testnet'` with a separate API key, on Sepolia / devnet / Shasta /
>    `BTC_T3` / preprod / XRP testnet.
> 4. Turn on the address-book whitelist so an unlisted destination is refused server-side. See
>    [Address book](https://sdk-guide.mintlify.site/sdk-guide/address-book.md).
>
> Do the first testnet run end to end (create wallet, fund, send, read history) before pointing
> anything at mainnet.

## Operations

> **Are there rate limits, and how do I handle them?**
>
> Yes, applied per operation rather than globally, and the tightest of them sit on the
> session-unlock and key-material calls, so a burst of unlocks for many users at once is the case
> most likely to hit one.
>
> A limited response comes back as `429` and the SDK raises `RateLimitError` with `retryAfter`
> parsed from the `Retry-After` header when the server sends one. Back off on that value; do not
> spin.

> **What are the timeouts?**
>
> Every request runs under a 30 second default, configurable through `timeout` in the SDK
> config. An overrun surfaces as `TimeoutError` carrying `timeoutMs` and the operation, which is
> distinct from `NetworkError` for a failed connection, so you can retry the two differently.
>
> Chain operations legitimately take longer than an API read; if you raise `timeout`, raise it for
> the instance that does signing and execution, and keep read paths tight.

> **How do I tell an SDK error from any other exception?**
>
> ```typescript theme={null}
> import { isTrustodianError } from 'vaults-multichain-sdk';
>
> try {
>   await sdk.sendTransaction(input);
> } catch (error) {
>   if (isTrustodianError(error)) {
>     console.error(error.code, error.message);
>   }
>   throw error;
> }
> ```
>
> Every SDK error extends `TrustodianBaseError` and carries a stable `code`. `isTrustodianError`
> is the type guard; `hasErrorCode(error, code)` narrows to one specific code. The full catalogue
> is in [Errors](https://sdk-guide.mintlify.site/sdk-guide/errors.md).


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