> For the complete documentation index, see [llms.txt](/llms.txt).

# Create a multichain dapp

In this tutorial, you'll build a React dapp that connects to four networks (Ethereum, Linea, Base, and Solana) using MetaMask Connect Multichain. Your dapp will handle wallet sign-in and sign-out, read balances across all four chains, sign messages, and send transactions on all four chains. You'll learn how to do the following:

- Set up a multichain session with a single connection prompt.
- Read account balances across EVM networks and Solana.
- Sign messages in both ecosystems.
- Send transactions on EVM and Solana.

## Key concepts[​](#key-concepts "Direct link to Key concepts")

This tutorial uses [scopes](/metamask-connect/multichain/concepts/scopes/), [account IDs](/metamask-connect/multichain/concepts/accounts/), and [sessions](/metamask-connect/multichain/concepts/sessions/) to identify chains and accounts across ecosystems. If you're unfamiliar with these concepts, review them before continuing.

This tutorial uses the following scopes:

| Chain            | Scope (CAIP-2)                          |
| ---------------- | --------------------------------------- |
| Ethereum Mainnet | eip155:1                                |
| Linea Mainnet    | eip155:59144                            |
| Base Mainnet     | eip155:8453                             |
| Solana Mainnet   | solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp |

note

Bitcoin and Tron support is coming soon. The Multichain API is designed to be ecosystem-agnostic, so new chains can be added without changing your integration pattern.

## Prerequisites[​](#prerequisites "Direct link to Prerequisites")

- [Node.js](https://nodejs.org/) version 19 or later installed.
- A package manager such as [npm](https://docs.npmjs.com/downloading-and-installing-node-js-and-npm), [Yarn](https://yarnpkg.com/), [pnpm](https://pnpm.io/installation), or [bun](https://bun.sh/).
- [MetaMask](https://metamask.io/download) installed in your browser.
- An [Infura API key](/developer-tools/dashboard/get-started/create-api/) from the [Infura dashboard](https://app.infura.io).

## Steps[​](#steps "Direct link to Steps")

### 1\. Scaffold the project[​](#1-scaffold-the-project "Direct link to 1. Scaffold the project")

Create a new React + TypeScript project with Vite:

```
npm create vite@latest multichain-dapp -- --template react-ts
cd multichain-dapp

```

Install the MetaMask Connect multichain client and Solana Kit:

- npm
- Yarn
- pnpm
- Bun

```
npm install @metamask/connect-multichain @solana/kit

```

```
yarn add @metamask/connect-multichain @solana/kit

```

```
pnpm add @metamask/connect-multichain @solana/kit

```

```
bun add @metamask/connect-multichain @solana/kit

```

`@solana/kit` is needed to query Solana balances directly, since [invokeMethod](/metamask-connect/multichain/reference/api/#wallet%5Finvokemethod) doesn't currently support `getBalance` for Solana.

### 2\. Initialize the multichain client[​](#2-initialize-the-multichain-client "Direct link to 2. Initialize the multichain client")

Create a file `src/multichain.ts`, and initialize the client using [createMultichainClient](/metamask-connect/multichain/reference/methods/#createmultichainclient):

src/multichain.ts

```
import { createMultichainClient } from '@metamask/connect-multichain'

export const SCOPES = {
  ETHEREUM: 'eip155:1',
  LINEA: 'eip155:59144',
  BASE: 'eip155:8453',
  SOLANA: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
} as const

let client: Awaited<ReturnType<typeof createMultichainClient>> | null = null

export async function getClient() {
  if (!client) {
    client = await createMultichainClient({
      dapp: {
        name: 'Multichain Tutorial Dapp',
        url: window.location.href,
      },
      api: {
        supportedNetworks: {
          [SCOPES.ETHEREUM]: 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.LINEA]: 'https://linea-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.BASE]: 'https://base-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.SOLANA]: 'https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
        },
      },
    })
  }
  return client
}

```

Configure MetaMask Connect Multichain with the following options:

- `dapp`: Your dapp's identity. MetaMask shows the `name` and `url` during the connection prompt so users know who is requesting access.
- `api.supportedNetworks`: A map of [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md)scope IDs to RPC endpoint URLs. Each entry tells the client which chains your dapp supports and where to send RPC requests.

### 3\. Connect (sign-in)[​](#3-connect-sign-in "Direct link to 3. Connect (sign-in)")

Call [connect](/metamask-connect/multichain/reference/methods/#connect) with the scopes you want. The user sees a single approval prompt for all four chains:

```
import { getClient, SCOPES } from './multichain'

const client = await getClient()

await client.connect([SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE, SCOPES.SOLANA], [])

```

The second argument is an optional array of [CAIP-10](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-10.md)account preferences. Pass an empty array to let the user choose their own accounts.

After connecting, retrieve the session to see which accounts the user authorized:

```
const session = await client.provider.getSession()

const ethAccounts = session.sessionScopes[SCOPES.ETHEREUM]?.accounts || []
const lineaAccounts = session.sessionScopes[SCOPES.LINEA]?.accounts || []
const baseAccounts = session.sessionScopes[SCOPES.BASE]?.accounts || []
const solAccounts = session.sessionScopes[SCOPES.SOLANA]?.accounts || []

```

Each account is a CAIP-10 string like `eip155:1:0xabc123...` or `solana:5eykt...:83ast...`. To extract the raw address, split on `:` and take everything after the second colon:

```
function extractAddress(caip10Account: string): string {
  return caip10Account.split(':').slice(2).join(':')
}

const ethAddress = extractAddress(ethAccounts[0])
// "0xabc123..."

```

### 4\. Disconnect (sign-out)[​](#4-disconnect-sign-out "Direct link to 4. Disconnect (sign-out)")

Call [disconnect](/metamask-connect/multichain/reference/methods/#disconnect) to end the session and clear all authorizations:

```
await client.disconnect()

```

This revokes the active session (`disconnect` wraps [wallet_revokeSession](/metamask-connect/multichain/reference/api/#wallet%5Frevokesession)). The user will need to approve a new connection prompt to use your dapp again.

### 5\. Fetch balances[​](#5-fetch-balances "Direct link to 5. Fetch balances")

#### EVM balances[​](#evm-balances "Direct link to EVM balances")

Use [invokeMethod](/metamask-connect/multichain/reference/methods/#invokemethod) with [eth_getBalance](/metamask-connect/evm/reference/json-rpc-api/eth%5FgetBalance/) to query the balance on any EVM chain in the session. The result is a hex-encoded wei value:

```
async function getEvmBalance(scope: string, accounts: string[]): Promise<string> {
  if (accounts.length === 0) return '0'

  const address = extractAddress(accounts[0])
  const balanceHex = await client.invokeMethod({
    scope,
    request: {
      method: 'eth_getBalance',
      params: [address, 'latest'],
    },
  })

  const wei = BigInt(balanceHex as string)
  const eth = Number(wei) / 1e18
  return eth.toFixed(6)
}

const ethBalance = await getEvmBalance(SCOPES.ETHEREUM, ethAccounts)
const lineaBalance = await getEvmBalance(SCOPES.LINEA, lineaAccounts)
const baseBalance = await getEvmBalance(SCOPES.BASE, baseAccounts)

```

The same function works for all three EVM chains; only the `scope` changes.

#### Solana balance[​](#solana-balance "Direct link to Solana balance")

note

`invokeMethod` doesn't currently support `getBalance` for Solana. If this is something you'd like to see, [raise a feature request](https://builder.metamask.io/c/feature-request/10) on the MetaMask Builder Hub.

Use `@solana/kit` to query the Solana RPC directly:

```
import { address, createSolanaRpc } from '@solana/kit'

async function getSolBalance(accounts: string[]): Promise<string> {
  if (accounts.length === 0) return '0'

  const solAddress = extractAddress(accounts[0])
  const rpc = createSolanaRpc('https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY')
  const { value } = await rpc.getBalance(address(solAddress)).send()

  const sol = Number(value) / 1e9
  return sol.toFixed(6)
}

const solBalance = await getSolBalance(solAccounts)

```

### 6\. Sign a message[​](#6-sign-a-message "Direct link to 6. Sign a message")

#### EVM (`personal_sign`)[​](#evm-personal%5Fsign "Direct link to evm-personal_sign")

To sign a message on an EVM chain, hex-encode the message and use [invokeMethod](/metamask-connect/multichain/reference/methods/#invokemethod) with [personal_sign](/metamask-connect/evm/reference/json-rpc-api/personal%5Fsign/):

```
function toHex(str: string): string {
  return (
    '0x' + Array.from(new TextEncoder().encode(str), b => b.toString(16).padStart(2, '0')).join('')
  )
}

const evmAddress = extractAddress(ethAccounts[0])
const signature = await client.invokeMethod({
  scope: SCOPES.ETHEREUM,
  request: {
    method: 'personal_sign',
    params: [toHex('Hello from my multichain dapp!'), evmAddress],
  },
})
console.log('EVM signature:', signature)

```

#### Solana (`solana_signMessage`)[​](#solana-solana%5Fsignmessage "Direct link to solana-solana_signmessage")

Use [invokeMethod](/metamask-connect/multichain/reference/methods/#invokemethod) with `solana_signMessage` to sign a message on Solana:

```
const solAddress = extractAddress(solAccounts[0])
const signature = await client.invokeMethod({
  scope: SCOPES.SOLANA,
  request: {
    method: 'solana_signMessage',
    params: {
      message: btoa('Hello from my multichain dapp!'),
      pubkey: solAddress,
    },
  },
})
console.log('SOL signature:', signature)

```

### 7\. Send a transaction[​](#7-send-a-transaction "Direct link to 7. Send a transaction")

#### EVM transaction[​](#evm-transaction "Direct link to EVM transaction")

Use [invokeMethod](/metamask-connect/multichain/reference/methods/#invokemethod) with [eth_sendTransaction](/metamask-connect/evm/reference/json-rpc-api/eth%5FsendTransaction/) to send a transaction on any EVM scope:

```
const fromAddress = extractAddress(ethAccounts[0])

const txHash = await client.invokeMethod({
  scope: SCOPES.ETHEREUM,
  request: {
    method: 'eth_sendTransaction',
    params: [
      {
        from: fromAddress,
        to: '0x0000000000000000000000000000000000000000',
        value: '0x0',
      },
    ],
  },
})
console.log('EVM tx hash:', txHash)

```

To send on a different chain, change the `scope`; for example, `SCOPES.LINEA` or `SCOPES.BASE`. The same address format and RPC method works across all EVM chains.

#### Solana transaction[​](#solana-transaction "Direct link to Solana transaction")

Use [invokeMethod](/metamask-connect/multichain/reference/methods/#invokemethod) with `solana_signAndSendTransaction` to send a Solana base64-encoded transaction:

```
const result = await client.invokeMethod({
  scope: SCOPES.SOLANA,
  request: {
    method: 'solana_signAndSendTransaction',
    params: {
      transaction: '<base64-encoded-transaction>',
    },
  },
})
console.log('SOL tx signature:', result)

```

Building a Solana transaction requires assembling instructions, setting a fee payer, and fetching a recent block hash using `@solana/kit`. See [Send transactions](/metamask-connect/multichain/guides/send-transactions/) for a complete example.

## Full example[​](#full-example "Direct link to Full example")

The following is the complete source for the two main files. After scaffolding the project (Step 1), replace the contents of these files, run `npm run dev`, and open the dapp in your browser.

src/multichain.ts

```
import { createMultichainClient } from '@metamask/connect-multichain'

export const SCOPES = {
  ETHEREUM: 'eip155:1',
  LINEA: 'eip155:59144',
  BASE: 'eip155:8453',
  SOLANA: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp',
} as const

export const SCOPE_LABELS: Record<string, string> = {
  [SCOPES.ETHEREUM]: 'Ethereum',
  [SCOPES.LINEA]: 'Linea',
  [SCOPES.BASE]: 'Base',
  [SCOPES.SOLANA]: 'Solana',
}

let client: Awaited<ReturnType<typeof createMultichainClient>> | null = null

export async function getClient() {
  if (!client) {
    client = await createMultichainClient({
      dapp: {
        name: 'Multichain Tutorial Dapp',
        url: window.location.href,
      },
      api: {
        supportedNetworks: {
          [SCOPES.ETHEREUM]: 'https://mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.LINEA]: 'https://linea-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.BASE]: 'https://base-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
          [SCOPES.SOLANA]: 'https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY',
        },
      },
    })
  }
  return client
}

export function extractAddress(caip10Account: string): string {
  return caip10Account.split(':').slice(2).join(':')
}

export function toHex(str: string): string {
  return (
    '0x' + Array.from(new TextEncoder().encode(str), b => b.toString(16).padStart(2, '0')).join('')
  )
}

```

src/App.tsx

```
import { useState } from 'react'
import { address, createSolanaRpc } from '@solana/kit'
import { getClient, SCOPES, SCOPE_LABELS, extractAddress, toHex } from './multichain'

type ChainAccounts = Record<string, string[]>

export default function App() {
  const [connected, setConnected] = useState(false)
  const [accounts, setAccounts] = useState<ChainAccounts>({})
  const [balances, setBalances] = useState<Record<string, string>>({})
  const [log, setLog] = useState<string[]>([])

  const addLog = (entry: string) =>
    setLog(prev => [`[${new Date().toLocaleTimeString()}] ${entry}`, ...prev])

  // --- Connect / Disconnect ---

  const handleConnect = async () => {
    try {
      const client = await getClient()
      await client.connect([SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE, SCOPES.SOLANA], [])
      const session = await client.provider.getSession()
      const accts: ChainAccounts = {}
      for (const scope of Object.values(SCOPES)) {
        accts[scope] = session.sessionScopes[scope]?.accounts || []
      }
      setAccounts(accts)
      setConnected(true)
      addLog('Connected to all chains.')
    } catch (err: unknown) {
      addLog(`Connection failed: ${(err as Error).message}`)
    }
  }

  const handleDisconnect = async () => {
    try {
      const client = await getClient()
      await client.disconnect()
      setConnected(false)
      setAccounts({})
      setBalances({})
      addLog('Disconnected.')
    } catch (err: unknown) {
      addLog(`Disconnect failed: ${(err as Error).message}`)
    }
  }

  // --- Balances ---

  const fetchBalances = async () => {
    const client = await getClient()
    const result: Record<string, string> = {}

    for (const scope of [SCOPES.ETHEREUM, SCOPES.LINEA, SCOPES.BASE] as const) {
      const accts = accounts[scope] || []
      if (accts.length > 0) {
        try {
          const addr = extractAddress(accts[0])
          const hex = (await client.invokeMethod({
            scope,
            request: { method: 'eth_getBalance', params: [addr, 'latest'] },
          })) as string
          result[scope] = (Number(BigInt(hex)) / 1e18).toFixed(6)
        } catch (err: unknown) {
          result[scope] = `Error: ${(err as Error).message}`
        }
      }
    }

    const solAccts = accounts[SCOPES.SOLANA] || []
    if (solAccts.length > 0) {
      try {
        const solAddr = extractAddress(solAccts[0])
        const rpc = createSolanaRpc('https://solana-mainnet.infura.io/v3/YOUR_INFURA_API_KEY')
        const { value } = await rpc.getBalance(address(solAddr)).send()
        result[SCOPES.SOLANA] = (Number(value) / 1e9).toFixed(6)
      } catch (err: unknown) {
        result[SCOPES.SOLANA] = `Error: ${(err as Error).message}`
      }
    }

    setBalances(result)
    addLog('Balances fetched.')
  }

  // --- Sign message ---

  const signEvmMessage = async () => {
    try {
      const client = await getClient()
      const addr = extractAddress(accounts[SCOPES.ETHEREUM]?.[0] || '')
      const sig = await client.invokeMethod({
        scope: SCOPES.ETHEREUM,
        request: {
          method: 'personal_sign',
          params: [toHex('Hello from my multichain dapp!'), addr],
        },
      })
      addLog(`EVM signature: ${sig}`)
    } catch (err: unknown) {
      addLog(`EVM sign failed: ${(err as Error).message}`)
    }
  }

  const signSolMessage = async () => {
    try {
      const client = await getClient()
      const solAddress = extractAddress(accounts[SCOPES.SOLANA]?.[0] || '')
      const sig = await client.invokeMethod({
        scope: SCOPES.SOLANA,
        request: {
          method: 'solana_signMessage',
          params: {
            message: btoa('Hello from my multichain dapp!'),
            pubkey: solAddress,
          },
        },
      })
      addLog(`SOL signature: ${JSON.stringify(sig)}`)
    } catch (err: unknown) {
      addLog(`SOL sign failed: ${(err as Error).message}`)
    }
  }

  // --- Send transaction ---

  const sendEvmTransaction = async () => {
    try {
      const client = await getClient()
      const addr = extractAddress(accounts[SCOPES.ETHEREUM]?.[0] || '')
      const txHash = await client.invokeMethod({
        scope: SCOPES.ETHEREUM,
        request: {
          method: 'eth_sendTransaction',
          params: [{ from: addr, to: addr, value: '0x0' }],
        },
      })
      addLog(`EVM tx hash: ${txHash}`)
    } catch (err: unknown) {
      addLog(`EVM tx failed: ${(err as Error).message}`)
    }
  }

  // --- Render ---

  return (
    <div
      style={{ maxWidth: 720, margin: '0 auto', padding: 32, fontFamily: 'system-ui, sans-serif' }}>
      <h1>Multichain Dapp</h1>

      {!connected ? (
        <button onClick={handleConnect}>Connect Wallet</button>
      ) : (
        <>
          <button onClick={handleDisconnect}>Disconnect</button>
          <h2>Accounts</h2>
          {Object.entries(accounts).map(([scope, accts]) => (
            <p key={scope}>
              <strong>{SCOPE_LABELS[scope] || scope}:</strong>{' '}
              <code>{accts.length > 0 ? extractAddress(accts[0]) : 'none'}</code>
            </p>
          ))}
          <h2>Balances</h2>
          <button onClick={fetchBalances}>Fetch Balances</button>
          {Object.entries(balances).map(([scope, bal]) => (
            <p key={scope}>
              <strong>{SCOPE_LABELS[scope] || scope}:</strong> {bal}
            </p>
          ))}
          <h2>Sign Message</h2>
          <button onClick={signEvmMessage}>Sign (EVM)</button>{' '}
          <button onClick={signSolMessage}>Sign (Solana)</button>
          <h2>Send Transaction</h2>
          <button onClick={sendEvmTransaction}>Send 0 ETH to Self</button>
        </>
      )}

      <h2>Log</h2>
      <pre
        style={{
          background: '#1a1a1a',
          color: '#e0e0e0',
          padding: 16,
          borderRadius: 8,
          maxHeight: 300,
          overflow: 'auto',
          fontSize: 13,
        }}>
        {log.length > 0 ? log.join('\n') : 'No activity yet.'}
      </pre>
    </div>
  )
}

```

## Best practices[​](#best-practices "Direct link to Best practices")

- **Handle user rejection.**Users can decline the connection prompt or any signing request. Always wrap SDK calls in `try/catch` and show a meaningful message when the user cancels.
- **Request only the scopes you need.**Don't request access to chains your dapp doesn't use. A shorter list of scopes makes the approval prompt clearer and builds trust.
- **Use CAIP-2 constants.**Define scope IDs in one place (as shown in `src/multichain.ts`) instead of scattering string literals across your codebase.
- **Leverage session persistence.**Sessions survive page reloads and new tabs. Check for an existing session on startup with `getSession` before prompting the user to connect again.
- **Show chain context clearly.**When displaying accounts, balances, or transaction prompts, always label which chain the action applies to. Users managing multiple chains need clear visual cues to avoid sending assets on the wrong network.
- **Degrade gracefully.**If the user declines access to some scopes, your dapp should still work for the chains they did approve. Check `session.sessionScopes` for each scope before calling `invokeMethod`.

## Next steps[​](#next-steps "Direct link to Next steps")

- See the [Multichain SDK methods](/metamask-connect/multichain/reference/methods/) for all available methods and events.
- See the [Multichain API reference](/metamask-connect/multichain/reference/api/) for all available methods and events.
- See [Sign transactions](/metamask-connect/multichain/guides/sign-transactions/) for more signing patterns including Solana signing construction.
- See [Send transactions](/metamask-connect/multichain/guides/send-transactions/) for more transaction patterns including Solana transfer construction.
