> ## Documentation Index
> Fetch the complete documentation index at: https://docs.portalhq.io/llms.txt
> Use this file to discover all available pages before exploring further.

> The eject feature allows a user to construct a private key that can be imported into another wallet manager, such as MetaMask.

# Eject a wallet

# Eject a wallet

Eject combines a user's two matching MPC backup shares into a raw private key, letting them move a wallet off MPC and, on EVM chains, import it into a wallet manager like MetaMask.

<Danger>
  **Warning:** Providing the custodian backup share to the client device puts both MPC shares on a single device, removing the multi-party security benefits of MPC. This operation should only be done for users who want to move off of MPC and into a single private key.
</Danger>

## Ejecting a Wallet

To eject the private keys for your wallets, the two matching backup shares need to be combined. This is done by fetching the backup shares and then combining them with the [@portal-hq/eject-js](https://www.npmjs.com/package/@portal-hq/eject-js) NPM package.

`@portal-hq/eject-js` combines two **plaintext** MPC shares into a raw private key. It does **not** fetch or decrypt shares for you - your code is responsible for retrieving each share and, when using Portal-Managed Backups, decrypting the client backup share before passing it in.

<Note>
  **Share encodings differ.** The client backup share you get from `/v1/backup` is **base64-encoded JSON**, so you must base64-decode it before passing it to eject-js. The custodian backup share is a **JSON string** (the API returns it JSON-stringified); pass it to eject-js as-is. Only the client share is base64-encoded and needs decoding. Both samples below use this helper for the client share:

  ```typescript theme={null}
  // Decodes the base64 client backup share (from /v1/backup) into the JSON eject-js expects.
  const decodeClientBackupShare = (base64Share) => Buffer.from(base64Share, 'base64').toString('utf8')
  ```

  Store the client share **encrypted** at rest, as [Back up a wallet](./backup) describes. Once you decrypt it, its base64 form is what `/v1/recover` consumes directly; only eject-js needs it base64-decoded, so decode at eject time.
</Note>

### Prerequisites

* Install the eject package: `npm install @portal-hq/eject-js`.
* A [Custodian API key](/resources/authentication-and-api-keys). The endpoints below are Custodian API endpoints and must be called from your backend - never expose a Custodian API key in client code.
* For the Portal-Managed Backups flow, your environment must have **Portal-Managed Backups enabled**. If it is not, the `ejectable-backup-shares` endpoint returns a `400`.

### With Portal-Managed Backups

With Portal-Managed Backups, both backup shares are stored (encrypted) with Portal. To eject a wallet's private key, fetch both shares from the Custodian API and combine them:

* **Custodian Backup Share** - returned already decrypted by Portal, ready to pass to eject.
* **Client Backup Share** - returned as `encryptedClientBackupShare`, which is the ciphertext your application produced at backup time. You must decrypt it before combining, reversing the encryption your application applied when it [backed up the wallet](./backup).

The general steps are:

1. Get the Portal client's details and find their `SECP256K1` and `ED25519` wallets.
2. Fetch each wallet's ejectable backup shares (`encryptedClientBackupShare` + `custodianBackupShare`).
3. Decrypt each `encryptedClientBackupShare` (see the note below).
4. Combine each pair of shares to recover the private keys.

```typescript theme={null}
import axios from 'axios'
import { recoverSecp256k1Key, recoverEd25519Key } from '@portal-hq/eject-js'

const clientId = 'example-client-id'

// Authenticate every request with your Custodian API key.
const api = axios.create({
  baseURL: 'https://api.portalhq.io/api/v3',
  headers: { Authorization: `Bearer ${process.env.PORTAL_CUSTODIAN_API_KEY}` },
})

// 1. Get the Portal client and find their wallets.
const { data: clientDetails } = await api.get(`/custodians/me/clients/${clientId}`)
const secp256k1Wallet = clientDetails.wallets.find((wallet) => wallet.curve === 'SECP256K1')
const ed25519Wallet = clientDetails.wallets.find((wallet) => wallet.curve === 'ED25519')

// 2. Fetch each wallet's ejectable backup shares.
const { data: secp256k1Shares } = await api.get(`/custodians/me/clients/${clientId}/wallets/${secp256k1Wallet.id}/ejectable-backup-shares`)
const { data: ed25519Shares } = await api.get(`/custodians/me/clients/${clientId}/wallets/${ed25519Wallet.id}/ejectable-backup-shares`)

// 3. Decrypt the client backup shares, then base64-decode them. `decryptClientBackupShare`
//    is your own implementation that reverses the encryption applied at backup time.
const secp256k1ClientShare = decodeClientBackupShare(await decryptClientBackupShare(secp256k1Shares.encryptedClientBackupShare))
const ed25519ClientShare = decodeClientBackupShare(await decryptClientBackupShare(ed25519Shares.encryptedClientBackupShare))

// 4. Recover the private keys. custodianBackupShare is a JSON string from the API - pass it as-is (eject-js parses it).
const secp256k1PrivateKey = await recoverSecp256k1Key(secp256k1ClientShare, secp256k1Shares.custodianBackupShare) // hex (EVM)
const ed25519PrivateKey = await recoverEd25519Key(ed25519ClientShare, ed25519Shares.custodianBackupShare) // base58 (Solana)
```

<Note>
  `decryptClientBackupShare` is a placeholder for your own decryption logic. With the Enclave MPC API, **your application encrypts the client backup share itself at backup time** - Portal has no built-in backup method here and only stores the ciphertext you give it. Eject must therefore reverse that same encryption with the same key. If you followed the recommendation in [Back up a wallet](./backup) and used a KMS with envelope encryption, decrypt using that same KMS key. The `custodianBackupShare` is returned already decrypted and needs no such step.

  If you stored the whole `{ share, id }` object returned by `/v1/backup` (rather than just the `share` string), take its `.share` field before calling `decodeClientBackupShare`.
</Note>

<Note>
  **`SECP256K1`** is the curve used by Ethereum and **`ED25519`** is the curve used by Solana.

  You can learn more about signing algorithms and curves [here](http://ethanfast.com/top-crypto.html).
</Note>

### With Self-Managed Backups

With Self-Managed Backups you store both backup shares yourself during backup. The eject transforms are the same as above; only the fetch location differs - you read both shares from your own backend instead of from Portal.

* **Client Backup Shares** - the client backup shares you stored. Decrypt them (reversing whatever encryption you applied at backup) and base64-decode them, exactly as in the Portal-Managed flow.
* **Custodian Backup Shares** - the custodian backup shares delivered to your backup webhook as a JSON string. Pass them to eject as-is.

```typescript theme={null}
import { recoverSecp256k1Key, recoverEd25519Key } from '@portal-hq/eject-js'

const decodeClientBackupShare = (base64Share) => Buffer.from(base64Share, 'base64').toString('utf8')

// Fetch + decrypt + decode the client backup shares you stored during backup.
const secp256k1ClientShare = decodeClientBackupShare(await decryptClientBackupShare(await fetchSecp256k1ClientBackupShare()))
const ed25519ClientShare = decodeClientBackupShare(await decryptClientBackupShare(await fetchEd25519ClientBackupShare()))

// Fetch the custodian backup shares you stored during backup (a JSON string; pass as-is).
const secp256k1CustodianShare = await fetchSecp256k1CustodianBackupShare()
const ed25519CustodianShare = await fetchEd25519CustodianBackupShare()

// Recover a secp256k1 private key (EVM).
const secp256k1PrivateKey = await recoverSecp256k1Key(secp256k1ClientShare, secp256k1CustodianShare)

// Recover an ed25519 private key (Solana).
const ed25519PrivateKey = await recoverEd25519Key(ed25519ClientShare, ed25519CustodianShare)
```

## Using the ejected keys

The recovered `SECP256K1` key (hex) can be imported into most third-party wallets - for example, MetaMask's private-key import.

The `ED25519` key (base58) is a valid Solana key, but it is **not** directly importable into most third-party Solana wallets: a nuance of MPC distributed key generation for `ED25519` prevents it. Portal's open-source [scalarwallet.org](https://scalarwallet.org) (self-host or hosted) lets you use it. See [Eject](/resources/eject) for more detail.
