Skip to main content

Overview

Trustodian is non-custodial: the backend never holds a complete private key. The account’s recovery phrase is the one custody secret. It is split 2-of-3, and every wallet is re-derived from it when a key is needed, so no wallet has a key of its own to store. See Recovery phrase for the seed itself; this page covers the shares, the wallet password and the session.
Everything on this page runs in your backend process. The SDK holds the API key, the end-user wallet password and reconstructed private keys in memory. Never ship it to a browser or a mobile client.

The three shares

Every secret is split into three shares; any two of them reconstruct it. Reconstruction always happens inside your process, never on the server:
  • Signing uses the device share plus the server share.
  • Recovery, rotation and password change use the server share plus the recovery share, because the device share is the piece that is missing or stale.
Every reconstruction is checked: the key that comes out must derive the address the wallet claims, or the operation fails instead of signing.

The wallet password

The wallet password never leaves your process and is never sent to Trustodian, not even hashed. It is turned into key material through an exchange with the server in which the server learns nothing about the password itself (an oblivious PRF). Two independent keys come out of it: That is why create, import, recover, rotate, change password and seed reads all take a password argument even when the session is already unlocked: they touch the recovery share, and the session cannot supply the recovery key.

Strength rules

setWalletPassword and changeWalletPassword reject a new password that does not meet all of these. Unlocking is deliberately exempt, so an account created under the old rules still opens. Validate in your own UI before you call the SDK, so the user sees the rule next to the field rather than as a thrown error:
When the session expires, the session key is wiped and any further crypto call throws SESSION_LOCKED. See Authentication for unlocking, locking and the keyTtlMs session TTL.

The share store

You must supply a shareStore, or the SDK constructor throws. It is where the device share for each wallet lives. This is the contract:
Implementation rules:
  • save overwrites. Rotation, recovery and password change all save a new record for the same (userRef, walletId), so upsert on that pair.
  • version is opaque to you. Store it and return it unchanged; the SDK uses it to tell a current device share from a stale one.
  • findByAddress must normalize XRP. Addresses match exactly for every chain except XRP, which is lower-cased on both sides.
  • delete must actually delete. The SDK removes a device share once it has established that the share is unusable, before rebuilding it.
deviceShareHex is key material. Combined with the server share it rebuilds a private key. Store it encrypted at rest, keep it out of logs, backups and error payloads, and scope every read to a single userRef.
InMemoryShareStore is for local development and tests only. It dies with the process, so every device share is lost and every wallet needs a password-based recovery on the next start.

Wallets the user also uses elsewhere

A user who also uses the Trustodian web or mobile app has a separate set of encrypted shares for the same wallets, so a rotation on one side does not invalidate the other. The wallet password is shared across them; the device share is not. The first time your integration touches a wallet that was created in another app, the SDK sets your side up automatically on the first operation. There is nothing to configure.

Keeping a device share current

Before signing, the SDK checks that the device share it holds for the wallet is still the current one and, if it is not, rebuilds it from the server and recovery shares. That rebuild needs the wallet password. There are two ways to work with this:
  • Pass walletPassword on the crypto call and the SDK recovers silently before signing. This is the simple path.
  • Or recover explicitly, when you would rather control when it happens:
isDeviceShareRecoveryError(error) is true exactly when the failure is one the wallet password can fix: no device share on this host, a stale one, or one that no longer decrypts.
Seed wallets skip all of this. They have no device share; signing re-derives the key from the account seed. See Recovery phrase.

Encryption key versions

The server key used in the password exchange is versioned. Every encrypted share records the version it was wrapped under, and the SDK reads that version off each share and derives the matching key. An account whose shares sit on several versions at once therefore keeps working. Shares move onto the current version only when someone asks. Nothing rotates on unlock, because the rotate endpoint allows three calls an hour and the SDK will not spend that budget on your behalf.
Call it once after a session is unlocked. It returns { rotated: 0, skipped: true } when nothing is stale, so it is safe to call every time. Cache the attempt for the life of the session — including a failed one, since a failure leaves the account exactly as it was and retrying only burns the hourly budget the user may need for their own recovery.
Ship SDK v3.0.0 before the platform raises the active key version, and make sure something in your integration actually calls rotateOprfKeyVersion. A rotation triggered from any client re-wraps this account’s shares everywhere, including yours. An integration that never rotates stays on an older version indefinitely and will eventually fail to decrypt its own shares, burning the wallet-password lockout counter on every attempt.
stepUp is accepted but not required: the passkey step-up the web app performs is skipped for API-key callers, so your key’s own permissions are the gate. Rotating an account that has a recovery phrase needs wallet_management.

Share recovery API

Unlocking and locking a session, and reading password status, live in Authentication. The methods below rebuild device shares and rotate the password.

Batch recovery

Use recoverWalletsBatch to rebuild many device shares at once, after restoring a host or after the user changed their password elsewhere. Large lists are chunked automatically, and a wallet that fails is reported in the results rather than aborting the batch. The password is checked once for the whole call, so a wrong one spends a single lockout attempt.
The same call also sweeps the account’s Bitcoin and Cardano vault seats and reports them separately in vaultKeys, each entry recovered, skipped or failed. An employee who accepted a seat on another device gets its local half back here, so pass the call an empty wallets list when the seats are all you need to repair.

Changing the wallet password

changeWalletPassword re-encrypts every share under keys derived from the new password: wallet shares, vault activation shares, provisioned vault seats, and, if the account has a recovery phrase, the seed shares. It is a full re-encryption pass, not a flag on the server. New device shares are written to your share store, and the session continues under the new password. The result counts what moved, including cosignerKeysRotated for vault seats, and oprfKeyVersion reports the encryption key version everything now sits on. The change proves the current password to the server, not only to your process: the request carries a proof derived from currentPassword, which the server compares with the one it stored when the password was last unlocked. Requires SDK v3.3.2 or newer.
On an account enrolled in passkey or 2FA step-up that has a recovery phrase, pass stepUp. Without it the change fails rather than committing half-rotated. backupInvalidated in the result tells you a recovery-phrase backup recorded before the password change is no longer usable.

Wallet creation

createWallet accepts an optional mode, but you should omit it. Every account reaching the SDK already has a recovery phrase, so the wallet is derived from the account seed: shareless, with nothing written to your share store. The private key exists only in your process, and what crosses the wire is an address.
importWallet({ chain, privateKey, userRef, password }) takes an existing private key, checks that it derives the address it claims, and splits it the same way. It accepts the key formats the major wallets export. An imported key can never come from the account seed, so its device share is written to your share store, unlike a wallet created from the seed.

Exporting private keys

As of SDK v3.0.0, exporting a wallet’s private key is no longer available over the API-key/SDK transport. sdk.exportWallet(...) throws PermissionDeniedError immediately. Private-key export is available only in the first-party Trustodian web app.

What is kept in memory, and for how long

The SDK wipes every secret buffer it owns as soon as it is finished with it: derived keys, decrypted shares and reconstructed private keys.
Two things cannot be wiped: JavaScript strings (the wallet password, a recovery phrase shown at establish time) and anything you copy out of the SDK. Keep those alive for as short a time as possible, and never place them in a log line, an error message, an APM span or a crash dump.
The reconstructed account seed is never kept between calls: each seed operation reconstructs, derives and wipes, so it needs the wallet password every time. seedEntropyCache is deprecated and ignored. See Strict authentication.

Vault activation shares

Creating and activating a multisig vault on Solana, Tron and XRP produces an activation secret, split the same 2-of-3 way. The SDK handles it when you pass an unlocked userRef to createMultisig / activateMultisig, and changeWalletPassword rotates it along with everything else. See Multisig and Organization vaults.

Security responsibilities

No single party holds two usable shares. Compromise of the Trustodian side yields ciphertext with no key; compromise of your share store yields one share, which rebuilds nothing.
Never log wallet passwords, device shares, derived keys, reconstructed private keys, or recovery phrases. Redact them from error serializers too, since most logging stacks serialize error objects wholesale.
Last modified on October 27, 2026