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

# Accounts and keys

> Register accounts, read addresses, choose a constructor, derive from a wallet signature, and store encrypted recovery data.

## Register account

Call `ensureRegistered()` once before the first mutating operation. The SDK
caches the registration attempt per client instance.

```ts theme={null}
await client.ensureRegistered();
```

* **Browser:** posts the user's public registration payload to your backend.
  The default route is `/api/unlink/register`.
* **Server or custodial:** pass a register callback when constructing the user
  client. The callback usually calls `admin.users.register(payload)`.

For direct backend code, call `admin.users.register(payload)` when you receive a
wire payload from a browser register route:

```ts theme={null}
import { createUnlinkAdmin } from "@unlink-xyz/sdk/admin";

const admin = createUnlinkAdmin({
  environment: "base-sepolia",
  apiKey,
});
await admin.users.register(req.body);
```

## Get address

Get the Bech32m Unlink address for this account.

```ts theme={null}
const address = await client.getAddress();
// "unlink1..."
```

## Account constructors

Every high-level client is bound to a signing-capable account. The constructor
you use decides which operations the account can perform.

| Constructor                     | Source                        | Transfer / withdraw | Execute |
| ------------------------------- | ----------------------------- | ------------------- | ------- |
| `account.fromMnemonic`          | BIP-39 mnemonic               | Yes                 | Yes     |
| `account.fromSeed`              | 64-byte seed                  | Yes                 | Yes     |
| `account.fromEthereumSignature` | EOA `personal_sign` signature | Yes                 | Yes     |
| `account.fromWallet`            | Wallet signature (browser)    | Yes                 | Yes     |
| `account.fromKeys`              | Raw keys                      | Yes                 | No      |

`execute()` needs a seed-backed account, so derive with `fromMnemonic`,
`fromSeed`, `fromEthereumSignature`, or `fromWallet`. `fromKeys` can transfer
and withdraw but cannot execute.

`account.fromMetaMask` remains available as a deprecated alias for
`account.fromWallet`.

## Derive account from a wallet signature

The SDK can derive an Unlink account from an EOA `personal_sign` signature
instead of a mnemonic. The [Quickstart](/quickstart) shows the one-shot
`account.fromWallet` wrapper. This section is the lower-level reference.

### Message format

The `buildDeriveSeedMessage` helper returns the exact string a wallet must sign:

```ts theme={null}
import { buildDeriveSeedMessage } from "@unlink-xyz/sdk/crypto";

const message = buildDeriveSeedMessage({
  appId: "your-app-id",
  chainId: 84532,
});
// "Unlink: derive identity\nTenant: your-app-id\nChain: 84532\nVersion: 1"
```

Rules:

* The message is the **single source of truth**. Bumping its format is a breaking change for every existing account.
* `appId` is embedded verbatim. The SDK rejects empty strings, LF/CR, or anything over 64 bytes UTF-8 but does not canonicalise casing or whitespace. Use a stable app identifier.
* `chainId` must be a positive integer.
* The message uses the literal label `Tenant:` for address compatibility, even though the parameter is named `appId`. Do not change the message text, or existing accounts derive differently.

### Sign and derive

If you control the signing path yourself, build the message, sign it, and pass
the signature into `account.fromEthereumSignature` with the same `appId` and
`chainId`.

```ts theme={null}
import { account, buildDeriveSeedMessage } from "@unlink-xyz/sdk/crypto";

const message = buildDeriveSeedMessage({
  appId: "your-app-id",
  chainId: 84532,
});

const signature = await walletClient.signMessage({
  account: evmAddress,
  message,
});

const unlinkAccount = account.fromEthereumSignature({
  signature,
  appId: "your-app-id",
  chainId: 84532,
});
```

The signature is canonicalised internally and expanded with HKDF-SHA256 into the
64-byte seed consumed by `account.fromSeed`. `appId` and `chainId` are bound
into the HKDF salt. Any mismatch derives a fresh, empty account instead of a
silent account swap.

Stability across wallets relies on RFC-6979 deterministic ECDSA, which every mainstream wallet uses today. For long-term recovery, prefer the keystore export (`account.export(keys)`) over re-deriving from a wallet signature each session.

## Get public key

Most integrations do not need the raw account public key. Use this only for
custom account storage.

```ts theme={null}
const [x, y] = await unlinkAccount.getPublicKey();
```

## User storage

`userStorage` stores up to two opaque base64 payloads for a generic application
`userId`. Engine does not interpret the payload. If `mode` is `"encrypted"`,
encrypt and decrypt client-side before calling Engine.

Create the client with the application user id. Dynamic, your own auth, or any
other session provider should sit behind your app's `requireUser()` boundary and
only supply this generic id to Engine:

```ts theme={null}
const client = createUnlinkClient({
  environment: "base-sepolia",
  account,
  userId: session.userId,
  authorizationToken: {
    body: ({ subjectType, unlinkAddress, userId }) =>
      subjectType === "user_storage"
        ? {
            subject_type: "user_storage",
            user_id: userId,
          }
        : {
            subject_type: "unlink_address",
            unlink_address: unlinkAddress,
          },
  },
});
```

For encrypted objects, store a base64-encoded envelope. The wrapping key is derived
locally by an unlock adapter, such as a passphrase adapter today or a WebAuthn
PRF adapter when available:

```ts theme={null}
import type { EncryptedStorageEnvelope } from "@unlink-xyz/sdk/browser";

const envelope: EncryptedStorageEnvelope = {
  version: 1,
  method: "passphrase:v1", // or "webauthn-prf:v1"
  alg: "AES-256-GCM",
  kdf: "PBKDF2-SHA256",
  salt,
  iv,
  ciphertext,
};

await client.userStorage.put("recovery", {
  mode: "encrypted",
  data: btoa(JSON.stringify(envelope)),
});

const { objects } = await client.userStorage.list();
const recovery = await client.userStorage.get("recovery");
await client.userStorage.delete("recovery");
```

Object keys and `userId` values may contain letters, numbers, `.`, `_`, and `-`,
but cannot be exactly `.` or `..`. Each payload is capped at 16,384 base64
characters.

<Warning>
  Never derive encryption keys from Dynamic JWTs, Dynamic user ids, email,
  wallet addresses, Dynamic signatures, or any other identity-provider value.
  For WebAuthn PRF, derive a client-side KEK from the authenticator PRF output
  and decrypt locally; Engine must never receive PRF output, KEKs, plaintext
  seeds, or decrypted object contents. Use passphrase unlock as the fallback
  when PRF support is unavailable.
</Warning>
