# Register Client Wallet

Create a non-custodial wallet whose private key stays on the user's device. Aurea verifies the signed challenge and stores only the address.

## Overview

This is step 2 of client-side wallet registration, used on tenants whose `walletMode` is `non_custodial` (on those tenants `POST /v1/wallets/` returns `400`). First fetch a challenge with [Non-Custodial Challenge](https://docs.aureahub.com/docs/wallets-client-challenge.md) and sign it with the new key.

The server loads the most recent unused challenge issued to the user, checks that it has not expired and that it was issued for the same `address`, and verifies the signature against that address. It then consumes the challenge and creates the wallet with `keyManagementScheme: "client_side"`. No private key, encrypted key or key share is stored.

- **Primary:** when `isPrimary` is omitted, the wallet becomes primary if it is the user's first wallet on that chain and network type.
- **Address format:** EVM addresses are stored checksummed; Solana addresses are stored as given.
- **Signing later:** transfers and swaps from a `client_side` wallet are prepared by the server and signed on the device — see [Sending a Transaction](https://docs.aureahub.com/docs/guide-send-transaction.md).
- **Gnosis gas airdrop:** if enabled for the tenant, a small xDAI amount (set by the deployment) is sent to a newly registered Gnosis mainnet wallet. The transfer is not awaited and never affects the response.

## Endpoint

### `POST /v1/wallets/client`

Authentication: bearer token required.

Verifies the signed challenge and creates a wallet with keyManagementScheme client_side. No key material is stored server-side.

**Request body**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `chain` | string | yes | Chain identifier, e.g. `gnosis`, `ethereum`, `solana`. Use `evm` to register the address on every supported EVM chain at once (see below). A testnet's chain registry id (e.g. `polygon-amoy`) is also accepted: the wallet is stored under the family chain (`polygon`) on that testnet. |
| `address` | string | yes | The address the challenge was issued for. |
| `signedChallenge` | string | yes | Signature over the 32 challenge bytes: 0x-prefixed EIP-191 signature (EVM) or hex-encoded Ed25519 signature (Solana). |
| `label` | string | no | Friendly label, returned as `label`. |
| `isPrimary` | boolean | no | Mark as primary. Default: `true` if this is the user's first wallet on this chain and network type, otherwise `false`. For `evm`, see below. |
| `isTestnet` | boolean | no | Register as a testnet wallet (default: `false`). |

**Responses**

`201` Created

```json
{
  "id": "3f8b2a1e-5c4d-4e7f-9a0b-1c2d3e4f5a6b",
  "chain": "gnosis",
  "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
  "walletType": "eoa",
  "isPrimary": true,
  "isTestnet": false,
  "label": null,
  "isWatchOnly": false,
  "keyManagementScheme": "client_side",
  "createdAt": "2026-09-11T10:00:00.000Z",
  "updatedAt": "2026-09-11T10:00:00.000Z"
}
```

`400` Bad Request

```json
{
  "statusCode": 400,
  "error": "BadRequestError",
  "message": "Address mismatch — the signed challenge was issued for a different address"
}
```

`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 }
}
```

## Multi-Chain EVM Registration

With `chain: "evm"` the same address is registered as one wallet per supported EVM chain — `gnosis`, `ethereum`, `polygon`, `bsc`, `arbitrum`, `optimism` and `base` — in a single database transaction, using a single challenge.

- Chains where this address is already registered in the tenant on the same network (`isTestnet`) are skipped instead of failing. An address registered on mainnet gets its testnet wallets, and the reverse.
- The `gnosis` wallet is primary unless you pass `isPrimary: false`; the other chains are created as non-primary.
- `label` and `isTestnet` apply to every wallet created.
- The response is the `gnosis` wallet.

## Errors

Error responses

|  |  |  |
| --- | --- | --- |
| 400 | — | Request validation failed — a required field is missing or empty. |
| 400 | — | `No active wallet claim challenge found. Call GET /wallets/client/challenge first.` |
| 400 | — | `Challenge has expired. Request a new one.` |
| 400 | — | `Address mismatch — the signed challenge was issued for a different address` |
| 400 | — | `Invalid Solana wallet address` |
| 409 | — | `A wallet with this address already exists in this tenant` — single-chain registration only, when the address is already registered on the same chain and network (`isTestnet`). |
| 422 | WRONG_SIGNER | The signature does not verify for `address`. |
| 422 | SIGNATURE_DECODE_FAILED | The EVM signature could not be decoded. |
| 500 | — | A malformed EVM `address` is not caught by validation and fails with `500`. |

## Implementation

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

const API = 'https://api.aureahub.com';

async function registerClientWallet(accessToken, chain = 'gnosis') {
  const bearer = { Authorization: `Bearer ${accessToken}` };
  const device = Wallet.createRandom();          // keep device.privateKey in secure storage

  // 1. Challenge bound to the new address
  const q = new URLSearchParams({ chain, address: device.address });
  const { challenge } = await fetch(`${API}/v1/wallets/client/challenge?${q}`, { headers: bearer })
    .then(r => r.json());

  // 2. Register with the signature over the challenge bytes
  const res = await fetch(`${API}/v1/wallets/client`, {
    method: 'POST',
    headers: { ...bearer, 'Content-Type': 'application/json' },
    body: JSON.stringify({
      chain,
      address: device.address,
      signedChallenge: await device.signMessage(getBytes('0x' + challenge)),
      label: 'My wallet'
    })
  });
  if (!res.ok) {
    const err = await res.json();
    throw new Error(err.message || `Registration failed: ${res.status}`);
  }
  return { wallet: await res.json(), device };   // wallet.keyManagementScheme === 'client_side'
}
```

---

Web version: https://docs.aureahub.com/#wallets-client-register
