# Confirm Client Custody

Final step of migrating a server-custodial wallet to client-side custody: prove the device holds the exported key, and the server removes its own copy.

## Overview

Step 4 of the key migration flow — [Request Export Token](https://docs.aureahub.com/docs/wallets-export-token.md) → [Export Encrypted Key](https://docs.aureahub.com/docs/wallets-export-key.md) → [Migration Challenge](https://docs.aureahub.com/docs/wallets-migration-challenge.md) → Confirm Client Custody. See the [Non-Custodial Key Lifecycle](https://docs.aureahub.com/docs/guide-non-custodial-keys.md) guide for the whole sequence.

The server loads the wallet's most recent unused migration challenge and verifies that `signedChallenge` was produced by the wallet's own private key (EIP-191 over the 32 challenge bytes for EVM wallets; hex-encoded Ed25519 for Solana). On success it performs, in one database transaction:

- marks the challenge as used;
- clears the server-held key material stored in the database and sets `keyManagementScheme` to `client_side`;
- marks the wallet's key for deletion and removes the wallet's permit nonce reservations.

A background job, which runs every 5 minutes, then disables the key share held in the key vault and marks the deletion as complete. After this call the server can no longer sign on behalf of the wallet.

> ⚠️ This cannot be undone through the API: [Cancel Migration](https://docs.aureahub.com/docs/wallets-cancel-migration.md) only works while the wallet is `client_side_pending`. Decrypt the exported key and store it securely on the device **before** calling this endpoint — from then on, treat the device copy as the only copy.

## Endpoint

### `POST /v1/wallets/confirm-client-custody`

Authentication: bearer token required.

Validates the signed migration challenge, then removes server-side key material and sets keyManagementScheme to client_side.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `walletId` | string (uuid) | yes | Wallet being migrated. |
| `signedChallenge` | string | yes | Signature over the 32 bytes of the migration challenge, made with the wallet's private key: 0x-prefixed EIP-191 signature (EVM) or hex-encoded Ed25519 signature (Solana). |

**Responses**

`200` OK

```json
{ "migrated": true }
```

`400` Bad Request

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "No active migration challenge found. Call GET /migration-challenge first."
}
```

`422` Wrong Signer

```json
{
  "statusCode": 422,
  "error": "BlockchainError",
  "message": "Challenge signature does not match the wallet address",
  "code": "WRONG_SIGNER",
  "retryable": false,
  "details": { "code": "WRONG_SIGNER", "retryable": false }
}
```

## Errors

Error responses

|  |  |  |
| --- | --- | --- |
| 400 | — | `No active migration challenge found. Call GET /migration-challenge first.` |
| 400 | — | `Migration challenge has expired. Request a new one.` |
| 404 | — | `Wallet not found` — unknown id, or the wallet belongs to another user. |
| 409 | — | `Wallet is already fully migrated to client-side custody` |
| 409 | — | `Key deletion is already pending. Contact support if stuck.` |
| 422 | WRONG_SIGNER | The signature was not made by the wallet's key. |
| 422 | SIGNATURE_DECODE_FAILED | The EVM signature could not be decoded. |

## Implementation

```javascript
import { getBytes } from 'ethers';

// localWallet: ethers Wallet built from the key decrypted in "Export Encrypted Key"
async function confirmClientCustody(accessToken, walletId, localWallet) {
  const API = 'https://api.aureahub.com';
  const bearer = { Authorization: `Bearer ${accessToken}` };

  const { challenge } = await fetch(
    `${API}/v1/wallets/migration-challenge?walletId=${walletId}`,
    { headers: bearer }
  ).then(r => r.json());

  const res = await fetch(`${API}/v1/wallets/confirm-client-custody`, {
    method: 'POST',
    headers: { ...bearer, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      walletId,
      signedChallenge: await localWallet.signMessage(getBytes('0x' + challenge))
    })
  });
  if (!res.ok) throw new Error((await res.json()).message);
  return res.json(); // { migrated: true }
}
```

---

Web version: https://docs.aureahub.com/#wallets-confirm-custody
