# Identity-First Setup

[Identity-backed encryption](/wallets/identity-backed-encryption) adds a second wrap of the user's Data Encryption Key (DEK), held against their signed-in identity instead of an authenticator. This page is the integration recipe for apps that want it to be the **default for every account**: the user signs in, gets a wallet, and trades or sends without ever seeing a WebAuthn prompt. Passkeys stay available for the users who want them.

The result you are building toward:

| Moment | What the user does |
| --- | --- |
| First sign-in | Email OTP or OAuth. Nothing else, no wallet-encryption screen |
| First wallet | Nothing. `createWallet()` runs with no prompt |
| Signing, sending, trading | Nothing. Their session is the proof |
| Sign-in on a new device | Nothing. No passkey to sync, no recovery flow |
| Export a private key | Confirms with a code sent to their email |

<Info>
  Nothing here removes a passkey wrap or weakens one. A user can hold both wraps at once, and either one opens the same DEK. Read [the trust tradeoff](/wallets/identity-backed-encryption#the-trust-tradeoff) before defaulting a whole user base to this: account-recovery strength becomes wallet-recovery strength.
</Info>

## Prerequisites

Identity-backed encryption runs on shared MoonPay infrastructure, so there is nothing to configure in your app. MoonPay enables it per app, and the Wallets page of your dashboard shows the status read-only. Ask MoonPay to turn it on for every app you plan to ship this way, including your development and staging apps.

At runtime, `usePasskeyStatus()` reports whether the app has it:

```tsx
import { usePasskeyStatus } from '@moon-x/react-sdk';

const {
  kmsWrapEnabled, // the app is configured for identity-backed encryption
  hasKmsWrap,     // this user holds a usable identity-backed wrap
  hasPasskey,     // this user holds at least one passkey wrap
  passkeys,
  loading,
  refresh,
} = usePasskeyStatus();
```

`kmsWrapEnabled`, `hasKmsWrap`, and `hasPasskey` are `null` until the first fetch resolves. Use explicit boolean checks to distinguish unavailable protection from status that is not yet known.

<Warning>
  Do not ship an identity-first flow against an app where `kmsWrapEnabled` is `false`. `enableAccountRecovery()` rejects with `account_recovery_unavailable`, and users are left with passkeys as the only way in, which is the flow you were trying to avoid.
</Warning>

## The setup flow

<Steps>
  <Step title="Add the wrap after sign-in">
    For a user with no wallet key material yet, `enableAccountRecovery()` is the whole setup, and it shows no UI at all. It bootstraps the DEK and wraps it against the user's identity. No passkey, no email code, and no screen: a first bootstrap is gated on the session alone, the same way a first passkey enrollment is, and there is no existing protection being widened to disclose.

    The same call behaves differently for a user who already has a wallet. There the DEK has to be unwrapped with a passkey and re-wrapped, which both fires a WebAuthn prompt and widens who can reach key material the user already holds, so the SDK shows a consent screen first. See [Rolling it out to an existing user base](#rolling-it-out-to-an-existing-user-base).

    ```tsx
    import { useEffect, useRef } from 'react';
    import { useMoonX, usePasskeyStatus } from '@moon-x/react-sdk';

    // Offers identity-backed encryption once per signed-in user who has no
    // way to unlock a wallet yet. Runs before the app needs a wallet, so
    // wallet creation later has something to encrypt against.
    export function useIdentityFirstSetup() {
      const { isAuthenticated, ready, enableAccountRecovery } = useMoonX();
      const { kmsWrapEnabled, hasKmsWrap, hasPasskey, loading, refresh } =
        usePasskeyStatus();
      const offered = useRef(false);

      useEffect(() => {
        if (!ready || !isAuthenticated || loading) return;
        if (kmsWrapEnabled !== true) return;
        // Already covered by either wrap: nothing to offer.
        if (hasKmsWrap === true || hasPasskey === true) return;
        if (offered.current) return;
        offered.current = true;

        enableAccountRecovery()
          .then(refresh)
          .catch((error) => {
            // A brand-new user has nothing to decline, so this only fires
            // on a real failure. An existing wallet holder can cancel the
            // consent screen, which rejects with kms_recovery_cancelled.
            if (error.message === 'kms_recovery_cancelled') return;
            console.error('identity-backed encryption setup failed', error);
          });
      }, [
        ready,
        isAuthenticated,
        loading,
        kmsWrapEnabled,
        hasKmsWrap,
        hasPasskey,
        enableAccountRecovery,
        refresh,
      ]);
    }
    ```

    <Note>
      Because the SDK shows nothing here, **the disclosure is yours to make.** The user's wallet becomes reachable by whoever can get into their account, and they should learn that from your onboarding copy, your security page, or your terms, in your own words. Say it once, somewhere they will actually read it.

      The call still resolves against the network, so treat it like any other async setup step: run it where a brief failure is recoverable, such as right after login, rather than in the middle of a checkout or a trade.

      Calling it more than once is safe. A call that loses the race, whether against a double-fired effect, a second tab, or a `createWallet()` that bootstraps at the same moment, re-checks the result and resolves rather than reporting a failure for a user who ended up correctly wrapped. Guard it with a ref anyway, as above, so you are not paying for redundant round trips.
    </Note>
  </Step>

  <Step title="Create the wallet with no prompt">
    Once the wrap exists, wallet creation is an ordinary call. The keyshare write and the ownership proof are minted from the user's session, so there is no biometric prompt and no email code.

    ```tsx
    import { useCreateWallet } from '@moon-x/react-sdk/ethereum';

    function CreateWalletButton() {
      const { createWallet } = useCreateWallet();

      return (
        <button onClick={() => createWallet()}>
          Create wallet
        </button>
      );
    }
    ```

    Wallets are never minted automatically, so call `createWallet()` when your product needs one. Signing, sending, and importing behave the same way from here: the session is the proof.
  </Step>

  <Step title="Gate wallet writes on either wrap">
    Apps written before identity-backed encryption existed gate their wallet buttons on `hasPasskey`. That check now refuses users who have a perfectly usable way in. Gate on either wrap instead:

    ```tsx
    const { hasPasskey, kmsWrapEnabled, hasKmsWrap, loading } = usePasskeyStatus();

    // Creating, importing, or signing needs something that can unlock the
    // DEK. Either wrap qualifies.
    const canUseWallets = hasPasskey === true ||
      (kmsWrapEnabled === true && hasKmsWrap === true);
    ```

    Handle both refusal codes when the check is bypassed or the state is stale. The SDK rejects with `passkey_required` when a passkey is the only route left, and `no_unlock_method` when the user has key material but no usable wrap at all.
  </Step>

  <Step title="Offer passkeys as an upgrade, not a requirement">
    Keep passkeys reachable from your settings surface for users who want a hardware-bound wrap. `registerPasskey()` handles both shapes: a first passkey for a user who holds neither wrap, and an added wrap for a user who is already identity-backed.

    ```tsx
    import { useRegisterPasskey, usePasskeyStatus } from '@moon-x/react-sdk';

    function AddPasskeyButton() {
      const { registerPasskey, isRegistering } = useRegisterPasskey();
      const { hasKmsWrap, refresh } = usePasskeyStatus();

      const handleClick = async () => {
        const result = await registerPasskey();
        if (result?.passkey_registered) await refresh();
      };

      return (
        <>
          <button onClick={handleClick} disabled={isRegistering}>
            Add a passkey
          </button>
          {hasKmsWrap && (
            <p>You have no passkey to confirm with, so this sends a code to your email first.</p>
          )}
        </>
      );
    }
    ```

    An identity-only user has no passkey to assert, so the SDK confirms the enrollment with an emailed code before creating the new credential. Tell them that up front, as above, or the email arrives unexplained.

    <Warning>
      `registerPasskey({ createWallets })`, the one-prompt register-and-create shortcut, is only for brand-new users with no key material. For a user who already holds an identity-backed wrap it rejects with `register_create_unavailable_for_kms_upgrade`. Create their wallets with `createWallet()` instead, which needs no prompt anyway.
    </Warning>
  </Step>
</Steps>

## If you also prompt for a passkey at signup

An app that already calls `registerPasskey()` during onboarding does not have to drop that flow to become identity-first. When the app is configured for identity-backed encryption, the SDK's enrollment screen grows a skip option on its own, and skipping resolves your call with `passkey_registered: false`.

Skipping does **not** create the identity wrap. Treat the skip as the branch into identity-first setup:

```tsx
const result = await registerPasskey();
if (!result?.passkey_registered) {
  // User skipped the passkey. Give them the other wrap instead of
  // leaving them with no way to unlock a wallet.
  await enableAccountRecovery();
}
await refresh();
```

<Tip>
  To keep the passkey mandatory instead, set `passkeyEnrollConfig.uiConfig.required: true` in your `MoonXProvider` config, which removes the skip option.
</Tip>

## What still asks the user for something

For an eligible identity-backed user with no passkeys, routine wallet operations and ephemeral-signer actions use session presence. Key export and passkey enrollment still require email OTP. `MoonXProvider` includes the email step-up prompt.

If the user has an enrolled passkey, the SDK tries it first. An eligible session can authorize supported operations if the passkey assertion fails or is canceled. Email OTP is available only to users with no passkeys and a verified email address.

| Operation | Identity-backed user with no passkeys sees |
| --- | --- |
| Sign, send, trade | Nothing |
| Create or import a wallet | Nothing |
| Export a private key | Emailed code |
| Add a passkey | Emailed code |
| Provision, re-provision, or revoke an ephemeral signer | Nothing |
| Remove the identity-backed wrap | Requires an enrolled passkey and a fresh assertion |

The email step-up prompt rejects with `presence_otp_cancelled` when the user closes it. Treat that as a cancellation, not a failure.

<Info>
  Removing the identity-backed wrap is passkey-only by design: a session or an emailed code cannot destroy the alternative that a passkey provides. This means `disableAccountRecovery()` is only worth offering to users who hold a passkey, and it rejects with `cannot_remove_last_wrap` when the remaining wrap would leave the DEK unopenable.
</Info>

## Set up an ephemeral signer

After identity-backed setup, call `useEphemeralSigner().provision()` with the wallet IDs the user selects. Provisioning and revocation use separate, scoped, single-use presence tokens that the SDK obtains from an eligible session.

See [Set up an identity-backed signer](/wallets/ephemeral-signers/identity-backed-setup) for the SDK version requirements, complete React examples, credential storage, and revocation flow.

## Errors worth handling by name

| Code | Meaning | What to do |
| --- | --- | --- |
| `account_recovery_unavailable` | The app is not configured for identity-backed encryption | Ask MoonPay to enable it; do not retry |
| `kms_recovery_cancelled` | The user declined the consent screen, which only existing wallet holders see | Stay quiet, let them start it again |
| `presence_otp_cancelled` | The user closed the email code prompt | Treat as a cancellation |
| `passkey_required` | The operation can only be proved with a passkey | Route the user to passkey enrollment |
| `no_unlock_method` | Key material exists but no usable wrap does | The user needs a passkey they still hold; see [Losing all passkeys](/wallets/passkeys#losing-all-passkeys) |
| `cannot_remove_last_wrap` | The removal would strand the DEK | Add the other wrap first |
| `register_create_unavailable_for_kms_upgrade` | Register-and-create was called for an existing identity-backed user | Call `createWallet()` separately |

## Rolling it out to an existing user base

New accounts take the path above. Existing accounts fall into two groups, and the difference matters because a DEK that already exists has to be unwrapped before a second wrap can be sealed:

* **No wallet yet.** Same as a new account. `enableAccountRecovery()` bootstraps the DEK and needs no passkey.
* **Wallet already created under a passkey.** `enableAccountRecovery()` asks for one passkey assertion, unwraps the DEK with it, and re-wraps it against the identity. Users who have lost every passkey cannot be migrated, because nothing can open their DEK. Offer the upgrade while their passkey still works, not after.

A reasonable rollout order:

1. Ship the `canUseWallets` gate first, so no surface depends on `hasPasskey` alone.
2. Add the settings surface with both wraps, and let existing passkey users opt in.
3. Turn on identity-first setup for new signups.

## Related

<CardGroup cols={2}>
  <Card title="Set up an identity-backed signer" icon="robot" href="/wallets/ephemeral-signers/identity-backed-setup">
    Provision and revoke delegated signing access through an eligible session.
  </Card>

  <Card title="Identity-Backed Encryption" icon="fingerprint" href="/wallets/identity-backed-encryption">
    What the second wrap is, the trust tradeoff, and the presence rules behind it.
  </Card>

  <Card title="Passkeys & Key Protection" icon="key" href="/wallets/passkeys">
    The passkey wrap, the enrollment lifecycle, and what losing every passkey means.
  </Card>

  <Card title="Presence Tokens" icon="lock" href="/authentication/user-authentication/presence-tokens">
    The short-lived proofs that gate every sensitive operation.
  </Card>

  <Card title="React SDK Configuration" icon="settings" href="/get-started/frontend-sdks/react/configuration">
    Provider options, including `passkeyEnrollConfig`.
  </Card>
</CardGroup>
