> ## 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

> Integrate Magic signers with Rhinestone smart accounts.

## Overview

Magic provides wallet infrastructure with two integration paths:

* **Embedded Wallets**: Client-side Magic SDK handles authentication and signing. Keys are managed by Magic and signing happens through their RPC provider.
* **Server Wallets**: Backend-managed wallets via Magic Express API. Keys live in Magic's TEE (Trusted Execution Environment) and signing happens through server-side API calls.

Both paths produce a viem-compatible account that becomes the owner of a Rhinestone smart account, enabling cross-chain functionality.

## Integration

<Tabs>
  <Tab title="Embedded Wallet">
    ### Prerequisites

    * A Magic account and publishable API key
    * Rhinestone API key
    * React application setup

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

          ```bash pnpm theme={null}
          pnpm add magic-sdk @rhinestone/sdk viem
          ```

          ```bash bun theme={null}
          bun install magic-sdk @rhinestone/sdk viem
          ```
        </CodeGroup>
      </Step>

      <Step title="Set Up Magic SDK">
        Initialize the Magic SDK in your application:

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

        const magic = new Magic(process.env.NEXT_PUBLIC_MAGIC_API_KEY)
        ```
      </Step>

      <Step title="Authenticate and Get Wallet Address">
        Use Magic's client-side SDK for authentication:

        ```tsx theme={null}
        // Login with Magic email OTP
        await magic.auth.loginWithEmailOTP({ email: "user@example.com" })

        // Get the wallet address
        const accounts = (await magic.rpcProvider.request({
          method: "eth_accounts",
        })) as string[]

        const address = accounts[0]
        ```

        Magic also supports SMS, social login, WebAuthn, and more. See [Magic's authentication overview](https://docs.magic.link/embedded-wallets/authentication/overview) for all options.
      </Step>

      <Step title="Create Rhinestone Account">
        Create a viem account from Magic's provider and pass it to Rhinestone:

        ```tsx theme={null}
        import { RhinestoneSDK, walletClientToAccount } from "@rhinestone/sdk"
        import { createWalletClient, custom } from "viem"
        import { toAccount } from "viem/accounts"
        import type { SignableMessage, TypedDataDefinition } from "viem"

        // Magic RPC requires BigInts as decimal strings
        function serializeBigInts(obj: unknown): unknown {
          if (obj === null || obj === undefined) return obj
          if (typeof obj === "bigint") return obj.toString(10)
          if (Array.isArray(obj)) return obj.map(serializeBigInts)
          if (typeof obj === "object") {
            const result: Record<string, unknown> = {}
            for (const key in obj) {
              result[key] = serializeBigInts((obj as Record<string, unknown>)[key])
            }
            return result
          }
          return obj
        }

        // 1. Wrap Magic's provider to handle chain switching
        //    The Rhinestone SDK calls wallet_switchEthereumChain before signing
        //    intents, but Magic embedded wallets don't support this. Since the
        //    signing itself (signTypedData) is chain-agnostic, we can safely
        //    no-op chain switch requests.
        const magicProvider = {
          request: async (args: { method: string; params?: any[] }) => {
            if (
              args.method === "wallet_switchEthereumChain" ||
              args.method === "wallet_addEthereumChain"
            ) {
              return null
            }
            return (magic.rpcProvider as any).request(args)
          },
        }

        const walletClient = createWalletClient({
          account: address as `0x${string}`,
          transport: custom(magicProvider),
        })

        const wrappedWalletClient = walletClientToAccount(walletClient)

        // 2. Wrap with toAccount to handle BigInt serialization
        //    for signTypedData (required by Magic's RPC)
        const account = toAccount({
          address: wrappedWalletClient.address,
          async signMessage({ message }: { message: SignableMessage }) {
            return wrappedWalletClient.signMessage({ message })
          },
          async signTransaction(transaction: any) {
            return wrappedWalletClient.signTransaction(transaction)
          },
          async signTypedData(typedData: TypedDataDefinition) {
            const serialized = serializeBigInts(typedData)
            return wrappedWalletClient.signTypedData(serialized as any)
          },
        })

        // 3. Create the Rhinestone account
        const rhinestone = new RhinestoneSDK({
          apiKey: process.env.NEXT_PUBLIC_RHINESTONE_API_KEY,
        })
        const rhinestoneAccount = await rhinestone.createAccount({
          owners: {
            type: "ecdsa",
            accounts: [account],
          },
        })
        ```
      </Step>
    </Steps>
  </Tab>

  <Tab title="Server Wallet">
    ### Prerequisites

    * A Magic account, publishable API key, and secret key
    * Rhinestone API key
    * A backend server (Node.js, Bun, etc.)
    * An OIDC provider registered with Magic Express (see below)

    <Steps>
      <Step title="Install Dependencies">
        <CodeGroup>
          ```bash npm theme={null}
          npm install @rhinestone/sdk viem
          ```

          ```bash pnpm theme={null}
          pnpm add @rhinestone/sdk viem
          ```

          ```bash bun theme={null}
          bun install @rhinestone/sdk viem
          ```
        </CodeGroup>

        <Note>Server wallets don't need the `magic-sdk` client package. You interact with the Magic Express API directly from your backend.</Note>
      </Step>

      <Step title="Register OIDC Provider">
        Server wallets use the [Magic Express API](https://magic.link/docs/wallets/wallet-types/server-wallets), which requires OIDC-based authentication. Your backend acts as an OIDC provider: it signs JWTs that Magic Express verifies using your public keys.

        Generate an RS256 key pair and register it with Magic (one-time setup):

        ```ts theme={null}
        // register-oidc-provider.ts
        const res = await fetch(
          "https://tee.express.magiclabs.com/v1/identity/provider",
          {
            method: "POST",
            headers: {
              "X-Magic-Secret-Key": process.env.MAGIC_SECRET_KEY,
              "Content-Type": "application/json",
            },
            body: JSON.stringify({
              issuer: "https://your-app.com", // Your backend URL
              audience: "magic-express",
              jwks_uri: "https://your-app.com/.well-known/jwks.json",
            }),
          }
        )

        const { id } = await res.json()
        // Save this as MAGIC_OIDC_PROVIDER_ID
        ```

        <Note>Your backend must expose a `/.well-known/jwks.json` endpoint serving the public key in JWKS format. Magic Express uses this to verify JWTs from your server.</Note>
      </Step>

      <Step title="Set Up Backend API">
        Create helper functions to interact with the Magic Express API:

        ```ts theme={null}
        const MAGIC_EXPRESS_API = "https://tee.express.magiclabs.com"

        function magicHeaders(jwt: string) {
          return {
            Authorization: `Bearer ${jwt}`,
            "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 getOrCreateWallet(jwt: string) {
          const res = await fetch(`${MAGIC_EXPRESS_API}/v1/wallet`, {
            method: "POST",
            headers: magicHeaders(jwt),
          })
          return res.json() as Promise<{ public_address: string }>
        }

        export async function signHash(jwt: string, hash: string) {
          const res = await fetch(`${MAGIC_EXPRESS_API}/v1/wallet/sign/data`, {
            method: "POST",
            headers: magicHeaders(jwt),
            body: JSON.stringify({ raw_data_hash: hash }),
          })
          return res.json() as Promise<{ signature: string }>
        }
        ```

        Expose wallet and signing endpoints for your frontend. These routes authenticate the user session, issue a JWT for Magic Express, and proxy the request:

        ```ts theme={null}
        // GET /api/wallet
        app.get("/api/wallet", async (c) => {
          const user = getAuthenticatedUser(c) // Your auth middleware
          if (!user) return c.json({ error: "Unauthorized" }, 401)

          const jwt = await issueJwt(user.id) // Sign JWT with your RS256 private key
          const data = await getOrCreateWallet(jwt)
          return c.json(data)
        })

        // POST /api/sign
        app.post("/api/sign", async (c) => {
          const user = getAuthenticatedUser(c)
          if (!user) return c.json({ error: "Unauthorized" }, 401)

          const { hash } = await c.req.json()
          const jwt = await issueJwt(user.id)
          const data = await signHash(jwt, hash)
          return c.json(data)
        })
        ```
      </Step>

      <Step title="Create Rhinestone Account">
        On the frontend, create a viem account that delegates signing to your backend, then pass it to Rhinestone:

        ```tsx theme={null}
        import { RhinestoneSDK } from "@rhinestone/sdk"
        import { toAccount } from "viem/accounts"
        import { hashTypedData, hashMessage } from "viem"
        import type { SignableMessage, TypedDataDefinition, Hex } from "viem"

        // 1. Get wallet address from your backend
        const walletRes = await fetch("/api/wallet", {
          credentials: "include",
        })
        const { public_address } = await walletRes.json()

        // 2. Create a viem account that signs via your backend
        const account = toAccount({
          address: public_address as Hex,
          async signMessage({ message }: { message: SignableMessage }) {
            const hash = hashMessage(message)
            const res = await fetch("/api/sign", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              credentials: "include",
              body: JSON.stringify({ hash }),
            })
            if (!res.ok) throw new Error((await res.json()).error)
            return (await res.json()).signature as Hex
          },
          async signTransaction() {
            throw new Error("signTransaction not supported")
          },
          async signTypedData(typedData: TypedDataDefinition) {
            const hash = hashTypedData(typedData)
            const res = await fetch("/api/sign", {
              method: "POST",
              headers: { "Content-Type": "application/json" },
              credentials: "include",
              body: JSON.stringify({ hash }),
            })
            if (!res.ok) throw new Error((await res.json()).error)
            return (await res.json()).signature as Hex
          },
        })

        // 3. Create the Rhinestone account
        const rhinestone = new RhinestoneSDK({
          apiKey: process.env.NEXT_PUBLIC_RHINESTONE_API_KEY!,
        })
        const rhinestoneAccount = await rhinestone.createAccount({
          owners: { type: "ecdsa", accounts: [account] },
        })
        ```

        <Note>The key pattern: `toAccount` creates a viem account where `signMessage` and `signTypedData` hash data locally, then send the hash to your backend. Your backend forwards it to Magic Express for signing. The private key never leaves Magic's TEE.</Note>
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Cross-Chain Transactions

Once initialized, both wallet types use the same Rhinestone API:

```tsx theme={null}
import { encodeFunctionData, parseUnits, erc20Abi } from 'viem'
import { baseSepolia, arbitrumSepolia } from 'viem/chains'

async function handleCrossChainTransfer(rhinestoneAccount) {
  const transaction = await rhinestoneAccount.sendTransaction({
    sourceChains: [baseSepolia],
    targetChain: arbitrumSepolia,
    calls: [
      {
        to: "USDC",
        data: encodeFunctionData({
          abi: erc20Abi,
          functionName: "transfer",
          args: ["0xrecipient", parseUnits("10", 6)],
        }),
      },
    ],
    tokenRequests: [
      {
        address: "USDC",
        amount: parseUnits("10", 6),
      },
    ],
  })
}
```

## Environment Variables

Make sure to set the following environment variables:

```bash theme={null}
# Embedded wallet
NEXT_PUBLIC_MAGIC_API_KEY=your_magic_publishable_key
NEXT_PUBLIC_RHINESTONE_API_KEY=your_rhinestone_api_key

# Server wallet (additional)
MAGIC_SECRET_KEY=your_magic_secret_key
MAGIC_API_KEY=your_magic_api_key
MAGIC_OIDC_PROVIDER_ID=your_oidc_provider_id
```

## Next Steps

* **See it in action**: Check out our [complete Magic example](https://github.com/rhinestonewtf/e2e-examples/tree/main/magic) for a full working app
* Learn more about [Magic's wallet types](https://magic.link/docs/wallets/wallet-types)
* Explore [chain abstraction](../../chain-abstraction/unified-balance) capabilities
* Check out [creating an account](../create-account) for more details
