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

# Magic

> Use a Magic embedded or server wallet as the owner of a Rhinestone smart account.

Magic offers client-side embedded wallets and server wallets. Both can provide the ECDSA signatures required by a Rhinestone smart account, but they use different trust boundaries.

<Tabs>
  <Tab title="Embedded wallet">
    Use this path when Magic authenticates and signs in the browser.

    <Steps>
      <Step title="Install and initialize Magic">
        ```bash theme={null}
        npm install magic-sdk @rhinestone/sdk viem
        ```

        ```ts theme={null}
        import { Magic } from 'magic-sdk'

        export const magic = new Magic(process.env.NEXT_PUBLIC_MAGIC_API_KEY!)
        ```

        The Magic key in this snippet is publishable. Keep the Rhinestone API key and any Magic secret key server-side.
      </Step>

      <Step title="Authenticate and create a viem account">
        Magic embedded wallets do not support `wallet_switchEthereumChain`, which the Rhinestone signing flow can request. Ignore only chain add and switch requests, and forward every other RPC method to Magic. The wrapper also converts `bigint` values before typed-data signing.

        ```ts theme={null}
        import { walletClientToAccount } from '@rhinestone/sdk/utils'
        import {
          createWalletClient,
          custom,
          type TypedData,
          type TypedDataDefinition,
        } from 'viem'
        import { toAccount } from 'viem/accounts'
        import { magic } from './magic'

        await magic.auth.loginWithEmailOTP({ email: userEmail })

        const addresses = (await magic.rpcProvider.request({
          method: 'eth_accounts',
        })) as `0x${string}`[]
        const address = addresses[0]
        if (!address) throw new Error('Magic did not return a wallet address')

        function serializeBigInts(value: unknown): unknown {
          if (typeof value === 'bigint') return value.toString(10)
          if (Array.isArray(value)) return value.map(serializeBigInts)
          if (value && typeof value === 'object') {
            return Object.fromEntries(
              Object.entries(value).map(([key, nested]) => [
                key,
                serializeBigInts(nested),
              ]),
            )
          }
          return value
        }

        const provider = {
          request: async (args: { method: string; params?: unknown[] }) => {
            if (
              args.method === 'wallet_switchEthereumChain' ||
              args.method === 'wallet_addEthereumChain'
            ) {
              return null
            }
            return magic.rpcProvider.request(args)
          },
        }

        const walletClient = createWalletClient({
          account: address,
          transport: custom(provider),
        })
        const adapted = walletClientToAccount(walletClient)

        if (!adapted.signMessage || !adapted.signTransaction || !adapted.signTypedData) {
          throw new Error('Magic does not support the required signing methods')
        }

        export const magicOwner = toAccount({
          address: adapted.address,
          signMessage: adapted.signMessage,
          signTransaction: adapted.signTransaction,
          signTypedData: async <
            const typedData extends TypedData | Record<string, unknown>,
            primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData,
          >(
            definition: TypedDataDefinition<typedData, primaryType>,
          ) =>
            adapted.signTypedData(
              serializeBigInts(definition) as TypedDataDefinition<
                typedData,
                primaryType
              >,
            ),
        })
        ```
      </Step>

      <Step title="Create the Rhinestone account">
        Authenticate the browser SDK with short-lived JWTs from your backend:

        ```ts theme={null}
        import { RhinestoneSDK } from '@rhinestone/sdk'
        import { magicOwner } from './magic-owner'

        const rhinestone = new RhinestoneSDK({
          auth: {
            mode: 'experimental_jwt',
            accessToken: async () => {
              const response = await fetch('/api/auth/access-token', {
                credentials: 'include',
              })
              if (!response.ok) throw new Error('Unable to authenticate with Rhinestone')
              return ((await response.json()) as { token: string }).token
            },
          },
        })

        const account = await rhinestone.createAccount({
          owners: { type: 'ecdsa', accounts: [magicOwner] },
        })
        ```

        See [security](/wallets/custom-signer/integration/security) for secure token issuance and sponsored-intent extension tokens.
      </Step>
    </Steps>
  </Tab>

  <Tab title="Server wallet">
    Magic Express server wallets require an OIDC provider registered with Magic. Your backend issues a user-bound JWT, obtains or creates that user's wallet, and sends message hashes to Magic for signing. Follow [Magic Express setup](https://docs.magic.link/server-wallets/express-api/getting-started) to register your issuer and JWKS endpoint first.

    <Steps>
      <Step title="Create Magic Express helpers">
        Run this code only on your backend:

        ```ts theme={null}
        import type { Hex } from 'viem'

        const magicExpressUrl = 'https://tee.express.magiclabs.com'

        function magicHeaders(userJwt: string) {
          return {
            Authorization: `Bearer ${userJwt}`,
            'X-Magic-API-Key': process.env.MAGIC_API_KEY!,
            'X-OIDC-Provider-ID': process.env.MAGIC_OIDC_PROVIDER_ID!,
            'X-Magic-Chain': 'ETH',
            'Content-Type': 'application/json',
          }
        }

        export async function getOrCreateMagicWallet(userJwt: string) {
          const response = await fetch(`${magicExpressUrl}/v2/wallet`, {
            method: 'POST',
            headers: magicHeaders(userJwt),
          })
          if (!response.ok) throw new Error('Unable to create Magic wallet')
          return response.json() as Promise<{ public_address: Hex }>
        }

        export async function signWithMagic(userJwt: string, hash: Hex) {
          const response = await fetch(`${magicExpressUrl}/v2/wallet/sign/data`, {
            method: 'POST',
            headers: magicHeaders(userJwt),
            body: JSON.stringify({ raw_data_hash: hash }),
          })
          if (!response.ok) throw new Error('Magic signing failed')
          return response.json() as Promise<{ signature: Hex }>
        }
        ```

        Validate the application session before issuing `userJwt`. Keep the OIDC private key and Magic credentials in a server-side secret manager.
      </Step>

      <Step title="Create a server-side owner and account">
        ```ts theme={null}
        import { RhinestoneSDK } from '@rhinestone/sdk'
        import {
          hashMessage,
          hashTypedData,
          type TypedData,
          type TypedDataDefinition,
        } from 'viem'
        import { toAccount } from 'viem/accounts'
        import { getOrCreateMagicWallet, signWithMagic } from './magic-express'

        const { public_address } = await getOrCreateMagicWallet(userJwt)
        const magicOwner = toAccount({
          address: public_address,
          async signMessage({ message }) {
            return (await signWithMagic(userJwt, hashMessage(message))).signature
          },
          async signTypedData<
            const typedData extends TypedData | Record<string, unknown>,
            primaryType extends keyof typedData | 'EIP712Domain' = keyof typedData,
          >(definition: TypedDataDefinition<typedData, primaryType>) {
            return (await signWithMagic(userJwt, hashTypedData(definition))).signature
          },
          async signTransaction() {
            throw new Error('Raw transaction signing is not enabled')
          },
        })

        const rhinestone = new RhinestoneSDK({
          auth: {
            mode: 'apiKey',
            apiKey: process.env.RHINESTONE_API_KEY!,
          },
        })

        const account = await rhinestone.createAccount({
          owners: { type: 'ecdsa', accounts: [magicOwner] },
        })
        ```

        Bind `userJwt` to the authenticated user and authorize every signing request. Do not expose a generic hash-signing endpoint to unauthenticated clients.
      </Step>
    </Steps>
  </Tab>
</Tabs>

For sponsored transactions, follow [sponsorship setup](/transactions/sponsorship/set-up). For the transaction lifecycle, continue to [send a transaction](/transactions/multichain/send-a-transaction).
