# Non-Custodial Challenge

Get a one-time challenge that proves the user controls the private key of a wallet generated on their device — step 1 of registering a client-side (non-custodial) wallet.

## Overview

On tenants whose `walletMode` is `non_custodial`, the private key is generated and kept on the user's device and Aurea stores only the address. Before the wallet is created, the device proves it holds the key: it fetches a challenge here, signs it with the new key, and submits the signature to [Register Client Wallet](https://docs.aureahub.com/docs/wallets-client-register.md).

- The challenge is 32 random bytes, hex-encoded (64 characters), valid for **10 minutes**.
- It is bound to the authenticated user and to the `address` you pass. Registration fails if a different address is submitted.
- Each call replaces any unused wallet-registration challenge previously issued to the same user, so it is safe to call again after a failure or expiry.
- For `chain=solana` the address is used as given (base58). For any other chain value the address is parsed as an EVM address, so it must be all-lowercase or correctly checksummed.

> ⚠️ Validate EVM addresses before calling. A malformed or wrongly checksummed address is not reported as a validation error — the request fails with `500`.

## Endpoint

### `GET /v1/wallets/client/challenge`

Authentication: bearer token required.

Issues a 32-byte random challenge (TTL 10 min) to sign with the new wallet's private key before registering it.

**Query parameters**

| Name | Type | Required | Description |
| --- | --- | --- | --- |
| `chain` | string | yes | Chain of the wallet being registered, e.g. `gnosis` or `solana`. Use `evm` to register one address on every supported EVM chain (see Register Client Wallet). A testnet's chain registry id (e.g. `polygon-amoy`) is also accepted. |
| `address` | string | yes | Address of the new wallet — the same value you will send to `POST /v1/wallets/client`. |

**Responses**

`200` OK

```json
{
  "challenge": "b54b30a6f3fbdd6cd84bfdd85aca1b2dc58c9c6b6d6c180e1c7a9bb7bd556f4c",
  "expiresAt": "2026-09-11T10:10:00.000Z"
}
```

**Example request**

```bash
curl "https://api.aureahub.com/v1/wallets/client/challenge?chain=gnosis&address=0x71C7656EC7ab88b098defB751B7401B5f6d8976F" \
  -H "Authorization: Bearer YOUR_TOKEN"
```

## Signing the Challenge

Sign the **32 bytes** that the hex string decodes to — not the 64-character text. The same rule applies to the [Migration Challenge](https://docs.aureahub.com/docs/wallets-migration-challenge.md).

- **EVM chains** — EIP-191 `personal_sign` over the challenge bytes, made with the new key. Send the 0x-prefixed signature as `signedChallenge`.
- **Solana** — Ed25519 detached signature over the challenge bytes, sent **hex-encoded** (not base58).

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

// EVM: EIP-191 personal_sign over the 32 challenge bytes
const signedChallenge = await localWallet.signMessage(getBytes('0x' + challenge));
```

```javascript
import nacl from 'tweetnacl';

// Solana: Ed25519 signature over the 32 challenge bytes, hex-encoded
const signedChallenge = Buffer.from(
  nacl.sign.detached(Buffer.from(challenge, 'hex'), keypair.secretKey)
).toString('hex');
```

## Implementation

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

// 1. Generate the key on the device — it never leaves it
const localWallet = Wallet.createRandom();

// 2. Ask for a challenge bound to this address
const params = new URLSearchParams({ chain: 'gnosis', address: localWallet.address });
const res = await fetch(`https://api.aureahub.com/v1/wallets/client/challenge?${params}`, {
  headers: { Authorization: `Bearer ${accessToken}` }
});
if (!res.ok) throw new Error(`Challenge error: ${res.status}`);
const { challenge, expiresAt } = await res.json();

// 3. Sign the challenge bytes, then call POST /v1/wallets/client before expiresAt
const signedChallenge = await localWallet.signMessage(getBytes('0x' + challenge));
```

---

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