What We’re Building

A cross-chain swap application built with Next.js that integrates Dynamic’s embedded MPC wallets with Mayan’s bridge aggregator. This app enables users to:
  • Execute seamless token swaps across major EVM networks including Ethereum, Polygon, BSC, Avalanche, Arbitrum, Optimism, and Base
  • Access competitive exchange rates through Mayan’s routing system
  • Complete cross-chain transactions with just a few clicks - no manual bridging required
  • Track swap progress in real-time with detailed transaction status updates
Note: This guide focuses exclusively on EVM-to-EVM swaps using a single route approach. We’ll cover Solana and Sui integration in separate guides. If you want to take a quick look at the final code, check out the GitHub repository.

Building the Application

Project Setup

Start by creating a new Dynamic project with React, Viem, Wagmi, and multi-chain support:
npx create-dynamic-app@latest mayan-dynamic-app --framework nextjs --library viem --wagmi true --chains ethereum --pm npm
  
cd mayan-dynamic-app

Install Mayan Dependencies

Add the Mayan SDK:
npm install @mayanfinance/swap-sdk
This installs the Mayan SDK for cross-chain routing and execution capabilities.

Configure Environment

Create a .env.local file with your Dynamic environment ID:
.env.local
NEXT_PUBLIC_DYNAMIC_ENVIRONMENT_ID=your-environment-id-here
Set your Dynamic environment ID for the integration. Make sure you enable the chains you want to support in the Dynamic dashboard.

Enable Transaction Simulation

  1. Go to your Dynamic dashboard
  2. Navigate to Developer SettingsEmbedded WalletsDynamic
  3. Enable “Show Confirmation UI” and “Transaction Simulation” toggles
This enables transaction previews showing asset transfers, fees, gas costs, and contract addresses before execution.
When enabled, users will see a comprehensive transaction preview like this when swapping tokens:

Define Supported Chains

Create src/constants/chains.ts to define the supported blockchain networks:
src/constants/chains.ts
import {
  mainnet,
  polygon,
  bsc,
  avalanche,
  arbitrum,
  optimism,
  base,
} from "viem/chains";

export type ChainKey =
  | "ethereum"
  | "polygon"
  | "bsc"
  | "avalanche"
  | "arbitrum"
  | "optimism"
  | "base";

export const EVM_CHAINS: { id: number; name: string; key: ChainKey }[] = [
  {
    id: mainnet.id,
    name: "Ethereum Mainnet",
    key: "ethereum",
  },
  {
    id: polygon.id,
    name: "Polygon",
    key: "polygon",
  },
  {
    id: bsc.id,
    name: "BSC",
    key: "bsc",
  },
  {
    id: avalanche.id,
    name: "Avalanche",
    key: "avalanche",
  },
  {
    id: arbitrum.id,
    name: "Arbitrum",
    key: "arbitrum",
  },
  {
    id: optimism.id,
    name: "Optimism",
    key: "optimism",
  },
  {
    id: base.id,
    name: "Base",
    key: "base",
  },
];

export const getChainById = (
  id: number | string
): { id: number | string; name: string; key: ChainKey } | undefined => {
  return EVM_CHAINS.find((chain) => chain.id === id);
};

export const getChainByKey = (
  key: ChainKey
): { id: number | string; name: string; key: ChainKey } | undefined => {
  return EVM_CHAINS.find((chain) => chain.key === key);
};
This configuration supports all major EVM chains for cross-chain swapping. You can find the list of all supported chains by Mayan here. Dynamic supports all EVM chains and more such as Solana, Sui, and more which can be found here.

Configure Wagmi

Create lib/wagmi.ts to configure wagmi with the supported chains:
lib/wagmi.ts
import { createConfig, http } from "wagmi";
import {
  mainnet,
  polygon,
  bsc,
  avalanche,
  arbitrum,
  optimism,
  base,
} from "wagmi/chains";

// Create wagmi config with the chains from your constants
export const config = createConfig({
  chains: [mainnet, polygon, bsc, avalanche, arbitrum, optimism, base],
  transports: {
    [mainnet.id]: http(),
    [polygon.id]: http(),
    [bsc.id]: http(),
    [avalanche.id]: http(),
    [arbitrum.id]: http(),
    [optimism.id]: http(),
    [base.id]: http(),
  },
});

Create Mayan API Client

Create src/lib/mayan-api.ts to handle token data fetching:
src/lib/mayan-api.tsx
import { EVM_CHAINS } from "@/constants/chains";
import type { MayanApiToken, MayanApiResponse } from "@/types/mayan-api";

const MAYAN_PRICE_API_BASE = "https://price-api.mayan.finance";

/**
 * Fetch all tokens for a specific EVM chain from Mayan Finance Price API
 */
export const fetchTokensForChain = async (
  chainId: number
): Promise<MayanApiToken[]> => {
  try {
    const chainKey = EVM_CHAINS.find((chain) => chain.id === chainId)?.key;

    if (!chainKey) {
      return [];
    }

  try {
      const response = await fetch(
        `${MAYAN_PRICE_API_BASE}/v3/tokens?chain=${chainKey}`
      );

      if (!response.ok) {
        throw new Error(
          `Failed to fetch tokens for chain ${chainKey}: ${response.statusText}`
        );
      }

      const data: MayanApiResponse = await response.json();
      const tokens = data[chainKey];

      if (!tokens || !Array.isArray(tokens)) {
        return [];
      }

      return tokens;
    } catch {
      return [];
    }
  } catch {
    return [];
  }
};
This API client fetches token information from Mayan’s price API for EVM chains. Note: You’ll need to create the MayanApiToken and MayanApiResponse types in src/types/mayan-api.ts. You can find the types here. You can see the API reference here.

Create Utility Functions

Create src/lib/utils.ts for explorer URLs and utility functions:
src/lib/utils.ts
export const getExplorerUrl = (txHash: string, chainId?: number) => {
  if (!chainId) return null;

  const explorers: { [key: number]: string } = {
    1: `https://etherscan.io/tx/${txHash}`,
    10: `https://optimistic.etherscan.io/tx/${txHash}`,
    137: `https://polygonscan.com/tx/${txHash}`,
    42161: `https://arbiscan.io/tx/${txHash}`,
    8453: `https://basescan.org/tx/${txHash}`,
    56: `https://bscscan.com/tx/${txHash}`,
    43114: `https://snowtrace.io/tx/${txHash}`,
    250: `https://ftmscan.com/tx/${txHash}`,
    100: `https://gnosisscan.io/tx/${txHash}`,
    1101: `https://zkevm.polygonscan.com/tx/${txHash}`,
    7777777: `https://explorer.zora.energy/tx/${txHash}`,
    11155420: `https://sepolia.optimism.io/tx/${txHash}`,
    11155111: `https://sepolia.etherscan.io/tx/${txHash}`,
    80001: `https://mumbai.polygonscan.com/tx/${txHash}`,
  };

  return explorers[chainId] || null;
};

export const formatAmount = (amount: string, decimals: number) => {
  try {
    return (BigInt(amount) / BigInt(10 ** decimals)).toString();
  } catch {
    return "0";
  }
};

export const openExternalLink = (url: string) => {
  window.open(url, "_blank", "noopener,noreferrer");
};

export const getMayanExplorerUrl = (swapHash: string) => {
  return `https://explorer.mayan.finance/tx/${swapHash}`;
};
These utilities handle blockchain explorer links and amount formatting.

Create Multi-Chain Swap Component

Now create the main src/components/MultiChainSwap.tsx component that orchestrates all the modular components:
src/components/MultiChainSwap.tsx
"use client";

import { useDynamicContext } from "@dynamic-labs/sdk-react-core";
import { useEffect, useState } from "react";
import { erc20Abi, maxUint256, parseUnits } from "viem";
import {
  useAccount,
  useChainId,
  usePublicClient,
  useSendTransaction,
  useWriteContract,
} from "wagmi";

import { EVM_CHAINS } from "@/constants/chains";
import { fetchTokensForChain } from "@/lib/mayan-api";
import type { SwapState } from "@/types/swap";
import type { Quote, Token } from "@mayanfinance/swap-sdk";
import {
  addresses,
  fetchQuote,
  getSwapFromEvmTxPayload,
} from "@mayanfinance/swap-sdk";
import ActionButtons from "./ActionButtons";
import RouteDisplay from "./RouteDisplay";
import StatusMessages from "./StatusMessages";
import SwapForm from "./SwapForm";
import { MayanApiToken } from "@/types/mayan-api";

export default function MultiChainSwap() {
  const { sdkHasLoaded } = useDynamicContext();
  const { address, isConnected } = useAccount();
  const chainId = useChainId();
  const { sendTransaction } = useSendTransaction();
  const { writeContract } = useWriteContract();
  const publicClient = usePublicClient();

  const [swapState, setSwapState] = useState<SwapState>({
    fromChain: EVM_CHAINS[0],
    toChain: EVM_CHAINS[1],
    fromToken: null,
    toToken: null,
    amount: "0.000001",
    quote: null,
    isLoading: false,
    error: null,
    txHash: null,
    mayanSwapHash: null,
    isExecuting: false,
    isCheckingAllowance: false,
    isApproving: false,
    needsApproval: false,
    allowanceAmount: BigInt(0),
  });
  const [fromTokens, setFromTokens] = useState<Token[]>([]);
  const [toTokens, setToTokens] = useState<Token[]>([]);
  const [isLoadingTokens, setIsLoadingTokens] = useState(false);

  const convertTokenDataToToken = (tokenData: MayanApiToken): Token =>
    ({
      contract: tokenData.contract,
      symbol: tokenData.symbol,
      name: tokenData.name,
      decimals: tokenData.decimals,
      logoURI: tokenData.logoURI || "",
      chainId: tokenData.chainId,
      mint: tokenData.contract,
      coingeckoId: "",
      supportsPermit: false,
      verified: true,
      standard: "erc20",
    } as Token);

  useEffect(() => {
    const loadTokens = async () => {
      if (!swapState.fromChain?.id || !swapState.toChain?.id) return;

      setIsLoadingTokens(true);
      try {
        const fromChainId = swapState.fromChain.id;
        const toChainId = swapState.toChain.id;

        if (typeof fromChainId !== "number" || typeof toChainId !== "number") {
          setFromTokens([]);
          setToTokens([]);
          return;
        }

        const [fromTokensResponse, toTokensResponse] = await Promise.all([
          fetchTokensForChain(fromChainId),
          fetchTokensForChain(toChainId),
        ]);

        setFromTokens(fromTokensResponse.map(convertTokenDataToToken));
        setToTokens(toTokensResponse.map(convertTokenDataToToken));

        setSwapState((prev) => ({
          ...prev,
          fromToken: null,
          toToken: null,
          quote: null,
        }));
      } catch {
        setFromTokens([]);
        setToTokens([]);
      } finally {
        setIsLoadingTokens(false);
      }
    };

    loadTokens();
  }, [swapState.fromChain?.id, swapState.toChain?.id]);

  const loadTokensForChain = async (
    chainId: number | string,
    isFromChain: boolean
  ) => {
    if (typeof chainId !== "number") {
      if (isFromChain) {
        setFromTokens([]);
      } else {
        setToTokens([]);
      }
      return;
    }

    setIsLoadingTokens(true);
    try {
      const tokens = await fetchTokensForChain(chainId);

      if (isFromChain) {
        setFromTokens(tokens.map(convertTokenDataToToken));
        setSwapState((prev) => ({
          ...prev,
          fromToken: null,
          quote: null,
        }));
      } else {
        setToTokens(tokens.map(convertTokenDataToToken));
        setSwapState((prev) => ({
          ...prev,
          toToken: null,
          quote: null,
        }));
      }
    } catch {
      if (isFromChain) {
        setFromTokens([]);
      } else {
        setToTokens([]);
      }
    } finally {
      setIsLoadingTokens(false);
    }
  };

  const isNativeToken = (token: Token | null): boolean => {
    if (!token) return false;
    const nativeAddresses = [
      "0x0000000000000000000000000000000000000000",
      "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
      "",
    ];
    return token.symbol === "ETH" && nativeAddresses.includes(token.contract);
  };

  const checkTokenAllowance = async (
    token: Token,
    amountNeeded: bigint
  ): Promise<{
    hasEnoughAllowance: boolean;
    currentAllowance: bigint;
  }> => {
    if (!address || !token || isNativeToken(token) || !publicClient) {
      return { hasEnoughAllowance: true, currentAllowance: BigInt(0) };
    }

    try {
      const currentAllowance = (await publicClient.readContract({
        address: token.contract as `0x${string}`,
        abi: erc20Abi,
        functionName: "allowance",
        args: [
          address as `0x${string}`,
          addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`,
        ],
      })) as bigint;

      return {
        hasEnoughAllowance: currentAllowance >= amountNeeded,
        currentAllowance,
      };
    } catch {
      throw new Error("Failed to check token allowance");
    }
  };

  const approveToken = async (
    token: Token,
    amount?: bigint
  ): Promise<string> => {
    if (!address || !token || isNativeToken(token)) {
      throw new Error("Invalid token or native token doesn't need approval");
    }

    const approvalAmount = amount || maxUint256;

    return new Promise<string>((resolve, reject) => {
      writeContract(
        {
          address: token.contract as `0x${string}`,
          abi: erc20Abi,
          functionName: "approve",
          args: [
            addresses.MAYAN_FORWARDER_CONTRACT as `0x${string}`,
            approvalAmount,
          ],
        },
        {
          onSuccess: (hash) => resolve(hash),
          onError: (error) => reject(error),
        }
      );
    });
  };

  const supportsPermit = (token: Token): boolean => {
    return token.supportsPermit || false;
  };

  const executeSwapQuote = async (
    quote: Quote,
    permit?: {
      value: bigint;
      deadline: number;
      v: number;
      r: string;
      s: string;
    } | null
  ) => {
    if (!sdkHasLoaded || !isConnected || !address || !quote) {
      throw new Error("Not ready");
    }

    try {
      const txPayload = getSwapFromEvmTxPayload(
        quote,
        address,
        address,
        {
          evm: "0x0000000000000000000000000000000000000000",
        },
        address,
        chainId,
        null,
        permit,
        {}
      );

      const txHash = await new Promise<string>((resolve, reject) => {
        sendTransaction(
          {
            to: txPayload.to as `0x${string}`,
            value: txPayload.value
              ? BigInt(txPayload.value.toString())
              : undefined,
            data: txPayload.data as `0x${string}`,
            gas: txPayload.gasLimit
              ? BigInt(txPayload.gasLimit.toString())
              : undefined,
            gasPrice: txPayload.gasPrice
              ? BigInt(txPayload.gasPrice.toString())
              : undefined,
          },
          {
            onSuccess: (hash) => resolve(hash),
            onError: (error) => reject(error),
          }
        );
      });

      return txHash;
    } catch (error) {
      throw error;
    }
  };

  const handleGetQuote = async () => {
    if (
      !swapState.fromChain ||
      !swapState.toChain ||
      !swapState.fromToken ||
      !swapState.toToken ||
      !swapState.amount ||
      !isConnected
    ) {
      setSwapState((prev) => ({
        ...prev,
        error: "Please fill in all required fields and connect wallet",
      }));
      return;
    }

    setSwapState((prev) => ({
      ...prev,
      isLoading: true,
      error: null,
      quote: null,
    }));

    try {
      const amountInWei = parseUnits(
        swapState.amount,
        swapState.fromToken.decimals
      );

      const quote = (
        await fetchQuote({
          amountIn64: amountInWei.toString(),
          fromToken: swapState.fromToken.contract,
          toToken: swapState.toToken.contract,
          fromChain: swapState.fromChain.key,
          toChain: swapState.toChain.key,
          slippageBps: "auto",
          referrerBps: 100,
        })
      )[0];

      if (!quote) {
        throw new Error("No quote available for this swap");
      }

      setSwapState((prev) => ({
        ...prev,
        quote,
        isLoading: false,
        error: null,
      }));
    } catch (error) {
      setSwapState((prev) => ({
        ...prev,
        error: (error as Error).message || "Failed to get quote",
        isLoading: false,
      }));
    }
  };

  const handleExecuteSwap = async () => {
    if (!swapState.quote || !isConnected || !swapState.fromToken) {
      setSwapState((prev) => ({
        ...prev,
        error:
          "No quote available, wallet not connected, or token not selected",
      }));
      return;
    }

    setSwapState((prev) => ({
      ...prev,
      isLoading: true,
      error: null,
      txHash: null,
      mayanSwapHash: null,
      isExecuting: true,
      isCheckingAllowance: true,
    }));

    try {
      const amountNeeded = parseUnits(
        swapState.amount,
        swapState.fromToken.decimals
      );

      if (!isNativeToken(swapState.fromToken)) {
        const { hasEnoughAllowance, currentAllowance } =
          await checkTokenAllowance(swapState.fromToken, amountNeeded);

        setSwapState((prev) => ({
          ...prev,
          isCheckingAllowance: false,
          needsApproval: !hasEnoughAllowance,
          allowanceAmount: currentAllowance,
        }));

        if (!hasEnoughAllowance) {
          if (supportsPermit(swapState.fromToken)) {
            try {
              setSwapState((prev) => ({
                ...prev,
                isApproving: true,
              }));

              setSwapState((prev) => ({
                ...prev,
                isApproving: false,
                needsApproval: false,
              }));
            } catch {
              await approveToken(swapState.fromToken, amountNeeded);
              await new Promise((resolve) => setTimeout(resolve, 2000));

              setSwapState((prev) => ({
                ...prev,
                isApproving: false,
                needsApproval: false,
              }));
            }
          } else {
            setSwapState((prev) => ({
              ...prev,
              isApproving: true,
            }));

            await approveToken(swapState.fromToken, amountNeeded);
            await new Promise((resolve) => setTimeout(resolve, 2000));

            setSwapState((prev) => ({
              ...prev,
              isApproving: false,
              needsApproval: false,
            }));
          }
        }
      } else {
        setSwapState((prev) => ({
          ...prev,
          isCheckingAllowance: false,
          needsApproval: false,
        }));
      }

      const txHash = await executeSwapQuote(swapState.quote);

      setSwapState((prev) => ({
        ...prev,
        isLoading: false,
        txHash,
        mayanSwapHash: null,
        isExecuting: false,
      }));
    } catch (error) {
      setSwapState((prev) => ({
        ...prev,
        error:
          error instanceof Error ? error.message : "Failed to execute swap",
        isLoading: false,
        isExecuting: false,
        isCheckingAllowance: false,
        isApproving: false,
      }));
    }
  };

  const handleClearError = () => {
    setSwapState((prev) => ({ ...prev, error: null }));
  };

  const handleClearTxHash = () => {
    setSwapState((prev) => ({ ...prev, txHash: null, mayanSwapHash: null }));
  };

  return (
    <div className="max-w-4xl mx-auto p-6 mt-20">
      <div className="text-center mb-8">
        <h1 className="text-3xl font-bold text-gray-900 mb-2">
          Mayan Cross-Chain Swap
        </h1>
        <p className="text-gray-600">
          Swap tokens across different blockchain networks using Mayan
        </p>
      </div>

      <SwapForm
        fromChain={swapState.fromChain}
        toChain={swapState.toChain}
        fromToken={swapState.fromToken}
        toToken={swapState.toToken}
        amount={swapState.amount}
        chains={EVM_CHAINS}
        fromTokens={fromTokens}
        toTokens={toTokens}
        isLoadingTokens={isLoadingTokens}
        onFromChainChange={(chain) => {
          setSwapState((prev) => ({
            ...prev,
            fromChain: chain,
            fromToken: null,
          }));
          if (chain) {
            loadTokensForChain(chain.id, true);
          }
        }}
        onToChainChange={(chain) => {
          setSwapState((prev) => ({
            ...prev,
            toChain: chain,
            toToken: null,
          }));
          if (chain) {
            loadTokensForChain(chain.id, false);
          }
        }}
        onFromTokenChange={(token) =>
          setSwapState((prev) => ({ ...prev, fromToken: token }))
        }
        onToTokenChange={(token) =>
          setSwapState((prev) => ({ ...prev, toToken: token }))
        }
        onAmountChange={(amount) =>
          setSwapState((prev) => ({ ...prev, amount }))
        }
        onRefreshTokens={loadTokensForChain}
      />

      <ActionButtons
        onGetQuote={handleGetQuote}
        onExecuteSwap={handleExecuteSwap}
        isLoading={swapState.isLoading}
        isExecuting={swapState.isExecuting}
        isCheckingAllowance={swapState.isCheckingAllowance}
        isApproving={swapState.isApproving}
        hasQuote={!!swapState.quote}
      />

      {swapState.fromToken &&
        !isNativeToken(swapState.fromToken) &&
        swapState.quote && (
          <div className="bg-blue-50 border border-blue-200 rounded-xl shadow-md p-4 mb-6">
            <div className="flex items-center space-x-3">
              <div className="w-6 h-6 bg-blue-500 rounded-full flex items-center justify-center">
                <svg
                  className="w-4 h-4 text-white"
                  fill="currentColor"
                  viewBox="0 0 20 20"
                >
                  <path
                    fillRule="evenodd"
                    d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
                    clipRule="evenodd"
                  />
                </svg>
              </div>
              <div>
                <h3 className="text-lg font-semibold text-blue-900">
                  Token Approval Status
                </h3>
                <p className="text-blue-700 mt-1">
                  {swapState.needsApproval
                    ? `Approval needed for ${swapState.fromToken.symbol}. This allows Mayan to swap your tokens.`
                    : `${swapState.fromToken.symbol} is approved for swapping.`}
                </p>
              </div>
            </div>
          </div>
        )}

      <StatusMessages
        error={swapState.error}
        txHash={swapState.txHash}
        mayanSwapHash={swapState.mayanSwapHash}
        chainId={chainId}
        onClearError={handleClearError}
        onClearTxHash={handleClearTxHash}
      />

      <RouteDisplay
        quote={swapState.quote}
        toTokenSymbol={swapState.toToken?.symbol}
      />
    </div>
  );
}
This integration enables seamless token swaps across multiple EVM chains using Dynamic, Wagmi, and Mayan SDK.
The system is built with modular UI components that manage user interactions, while the core swap logic is centralized in the MultiChainSwap component.

UI Components

The following UI components are referenced in the main component but need to be implemented separately:
  • SwapForm – Handles chain/token selection and provides search functionality.
  • ActionButtons – Manages the “Get Quote” and “Execute Swap” actions, handling loading states.
  • StatusMessages – Displays errors, transaction status, and links to the block explorer.
  • RouteDisplay – Shows swap details including routes, fees, and time estimates.
  • MultiChainSwap – Orchestrates all components and controls the overall swap state.
Note: The complete implementation of these UI components can be found in the GitHub repository. For brevity, this guide focuses on the core swap logic and integration patterns.
While individual components focus on UI and state management, the business logic lives in MultiChainSwap.
You can take a look at the source code for this example. Important: The UI components (SwapForm, ActionButtons, StatusMessages, RouteDisplay) referenced in this guide are not included here but are available in the complete GitHub repository. This guide focuses on the core integration logic and swap execution flow.

Technical Implementation

Wallet & Account Management

  • useDynamicContextsdkHasLoaded ensures the MPC wallet is initialized before approvals or transactions.
  • useAccount / useChainId → Access current wallet address and connected chain.
    Dynamic’s Wagmi connector makes embedded wallets first-class citizens.

Quote Generation

  • fetchQuote → Calls Mayan SDK using input amount, source/target token contracts, and chains.
  • Returns one or more route candidates → integration uses the best quote.

Transaction Preparation

  • getSwapFromEvmTxPayload → Prepares an EVM transaction (to, data, value, optional gas).
  • Abstracts route details, so the app only needs to sign and send.

Token Approval Process

  • CheckpublicClient.readContract calls ERC20.allowance(address, MayanForwarder).
  • Approve → If insufficient, useWriteContract executes approve(MayanForwarder, amount).
  • Permit (optional) → If token supports EIP-2612, a permit signature (createPermitSignature) can be used instead of an on-chain approval.

Swap Execution

  • useSendTransaction → Sends the final payload when allowance is satisfied.
  • Dynamic signs & broadcasts the tx → returns txHash for tracking in the UI/StatusMessages.

Advanced Configuration

Referral Fee Setup

You can define referral fees per swap:
// Add a referrer fee to the quote (100 bps = 1%)
const quote = (
  await fetchQuote({
    amountIn64: amountInWei.toString(),
    fromToken: swapState.fromToken.contract,
    toToken: swapState.toToken.contract,
    fromChain: swapState.fromChain.key,
    toChain: swapState.toChain.key,
    slippageBps: "auto",
    referrerBps: 100, // <-- 100 bps = 1%
  })
)[0];

// Provide the EVM address that should receive the referral fee
const txPayload = getSwapFromEvmTxPayload(
  quote,
  address,
  address,
  {
    evm: "0x0000000000000000000000000000000000000000", // <-- fee recipient
  },
  address,
  chainId,
  null,
  permit,
  {}
);

Fee Configuration Parameters

  • referrerBps → fee in basis points (25 = 0.25%, 50 = 0.5%, 100 = 1%).
  • evm → your wallet address to receive referral fees.
  • Omit or set to 0 → disables referral fees.

Run the Application

Start the development server:
npm run dev
This starts the development server at http://localhost:3000.

Configure CORS

Add your local development URL to the CORS origins in your Dynamic dashboard under Developer SettingsCORS Origins.

Conclusion

This integration demonstrates Dynamic’s MPC wallets connecting to Mayan’s cross-chain bridge aggregator for seamless EVM token swaps. The implementation focuses on a single route approach, using the best available route for each swap.

Additional Resources