Skip to main content
Generate Yield

Live Demo

Explore a working fintech app where users deposit USDC and earn yield

Supported Networks

Yield.xyz supports Ethereum, Base, Solana, and 50+ other chains across EVM and non-EVM networks. Testnet support includes Stellar-Testnet, Base-Sepolia, Ethereum-Sepolia, and Solana Devnet. For the complete and up-to-date list, see the Yield.xyz Supported Networks documentation.
This guide uses Base network with testnet wallets connected to mainnet yield opportunities, giving you a realistic experience without using real funds.

Prerequisites

Before you start, make sure you have:
Since this guide uses testnet tokens, deposits will not actually earn yield. For production, use mainnet USDC and ensure your Crossmint wallet is configured for mainnet.Need help launching in production or getting access to Yield.xyz? Contact sales.
1

Get a Yield.xyz API Key

Request an API key from Yield.xyz and add it to your environment:
.env.local
NEXT_PUBLIC_YIELD_API_KEY=YOUR_YIELD_XYZ_API_KEY
2

Create the Yield API utilities

Create a file with all the Yield.xyz API functions and types:
  • Fetch Yields: Retrieve available USDC yield opportunities on Base
  • Enter Yield: Deposit USDC into a yield-generating position
  • Exit Yield: Withdraw USDC from an active yield position
  • Get Yield Balance: Check the current balance in a yield position
  • Get Active Positions: Retrieve all active yield positions for a wallet
yield-api.ts
const YIELD_API_URL = "https://api.yield.xyz";
const YIELD_NETWORK = "base"; // Hardcoded to Base network

// Types
export interface YieldOpportunity {
  id: string;
  network: string;
  providerId: string;
  metadata: { name: string; description?: string };
  rewardRate: { total: number; rateType: "APY" | "APR" };
  status: { enter: boolean; exit: boolean };
}

export interface YieldAction {
  id: string;
  intent: "enter" | "exit";
  yieldId: string;
  amount: string;
  amountUsd: string;
  status: "CREATED" | "PROCESSING" | "SUCCESS" | "FAILED";
  createdAt: string;
}

// Fetch available yield opportunities
export async function fetchYields(): Promise<YieldOpportunity[]> {
  const response = await fetch(
    `${YIELD_API_URL}/v1/yields?network=${YIELD_NETWORK}&token=USDC&limit=10`,
    {
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": process.env.NEXT_PUBLIC_YIELD_API_KEY!,
      },
    }
  );
  if (!response.ok) {
    throw new Error("Failed to fetch yields");
  }

  const data = await response.json();
  return data.data || data.items || [];
}

// Enter a yield position (deposit)
export async function enterYield(
  yieldId: string,
  address: string,
  amount: string
) {
  const response = await fetch(`${YIELD_API_URL}/v1/actions/enter`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": process.env.NEXT_PUBLIC_YIELD_API_KEY!,
    },
    body: JSON.stringify({
      yieldId,
      address,
      arguments: { amount }, // Human-readable (e.g., "100" for 100 USDC)
    }),
  });

  if (!response.ok) {
    throw new Error("Failed to create deposit");
  }

  return response.json();
}

// Exit a yield position (withdraw)
export async function exitYield(
  yieldId: string,
  address: string,
  amount?: string
) {
  const response = await fetch(`${YIELD_API_URL}/v1/actions/exit`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-KEY": process.env.NEXT_PUBLIC_YIELD_API_KEY!,
    },
    body: JSON.stringify({
      yieldId,
      address,
      arguments: amount ? { amount } : { useMaxAmount: true },
    }),
  });

  if (!response.ok) {
    throw new Error("Failed to create withdrawal");
  }

  return response.json();
}

// Get balance for a yield position
export async function getYieldBalance(
  yieldId: string,
  address: string
): Promise<string> {
  const response = await fetch(
    `${YIELD_API_URL}/v1/yields/${yieldId}/balances?address=${address}`,
    {
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": process.env.NEXT_PUBLIC_YIELD_API_KEY!,
      },
    }
  );

  if (!response.ok) {
    return "0";
  }

  const data = await response.json();
  const activeBalance = data.balances?.find((b: any) => b.type === "active");
  return activeBalance?.amount || "0";
}

// Get user's active positions
export async function getActivePositions(
  address: string
): Promise<YieldAction[]> {
  const response = await fetch(
    `${YIELD_API_URL}/v1/actions?address=${address}&limit=20`,
    {
      headers: {
        "Content-Type": "application/json",
        "X-API-KEY": process.env.NEXT_PUBLIC_YIELD_API_KEY!,
      },
    }
  );

  if (!response.ok) {
    return [];
  }

  const { items = [] } = await response.json();

  // Group by yieldId and filter to active positions (no exit after enter)
  const byYield = new Map<string, YieldAction[]>();
  for (const action of items) {
    const list = byYield.get(action.yieldId) || [];
    list.push(action);
    byYield.set(action.yieldId, list);
  }

  const active: YieldAction[] = [];
  for (const [, actions] of byYield) {
    const sorted = actions.sort(
      (a, b) =>
        new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
    );
    const lastEnter = sorted.find((a) => a.intent === "enter");
    const hasExit = sorted.some(
      (a) =>
        a.intent === "exit" &&
        lastEnter &&
        new Date(a.createdAt) > new Date(lastEnter.createdAt)
    );
    if (lastEnter && !hasExit) active.push(lastEnter);
  }
  return active;
}
3

Build a yield component with deposit and withdraw

Create a component that displays yields, handles deposits, and shows active positions:
yield-app.tsx
import { useState, useEffect } from "react";
import { useWallet, EVMWallet } from "@crossmint/client-sdk-react-ui";
import {
  fetchYields,
  enterYield,
  exitYield,
  getActivePositions,
  getYieldBalance,
  YieldOpportunity,
  YieldAction,
} from "./yield-api";

export function YieldApp() {
  const { wallet } = useWallet();
  const [yields, setYields] = useState<YieldOpportunity[]>([]);
  const [positions, setPositions] = useState<YieldAction[]>([]);
  const [selectedYield, setSelectedYield] = useState<string | null>(null);
  const [amount, setAmount] = useState("");
  const [loading, setLoading] = useState(false);

  // Load yields
  useEffect(() => {
    fetchYields().then(setYields);
  }, []);

  useEffect(() => {
    if (wallet?.address) {
      getActivePositions(wallet.address).then(setPositions);
    }
  }, [wallet?.address]);

  // Execute transactions through Crossmint wallet
  const executeTransactions = async (transactions: any[]) => {
    const evmWallet = EVMWallet.from(wallet!);
    const sorted = transactions.sort((a, b) => a.stepIndex - b.stepIndex);

    for (const tx of sorted) {
      const unsigned = JSON.parse(tx.unsignedTransaction);
      await evmWallet.sendTransaction({
        to: unsigned.to,
        data: unsigned.data,
        value: unsigned.value || "0x0",
      });
    }
  };

  const handleDeposit = async (yieldId: string) => {
    if (!wallet?.address || !amount) {
      return;
    }

    setLoading(true);
    try {
      const { transactions } = await enterYield(
        yieldId,
        wallet.address,
        amount
      );
      await executeTransactions(transactions);
      setPositions(await getActivePositions(wallet.address));
      setAmount("");
      setSelectedYield(null);
    } catch (err) {
      console.error("Deposit failed:", err);
    }
    setLoading(false);
  };

  const handleWithdraw = async (yieldId: string) => {
    if (!wallet?.address) {
      return;
    }

    setLoading(true);
    try {
      // Fetch current balance to pass to exit
      const balance = await getYieldBalance(yieldId, wallet.address);
      const { transactions } = await exitYield(
        yieldId,
        wallet.address,
        balance
      );
      await executeTransactions(transactions);
      setPositions(await getActivePositions(wallet.address));
    } catch (err) {
      console.error("Withdraw failed:", err);
    }
    setLoading(false);
  };

  if (!wallet) {
    return <p>Connect your wallet to view yield opportunities.</p>;
  }

  return (
    <div className="space-y-6 p-4">
      <h2 className="text-xl font-bold">💰 Earn Yield on USDC</h2>

      {/* Yield Opportunities */}
      <div className="space-y-3">
        <h3 className="font-semibold">Available Yields</h3>
        {yields.map((y) => (
          <div
            key={y.id}
            onClick={() =>
              setSelectedYield(selectedYield === y.id ? null : y.id)
            }
            className={`cursor-pointer rounded-xl border p-4 ${
              selectedYield === y.id ? "border-blue-500" : ""
            }`}
          >
            <div className="flex justify-between">
              <span className="font-semibold">{y.metadata.name}</span>
              <span className="text-green-600">
                {(y.rewardRate.total * 100).toFixed(2)}% APY
              </span>
            </div>
            <p className="text-sm text-gray-500">{y.providerId}</p>

            {selectedYield === y.id && (
              <div
                className="mt-3 flex gap-2"
                onClick={(e) => e.stopPropagation()}
              >
                <input
                  type="number"
                  placeholder="Amount (USDC)"
                  value={amount}
                  onChange={(e) => setAmount(e.target.value)}
                  className="flex-1 rounded border px-3 py-2"
                />
                <button
                  onClick={() => handleDeposit(y.id)}
                  disabled={loading || !amount}
                  className="rounded bg-blue-600 px-4 py-2 text-white hover:bg-blue-700 disabled:opacity-50"
                >
                  {loading ? "..." : "Deposit"}
                </button>
              </div>
            )}
          </div>
        ))}
      </div>

      {/* Active Positions */}
      {positions.length > 0 && (
        <div className="space-y-3">
          <h3 className="font-semibold">Your Positions</h3>
          {positions.map((p) => (
            <div
              key={p.id}
              className="flex items-center justify-between rounded-xl border p-4"
            >
              <div>
                <span className="font-semibold">{p.yieldId.split("-")[2]}</span>
                <p className="text-sm text-gray-500">${p.amountUsd} USDC</p>
              </div>
              <button
                onClick={() => handleWithdraw(p.yieldId)}
                disabled={loading}
                className="rounded border border-red-200 bg-red-50 px-4 py-2 text-red-600 hover:bg-red-100"
              >
                {loading ? "..." : "Withdraw"}
              </button>
            </div>
          ))}
        </div>
      )}
    </div>
  );
}
4

Render the yield app

Add the yield component to your application (inside your Crossmint providers):
page.tsx
"use client";

import { Providers } from "./providers";
import { YieldApp } from "./yield-app";

export default function Home() {
  return (
    <Providers>
      <YieldApp />
    </Providers>
  );
}

Next Steps