Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.dynamic.xyz/docs/llms.txt

Use this file to discover all available pages before exploring further.

Overview

Aave is a lending protocol where users supply stablecoins to earn interest from borrowers, or borrow assets against their existing balance. This guide walks through integrating Aave V3 into a Next.js app with Dynamic embedded wallets. For the final code, see the GitHub repository.

How it works

Users supply a stablecoin to earn yield — deposit USDC, earn USDC. Interest paid by borrowers flows back to suppliers proportionally. Borrowers must maintain enough collateral to keep their position healthy; if it falls below a minimum ratio, the position can be liquidated. APY is variable and adjusts based on market demand.

Setup

Project setup

Follow the JS SDK Quickstart to initialize a Next.js app with Dynamic. Scaffold a Next.js app with create-next-app and mirror the provider wiring from the quickstart or the GitHub repository.
In the Dynamic dashboard, enable Ethereum under Chains & Networks, enable Embedded wallets under Wallets, and add your app’s origin under Security → Allowed Origins.

Install dependencies

npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @aave/react
For more details on the Aave SDK, see the Aave React SDK docs.

Environment variables

.env.local
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
Your environment ID is in the Dynamic dashboard under Developer Settings → SDK & API Keys.

Initialize Dynamic

Create src/lib/dynamic.ts:
src/lib/dynamic.ts
import { createDynamicClient } from "@dynamic-labs-sdk/client";
import { addEvmExtension } from "@dynamic-labs-sdk/evm";

export const dynamicClient = createDynamicClient({
  environmentId: process.env.NEXT_PUBLIC_DYNAMIC_ENV_ID!,
  metadata: { name: "Aave Yield" },
});

addEvmExtension();

Create the Aave client

src/lib/aave.ts
import { AaveClient } from "@aave/react";

export const client = AaveClient.create();

Configure providers

Create src/lib/providers.tsx with a custom wallet context and wrap with AaveProvider:
src/lib/providers.tsx
"use client";

import { createContext, useContext, useState, useEffect, useCallback, type ReactNode } from "react";
import { getWalletAccounts, onEvent, isSignedIn, logout, detectOAuthRedirect, completeSocialAuthentication } from "@dynamic-labs-sdk/client";
import { createWaasWalletAccounts } from "@dynamic-labs-sdk/client/waas";
import { isEvmWalletAccount, type EvmWalletAccount } from "@dynamic-labs-sdk/evm";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { AaveProvider } from "@aave/react";
import { client } from "./aave";
import { dynamicClient } from "./dynamic";

interface WalletContextValue {
  evmAccount: EvmWalletAccount | null;
  loggedIn: boolean;
  ensureEvmWallet: () => Promise<void>;
  disconnect: () => Promise<void>;
}

const WalletContext = createContext<WalletContextValue>({
  evmAccount: null,
  loggedIn: false,
  ensureEvmWallet: async () => {},
  disconnect: async () => {},
});

export function useWallet() { return useContext(WalletContext); }

const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 1000 * 60 * 5, refetchOnWindowFocus: false } } });

export default function Providers({ children }: { children: ReactNode }) {
  const [evmAccount, setEvmAccount] = useState<EvmWalletAccount | null>(null);
  const [loggedIn, setLoggedIn] = useState(false);

  const refresh = useCallback(() => {
    const accounts = getWalletAccounts(dynamicClient);
    setEvmAccount(accounts.find(isEvmWalletAccount) ?? null);
    setLoggedIn(isSignedIn(dynamicClient));
  }, []);

  const disconnect = useCallback(async () => {
    await logout(dynamicClient);
    setEvmAccount(null);
    setLoggedIn(false);
  }, []);

  const ensureEvmWallet = useCallback(async () => {
    try {
      const accounts = getWalletAccounts(dynamicClient);
      if (!accounts.some(isEvmWalletAccount) && isSignedIn(dynamicClient)) {
        await createWaasWalletAccounts({ chains: ["EVM"] }, dynamicClient);
      }
    } catch {}
    refresh();
  }, [refresh]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        const isOAuth = await detectOAuthRedirect({ url }, dynamicClient);
        if (isOAuth) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
          return;
        }
      } catch {}
      refresh();
    };
    handleOAuthRedirect();
    const unsubWallets = onEvent({ event: "walletAccountsChanged", listener: () => ensureEvmWallet() }, dynamicClient);
    const unsubLogout = onEvent({ event: "logout", listener: () => { setEvmAccount(null); setLoggedIn(false); } }, dynamicClient);
    return () => { unsubWallets(); unsubLogout(); };
  }, [refresh, ensureEvmWallet]);

  return (
    <WalletContext.Provider value={{ evmAccount, loggedIn, ensureEvmWallet, disconnect }}>
      <QueryClientProvider client={queryClient}>
        <AaveProvider client={client}>{children}</AaveProvider>
      </QueryClientProvider>
    </WalletContext.Provider>
  );
}

Enable transaction simulation

Dynamic’s embedded wallets include built-in transaction previews. To enable, go to Developer Settings → Embedded Wallets → Dynamic in the dashboard and toggle on Show Confirmation UI and Transaction Simulation. Users will see a breakdown of assets transferred, estimated fees, and the contract address before confirming any Aave transaction.

Get the wallet client

All Aave operations require a WalletClient from viem. Obtain it from the JS SDK EVM account:
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import { base } from "viem/chains";
import { useWallet } from "@/lib/providers";

const { evmAccount } = useWallet();

const walletClient = evmAccount
  ? createWalletClientForWalletAccount({ walletAccount: evmAccount, chain: base })
  : null;
Pass walletClient and the active chain ID into useTransactionOperations.

Core operations

The Aave SDK uses a plan-based pattern. Each operation (useSupply, useBorrow, etc.) returns a function that resolves to a transaction plan. The plan tells you what kind of transaction to send — a direct transaction, one that first needs a token approval, or a failure due to insufficient balance. useSendTransaction handles submitting the plan to the wallet. Create src/lib/useTransactionOperations.ts:
src/lib/useTransactionOperations.ts
import { useSendTransaction } from "@aave/react/viem";
import { WalletClient, createPublicClient, http, parseAbiItem } from "viem";
import { base } from "viem/chains";
import {
  bigDecimal,
  chainId,
  evmAddress,
  useBorrow,
  useRepay,
  useSupply,
  useWithdraw,
} from "@aave/react";

export function useTransactionOperations(
  walletClient: WalletClient | null,
  selectedChainId: number
) {
  const [supply, supplying] = useSupply();
  const [borrow, borrowing] = useBorrow();
  const [repay, repaying] = useRepay();
  const [withdraw, withdrawing] = useWithdraw();
  const [sendTransaction, sending] = useSendTransaction(walletClient || undefined);

  const isOperating =
    supplying.loading || borrowing.loading || repaying.loading ||
    withdrawing.loading || sending.loading;

  const executeSupply = async (marketAddress: string, currencyAddress: string, amount: string) => {
    if (!walletClient?.account?.address) return;

    const result = await supply({
      market: evmAddress(marketAddress),
      amount: { erc20: { currency: evmAddress(currencyAddress), value: bigDecimal(parseFloat(amount)) } },
      sender: evmAddress(walletClient.account.address),
      chainId: chainId(selectedChainId),
    }).andThen((plan) => {
      switch (plan.__typename) {
        case "TransactionRequest":
          return sendTransaction(plan);
        case "ApprovalRequired":
          return sendTransaction(plan.approval).andThen(() => sendTransaction(plan.originalTransaction));
        case "InsufficientBalanceError":
          throw new Error(`Insufficient balance: ${plan.required.value} required.`);
        default:
          throw new Error("Unknown transaction plan type");
      }
    });

    if (result.isErr()) throw result.error;
    return result.value;
  };

  // executeBorrow, executeRepay, executeWithdraw follow the same pattern...

  return { isOperating, executeSupply /*, executeBorrow, executeRepay, executeWithdraw */ };
}

Reading market and position data

Use these hooks from @aave/react to fetch available markets and the user’s positions:
import {
  useAaveMarkets,
  useUserSupplies,
  useUserBorrows,
  chainId as aaveChainId,
  evmAddress,
} from "@aave/react";
import { useWallet } from "@/lib/providers";

const { evmAccount } = useWallet();

// Available markets on the active chain
const { data: markets, loading: marketsLoading } = useAaveMarkets({
  chainIds: [aaveChainId(chainId)],
  user: evmAccount?.address ? evmAddress(evmAccount.address) : undefined,
});

// User's active supply positions
const { data: userSupplies } = useUserSupplies({
  markets: markets?.map((market) => ({
    chainId: market.chain.chainId,
    address: market.address,
  })) || [],
  user: evmAccount?.address ? evmAddress(evmAccount.address) : undefined,
});

Wiring it together

Here is how the wallet client, operations hook, and data hooks connect in a component:
src/components/MarketsInterface.tsx
"use client";

import {
  chainId as aaveChainId,
  evmAddress,
  useAaveMarkets,
  useUserBorrows,
  useUserSupplies,
} from "@aave/react";
import { createWalletClientForWalletAccount } from "@dynamic-labs-sdk/evm/viem";
import { useEffect, useState } from "react";
import { base } from "viem/chains";
import { useTransactionOperations } from "../lib/useTransactionOperations";
import { useWallet } from "@/lib/providers";

export function MarketsInterface() {
  const { evmAccount } = useWallet();
  const [chainId, setChainId] = useState(base.id);

  const walletClient = evmAccount
    ? createWalletClientForWalletAccount({ walletAccount: evmAccount, chain: base })
    : null;

  const { isOperating, executeSupply, executeBorrow, executeRepay, executeWithdraw } =
    useTransactionOperations(walletClient, chainId);

  const { data: markets, loading: marketsLoading } = useAaveMarkets({
    chainIds: [aaveChainId(chainId)],
    user: evmAccount?.address ? evmAddress(evmAccount.address) : undefined,
  });

  const { data: userSupplies } = useUserSupplies({
    markets: markets?.map((market) => ({ chainId: market.chain.chainId, address: market.address })) || [],
    user: evmAccount?.address ? evmAddress(evmAccount.address) : undefined,
  });

  const { data: userBorrows } = useUserBorrows({
    markets: markets?.map((market) => ({ chainId: market.chain.chainId, address: market.address })) || [],
    user: evmAccount?.address ? evmAddress(evmAccount.address) : undefined,
  });

  // markets, userSupplies, userBorrows, isOperating, and the execute* functions
  // are all available to render your UI.
}

Run the app

npm run dev
Add http://localhost:3000 to your allowed origins in the Dynamic dashboard under Developer Settings → CORS Origins.

Full source code

GitHub repository →

Additional resources