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
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:
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
import { AaveClient } from "@aave/react";
export const client = AaveClient.create();
Create src/lib/providers.tsx with a custom wallet context and wrap with AaveProvider:
"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
Add http://localhost:3000 to your allowed origins in the Dynamic dashboard under Developer Settings → CORS Origins.
Full source code
GitHub repository →
Additional resources