> ## Documentation Index
> Fetch the complete documentation index at: https://www.dynamic.xyz/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Earn yield on stablecoins with Pods

> Learn how to integrate Pods yield strategies with Dynamic's embedded wallets for stablecoin yield earning

## What We're Building

A React (Next.js) app that connects Dynamic's embedded wallets to Pods yield strategies, allowing users to:

* Deposit stablecoins into yield strategies
* Withdraw from positions
* Track yields and positions across multiple protocols

If you want to take a quick look at the final code, check out the [GitHub repository](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-stablecoin-yield-pods).

## Building the Application

### Project setup

Follow the [JS SDK Quickstart](/docs/javascript/reference/quickstart) to initialize a Next.js app with Dynamic. Scaffold with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app), then continue with the dependencies below.

<Info>
  **Dashboard:** Under **Chains & Networks**, enable **Ethereum** and any other EVM networks you use. Under **Sign-in Methods** and **Wallets**, enable what your flow needs (including **Embedded wallets**). Under **Security** → **Allowed Origins**, add the origin where the app runs (for example `http://localhost:3000`).
</Info>

### Install Dependencies

<CodeGroup>
  ```bash npm theme={"system"}
  npm install @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks viem
  ```

  ```bash yarn theme={"system"}
  yarn add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks viem
  ```

  ```bash pnpm theme={"system"}
  pnpm add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks viem
  ```

  ```bash bun theme={"system"}
  bun add @dynamic-labs-sdk/client @dynamic-labs-sdk/evm @dynamic-labs-sdk/react-hooks viem
  ```
</CodeGroup>

### Configure Dynamic Environment

Create a `.env.local` file with your Dynamic environment ID and Pods API key:

```env .env.local theme={"system"}
NEXT_PUBLIC_DYNAMIC_ENV_ID=your-environment-id-here
NEXT_PUBLIC_PODS_API_KEY=your-pods-api-key-here
NEXT_PUBLIC_PODS_API_URL=https://api.pods.finance
```

You can find your Environment ID in the Dynamic developer console under Developer Settings → SDK & API Keys.

For the Pods API key, visit [Pods](https://pods.finance) to get your API key.

If you use Dynamic with ZeroDev smart wallets, configure the ZeroDev project for the same EVM network enabled in your Dynamic wallet environment. Enable ZeroDev in Dynamic's smart wallet or sponsor gas settings, save the dashboard changes, then reconnect in the app before running a deposit.

### Initialize Dynamic

Create `src/lib/dynamic.ts`:

```typescript src/lib/dynamic.ts theme={"system"}
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: "Pods Yield" },
});

addEvmExtension();
```

### Configure Providers

Create `src/lib/providers.tsx`. `DynamicProvider` owns reactive auth state; `InnerProviders` reads it via `useUser()` and `useGetWalletAccounts()` from `@dynamic-labs-sdk/react-hooks`:

```typescript src/lib/providers.tsx theme={"system"}
"use client";

import {
  createContext, useContext, 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 { DynamicProvider, useUser, useGetWalletAccounts } from "@dynamic-labs-sdk/react-hooks";
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 } },
});

function InnerProviders({ children }: { children: ReactNode }) {
  const { data: user } = useUser();
  const loggedIn = user !== null;
  const { data: walletAccounts = [] } = useGetWalletAccounts();
  const evmAccount = walletAccounts.find(isEvmWalletAccount) ?? null;

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

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

  useEffect(() => {
    const unsub = onEvent(
      { event: "walletAccountsChanged", listener: () => { void ensureEvmWallet(); } },
      dynamicClient,
    );
    return () => unsub?.();
  }, [ensureEvmWallet]);

  useEffect(() => {
    const handleOAuthRedirect = async () => {
      if (typeof window === "undefined") return;
      try {
        const url = new URL(window.location.href);
        if (await detectOAuthRedirect({ url }, dynamicClient)) {
          await completeSocialAuthentication({ url }, dynamicClient);
          await ensureEvmWallet();
          window.history.replaceState({}, "", window.location.pathname);
        }
      } catch {}
    };
    handleOAuthRedirect();
  }, [ensureEvmWallet]);

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

export default function Providers({ children }: { children: ReactNode }) {
  return (
    <DynamicProvider client={dynamicClient}>
      <InnerProviders>{children}</InnerProviders>
    </DynamicProvider>
  );
}
```

### Create Pods Client

Create `src/lib/pods.ts` for the Pods API client. This file contains all the API communication logic for interacting with the Pods service.

All type definitions are available in [`src/lib/pods-types.ts`](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-stablecoin-yield-pods/src/lib/pods-types.ts) in the GitHub repository.

```typescript src/lib/pods.ts theme={"system"}
const PODS_API_BASE = process.env.NEXT_PUBLIC_PODS_API_URL || "https://api.pods.finance";
const PODS_API_KEY = process.env.NEXT_PUBLIC_PODS_API_KEY;

async function fetchFromPodsAPI<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
  const response = await fetch(`${PODS_API_BASE}${endpoint}`, {
    ...options,
    headers: {
      "Content-Type": "application/json",
      "x-api-key": PODS_API_KEY!,
      ...options.headers,
    },
  });

  if (!response.ok) {
    const error = await response.text();
    throw new Error(`Pods API error: ${error}`);
  }

  return response.json();
}

// getStrategies, getStrategy, getDepositBytecode, getWithdrawBytecode, getWalletPositions...

export const client = {
  getStrategies,
  getStrategy,
  getDepositBytecode,
  getWithdrawBytecode,
  getWalletPositions,
};
```

You can get detailed information about the Pods API [here](https://docs.pods.finance/api-reference/endpoint/get).

### Create ZeroDev Batch Helper

Create `src/lib/zerodev-batch.ts`. This is the local helper imported by `useTransactionOperations.ts`; it converts the complete Pods `bytecode` array into one ZeroDev call array.

```typescript src/lib/zerodev-batch.ts theme={"system"}
import type { Hex } from "viem";

interface PodsBytecodeCall {
  to: Hex;
  data: Hex;
  value: string;
}

export interface BatchCall {
  to: Hex;
  data: Hex;
  value: bigint;
}

export function bytecodeToBatchCalls(bytecode: PodsBytecodeCall[]): BatchCall[] {
  if (bytecode.length === 0) {
    throw new Error("Pods returned no bytecode calls to batch");
  }

  return bytecode.map((call, index) => ({
    to: call.to,
    data: call.data,
    value: parseCallValue(call.value, index),
  }));
}

function parseCallValue(value: string, index: number): bigint {
  let parsed: bigint;
  try {
    parsed = BigInt(value);
  } catch {
    throw new Error(`bytecode[${index}].value is not a valid integer`);
  }

  if (parsed < BigInt(0)) {
    throw new Error(`bytecode[${index}].value must not be negative`);
  }

  return parsed;
}

export function readTransactionHash(receipt: unknown): Hex | undefined {
  if (typeof receipt !== "object" || receipt === null) return undefined;
  const root = receipt as Record<string, unknown>;
  if (isHex(root.transactionHash)) return root.transactionHash;
  const nested = root.receipt;
  if (typeof nested !== "object" || nested === null) return undefined;
  const nestedRecord = nested as Record<string, unknown>;
  return isHex(nestedRecord.transactionHash)
    ? nestedRecord.transactionHash
    : undefined;
}

function isHex(value: unknown): value is Hex {
  return typeof value === "string" && /^0x[a-fA-F0-9]+$/.test(value);
}
```

### Create Transaction Operations Hook

Create `src/lib/useTransactionOperations.ts` for handling deposits and withdrawals using the Dynamic ZeroDev smart wallet connector. Pods `bytecode` must be submitted atomically; convert it to ZeroDev calls and submit the full array once:

```typescript src/lib/useTransactionOperations.ts theme={"system"}
import { useState, useCallback } from "react";
import { client as podsClient } from "./pods";
import { bytecodeToBatchCalls, type BatchCall, type Hex } from "./zerodev-batch";
import type { Strategy } from "./pods-types";
import { useWallet } from "@/lib/providers";

interface KernelBatchClient {
  sendTransaction?: (args: { calls: BatchCall[] }) => Promise<Hex>;
  sendUserOperation?: (args: { calls: BatchCall[] }) => Promise<Hex>;
}

interface ZeroDevConnectorLike {
  getAccountAbstractionProvider?: () => unknown;
}

async function getKernelClient(evmAccount: { connector?: unknown }): Promise<KernelBatchClient> {
  const connector = evmAccount.connector as ZeroDevConnectorLike;
  const kernelClient = connector.getAccountAbstractionProvider?.();
  if (!kernelClient) throw new Error("ZeroDev kernel client is not available");
  return kernelClient as KernelBatchClient;
}

async function submitBatchedCalls(kernelClient: KernelBatchClient, calls: BatchCall[]): Promise<Hex> {
  if (kernelClient.sendUserOperation) return kernelClient.sendUserOperation({ calls });
  if (kernelClient.sendTransaction) return kernelClient.sendTransaction({ calls });
  throw new Error("ZeroDev kernel client cannot send batched calls");
}

export function useTransactionOperations(_walletClient: unknown, selectedChainId: number) {
  const { evmAccount } = useWallet();
  const [isOperating, setIsOperating] = useState(false);
  const [operationError, setOperationError] = useState<Error | null>(null);

  const executeDeposit = useCallback(async (strategy: Strategy, amount: string) => {
    const walletAddress = evmAccount?.address;
    if (!walletAddress || !evmAccount) throw new Error("Wallet not connected");

    setIsOperating(true);
    try {
      const amountInSmallestUnit = BigInt(
        Math.floor(parseFloat(amount) * 10 ** strategy.assetDecimals)
      ).toString();

      const { bytecode } = await podsClient.getDepositBytecode({
        strategyId: strategy.id,
        chainId: selectedChainId,
        amount: amountInSmallestUnit,
        asset: strategy.assetName,
        wallet: walletAddress,
      });

      const calls = bytecodeToBatchCalls(bytecode);
      const kernelClient = await getKernelClient(evmAccount);
      const submittedHash = await submitBatchedCalls(kernelClient, calls);
      return submittedHash;
    } catch (error) {
      const err = error instanceof Error ? error : new Error(String(error));
      setOperationError(err);
      throw err;
    } finally {
      setIsOperating(false);
    }
  }, [evmAccount, selectedChainId]);

  // executeWithdraw follows the same pattern...

  return { isOperating, operationError, executeDeposit /*, executeWithdraw */ };
}
```

The important detail is that `submitBatchedCalls` receives the full `calls` array once. Do not send each Pods `bytecode` item as its own transaction.

### Build Yield Interface Component

Create `src/components/YieldInterface.tsx` for the main UI. It reads `evmAccount` from `useWallet()` instead of `useDynamicContext()`:

```typescript src/components/YieldInterface.tsx theme={"system"}
"use client";

import { useEffect, useState, useCallback } from "react";
import { base } from "viem/chains";
import { useTransactionOperations } from "../lib/useTransactionOperations";
import { client as podsClient } from "../lib/pods";
import { useWallet } from "@/lib/providers";
import type { Strategy, WalletPositions } from "../lib/pods-types";

export function YieldInterface() {
  const { evmAccount } = useWallet();
  const [selectedChainId, setSelectedChainId] = useState(base.id);
  const [strategies, setStrategies] = useState<Strategy[]>([]);
  const [positions, setPositions] = useState<WalletPositions | null>(null);

  const { isOperating, executeDeposit, executeWithdraw } =
    useTransactionOperations(null, selectedChainId);

  const fetchStrategies = useCallback(async () => {
    const response = await podsClient.getStrategies(selectedChainId, 100);
    setStrategies(response.data.filter((s) => s.isActive !== false));
  }, [selectedChainId]);

  useEffect(() => { fetchStrategies(); }, [fetchStrategies]);

  useEffect(() => {
    if (!evmAccount?.address) { setPositions(null); return; }
    podsClient.getWalletPositions(evmAccount.address).then(setPositions).catch(() => setPositions(null));
  }, [evmAccount?.address]);

  // render strategies and positions...
}
```

### Run the Application

Start the development server:

<CodeGroup>
  ```bash npm theme={"system"}
  npm run dev
  ```

  ```bash yarn theme={"system"}
  yarn dev
  ```

  ```bash pnpm theme={"system"}
  pnpm dev
  ```

  ```bash bun theme={"system"}
  bun dev
  ```
</CodeGroup>

The application will be available at `http://localhost:3000`.

### Configure CORS

Add your local development URL to the CORS origins in your Dynamic developer console under Developer Settings > CORS Origins.

## Full source code

[GitHub repository →](https://github.com/dynamic-labs/examples/tree/main/examples/nextjs-stablecoin-yield-pods)

### Additional Resources

* [Earn yield with Aave](/docs/recipes/integrations/yield/aave)
* [Dynamic JS SDK](/docs/javascript/reference/quickstart)
* [Pods Documentation](https://docs.pods.finance)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.