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.
Integration
- Embedded Wallet
- Server Wallet
Prerequisites
- A Magic account and publishable API key
- Rhinestone API key
- React application setup
Set Up Magic SDK
Initialize the Magic SDK in your application:
Copy
Ask AI
import { Magic } from 'magic-sdk'
const magic = new Magic(process.env.NEXT_PUBLIC_MAGIC_API_KEY)
Authenticate and Get Wallet Address
Use Magic’s client-side SDK for authentication:Magic also supports SMS, social login, WebAuthn, and more. See Magic’s authentication overview for all options.
Copy
Ask AI
// 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]
Create Rhinestone Account
Create a viem account from Magic’s provider and pass it to Rhinestone:
Copy
Ask AI
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],
},
})
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)
Install Dependencies
Copy
Ask AI
npm install @rhinestone/sdk viem
Server wallets don’t need the
magic-sdk client package. You interact with the Magic Express API directly from your backend.Register OIDC Provider
Server wallets use the Magic Express API, 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):
Copy
Ask AI
// 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
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.Set Up Backend API
Create helper functions to interact with the Magic Express API:Expose wallet and signing endpoints for your frontend. These routes authenticate the user session, issue a JWT for Magic Express, and proxy the request:
Copy
Ask AI
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 }>
}
Copy
Ask AI
// 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)
})
Create Rhinestone Account
On the frontend, create a viem account that delegates signing to your backend, then pass it to Rhinestone:
Copy
Ask AI
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] },
})
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.Cross-Chain Transactions
Once initialized, both wallet types use the same Rhinestone API:Copy
Ask AI
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:Copy
Ask AI
# 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 for a full working app
- Learn more about Magic’s wallet types
- Explore chain abstraction capabilities
- Check out creating an account for more details