Skip to main content
This guide covers every MetaMask connection scenario you’ll encounter in a headless integration: desktop with the extension installed, desktop without the extension (QR code), and mobile (deeplink). Each scenario uses a different SDK function, but they all follow the same connect-then-verify pattern. Prerequisites:
import { createDynamicClient, initializeClient } from '@dynamic-labs-sdk/client';
import { addEvmExtension } from '@dynamic-labs-sdk/evm';

const client = createDynamicClient({
  environmentId: 'YOUR_ENVIRONMENT_ID',
});

addEvmExtension(client);
await initializeClient();

Scenario 1: Desktop — Extension installed

When the MetaMask browser extension is installed, it announces itself via EIP-6963 and the SDK registers it as a wallet provider. Connect directly through the provider — no QR code or deeplink needed.

Detect the extension

Use getMetaMaskEvmExtensionWalletProviderKey to check whether MetaMask is available as a browser extension. It returns the wallet provider key (e.g. 'metamaskevm') when installed, or undefined when not.
import { getMetaMaskEvmExtensionWalletProviderKey } from '@dynamic-labs-sdk/evm/metamask';

const extensionKey = getMetaMaskEvmExtensionWalletProviderKey();

if (extensionKey) {
  console.log('MetaMask extension detected:', extensionKey);
} else {
  console.log('MetaMask extension not installed — fall back to QR or deeplink');
}

Connect and verify

Pass the detected key to connectAndVerifyWithWalletProvider. This opens the MetaMask extension popup, the user approves the connection, then signs a verification message — all in one call.
import { connectAndVerifyWithWalletProvider } from '@dynamic-labs-sdk/client';
import { getMetaMaskEvmExtensionWalletProviderKey } from '@dynamic-labs-sdk/evm/metamask';

async function connectMetaMaskExtension() {
  const extensionKey = getMetaMaskEvmExtensionWalletProviderKey();

  if (!extensionKey) {
    throw new Error('MetaMask extension not available');
  }

  const walletAccount = await connectAndVerifyWithWalletProvider({
    walletProviderKey: extensionKey,
  });

  console.log('Connected:', walletAccount.address);
  return walletAccount;
}
If you want to separate connection from verification (e.g. connect first, verify later on a button click):
import {
  connectWithWalletProvider,
  verifyWalletAccount,
} from '@dynamic-labs-sdk/client';

// Step 1: Connect
const walletAccount = await connectWithWalletProvider({
  walletProviderKey: extensionKey,
});

// Step 2: Verify (e.g. on a separate button click)
await verifyWalletAccount({ walletAccount });

Scenario 2: Desktop — No extension (QR code)

When MetaMask isn’t installed as a browser extension, use the MetaMask SDK URI pairing. This generates a URI that you render as a QR code. The user scans it with the MetaMask mobile app, approves the connection, and your dapp receives the wallet account.

Connect and verify

connectAndVerifyWithMetaMaskUriEvm returns { uri, approval }. Render uri as a QR code; approval() resolves after the user approves the connection and signs the verification message in their MetaMask mobile app.
import { connectAndVerifyWithMetaMaskUriEvm } from '@dynamic-labs-sdk/evm/metamask';
import QRCode from 'qrcode';

async function connectMetaMaskQR() {
  const { uri, approval } = await connectAndVerifyWithMetaMaskUriEvm();

  // Render QR code — use any QR library (qrcode, react-qr-code, etc.)
  const qrDataUrl = await QRCode.toDataURL(uri);
  document.getElementById('qr-code').src = qrDataUrl;

  // Wait for user to scan QR and approve in MetaMask mobile
  const { walletAccounts } = await approval();
  console.log('Connected:', walletAccounts[0]?.address);

  return walletAccounts;
}

Connect without verifying

If you want to defer verification, use connectWithMetaMaskUriEvm instead:
import { connectWithMetaMaskUriEvm } from '@dynamic-labs-sdk/evm/metamask';
import { verifyWalletAccount } from '@dynamic-labs-sdk/client';
import QRCode from 'qrcode';

async function connectMetaMaskQRThenVerifyLater() {
  const { uri, approval } = await connectWithMetaMaskUriEvm();

  document.getElementById('qr-code').src = await QRCode.toDataURL(uri);

  // Connection only — no verification signature yet
  const { walletAccounts } = await approval();
  const walletAccount = walletAccounts[0];

  // Later, on a separate user action:
  await verifyWalletAccount({ walletAccount });
}

Clear stale sessions

If the user previously connected via QR and the session expired, the MetaMask SDK may spend up to 10 seconds trying to resume it before emitting a fresh URI. Call clearMetaMaskSessionStorage before starting a new pairing to skip the resume timeout:
import { clearMetaMaskSessionStorage } from '@dynamic-labs-sdk/metamask';

await clearMetaMaskSessionStorage();
const { uri, approval } = await connectAndVerifyWithMetaMaskUriEvm();

On mobile, you can’t show a QR code (the user is already on their phone). Instead, generate the same MetaMask SDK URI and append it to MetaMask’s deeplink. This opens the MetaMask app directly, where the user approves the connection.

Connect and verify

import {
  isMobile,
  appendConnectionUriToDeeplink,
} from '@dynamic-labs-sdk/client';
import { connectAndVerifyWithMetaMaskUriEvm } from '@dynamic-labs-sdk/evm/metamask';

const METAMASK_DEEPLINK = 'https://metamask.app.link/wc';

async function connectMetaMaskMobile() {
  const { uri, approval } = await connectAndVerifyWithMetaMaskUriEvm();

  // Append the pairing URI to MetaMask's deeplink and open it
  const deeplink = appendConnectionUriToDeeplink({
    connectionUri: uri,
    deeplinkUrl: METAMASK_DEEPLINK,
  });

  window.open(deeplink, '_blank');

  // Resolves once the user approves in MetaMask
  const { walletAccounts } = await approval();
  console.log('Connected:', walletAccounts[0]?.address);

  return walletAccounts;
}
Instead of hardcoding the deeplink URL, you can fetch it from the SDK’s wallet catalogue:
import {
  isMobile,
  getWalletConnectCatalog,
  appendConnectionUriToDeeplink,
} from '@dynamic-labs-sdk/client';
import { connectAndVerifyWithMetaMaskUriEvm } from '@dynamic-labs-sdk/evm/metamask';

async function connectMetaMaskMobileWithCatalog() {
  const { uri, approval } = await connectAndVerifyWithMetaMaskUriEvm();

  const catalog = await getWalletConnectCatalog();
  const metamask = catalog.wallets['metamask'];
  const baseDeeplink = metamask?.deeplinks?.native ?? metamask?.deeplinks?.universal;

  if (!baseDeeplink) {
    throw new Error('MetaMask deeplink not found in wallet catalogue');
  }

  window.open(
    appendConnectionUriToDeeplink({ connectionUri: uri, deeplinkUrl: baseDeeplink }),
    '_blank',
  );

  const { walletAccounts } = await approval();
  return walletAccounts;
}

Putting it all together

Here’s a complete example that detects the user’s environment and picks the right MetaMask connection flow automatically:
import {
  createDynamicClient,
  initializeClient,
  waitForClientInitialized,
  isMobile,
  connectAndVerifyWithWalletProvider,
  getWalletConnectCatalog,
  appendConnectionUriToDeeplink,
} from '@dynamic-labs-sdk/client';
import { addEvmExtension } from '@dynamic-labs-sdk/evm';
import {
  getMetaMaskEvmExtensionWalletProviderKey,
  connectAndVerifyWithMetaMaskUriEvm,
} from '@dynamic-labs-sdk/evm/metamask';
import QRCode from 'qrcode';

// --- Setup ---
const client = createDynamicClient({
  environmentId: 'YOUR_ENVIRONMENT_ID',
});

addEvmExtension(client);
await initializeClient();

// --- Connect MetaMask ---
async function connectMetaMask() {
  await waitForClientInitialized();

  // 1. Desktop with extension — connect directly
  const extensionKey = getMetaMaskEvmExtensionWalletProviderKey();

  if (extensionKey && !isMobile()) {
    const walletAccount = await connectAndVerifyWithWalletProvider({
      walletProviderKey: extensionKey,
    });
    return [walletAccount];
  }

  // 2. No extension — use MetaMask SDK URI pairing
  const { uri, approval } = await connectAndVerifyWithMetaMaskUriEvm();

  if (isMobile()) {
    // 2a. Mobile — open MetaMask via deeplink
    const catalog = await getWalletConnectCatalog();
    const metamask = catalog.wallets['metamask'];
    const baseDeeplink = metamask?.deeplinks?.native ?? metamask?.deeplinks?.universal;

    if (baseDeeplink) {
      window.open(
        appendConnectionUriToDeeplink({ connectionUri: uri, deeplinkUrl: baseDeeplink }),
        '_blank',
      );
    }
  } else {
    // 2b. Desktop without extension — show QR code
    const qrDataUrl = await QRCode.toDataURL(uri);
    document.getElementById('qr-code').src = qrDataUrl;
  }

  const { walletAccounts } = await approval();
  return walletAccounts;
}

Using the Wallet Options Catalogue

For a more automated approach, getWalletOptionsCatalogue merges all connection methods (extension, MetaMask SDK URI, WalletConnect, in-app browser) into a single ordered list per wallet. MetaMask’s entry will include a withWalletProvider option when the extension is installed, plus metamaskSdkUri options for QR/deeplink.
import { getWalletOptionsCatalogue, isMobile } from '@dynamic-labs-sdk/client';

const walletOptions = await getWalletOptionsCatalogue({
  includeMobileOptions: true,
});

// Find MetaMask
const metamask = walletOptions.find((w) => w.name === 'MetaMask');

if (metamask) {
  // connectionOptions[0] is the highest-priority method:
  //   - 'withWalletProvider' if the extension is installed
  //   - 'metamaskSdkUri' for QR/deeplink otherwise
  const [best] = metamask.connectionOptions;

  switch (best?.type) {
    case 'withWalletProvider':
      // Extension installed — connect directly
      break;
    case 'metamaskSdkUri':
      // QR code (desktop) or deeplink (mobile)
      break;
    case 'inAppBrowser':
      // Open dapp inside MetaMask's in-app browser
      break;
  }
}
See Build a Wallet Picker for the full pattern with click handlers.