LogoEKX.AI
  • 趋势
  • 回测
  • 扫描器
  • 功能
  • 价格
  • 博客
  • Reports
  • 联系我们
周末动手:零后端构建多链加密货币投资组合追踪器
2025/12/18

周末动手:零后端构建多链加密货币投资组合追踪器

用Next.js和免费API构建自己的统一加密货币仪表板,无需后端服务器。完整实战教程含代码示例。

我在Ethereum上有代币。Arbitrum上也有一些。Polygon上还有几个NFT。每天早上我要打开四个不同的区块浏览器才能看清我到底持有什么。这很繁琐,容易出错,而且完全没必要。

上周末我构建了一个统一的投资组合追踪器,它可以从多条链拉取余额,计算总USD价值,并在一个干净的仪表板上显示所有内容。没有后端服务器。没有数据库。只是一个调用免费API的Next.js应用,部署到Vercel零成本。

整个项目在周六和周日花了大约12小时。如果你会写基础的React,你也可以做到。

这篇教程将完整介绍我是如何构建它的。不是那种一切都完美运行的美化版本。而是真实版本,包括我发现的API怪癖、速率限制的坑,以及真正重要的架构决策。

多链投资组合追踪器架构Next.js 前端EthereumArbitrumPolygonBSCAvalanche免费API(无后端)Covalent • CoinGecko • Moralis • Alchemy

为什么要自己构建而不是使用现有工具

市面上已经有几十个投资组合追踪器了。Zerion、Zapper、DeBank。它们都很好。我也用。但它们有一些局限性促使我自己构建。

首先是隐私。每次你将钱包连接到第三方追踪器,你就是在信任他们掌握你完整的财务档案。他们确切知道你持有什么,什么时候买的,以及价值多少。其中一些服务会将这些数据变现。

其次是定制化。我想要一些不存在的特定功能。当某个仓位跌破阈值时的实时警报。按策略自定义资产分组。与我自己的交易信号集成。没有现有工具能完全满足我的需求。

第三是学习。自己构建东西比任何教程都能让你学到更多。你会发现边缘情况,理解API限制,并培养出单纯阅读文档无法获得的区块链数据直觉。

我构建的追踪器完全在浏览器中运行。你的钱包地址永远不会触及我的服务器,因为根本没有服务器。所有事情都在客户端发生,API密钥存储在环境变量中,由Vercel在构建时注入。

真正有效的技术栈

尝试了几种组合之后,以下是效果最好的。

Next.js 14 配合App Router。服务器组件让你可以在不向客户端暴露API密钥的情况下获取数据。内置缓存优雅地处理速率限制。而Vercel部署只需一键。

Tailwind CSS 用于样式。周末项目没时间浪费在CSS架构上。Tailwind让你快速移动,结果看起来也很专业。

Covalent API 用于多链余额查询。这是关键发现。Covalent提供了一个统一的API,可以在100多条链上工作。一个API调用返回特定链上某个地址的所有代币余额。免费层每月给你100,000积分,对个人使用来说足够了。

CoinGecko API 用于价格数据。免费层,基础端点不需要API密钥。根据端点不同,每分钟限制10-30次调用,但对于每几分钟刷新一次的投资组合追踪器来说足够了。

React Query 用于数据获取和缓存。自动处理加载状态、错误状态和后台重新获取。又少了一件需要从头构建的事情。

API对比:该用哪个Covalent✓ 100+链统一API✓ 代币余额+元数据✓ 包含NFT支持✓ 每月10万免费积分✗ 价格数据有延迟✗ 部分链响应慢最适合:余额查询CoinGecko✓ 实时价格数据✓ 基础功能无需API密钥✓ 历史价格图表✓ 市值排名✗ 无钱包余额数据✗ 有速率限制(10-30/分)最适合:价格查询Moralis✓ 钱包组合端点✓ 包含USD价值✓ 包含DeFi仓位✓ 4万免费计算单位✗ 链支持有限✗ 定价层级复杂最适合:快速MVP

项目设置:第一小时

从Create Next App开始。使用App Router和TypeScript。如果你喜欢更扁平的结构,可以跳过src目录。

npx create-next-app@latest crypto-tracker --typescript --tailwind --app
cd crypto-tracker

安装你需要的依赖。

npm install @tanstack/react-query axios

为API密钥创建环境文件。你需要一个Covalent API密钥,可以在covalenthq.com免费获取。

COVALENT_API_KEY=your_key_here
NEXT_PUBLIC_SUPPORTED_CHAINS=1,137,42161,56,43114

链ID分别代表Ethereum (1)、Polygon (137)、Arbitrum (42161)、BSC (56)和Avalanche (43114)。根据你使用的链来增减。

构建核心数据层

架构有三层。定义支持网络的链配置。查询Covalent的余额获取器。以及用CoinGecko的USD价值来丰富余额的价格聚合器。

这是链配置。这个文件定义了应用需要了解的关于每条支持链的所有信息。

// lib/chains.ts
export const CHAINS = {
  1: {
    name: 'Ethereum',
    symbol: 'ETH',
    color: '#627EEA',
    covalentChainId: 'eth-mainnet',
    coingeckoId: 'ethereum',
    explorer: 'https://etherscan.io'
  },
  137: {
    name: 'Polygon',
    symbol: 'MATIC',
    color: '#8247E5',
    covalentChainId: 'matic-mainnet',
    coingeckoId: 'polygon-pos',
    explorer: 'https://polygonscan.com'
  },
  42161: {
    name: 'Arbitrum',
    symbol: 'ETH',
    color: '#12AAFF',
    covalentChainId: 'arbitrum-mainnet',
    coingeckoId: 'arbitrum-one',
    explorer: 'https://arbiscan.io'
  },
  56: {
    name: 'BSC',
    symbol: 'BNB',
    color: '#F0B90B',
    covalentChainId: 'bsc-mainnet',
    coingeckoId: 'binance-smart-chain',
    explorer: 'https://bscscan.com'
  },
  43114: {
    name: 'Avalanche',
    symbol: 'AVAX',
    color: '#E84142',
    covalentChainId: 'avalanche-mainnet',
    coingeckoId: 'avalanche',
    explorer: 'https://snowtrace.io'
  }
} as const;

export type ChainId = keyof typeof CHAINS;

余额获取器是一个查询Covalent的服务器操作。服务器操作在这里很完美,因为它们在服务器上运行,保持你的API密钥隐藏。

// app/actions/balances.ts
'use server'

import { CHAINS, ChainId } from '@/lib/chains';

interface TokenBalance {
  contract_address: string;
  contract_name: string;
  contract_ticker_symbol: string;
  contract_decimals: number;
  balance: string;
  quote: number;
  logo_url: string;
}

interface ChainBalances {
  chainId: ChainId;
  chainName: string;
  tokens: TokenBalance[];
  totalUsd: number;
}

export async function getBalancesForChain(
  address: string,
  chainId: ChainId
): Promise<ChainBalances> {
  const chain = CHAINS[chainId];
  const apiKey = process.env.COVALENT_API_KEY;

  const response = await fetch(
    `https://api.covalenthq.com/v1/${chain.covalentChainId}/address/${address}/balances_v2/?key=${apiKey}`,
    { next: { revalidate: 60 } }
  );

  if (!response.ok) {
    throw new Error(`Failed to fetch balances for ${chain.name}`);
  }

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

  const tokens = items
    .filter((item: any) => parseFloat(item.balance) > 0)
    .map((item: any) => ({
      contract_address: item.contract_address,
      contract_name: item.contract_name,
      contract_ticker_symbol: item.contract_ticker_symbol,
      contract_decimals: item.contract_decimals,
      balance: item.balance,
      quote: item.quote || 0,
      logo_url: item.logo_url
    }));

  const totalUsd = tokens.reduce((sum: number, t: TokenBalance) => sum + t.quote, 0);

  return {
    chainId,
    chainName: chain.name,
    tokens,
    totalUsd
  };
}

export async function getAllBalances(address: string): Promise<ChainBalances[]> {
  const chainIds = Object.keys(CHAINS).map(Number) as ChainId[];

  const results = await Promise.allSettled(
    chainIds.map(chainId => getBalancesForChain(address, chainId))
  );

  return results
    .filter((r): r is PromiseFulfilledResult<ChainBalances> => r.status === 'fulfilled')
    .map(r => r.value);
}

注意使用Promise.allSettled而不是Promise.all。如果某条链的API调用失败,我们仍然可以从其他链获取结果。这比你想象的更重要。区块链API不稳定。单独的链端点会随机宕机。你的应用应该优雅地处理部分失败。

next: { revalidate: 60 }选项告诉Next.js缓存响应60秒。这可以防止用户反复刷新时对API的频繁请求。

数据流:从钱包到仪表板用户输入0x1234...abcd服务器操作getAllBalances()Covalent ETHCovalent PolygonCovalent ArbitrumCovalent BSCPromise.allSettled() - 并行请求聚合过滤 + 排序 + 汇总仪表板UI响应结构chainId: 1tokens: [ETH, USDC, ...]totalUsd: 12,345.67

构建仪表板组件

仪表板组件是所有内容汇聚的地方。它接收聚合的余额数据,并以真正有用的方式呈现。

// components/portfolio-dashboard.tsx
'use client'

import { useState } from 'react';
import { useQuery } from '@tanstack/react-query';
import { getAllBalances } from '@/app/actions/balances';
import { CHAINS, ChainId } from '@/lib/chains';

function formatUsd(value: number): string {
  return new Intl.NumberFormat('en-US', {
    style: 'currency',
    currency: 'USD',
    minimumFractionDigits: 2,
    maximumFractionDigits: 2
  }).format(value);
}

function formatTokenBalance(balance: string, decimals: number): string {
  const value = parseFloat(balance) / Math.pow(10, decimals);
  if (value < 0.0001) return '<0.0001';
  if (value < 1) return value.toFixed(4);
  if (value < 1000) return value.toFixed(2);
  return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
}

export function PortfolioDashboard({ address }: { address: string }) {
  const { data, isLoading, error, refetch } = useQuery({
    queryKey: ['balances', address],
    queryFn: () => getAllBalances(address),
    staleTime: 60 * 1000,
    refetchInterval: 5 * 60 * 1000
  });

  if (isLoading) {
    return (
      <div className="flex items-center justify-center h-64">
        <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-500" />
      </div>
    );
  }

  if (error) {
    return (
      <div className="bg-red-500/10 border border-red-500/20 rounded-lg p-4">
        <p className="text-red-400">加载投资组合数据失败</p>
        <button
          onClick={() => refetch()}
          className="mt-2 text-sm text-red-300 underline"
        >
          重试
        </button>
      </div>
    );
  }

  const totalPortfolioValue = data?.reduce((sum, chain) => sum + chain.totalUsd, 0) || 0;
  const sortedChains = [...(data || [])].sort((a, b) => b.totalUsd - a.totalUsd);

  return (
    <div className="space-y-6">
      <div className="bg-gradient-to-r from-purple-500/20 to-cyan-500/20 rounded-xl p-6">
        <p className="text-gray-400 text-sm">总投资组合价值</p>
        <p className="text-4xl font-bold text-white mt-1">
          {formatUsd(totalPortfolioValue)}
        </p>
        <p className="text-gray-500 text-sm mt-2">
          跨{sortedChains.length}条链 • 刚刚更新
        </p>
      </div>

      <div className="grid gap-4">
        {sortedChains.map(chain => (
          <ChainCard key={chain.chainId} chain={chain} />
        ))}
      </div>
    </div>
  );
}

组件使用React Query的staleTime和refetchInterval来平衡新鲜度和API使用量。数据在60秒内被认为是新鲜的,并在后台每5分钟自动重新获取。

部署到Vercel:最后一步

部署是最简单的部分。将代码推送到GitHub,然后将仓库连接到Vercel。

唯一需要的配置是在Vercel仪表板中设置环境变量。添加COVALENT_API_KEY和你的密钥。Vercel在构建时注入这些,所以它们永远不会暴露给客户端。

git init
git add .
git commit -m "Initial commit"
git remote add origin https://github.com/yourusername/crypto-tracker.git
git push -u origin main

在Vercel中,点击"New Project",从GitHub导入,然后部署。就这样。你的投资组合追踪器上线了。

值得添加的高级功能

基本的追踪器可以工作了,但还有改进空间。以下是我在接下来几周添加的功能。

ENS解析让用户输入名称而不是地址。viem库优雅地处理这个问题。

import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';

const client = createPublicClient({
  chain: mainnet,
  transport: http()
});

async function resolveAddress(input: string): Promise<string> {
  if (input.endsWith('.eth')) {
    const resolved = await client.getEnsAddress({ name: input });
    if (!resolved) throw new Error('未找到ENS名称');
    return resolved;
  }
  return input;
}

投资组合历史随时间追踪价值。在localStorage或IndexedDB中存储快照,然后用Recharts等库渲染图表。

价格警报在仓位越过阈值时通知你。这需要某种持久化和通知机制。Web Push API用于浏览器通知,或者你可以集成Telegram或Discord webhooks。

DeFi仓位追踪比较棘手。像Aave、Compound和Uniswap这样的协议有自定义ABI。你需要直接查询它们的合约才能获得准确的仓位数据。Moralis和DeBank API提供这个,但免费层有限制。

超越基础追踪:发现早期机会

投资组合追踪器告诉你拥有什么。但在加密货币中,你还没拥有的东西往往更重要。正在上涨的币,积累异常交易量的代币,价格变动前发生的聪明钱流动。

这就是专用扫描工具发挥作用的地方。EKX.AI的Trending Scanner同时监控多条链的链上活动模式。它检测异常钱包行为、流动性变化和历史上在重大价格变动之前出现的积累模式。

扫描器不会告诉你该买什么。但它会浮现你在区块浏览器上手动滚动永远不会发现的信号。早期信号随时间复利。周一发现的模式可能会影响你周三建立的仓位。

构建自己的投资组合追踪器是理解区块链数据的第一步。下一步是知道要寻找什么。自动信号检测工具通过浮现你可能错过的机会来补充手动追踪。

常见陷阱及解决方法

在构建这个并与他人分享之后,某些问题反复出现。

速率限制比你预期的更严重。CoinGecko的免费层每分钟大约允许30个请求。如果你的仪表板在加载时查询50个代币的价格,你会被限流。解决办法是激进的缓存和尽可能批量请求。

陈旧价格让用户困惑。Covalent的价格数据可能延迟数小时。为了获得准确的价值,从CoinGecko单独获取价格并按合约地址匹配。这增加了复杂性但给出更好的结果。

缺失代币发生在新的或冷门的代币不在Covalent数据库中时。它们会显示余额但没有价格数据。考虑在API返回不完整数据时回退到合约调用获取代币元数据。

链重组可能导致临时不一致。交易显示,然后消失,然后再次出现。在应用层面你对此无能为力。只需意识到这会发生,当余额闪烁时不要恐慌。

构建这个我学到了什么

最有价值的教训不是技术性的。而是理解区块链数据实际上是如何流经基础设施栈的。

当你向Covalent查询余额时,它们并不是在实时查询区块链。它们运行索引器处理区块并将数据存储在传统数据库中。你的API调用命中它们的数据库。这就是为什么数据可能延迟,以及为什么不同的提供商对同一地址显示不同的值。

理解这一点改变了你对构建加密应用的思考方式。你不是在从单一真相来源读取。你是在从某人对区块链状态的解释中读取。多个数据源有助于三角定位准确性。

这个周末项目变成了我每天都在使用的东西。更重要的是,它给了我区块链数据的直觉,这是我用任何其他方式都无法获得的。

如果你构建东西,你就理解东西。如果你只是使用别人构建的工具,你总是依赖于他们的选择和限制。这个追踪器教会了我关于多链加密基础设施的东西,比几个月阅读文档学到的还多。

多链投资组合追踪器封面

构建你自己的追踪器。发现你自己的边缘情况。那才是真正学习发生的地方。

Next.js 开发

想要更全面的市场分析?查看 信号预览,试用 完整扫描器,并查看 定价。

Ready to test signals with real data?

Start scanning trend-oversold signals now

See live market signals, validate ideas, and track performance with EKX.AI.

Open ScannerView Pricing
全部文章

作者

avatar for Jimmy Su
Jimmy Su

分类

    为什么要自己构建而不是使用现有工具真正有效的技术栈项目设置:第一小时构建核心数据层构建仪表板组件部署到Vercel:最后一步值得添加的高级功能超越基础追踪:发现早期机会常见陷阱及解决方法构建这个我学到了什么

    更多文章

    如何自动化检测公允价值缺口(FVG)实现更智能的交易

    如何自动化检测公允价值缺口(FVG)实现更智能的交易

    用Python自动化FVG检测,从数据获取到可视化和回测。告别手动画水平线,构建你自己的SMC工具。

    avatar for Jimmy Su
    Jimmy Su
    2025/12/18
    Time-to-Peak Distribution: What It Means for Exits
    产品

    Time-to-Peak Distribution: What It Means for Exits

    Analyze how the distribution of time-to-peak metrics influences crypto exit strategies. Learn to identify the narrow profit windows in Bitcoin and altcoins.

    avatar for Jimmy Su
    Jimmy Su
    2026/01/06
    手动安装
    公司产品

    手动安装

    从零开始创建一个新的 Fumadocs 项目

    avatar for Jimmy Su
    Jimmy Su
    2025/03/14

    邮件列表

    加入我们的社区

    订阅邮件列表,及时获取最新消息和更新

    LogoEKX.AI

    AI 比大众更早发现趋势资产

    TwitterX (Twitter)Email
    产品
    • 趋势
    • 回测
    • 扫描器
    • 功能
    • 价格
    • 常见问题
    资源
    • 博客
    • Reports
    • 方法论
    公司
    • 关于我们
    • 联系我们
    法律
    • Cookie政策
    • 隐私政策
    • 服务条款
    © 2026 EKX.AI. All Rights Reserved.|Traded as Linkup Ai., Co Ltd