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

# How to build a portfolio viewer using Moralis API

When building a web3 app, it is essentially for the app to show some information about the connected wallet like ERC20 token balances, ERC721 token balances, transaction history etc...

Moralis has a wide range of out-of-the-box APIs for the most popular features like wallet native balance, fetch all NFTs and collections, wallet age and activity, and many more...

In this guide, you will learn how to build a simple ERC20 token balances view for the connected wallet using Moralis API and NextJS.

<Note>
  If you wish to try the portfolio viewer before building it, you can do so [here](https://portfolio-dashboard-example.vercel.app)
</Note>

<img src="https://mintcdn.com/monadfoundation-40611fb6/5Mt9_Scj9fq4fC68/static/img/guides/moralis-api-guide/1.png?fit=max&auto=format&n=5Mt9_Scj9fq4fC68&q=85&s=e56ed98cbf9451165e6f6ed214321c30" alt="portfolio view" width="2020" height="1460" data-path="static/img/guides/moralis-api-guide/1.png" />

## Requirements

Before you begin, you'll need:

1. **Moralis API Key**: Get it from [Moralis Dashboard](https://admin.moralis.io/)
2. **Reown Project ID**: Get it from [Reown Cloud](https://cloud.reown.com/)
   * You can also check out the [Reown AppKit guide for Monad](http://docs.monad.xyz/guides/reown-guide)

## Step 1: Initializing the project

### Create Nextjs app

```
npx create-next-app@latest portfolio-app --typescript --tailwind --eslint --app --src-dir --import-alias "@/*"
```

### Install ShadCN components

```
npx shadcn@latest add button card tabs
```

### Install Tanstack React Query (for better async state management)

```
npm install @tanstack/react-query
```

## Step 2: Environment Setup

Add your Moralis API key and Reown Project ID to `.env.local`:

```bash theme={null}
MORALIS_API_KEY=
NEXT_PUBLIC_PROJECT_ID=
```

## Step 3: Connect Wallet functionality

### Create the `ContextProvider` component

Create a folder named `context` in the `src` directory, inside the folder create a file named `index.ts`

```ts title="src/context/index.ts" theme={null}
"use client";

import { wagmiAdapter, projectId } from "@/config";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { createAppKit } from "@reown/appkit/react";
import { monad } from "@reown/appkit/networks";
import React, { type ReactNode } from "react";
import { WagmiProvider, type Config } from "wagmi";

// Set up queryClient
const queryClient = new QueryClient();

if (!projectId) {
  throw new Error("NEXT_PUBLIC_PROJECT_ID is not set");
}

// Set up metadata
const metadata = {
  name: "Portfolio Viewer",
  description: "View your crypto portfolio on Monad blockchain",
  url: "your-app-url",
  icons: ["your-app-icon"],
};

// Create the modal
const modal = createAppKit({
  adapters: [wagmiAdapter],
  projectId,
  networks: [monad],
  defaultNetwork: monad,
  metadata: metadata,
  features: {
    analytics: true,
  },
});

export function ContextProvider({ children }: { children: ReactNode }) {
  return (
    <WagmiProvider config={wagmiAdapter.wagmiConfig as Config}>
      <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
    </WagmiProvider>
  );
}
```

### Wrap the app with `ContextProvider` component in `layout.tsx`

```tsx title="src/app/layout.tsx" theme={null}
// imports

export const metadata: Metadata = {
  title: "Monad Portfolio App",
  description: "View your crypto portfolio on Monad blockchain",
};

export default function RootLayout({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) {
  return (
    <html lang="en" className={GeistSans.variable} style={{
      // @ts-ignore
      '--font-geist-sans': GeistSans.style.fontFamily,
      '--font-geist-mono': GeistMono.style.fontFamily,
    }}>
      <body className="font-sans min-h-screen flex flex-col bg-dot-pattern">
        <ContextProvider>
          <main className="flex-1 pb-6">{children}</main>
        </ContextProvider>
      </body>
    </html>
  );
}
```

### Create a custom `ConnectButton`

Create a file in `components/wallet` folder named `ConnectButton.tsx`

```tsx title="src/components/wallet/ConnectButton.tsx" theme={null}
"use client";

import { useAppKit, useAppKitAccount, useDisconnect } from "@reown/appkit/react";
import { Button } from "@/components/ui/Button";
import { ConnectIcon, type ConnectIconHandle } from "@/components/ui/ConnectIcon";

export function ConnectButton() {
  const { open } = useAppKit();
  const { address, isConnected } = useAppKitAccount();
  const { disconnect } = useDisconnect();

  if (isConnected && address) {
    return (
      <div className="inline-flex rounded-md shadow-sm">
        <a
          href={`https://monadvision.com/address/${address}`}
          target="_blank"
          rel="noopener noreferrer"
          className="inline-flex items-center px-8 h-10 bg-primary text-primary-foreground rounded-l-md font-semibold text-sm hover:bg-primary/90 transition-colors"
        >
          {formatAddress(address)}
        </a>
        <Button
          onClick={() => disconnect()}
          size="icon"
          className="h-10 w-10 rounded-l-none border-l border-primary-foreground/20"
        >
          <ConnectIcon size={16} />
        </Button>
      </div>
    );
  }

  return (
    <Button
      onClick={() => open()}
      size="lg"
      className="font-semibold shadow-sm gap-2"
    >
      <ConnectIcon ref={connectIconRef} size={20} />
      Connect Wallet
    </Button>
  );
}
```

We are using the `useAppKit` and `useAppKitAccount` hook for creating a custom connect button instead of the standard one provided by Reown AppKit.

### Create a `Header` component with custom `ConnectButton`

<img src="https://mintcdn.com/monadfoundation-40611fb6/5Mt9_Scj9fq4fC68/static/img/guides/moralis-api-guide/2.png?fit=max&auto=format&n=5Mt9_Scj9fq4fC68&q=85&s=5c0d29a01608b83e09c38136855b2b4f" alt="header" width="1994" height="680" data-path="static/img/guides/moralis-api-guide/2.png" />

Create a file named `Header.tsx` in `src/components/layout` folder and import the newly created `ConnectButton` component into it.

```tsx title="src/components/layout/Header.tsx" theme={null}
"use client";

import { ConnectButton } from "../wallet/ConnectButton";
import { ExternalLink } from "lucide-react";

export function Header() {
  return (
    <header className="sticky top-0 z-50 w-full border-b border-border bg-card/80 backdrop-blur supports-[backdrop-filter]:bg-card/60">
        <div className="flex h-16 items-center justify-between">
          <div className="flex items-center gap-3">
            <h1 className="text-xl font-bold text-foreground">
              Portfolio App
            </h1>
          </div>
          <div className="flex items-center gap-6">
            <ConnectButton />
          </div>
        </div>
    </header>
  );
}
```

Import the `Header` component in `layout.tsx`

```tsx title="src/app/layout.tsx" theme={null}
import { Header } from "@/components/layout/Header";

// rest of the code

<ContextProvider>
    <Header />
    <main className="flex-1 pb-6">{children}</main>
</ContextProvider>

// rest of the code
```

You should now be able to see a custom Connect Wallet button!

## Step 4: Fetching connected wallet token balances

### Creating a `/api/wallet/balances` route

Create a file named `route.ts` in the folder `src/app/api/wallet/balances/`

We are using the "Get Native & ERC20 Token Balances by Wallet" Moralis API endpoint to get the token balances. You can find the endpoint docs [here](https://docs.moralis.com/web3-data-api/evm/reference/wallet-api/get-wallet-token-balances-price?address=0xcB1C1FdE09f811B294172696404e88E658659905\&chain=0x8f\&token_addresses=\[]\&limit=25).

It takes in an address and a chain id and returns a list of ERC20 tokens (including Logo, Name, Symbol) and native balance of the address.

```ts title="src/app/api/wallet/balances/route.ts" theme={null}
import { NextRequest, NextResponse } from "next/server";

const MORALIS_API_BASE = "https://deep-index.moralis.io/api/v2.2";

export async function GET(request: NextRequest) {
  try {
    const { searchParams } = request.nextUrl;
    const address = searchParams.get("address");
    const chain = "0x8f"; // 0x8f = 143 (Monad chain ID in hex)

    if (!address) {
      return NextResponse.json(
        { error: "Address is required" },
        { status: 400 }
      );
    }

    // Fetch ERC20 token balances with price data from Moralis
    const data = await moralisRequest({
      endpoint: `/wallets/${address}/tokens`,
      params: {
        chain,
      },
    });

    return NextResponse.json(data);
  } catch (error) {
    console.error("Error fetching token balances:", error);

    const errorMessage = error instanceof Error ? error.message : "Failed to fetch token balances";

    return NextResponse.json(
      {
        error: errorMessage,
      },
      { status: 500 }
    );
  }
}

async function moralisRequest({
  endpoint,
  params,
}) {
  const apiKey = process.env.MORALIS_API_KEY;

  if (!apiKey) {
    throw new Error("MORALIS_API_KEY is not set");
  }

  const url = new URL(`${MORALIS_API_BASE}${endpoint}`);

  if (params) {
    Object.entries(params).forEach(([key, value]) => {
      url.searchParams.append(key, String(value));
    });
  }

  const response = await fetch(url.toString(), {
    method: "GET",
    headers: {
      "X-API-Key": apiKey,
      "Content-Type": "application/json",
    },
    next: {
      revalidate: 30, // Cache for 30 seconds
    },
  });

  if (!response.ok) {
    const errorData = await response.json().catch(() => ({}));
    const errorMessage = errorData.message || `Moralis API error: ${response.status}`;

    console.error("Moralis API Error:", {
      status: response.status,
      url: url.toString(),
      error: errorData,
    });

    throw new Error(errorMessage);
  }

  return response.json();
}
```

### Create a react hook called `useTokenBalances`

This react hook calls the `/api/wallet/balances/` endpoint that you created and gets the token balances data, formats it into a proper list and makes it available to consume in the frontend.

```ts title="src/hooks/useTokenBalances.ts" theme={null}
"use client";

import { useQuery } from "@tanstack/react-query";

async function fetchTokenBalances(
  address: string,
) {
  const response = await fetch(
    `/api/wallet/balances?address=${address}`
  );

  if (!response.ok) {
    const error = await response.json().catch(() => ({}));
    throw new Error(error.error || "Failed to fetch token balances");
  }

  const data = await response.json();

  // Check if data exists
  if (!data) {
    return [];
  }

  // Handle both response formats: direct array or object with result property
  const tokens = Array.isArray(data) ? data : data.result;

  if (!tokens || !Array.isArray(tokens)) {
    return [];
  }

  // Transform Moralis data to our Token type
  return tokens.map((token) => {
    const balanceFormatted = parseFloat(
      formatTokenBalance(token.balance, token.decimals)
    );

    // Calculate USD value if we have price but not value
    const usdValue = token.usd_value !== undefined
      ? token.usd_value
      : token.usd_price !== undefined
        ? token.usd_price * balanceFormatted
        : undefined;

    return {
      address: token.token_address,
      name: token.name,
      symbol: token.symbol,
      logo: token.logo || token.thumbnail,
      decimals: token.decimals,
      balance: token.balance,
      balanceFormatted,
      usdPrice: token.usd_price,
      usdValue,
      isNative: token.native_token || false,
      isSpam: token.possible_spam,
      verified: token.verified_contract,
    };
  });
}

export function useTokenBalances(address?: string, enabled: boolean = true) {
  return useQuery({
    queryKey: ["tokenBalances", address],
    queryFn: () => fetchTokenBalances(address!),
    enabled: enabled && !!address,
    staleTime: 30000, // 30 seconds
  });
}

function formatTokenBalance(
  balance: string | number,
  decimals: number = 18
): string {
  const balanceNum = typeof balance === "string" ? parseFloat(balance) : balance;
  const divisor = Math.pow(10, decimals);
  const formattedBalance = balanceNum / divisor;

  // Show more decimals for small amounts
  if (formattedBalance < 0.01) {
    return formattedBalance.toFixed(6);
  } else if (formattedBalance < 1) {
    return formattedBalance.toFixed(4);
  } else {
    return formattedBalance.toFixed(2);
  }
}
```

We will later use this hook in the UI components

## Step 5: Creating the UI

### Create a `TokenRow` component

<img src="https://mintcdn.com/monadfoundation-40611fb6/5Mt9_Scj9fq4fC68/static/img/guides/moralis-api-guide/3.png?fit=max&auto=format&n=5Mt9_Scj9fq4fC68&q=85&s=9825048fa2647166a1ae4f83369ee584" alt="token-row" width="1942" height="576" data-path="static/img/guides/moralis-api-guide/3.png" />

`TokenRow` component will display token details like name, symbol, logo, usd price, amount of tokens in connected wallet and the usd value.

Create a file named `TokenRow.tsx` in the folder `src/components/portfolio/tokens/`

```tsx title="src/components/portfolio/tokens/TokenRow.tsx" theme={null}
import Image from "next/image";

export function TokenRow({ token }) {
  return (
    <div className="grid grid-cols-12 gap-4 px-6 py-4 hover:bg-muted/30 transition-colors">
      {/* Token Info */}
      <div className="col-span-4 flex items-center gap-3">
        {token.logo ? (
          <Image
            src={token.logo}
            alt={token.name}
            width={40}
            height={40}
            className="rounded-full"
            onError={(e) => {
              e.currentTarget.style.display = "none";
            }}
          />
        ) : (
          <div className="w-10 h-10 rounded-full bg-primary/10 flex items-center justify-center text-primary font-bold border border-primary/20">
            {token.symbol.charAt(0)}
          </div>
        )}
        <div className="min-w-0">
          <div className="flex items-center gap-2">
            <p className="font-semibold truncate">{token.symbol}</p>
          </div>
          <p className="text-sm text-muted-foreground truncate">
            {token.name}
          </p>
        </div>
      </div>

      {/* Price */}
      <div className="col-span-2 flex items-center justify-end">
        {token.usdPrice !== undefined && token.usdPrice > 0 ? (
          <p className="text-sm font-medium">
            ${formatCurrency(token.usdPrice, 4)}
          </p>
        ) : (
          <p className="text-sm text-muted-foreground">-</p>
        )}
      </div>

      {/* Amount */}
      <div className="col-span-3 flex items-center justify-end">
        <p className="font-medium">{formatCurrency(token.balanceFormatted, 2)}</p>
      </div>

      {/* USD Value */}
      <div className="col-span-3 flex items-center justify-end">
        {token.usdValue !== undefined && token.usdValue > 0 ? (
          <p className="font-semibold text-primary">
            {formatUSD(token.usdValue)}
          </p>
        ) : (
          <p className="text-sm text-muted-foreground">-</p>
        )}
      </div>
    </div>
  );
}

// Helper function to format values in the UI

function formatUSD(value: number): string {
  if (value === 0) return "$0.00";
  if (value < 0.01) return "< $0.01";
  if (value < 1) return `$${value.toFixed(4)}`;
  if (value < 1000) return `$${value.toFixed(2)}`;
  if (value < 1000000) return `$${(value / 1000).toFixed(2)}K`;
  return `$${(value / 1000000).toFixed(2)}M`;
}

function formatCurrency(value: number, decimals: number = 2): string {
  return value.toLocaleString("en-US", {
    minimumFractionDigits: decimals,
    maximumFractionDigits: decimals,
  });
}
```

### Create a `TokenList` component

<img src="https://mintcdn.com/monadfoundation-40611fb6/5Mt9_Scj9fq4fC68/static/img/guides/moralis-api-guide/4.png?fit=max&auto=format&n=5Mt9_Scj9fq4fC68&q=85&s=9d00b030ffd2450fb0a6d51cc10935a8" alt="token-list" width="1950" height="604" data-path="static/img/guides/moralis-api-guide/4.png" />

Create a `TokenList.tsx` file in `src/components/portfolio` folder and import the newly created `TokenRow.tsx` component in it.

```tsx title="src/components/portfolio/TokenList.tsx" theme={null}
import { TokenRow } from "./tokens/TokenRow";

export function TokenList({
  tokens,
  isLoading,
  error,
  showLowValueTokens = false,
}) {

  if (isLoading) {
    return (
      // Loading state
    );
  }

  if (error) {
    return (
      // Error state
    );
  }

  
  if (tokens.length === 0) {
    return (
        // Empty state 
    );
  }

  return (
    <div className="divide-y divide-border">
        {tokens.map((token) => (
            <TokenRow key={token.address} token={token} />
        ))}
    </div>
  );
}
```

### Create a `PortfolioDashboard` component

<img src="https://mintcdn.com/monadfoundation-40611fb6/5Mt9_Scj9fq4fC68/static/img/guides/moralis-api-guide/5.png?fit=max&auto=format&n=5Mt9_Scj9fq4fC68&q=85&s=0834bd930f73bf2e36dee0251350b5be" alt="dashboard" width="1994" height="846" data-path="static/img/guides/moralis-api-guide/5.png" />

The `PortfolioDashboard` component will display the `TokenList` and can be further modified to have more components presenting information that you would like your user to see.

```tsx title="src/components/portfolio/PortfolioDashboard.tsx" theme={null}
"use client";

import { useState, useEffect } from "react";
import { useAccount } from "wagmi";
import { useTokenBalances } from "@/hooks/useTokenBalances";
import { TokenList } from "./TokenList";

export function PortfolioDashboard() {
  const { address, isConnected } = useAccount();
  const [mounted, setMounted] = useState(false);

  useEffect(() => {
    setMounted(true);
  }, []);

  const {
    data: tokens = [],
    isLoading,
    error,
    refetch,
    isFetching,
  } = useTokenBalances(address, isConnected);

  // Prevent hydration mismatch - don't render until mounted
  if (!mounted) {
    return null;
  }

  // Not connected state
  if (!isConnected) {
    return (
       // UI for wallet not connected state
    );
  }

  return (
    <div className="space-y-8">
        <TokenList
            tokens={tokens}
            isLoading={isLoading}
            error={error?.message}
            showLowValueTokens={showLowValueTokens}
        />
    </div>
  );
}
```

<Tip>
  Check out the other [Moralis API endpoints](https://docs.moralis.com/web3-data-api/evm/api-reference) and create visual components for the same!
</Tip>

## Conclusion

In this guide you learned how to use Moralis API to get wallet token balances and presenting them in a UI.

It is highly recommended you check out the other API endpoints and add more features to this project you created!

You can find a complete portfolio dashboard project [here](https://github.com/monad-developers/portfolio-dashboard-example) for inspiration.

## Useful links

* [Live portfolio app](https://portfolio-dashboard-example.vercel.app)
* [Moralis API reference](https://docs.moralis.com/web3-data-api/evm/api-reference)
* [Portfolio dashboard project](https://github.com/monad-developers/portfolio-dashboard-example)
* [Reown AppKit Docs](https://docs.reown.com/appkit/next/core/installation)
