Skip to main content

Overview

This guide shows how to use Magic’s Embedded Wallet to supply USDC to AAVE V3 on Base using the @aave/client SDK. Users deposit USDC into AAVE’s lending pool and receive aUSDC — an interest-bearing token that grows in value over time. When they’re ready, they can withdraw their USDC plus earned yield.

Prerequisites

Before starting, ensure you have:
  1. A Magic Publishable API Key from your Magic Dashboard
  2. A Base RPC endpoint (e.g., from Alchemy or QuickNode)
  3. USDC on Base in the user’s wallet
  4. ETH on Base for gas fees (typically fractions of a cent)

How It Works

  1. User authenticates with Magic
  2. The AAVE SDK generates a supply transaction (handling approval automatically)
  3. The transaction is sent through the user’s Embedded Wallet via viem
  4. The user receives aUSDC, which accrues interest automatically
  5. When ready, the user withdraws their USDC plus earned yield

Setting Up the Clients

Install dependencies and initialize Magic, viem, and the AAVE client.
npm install magic-sdk viem @aave/client
TypeScript
import { Magic } from "magic-sdk";
import { createWalletClient, createPublicClient, custom, http } from "viem";
import { base } from "viem/chains";
import { AaveClient, evmAddress } from "@aave/client";
import { sendWith } from "@aave/client/viem";

const magic = new Magic("YOUR_PUBLISHABLE_KEY", {
  network: {
    rpcUrl: "https://base-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY",
    chainId: 8453,
  },
});

const publicClient = createPublicClient({
  chain: base,
  transport: http("https://base-mainnet.g.alchemy.com/v2/YOUR_ALCHEMY_KEY"),
});

const walletClient = createWalletClient({
  chain: base,
  transport: custom(magic.rpcProvider),
});

const [userAddress] = await walletClient.getAddresses();

const aaveClient = AaveClient.create();

Contract Addresses

TypeScript
// AAVE V3 Pool on Base (market address)
const AAVE_POOL = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5";

// USDC on Base (native, issued by Circle)
const USDC_ADDRESS = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
const USDC_DECIMALS = 6;

Supplying USDC

Use the AAVE SDK’s supply action to generate the transaction, then send it via the Magic wallet. The SDK returns an execution plan that may require an approval step before the supply transaction.
TypeScript
import { supply } from "@aave/client/actions";

async function supplyUSDC(amount: string) {
  const result = await supply(aaveClient, {
    market: AAVE_POOL,
    amount: {
      erc20: {
        currency: USDC_ADDRESS,
        value: amount,
      },
    },
    sender: evmAddress(userAddress),
    chainId: 8453,
  });

  if (result.isErr()) {
    throw new Error(`Supply failed: ${result.error}`);
  }

  const plan = result.value;

  // Handle approval if required
  if (plan.__typename === "ApprovalRequired") {
    const approveHash = await walletClient.sendTransaction({
      to: plan.approval.to,
      value: BigInt(plan.approval.value),
      data: plan.approval.data,
      account: userAddress,
    });
    await publicClient.waitForTransactionReceipt({ hash: approveHash });

    const supplyHash = await walletClient.sendTransaction({
      to: plan.originalTransaction.to,
      value: BigInt(plan.originalTransaction.value),
      data: plan.originalTransaction.data,
      account: userAddress,
    });
    return await publicClient.waitForTransactionReceipt({ hash: supplyHash });
  }

  if (plan.__typename === "TransactionRequest") {
    const hash = await walletClient.sendTransaction({
      to: plan.to,
      value: BigInt(plan.value),
      data: plan.data,
      account: userAddress,
    });
    return await publicClient.waitForTransactionReceipt({ hash });
  }

  throw new Error(`Unhandled plan type: ${plan.__typename}`);
}

// Supply 10 USDC
await supplyUSDC("10");
The AAVE SDK handles the approval check internally. If the Pool already has sufficient allowance, it returns a TransactionRequest directly. Otherwise, it returns ApprovalRequired with both the approval and supply transactions.

Withdrawing USDC

Use the withdraw action to pull USDC back from the lending pool, including any earned yield.
TypeScript
import { withdraw } from "@aave/client/actions";

async function withdrawUSDC(amount: string) {
  const result = await withdraw(aaveClient, {
    market: AAVE_POOL,
    amount: {
      erc20: {
        currency: USDC_ADDRESS,
        value: { exact: amount },
      },
    },
    sender: evmAddress(userAddress),
    chainId: 8453,
  });

  if (result.isErr()) {
    throw new Error(`Withdraw failed: ${result.error}`);
  }

  const plan = result.value;

  if (plan.__typename === "TransactionRequest") {
    const hash = await walletClient.sendTransaction({
      to: plan.to,
      value: BigInt(plan.value),
      data: plan.data,
      account: userAddress,
    });
    return await publicClient.waitForTransactionReceipt({ hash });
  }

  throw new Error(`Unhandled plan type: ${plan.__typename}`);
}

// Withdraw 5 USDC
await withdrawUSDC("5");

Checking Position

Read the user’s aUSDC balance to see their current position including accrued yield. The aToken address is retrieved from the Pool’s reserve data.
TypeScript
import { formatUnits } from "viem";

const AAVE_POOL_ADDRESS = "0xA238Dd80C259a72e81d7e4664a9801593F98d1c5";

const poolAbi = [
  {
    name: "getReserveData",
    type: "function",
    inputs: [{ name: "asset", type: "address" }],
    outputs: [
      {
        type: "tuple",
        components: [
          { name: "configuration", type: "uint256" },
          { name: "liquidityIndex", type: "uint128" },
          { name: "currentLiquidityRate", type: "uint128" },
          { name: "variableBorrowIndex", type: "uint128" },
          { name: "currentVariableBorrowRate", type: "uint128" },
          { name: "currentStableBorrowRate", type: "uint128" },
          { name: "lastUpdateTimestamp", type: "uint40" },
          { name: "id", type: "uint16" },
          { name: "aTokenAddress", type: "address" },
          { name: "stableDebtTokenAddress", type: "address" },
          { name: "variableDebtTokenAddress", type: "address" },
          { name: "interestRateStrategyAddress", type: "address" },
          { name: "accruedToTreasury", type: "uint128" },
          { name: "unbacked", type: "uint128" },
          { name: "isolationModeTotalDebt", type: "uint128" },
        ],
      },
    ],
    stateMutability: "view",
  },
] as const;

const erc20BalanceAbi = [
  {
    name: "balanceOf",
    type: "function",
    inputs: [{ name: "account", type: "address" }],
    outputs: [{ type: "uint256" }],
    stateMutability: "view",
  },
] as const;

async function getPosition() {
  // Get the aToken address for USDC
  const reserveData = await publicClient.readContract({
    address: AAVE_POOL_ADDRESS,
    abi: poolAbi,
    functionName: "getReserveData",
    args: [USDC_ADDRESS],
  });

  // Read aToken balance (includes accrued yield)
  const aTokenBalance = await publicClient.readContract({
    address: reserveData.aTokenAddress,
    abi: erc20BalanceAbi,
    functionName: "balanceOf",
    args: [userAddress],
  });

  // Read USDC wallet balance
  const usdcBalance = await publicClient.readContract({
    address: USDC_ADDRESS,
    abi: erc20BalanceAbi,
    functionName: "balanceOf",
    args: [userAddress],
  });

  return {
    supplied: formatUnits(aTokenBalance, USDC_DECIMALS),
    wallet: formatUnits(usdcBalance, USDC_DECIMALS),
  };
}

const position = await getPosition();
console.log(`Supplied: ${position.supplied} USDC`);
console.log(`Wallet: ${position.wallet} USDC`);
The aUSDC balance increases over time as interest accrues. Unlike vault-based protocols, AAVE’s aTokens are rebasing — the balance itself grows, so 1 aUSDC always equals 1 USDC of underlying value.

Key Dependencies

PackagePurpose
magic-sdkMagic authentication and Embedded Wallet
viemEthereum client for transaction execution
@aave/clientAAVE V3 SDK for building supply and withdraw transactions

Troubleshooting

Symptoms: The supply transaction fails or reverts on-chain.Solutions:
  • Check that the USDC approval was confirmed before the supply transaction
  • Verify the user has sufficient USDC balance on Base
  • Ensure the user has ETH on Base for gas fees
  • Confirm the USDC address is correct (native USDC, not bridged USDbC)
Symptoms: The withdrawn amount is less than what was supplied.Solutions:
  • This is unlikely with AAVE supply — depositors earn yield, not lose it
  • Check that no other address withdrew on your behalf
  • Verify you’re reading the aToken balance (not USDC balance) to see your position
Symptoms: The aUSDC balance appears static.Solutions:
  • AAVE yield accrues continuously but can be very small over short periods
  • On Base, supply APY for USDC depends on borrowing demand — check AAVE’s dashboard for current rates
  • The balance is correct — yield accrual just takes time to be visible at small amounts
Symptoms: The supply action returns a plan with __typename: "InsufficientBalanceError".Solutions:
  • The user doesn’t have enough USDC to supply the requested amount
  • Check the plan.required.value field for the amount needed
  • Verify the token balance on Base before calling supply

Resources

Magic Embedded Wallets

Learn about Magic’s Embedded Wallet product

AAVE V3 Documentation

Official AAVE protocol documentation

EVM Transaction Signing

Guide for signing transactions with Magic wallets

AAVE Base Markets

View current AAVE supply rates on Base