如何使用 Tokens API 研究 Solana 代币交易

QuickNode 发布于 2026-06-05 阅读 212

本文介绍了如何使用 Tokens API 构建一个 TypeScript CLI 工具,用于研究 Solana 上的任意代币。

概述

Solana 上的热门资产(如比特币或 xStocks)以多个竞争性的铸币形式存在,这些铸币来自不同的发行方、流动性池和信任等级。本指南将引导你使用 Tokens API 寻找任何代币的最佳铸币,比较每个链上变体的流动性和风险,并在投入资金之前检查实时市场状况。

注意:本指南仅供教育参考。Quicknode 不提供财务建议,也不认可任何交易策略。在做出任何投资决策前,请务必自行研究。

TL;DR

  • 构建一个 TypeScript CLI,使用 Assets API 研究任何 Solana 资产,接受股票代码或原始铸币地址
  • 将资产解析为其规范 ID,然后通过一次调用 variant-top-markets 列出每个链上变体及其顶级去中心化交易所 (DEX) 市场
  • 对变体进行评分和排名,找出活跃交易的池,并为每个变体提取风险等级

你将完成的任务

  • 设置一个 TypeScript 项目,并对 Tokens.xyz Assets API 进行身份验证
  • 将符号或铸币地址解析为规范资产
  • 根据流动性、交易量和风险,列出并比较该资产的每个链上变体
  • 使用执行质量评分对变体进行排名,并推荐最佳铸币
  • 读取实时市场数据,得出 GO(行动)或 WAIT(等待)时机信号

你需要准备

本指南假设你对 TypeScript、REST API 以及 Solana 代币(铸币地址、SPL 代币和 DEX 流动性池)有基本了解。

你还需要:

本指南使用以下依赖项:

依赖项 版本
chalk 5.3.0
tsx 4.19.0
typescript 5.5.0

什么是 Tokens API?

Tokens.xyz(“Solana 上的代币”)是一个针对 Solana 上链上资产的代币搜索和流动性聚合器,由 Solana 基金会作为公共产品构建和托管。它覆盖了大约 300 种规范资产,涵盖 包装代币、代币化股票、ETF、金属和大宗商品,并为每个链上表示形式提供实时价格、交易量、流动性和风险数据。

在编写代码之前,需要理解的核心概念是规范资产和变体之间的区别:

  • 一个 规范资产 有一个简洁的 slug,如 bitcoinusdtesla。它代表底层概念,无论其在链上如何持有。
  • 一个 变体 是一个特定的链上铸币,由 Solana 代币地址标识。一个规范资产可以有多个变体。例如,usd 解析为 USDC、USDT 和 PYUSD,每个都带有其 kind(如 nativewrappedbridgedstablecoin)、liquidityTiertier1 流动性最强,直至 tier3)、trustTierissuer

这种模型使本指南中的比较成为可能。你不必将 WBTC 和 cbBTC 当成两个不相关的地址,只需查询一次比特币,就能获得在 Solana 上持有它的所有方式,且已分组归类。

设置项目

创建一个项目目录并初始化:

mkdir tokens-research && cd tokens-research
npm init -y
npm install chalk
npm install -D tsx typescript @types/node

package.json 中设置 "type": "module",以便 ES Module 导入正确解析:

npm pkg set type=module

在项目根目录创建一个 .env 文件,包含你的 API 密钥:

.env

TOKENS_API_KEY=your_api_key_here

添加一个 .gitignore 文件,防止 .env 中的 API 密钥被意外提交:

.gitignore

node_modules/
.env

创建一个名为 research.ts 的文件。从导入语句、密钥和参数检查以及一个小型 get 辅助函数开始,该函数会在每个请求中附加 x-api-key 头。脚本接受一个参数,可以是像 BTC 这样的股票代码,也可以是原始的 Solana 铸币地址。

research.ts

import chalk from 'chalk';

const API_KEY = process.env.TOKENS_API_KEY;
if (!API_KEY) { console.error(chalk.red('TOKENS_API_KEY 未设置')); process.exit(1); }

const SYMBOL = process.argv[2];
if (!SYMBOL) { console.error(chalk.red('用法: tsx research.ts <symbol>  例如 tsx --env-file=.env research.ts BTC')); process.exit(1); }

const BASE = 'https://api.tokens.xyz/v1';

async function get<T = any>(path: string): Promise<T> {
  const res = await fetch(`${BASE}${path}`, { headers: { 'x-api-key': API_KEY! } });
  if (!res.ok) throw new Error(`${res.status} ${path}`);
  return res.json() as T;
}

接下来,在 get 函数下方添加一些格式化辅助函数。这些函数让终端输出更易读:usd 缩写美元金额,ago 将 Unix 时间戳转换为相对时间,section 打印章节标题,tierColorgradeColor 根据质量对文本着色。

research.ts

// 辅助函数

function usd(value: number | undefined | null): string {
  if (value == null) return 'n/a';
  if (value >= 1e9) return `$${(value / 1e9).toFixed(2)}B`;
  if (value >= 1e6) return `$${(value / 1e6).toFixed(2)}M`;
  if (value >= 1e3) return `$${(value / 1e3).toFixed(1)}K`;
  return `$${value.toFixed(2)}`;
}

function ago(timestamp: number | undefined | null): string {
  if (!timestamp) return '未知';
  const seconds = Math.floor(Date.now() / 1000) - timestamp;
  return seconds < 60 ? `${seconds}秒前` : seconds < 3600 ? `${Math.floor(seconds / 60)}分钟前` : `${Math.floor(seconds / 3600)}小时前`;
}

function section(title: string) {
  console.log('\n' + chalk.white.bold('━'.repeat(62)));
  console.log(chalk.white.bold(`  ${title}`));
  console.log(chalk.white.bold('━'.repeat(62)) + '\n');
}

function tierColor(tier: string | undefined, text: string): string {
  if (tier === 'tier1') return chalk.green(text);
  if (tier === 'tier2') return chalk.yellow(text);
  return chalk.red(text);
}

function gradeColor(grade: string | undefined): string {
  if (!grade) return chalk.gray('n/a');
  if (['A+', 'A', 'B+'].includes(grade)) return chalk.green(grade);
  if (grade === 'B') return chalk.yellow(grade);
  return chalk.red(grade);
}

脚本的其余部分位于一个 main 函数内部。以下每个部分都会按顺序向 main 函数体添加一个代码块。现在开始此函数并逐步填充:

research.ts

// main

async function main() {
  // 接下来各节中的代码块按顺序放在这里
}

main().catch(err => { console.error(chalk.red(err.message)); process.exit(1); });

解析规范资产

一切从规范的 assetId 开始。脚本接受两种输入,因此首先检查参数是否看起来像 base58 Solana 铸币地址。如果是,则调用 resolve 将铸币映射到其规范资产。否则调用 search 按名称或股票代码查找资产。

  • GET /assets/resolve?mint={address} 将原始铸币映射到其规范的 assetId,并返回资产的 namecategory
  • GET /assets/search?q={symbol} 将名称或股票代码解析为规范资产,并返回最佳匹配项。

将此代码作为 main 内部的第一个代码块添加:

research.ts

  // 解析规范资产(接受符号/名称或原始 Solana 铸币地址)
  const isMint = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(SYMBOL);
  let assetId: string, name: string, category: string;

  if (isMint) {
    const resolved = await get<any>(`/assets/resolve?mint=${SYMBOL}`);
    if (!resolved?.assetId) { console.error(chalk.red(`无法解析铸币 "${SYMBOL}"`)); process.exit(1); }
    assetId = resolved.assetId;
    name = resolved.asset?.name ?? assetId;
    category = resolved.asset?.category ?? 'crypto';
  } else {
    const search = await get(`/assets/search?q=${encodeURIComponent(SYMBOL)}&limit=3`);
    const hit = search.results?.[0];
    if (!hit) { console.error(chalk.red(`"${SYMBOL}" 没有结果`)); process.exit(1); }
    ({ assetId, name, category } = hit);
  }

  console.log(chalk.bold.white(`\n  Tokens API — Solana 上的 ${name}\n`));

此代码块运行后,你将获得一个 assetId(规范 slug)、一个人类可读的 name 和一个 category(例如 cryptostablecoinequity),后续步骤会复用它们。

发现链上选项

此步骤揭示了代币碎片化问题。一次调用 variant-top-markets 即可返回规范资产的每个链上变体,并在同一响应中附加每个变体的顶级 DEX 市场。这一调用替代了原本需要三到四个单独请求来获取变体列表、市场数据和池地址的操作。

GET /assets/{assetId}/variant-top-markets 每个变体返回:

  • mint:Solana 代币地址
  • symbolnamelabel:标识字段
  • kindliquidityTiertrustTierissuer:分类
  • topMarket.liquiditytopMarket.volume24h:池深度和 24 小时交易量
  • topMarket.source:DEX 名称
  • topMarket.name:交易对
  • topMarket.address:池地址
  • topMarket.trade24htopMarket.uniqueWallet24h:活动计数

main 函数内部添加下一个代码块。它获取变体,然后预先获取最多 10 个变体的风险等级(在下一节中使用),最后按流动性层级以及层级内的流动性进行排序:

research.ts

  // 在显示之前获取变体和风险评分,以便排序拥有所有数据
  const topMarketsRes = await get(`/assets/${assetId}/variant-top-markets?limit=50`);
  const rawVariants: any[] = topMarketsRes.variants ?? [];
  if (!rawVariants.length) { console.error(chalk.red('未找到链上变体')); process.exit(1); }

  type Risk = { score: number; grade: string; label: string; isTrustedLaunch: boolean; caps: any[] };
  const riskMap = new Map<string, Risk>();
  for (const variant of rawVariants.slice(0, 10)) {
    try {
      const risk = await get<any>(`/assets/risk-summary?mint=${variant.mint}`);
      riskMap.set(variant.mint, risk);
    } catch { /* 此变体无风险数据 */ }
  }

  const TIER_ORDER: Record<string, number> = { tier1: 0, tier2: 1, tier3: 2 };
  const variants: any[] = [...rawVariants].sort((a, b) => {
    const tierDiff = (TIER_ORDER[a.liquidityTier] ?? 3) - (TIER_ORDER[b.liquidityTier] ?? 3);
    if (tierDiff !== 0) return tierDiff;
    return (b.topMarket?.liquidity ?? 0) - (a.topMarket?.liquidity ?? 0);
  });

现在将变体打印为表格。turnover 列(24 小时交易量除以流动性)是哪些池被实际使用的第一个线索。高层级但周转率接近零是一个警告信号,你将在下一步中据此采取行动。

research.ts

  // 步骤 1
  section('步骤 1 — 发现链上选项');

  console.log(`  ${chalk.white(variants.length + ' 个链上表示' + (variants.length !== 1 ? 's' : '') + ' 的 ' + name)}\n`);

  const COL = [4, 10, 22, 8, 12, 12, 10, 0];
  const head = ['#', 'Symbol', 'Issuer', 'Tier', 'Liquidity', 'Vol 24h', 'Turnover', 'Top DEX'];
  console.log('  ' + head.map((h, i) => chalk.cyan(COL[i] ? h.padEnd(COL[i]) : h)).join(''));
  console.log('  ' + '─'.repeat(92));

  variants.forEach((variant, index) => {
    const topMarket = variant.topMarket;
    const liquidity = topMarket?.liquidity ?? 0;
    const volume24h = topMarket?.volume24h ?? 0;
    const turnover = liquidity > 0 ? volume24h / liquidity : 0;
    const turnoverColor = (text: string) => turnover > 5 ? chalk.red(text) : turnover > 3 ? chalk.yellow(text) : chalk.green(text);
    const tier = (variant.liquidityTier ?? 'n/a').padEnd(8);
    console.log(
      '  ' +
      String(index + 1).padEnd(4) +
      (variant.symbol ?? 'n/a').padEnd(10) +
      (variant.label ?? variant.issuer ?? 'unknown').slice(0, 20).padEnd(22) +
      tierColor(variant.liquidityTier, tier) +
      usd(liquidity).padEnd(12) +
      usd(volume24h).padEnd(12) +
      turnoverColor(`${turnover.toFixed(1)}x`.padEnd(10)) +
      (topMarket?.source ?? 'n/a')
    );
  });

比较变体

这是 API 组合数据的独特价值所在。脚本会打印每个变体的详细信息,包括风险等级,然后对其进行排名并推荐一个。

三个端点提供此步骤的数据:

  • GET /assets/risk-summary?mint={mint}(已在上方获取)返回一个 score(0 到 100,越高越安全)、一个字母 grade、一个人类可读的 label、一个 isTrustedLaunch 标志,以及一个 caps 数组,其中包含命名的风险因素,每个因素都有一个 tone,值为 successwarningdanger
  • GET /assets/{assetId}/risk-details?mint={mint} 增加了按因素分解的功能,每个因素都包含 valuebenchmark,因此可以解释为什么某个代币获得了该等级,而不仅仅是报告等级。
  • GET /assets/{assetId}/profile 返回规范资产的外部简介(websitetwittermarketCapsupply)。

首先,打印每个变体的详细信息。在 main 函数中添加此代码块:

research.ts

  // 步骤 2
  section('步骤 2 — 比较与选择');

  for (const variant of variants) {
    const topMarket = variant.topMarket;
    const risk = riskMap.get(variant.mint);
    const liquidity = topMarket?.liquidity ?? 0;
    const volume24h = topMarket?.volume24h ?? 0;
    const turnover = liquidity > 0 ? volume24h / liquidity : 0;
    const turnoverColor = (text: string) => turnover > 5 ? chalk.red(text) : turnover > 3 ? chalk.yellow(text) : chalk.green(text);
    const tierLabel = variant.liquidityTier ?? 'n/a';

    console.log(`  ${chalk.white.bold(variant.symbol ?? variant.mint.slice(0, 8))}  ${chalk.gray(variant.label ?? '')}  ${tierColor(variant.liquidityTier, tierLabel)}`);
    console.log(`  ${'发行方:'.padEnd(18)} ${variant.issuer ?? variant.label ?? 'unknown'}`);
    console.log(`  ${'铸币:'.padEnd(18)} ${chalk.gray(variant.mint)}`);
    console.log(`  ${'流动性:'.padEnd(18)} ${usd(liquidity)}`);
    console.log(`  ${'24小时交易量:'.padEnd(18)} ${usd(volume24h)}`);
    console.log(`  ${'周转率:'.padEnd(18)} ${turnoverColor(`${turnover.toFixed(1)}x`)}  ${turnover > 5 ? chalk.red('池承受压力') : turnover < 0.1 ? chalk.yellow('活动非常低') : chalk.green('健康')}`);
    if (topMarket?.trade24h != null)       console.log(`  ${'24小时交易数:'.padEnd(18)} ${topMarket.trade24h.toLocaleString()}`);
    if (topMarket?.uniqueWallet24h != null) console.log(`  ${'24小时钱包数:'.padEnd(18)} ${topMarket.uniqueWallet24h.toLocaleString()}`);
    if (risk) {
      console.log(`  ${'风险:'.padEnd(18)} ${gradeColor(risk.grade)}  评分 ${risk.score}/100  ${risk.isTrustedLaunch ? chalk.green('✓ 可信启动') : chalk.yellow('未验证')}`);
      for (const cap of risk.caps ?? []) {
        const capColor = cap.tone === 'success' ? chalk.green : cap.tone === 'warning' ? chalk.yellow : chalk.red;
        console.log(`  ${''.padEnd(20)}${capColor('▸')} ${cap.name}: ${capColor(cap.value)}`);
      }
    } else {
      console.log(`  ${'风险:'.padEnd(18)} ${chalk.gray('n/a')}`);
    }
    console.log(`  ${'顶级DEX:'.padEnd(18)} ${topMarket?.source ?? 'n/a'}  ${topMarket?.name ? chalk.gray(`(${topMarket.name})`) : ''}`);
    console.log();
  }

现在选择最佳选项。仅凭原始流动性会产生误导。脚本计算一个 执行分数,即 liquidity × min(turnover, 3),其中 turnover = volume24h / liquidity。将周转率上限设为 3 可以防止一个很小但交易极其频繁的池超越一个真正深度大的池,同时仍然奖励那些实际有成交量的池。

为什么这很重要?来看 API 返回的真实数据:

  • Solana 上的 WBTC:7591 万美元的流动性,但 24 小时成交量只有 0.44 美元(一笔交易,一个钱包)。执行分数约为 0.44。
  • Solana 上的 cbBTC:545 万美元的流动性,24 小时成交量达 1763 万美元(3.2 倍周转率,767 个钱包)。执行分数约为 1635 万。

cbBTC 尽管原始流动性少了 14 倍,但优势大约为 3700 万倍。在本例中,WBTC 的流动性只是纸面上的,但没有人交易它,这意味着在实践中价格过时且点差大。仅凭一个醒目的流动性数字就会指向更差的铸币。因为 API 同时返回 liquidityvolume24htrade24huniqueWallet24h,你可以一眼看穿它。

添加推荐代码块,该代码块按执行分数排序,并将获胜池链接到 DexScreener:

research.ts

  // 推荐:最高执行分数(按实际周转率加权的流动性)
  const execScore = (variant: any) => {
    const liquidity = variant.topMarket?.liquidity ?? 0;
    const volume24h = variant.topMarket?.volume24h ?? 0;
    const turnover = liquidity > 0 ? volume24h / liquidity : 0;
    return liquidity * Math.min(turnover, 3);
  };
  const ranked = [...variants].sort((a, b) => execScore(b) - execScore(a));
  const best = ranked[0];
  const runner = ranked[1];
  const bestRisk = riskMap.get(best.mint);
  const liqRatio = runner && (runner.topMarket?.liquidity ?? 0) > 0
    ? Math.round((best.topMarket?.liquidity ?? 0) / (runner.topMarket?.liquidity ?? 0))
    : null;

  console.log(chalk.green.bold(`  ✓ 推荐: ${best.symbol ?? best.mint.slice(0, 8)}`));
  if (liqRatio && liqRatio > 1) {
    const riskNote = bestRisk ? ` 和 ${bestRisk.grade ?? 'n/a'} 风险等级` : '';
    console.log(`  原因: 流动性比下一个选项多 ${liqRatio}x${riskNote}`);
  }
  console.log(`  DEX:    ${best.topMarket?.source ?? 'n/a'}`);
  if (best.topMarket?.address) {
    console.log(`  池:   ${chalk.cyan(`https://dexscreener.com/solana/${best.topMarket.address}`)}`);
  }

  const chosenMint: string = best.mint;

最后,为推荐的铸币获取详细的风险分解和资产简介。风险详情逐一解释等级的原因,简介则添加了外部背景信息。

research.ts

  // 推荐变体的风险详情(解释为何获得该等级)
  try {
    const detailsRes = await get<any>(`/assets/${assetId}/risk-details?mint=${chosenMint}`);
    const factors: any[] = detailsRes.factors ?? detailsRes.data?.factors ?? [];
    if (factors.length) {
      console.log(`\n  ${chalk.yellow('风险因素分解')}  ${chalk.gray('(为何获得此等级)')}`);
      for (const factor of factors) {
        const factorColor = (factor.score ?? 0) >= 70 ? chalk.green : (factor.score ?? 0) >= 40 ? chalk.yellow : chalk.red;
        const benchmark = factor.benchmark != null ? chalk.gray(` · 基准: ${factor.benchmark}`) : '';
        const value = factor.value != null ? chalk.gray(` · 值: ${factor.value}`) : '';
        console.log(`  ${factorColor((factor.name ?? '').padEnd(22))} 评分: ${factorColor(String(factor.score ?? 'n/a'))}${value}${benchmark}`);
        if (factor.description) console.log(`  ${''.padEnd(22)} ${chalk.gray(factor.description)}`);
      }
    }
  } catch { console.log(chalk.yellow('  风险因素分解不可用')); }

  // 简介(规范资产)。有意跳过描述:对于代币化股票,它是发行方的文案,而非对底层公司的描述。
  try {
    const profileRes = await get<any>(`/assets/${assetId}/profile`);
    const profile = profileRes.profile?.ok !== false ? (profileRes.profile?.data ?? null) : null;
    if (profile) {
      console.log(`\n  ${chalk.yellow('资产简介')}`);
      if (profile.website)     console.log(`  ${'网站:'.padEnd(14)} ${profile.website}`);
      if (profile.twitter)     console.log(`  ${'Twitter:'.padEnd(14)} ${profile.twitter}`);
      if (profile.marketCap)   console.log(`  ${'市值:'.padEnd(14)} ${usd(profile.marketCap)}`);
      if (profile.supply)      console.log(`  ${'供应量:'.padEnd(14)} ${Number(profile.supply).toLocaleString()}`);
    }
  } catch { console.log(chalk.yellow('  资产简介不可用')); }

检查市场状况

推荐告诉你使用哪个铸币。最后一步回答现在是否是使用它的好时机。它读取所选铸币的实时快照,基于新鲜度进行门控,衡量交易量加速情况,检查参与度,并与热门列表进行交叉引用,然后给出裁定结果。

两个端点驱动此操作:

  • GET /assets/{assetId}/variant-market?mint={mint} 返回一个实时缓存的快照:pricepriceChange1hPercentpriceChange24hPercentlastTradeAt(Unix 秒数)和一个 metricsSource。当 metricsSourceclickhouse_trades 时,你还会获得窗口化的交易量和计数(volume5mUSDvolume1hUSDvolume24hUSDtrade24huniqueWallet24h 等)。当来源为其他时,你只有 1 小时和 24 小时的粒度,因此脚本会相应地回退。
  • GET /assets/trending?category={category}&limit=50 是一个预先构建的动量复合排名,根据短窗口交易量加速和交易新鲜度对铸币进行排序。它作为交叉检查。

首先获取快照并应用新鲜度门控。如果最后一笔交易在 10 分钟以上,则该池已过时,裁定结果为 WAIT,因为下面的每个时机信号都是在死数据上计算的。在 main 函数中添加此代码块:

research.ts

  // 步骤 3
  section('步骤 3 — 检查市场状况');

  let snapshot: any;
  try {
    const snapshotRes = await get<any>(`/assets/${assetId}/variant-market?mint=${chosenMint}`);
    snapshot = snapshotRes.market ?? snapshotRes;
  } catch (err: any) {
    console.log(chalk.red(`  无法获取快照: ${err.message}`));
    return;
  }

  const ageSeconds = snapshot.lastTradeAt ? Math.floor(Date.now() / 1000) - snapshot.lastTradeAt : Infinity;
  const fresh = ageSeconds < 600;

  console.log(`  ${chalk.white.bold(best.symbol ?? chosenMint.slice(0, 8))}  ${chalk.gray(`${best.topMarket?.source ?? 'n/a'} · ${best.topMarket?.name ?? ''} · 最后一笔交易 ${ago(snapshot.lastTradeAt)}`)}\n`);
  console.log(`  ${'价格:'.padEnd(18)} $${snapshot.price?.toFixed(4) ?? 'n/a'}`);
  if (snapshot.priceChange1hPercent != null) {
    const change = snapshot.priceChange1hPercent;
    console.log(`  ${'1小时涨跌:'.padEnd(18)} ${change >= 0 ? chalk.green(`+${change.toFixed(2)}%`) : chalk.red(`${change.toFixed(2)}%`)}`);
  }
  if (snapshot.priceChange24hPercent != null) {
    const change = snapshot.priceChange24hPercent;
    console.log(`  ${'24小时涨跌:'.padEnd(18)} ${change >= 0 ? chalk.green(`+${change.toFixed(2)}%`) : chalk.red(`${change.toFixed(2)}%`)}`);
  }
  console.log();

  if (!fresh) {
    console.log(chalk.red(`  池已过时(最后一笔交易在 ${ago(snapshot.lastTradeAt)})— 时机信号不可靠\n`));
    console.log('  ' + '─'.repeat(56));
    console.log(`  ${chalk.red.bold('裁定: WAIT')}  —  池不活跃`);
    console.log('  ' + '─'.repeat(56) + '\n');
    return;
  }

接下来,衡量交易量加速。思路是将每个窗口归一化为每小时速率并进行比较。如果最近 5 分钟,投影到一小时(volume5mUSD × 12),远高于过去的 1 小时交易量,则活动正在加速。比率达到 1.5 倍或更高视为加速,低于 0.5 倍视为衰减,介于两者之间视为稳定。当更丰富的 clickhouse_trades 窗口不可用时,脚本会回退到将 1 小时速度与 24 小时基线进行比较。

research.ts

  // 交易量加速:将窗口归一化为每小时速率并进行比较
  let timingSignal = 'unknown';
  const hasWindowed = snapshot.metricsSource === 'clickhouse_trades' && snapshot.volume5mUSD != null;
  if (hasWindowed) {
    const pace5m = snapshot.volume1hUSD > 0 ? (snapshot.volume5mUSD * 12) / snapshot.volume1hUSD : 0;
    const pace1h = snapshot.volume24hUSD > 0 ? (snapshot.volume1hUSD * 24) / snapshot.volume24hUSD : 0;
    const trendColor = pace5m >= 1.5 ? chalk.green : pace5m < 0.5 ? chalk.red : chalk.yellow;
    const label = pace5m >= 1.5 ? '▲ 加速' : pace5m < 0.5 ? '▼ 衰减' : '→ 稳定';
    console.log(`  ${'交易量趋势:'.padEnd(18)} ${trendColor(label)}  ${chalk.gray(`(5分钟速度 ${pace5m.toFixed(1)}x 1小时平均 · 1小时速度 ${pace1h.toFixed(1)}x 日平均)`)}`);
    timingSignal = pace5m >= 1.5 ? 'accelerating' : pace5m < 0.5 ? 'fading' : 'steady';
  } else if (snapshot.volume1hUSD != null && snapshot.volume24hUSD > 0) {
    const pace1h = (snapshot.volume1hUSD * 24) / snapshot.volume24hUSD;
    const trendColor = pace1h >= 1.5 ? chalk.green : pace1h < 0.5 ? chalk.red : chalk.yellow;
    const label = pace1h >= 1.5 ? '▲ 加速' : pace1h < 0.5 ? '▼ 衰减' : '→ 稳定';
    console.log(`  ${'交易量趋势:'.padEnd(18)} ${trendColor(label)}  ${chalk.gray(`(1小时速度 ${pace1h.toFixed(1)}x 日平均)`)}`);
    timingSignal = pace1h >= 1.5 ? 'accelerating' : pace1h < 0.5 ? 'fading' : 'steady';
  } else {
    console.log(`  ${'交易量趋势:'.padEnd(18)} ${chalk.gray('n/a')}`);
  }

  // 参与度:每钱包交易数可以提示活动是自然的还是集中的
  let organicSignal = 'unknown';
  if (snapshot.trade24h && snapshot.uniqueWallet24h) {
    const tradesPerWallet = snapshot.trade24h / snapshot.uniqueWallet24h;
    const participationColor = tradesPerWallet <= 5 ? chalk.green : tradesPerWallet <= 15 ? chalk.yellow : chalk.red;
    const participationLabel = tradesPerWallet <= 5 ? '广泛' : tradesPerWallet <= 15 ? '混合' : '集中';
    console.log(`  ${'参与度:'.padEnd(18)} ${snapshot.uniqueWallet24h.toLocaleString()} 个钱包 · ${snapshot.trade24h.toLocaleString()} 笔交易  ${participationColor(`(${tradesPerWallet.toFixed(1)} 交易/钱包, ${participationLabel})`)}`);
    organicSignal = participationLabel;
  } else if (snapshot.uniqueWallet24h) {
    console.log(`  ${'参与度:'.padEnd(18)} ${snapshot.uniqueWallet24h.toLocaleString()} 个唯一钱包(24小时)`);
  }

参与度检查将 24 小时交易数除以 24 小时唯一钱包数。比率低(每个钱包 5 笔或更少)表明广泛、自然的活动。比率高(超过 15)表明少数地址(可能是机器人)完成了大部分交易。

最后,与热门列表进行交叉检查,并将所有信息综合成一个裁定结果。当池是新鲜的、没有衰减趋势且交易分散时,结果为 GO,否则为 WAIT,并且始终附带具体原因。

research.ts

  // 热门交叉检查:该资产在其类别中是否位列前 50?
  let trendRank: number | null = null;
  try {
    const trendRes = await get<any>(`/assets/trending?category=${category}&limit=50`);
    const found = trendRes.trending?.find((item: any) => item.assetId === assetId || item.mint === chosenMint);
    if (found) {
      trendRank = found.rank;
      const rankColor = found.rank <= 10 ? chalk.green : found.rank <= 25 ? chalk.yellow : chalk.white;
      console.log(`  ${'热门:'.padEnd(18)} ${rankColor(`#${found.rank}`)}  ${chalk.gray(`评分 ${found.trending?.score?.toFixed(1) ?? 'n/a'}`)}`);
    } else {
      console.log(`  ${'热门:'.padEnd(18)} ${chalk.gray('不在前 50 名')}`);
    }
  } catch { console.log(`  ${'热门:'.padEnd(18)} ${chalk.yellow('不可用')}`); }

  // 裁定:如果新鲜 + 未衰减 + 未集中,则为 GO
  const go = fresh && timingSignal !== 'fading' && organicSignal !== 'concentrated';
  const reason =
    timingSignal === 'accelerating' && organicSignal === 'broad' ? '强劲动量,参与广泛' :
    timingSignal === 'accelerating' ? '交易量加速;进入前请验证参与度' :
    timingSignal === 'fading' ? '活动正在下降;等待更好时机' :
    organicSignal === 'concentrated' ? '交易活动集中;可能存在机器人' :
    trendRank != null && trendRank <= 10 ? '热门代币,市场活跃' :
    '市场活跃,无异常信号';

  console.log('\n  ' + '─'.repeat(56));
  console.log(`  ${go ? chalk.green.bold('裁定: GO') : chalk.red.bold('裁定: WAIT')}  —  ${reason}`);
  console.log('  ' + '─'.repeat(56) + '\n');

至此,main 函数体结束。确保设置部分最后的 main().catch(...) 行仍在文件底部。

运行脚本

使用股票代码运行工具。--env-file 标志加载你的 .env 文件,无需额外包:

npx tsx --env-file=.env research.ts BTC

或者传递原始铸币地址,脚本将首先将其解析为规范资产:

npx tsx --env-file=.env research.ts cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij

你将看到所有三个步骤依次打印:链上选项列表、每个变体的比较(带有推荐和 DexScreener 池链接),以及以 GO 或 WAIT 裁定结尾的时机分析。

  Tokens API — Solana 上的 Bitcoin

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  步骤 1 — 发现链上选项
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  8 个链上表示的 Bitcoin

  #   Symbol    Issuer                Tier    Liquidity   Vol 24h     Turnover  Top DEX
  ────────────────────────────────────────────────────────────────────────────────────────────
  1   cbBTC     cbBTC                 tier1   $5.54M      $17.06M     3.1x      Orca
  2   WBTC      unknown               tier2   $75.91M     $0.44       0.0x      Orca
  3   xBTC      unknown               tier3   $2.22M      $504.7K     0.2x      Orca
  4   zenBTC    unknown               tier3   $963.3K     $39.0K      0.0x      Orca
  5   zBTC      zBTC                  tier3   $763.2K     $63.2K      0.1x      Saber
  6   tBTC      unknown               tier3   $143.5K     $0.00       0.0x      Orca
  7   WBTC      unknown               tier3   $48.2K      $6.4K       0.1x      Raydium Clamm
  8   21BTC     unknown               tier3   $1.9K       $0.00       0.0x      Cropper

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  步骤 2 — 比较与选择
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  cbBTC  cbBTC  tier1
  发行方:            cbBTC
  铸币:              cbbtcf3aa273Sn1dMGVU3ZtLv9yWSyUAanBni19YWDaznnkn
  流动性:            $5.54M
  24小时交易量:      $17.06M
  周转率:            3.1x  健康
  24小时交易数:      4,811
  24小时钱包数:      767
  风险:              A  评分 86/100  未验证
  顶级DEX:           Orca  (SOL-cbBTC)

  ...(其余 7 个变体的详细信息)...

  ✓ 推荐: cbBTC
  原因: 流动性比下一个选项多 3x 和 A 风险等级
  DEX:    Orca
  池:   https://dexscreener.com/solana/CeaZcxBNLpJWtxzt58qQmfMBtJY8pQLvursXTJYGQpbN

  资产简介
  市值:    $1251.60B

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  步骤 3 — 检查市场状况
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

  cbBTC  Orca · SOL-cbBTC · 最后一笔交易在 17分钟前

  价格:             $63854.1696
  1小时涨跌:         -0.09%
  24小时涨跌:        -3.32%

  池已过时(最后一笔交易在 17分钟前)— 时机信号不可靠

  ────────────────────────────────────────────────────────
  裁定: WAIT  —  池不活跃
  ────────────────────────────────────────────────────────

总结

你已经构建了一个研究工具,可以将单个股票代码或铸币地址转化为实际决策。它将资产解析为其规范 ID,在一次调用中列出每个链上变体及其市场,通过一个能看穿误导性流动性数字的执行质量评分对它们进行排名,评估其风险,并读取实时活动来判断现在是否是采取行动的好时机。

在 Solana 上交易碎片化资产时,最困难的部分是知道在多个铸币中该用哪一个。你现在拥有一个可重复的、基于 API 的工作流程,用于在承诺任何铸币之前研究 Solana 代币交易。在此基础上,你可以将这些信号连接到仪表板、投资组合跟踪器或交易机器人的前端。

后续步骤

示例脚本使用了五个端点,但 Assets API 还有更多功能。以下是一些扩展思路:

  • 添加价格图表。 使用 GET /assets/{assetId}/ohlcv?mint={mint}&interval={interval}(间隔从 1m1W)为推荐的变体渲染蜡烛图历史,或使用 GET /assets/{assetId}/price-chart 获取规范价格视图,该视图会自动选择最佳可用来源。
  • 批量估价钱包持仓。Quicknode Solana 端点(用于读取钱包的代币账户)与 POST /assets/market-snapshots(每次调用最多 250 个铸币)配对,以 获取 Solana 钱包持有的所有代币账户,并在一次往返中将每个铸币映射到其规范资产。
  • 构建观察列表。 使用 GET /assets/curated(按 24 小时交易量排序,使用 groupBy=assetgroupBy=mint)进行种子填充,然后使用 GET /assets/variant-markets?mints={csv}(最多 50 个铸币)刷新价格,该调用还会将每个铸币解析为其规范的 assetId
  • 比较中心化和去中心化定价。 使用 GET /assets/{assetId}/tickers 拉取交易所级别的 tickers(交易所、交易对、最新价格、交易量、买价、卖价),并在链上数据旁边显示滑点或套利背景信息。
  • 添加重试逻辑。 脚本中的 get 辅助函数在失败时不会重试。对于生产环境,将 fetch 调用包装在重试循环中,使用带有抖动的指数退避(base = 250 * 2 ** attempt 毫秒,最多 150 毫秒抖动,四次尝试),并积极缓存响应,因为大多数字段每几秒才变化一次。

常见问题

Tokens API 中规范资产和变体有什么区别?

规范资产是由简洁 slug(如 bitcoin、usd 或 tesla)标识的底层概念。变体是代表该资产的特定链上铸币(Solana 代币地址),例如比特币的 WBTC 或 cbBTC。一个规范资产可以有多个变体,每个都有其自己的发行方、流动性层级、信任层级和种类。规范模型让你只需查询一次资产,就能获得在链上持有它的所有方式,并且已经分组。

为什么脚本按执行分数而不是流动性对变体进行排名?

原始流动性可能具有误导性。一个池可能持有数千万美元的流动性,却几乎没有交易,这意味着在实践中价格过时且点差大。执行分数(流动性乘以 min(turnover, 3),其中周转率是 24 小时交易量除以流动性)会奖励那些实际上被使用的池。在测试中,cbBTC 尽管原始流动性少了 14 倍,但优势约为 3700 万倍,因为 WBTC 的深度池在 24 小时内只有一笔交易。

Tokens.xyz 会执行交易或托管资金吗?

不会。Tokens.xyz 是只读的。它仅处理发现、定价和风险数据。要对推荐采取行动(读取钱包余额、构建交换或发送交易),你需要使用 Solana RPC 端点,例如来自 Quicknode 的端点。API 提供研究;执行在链上通过独立基础设施进行。

为什么某些代币返回的时机数据比其他代币更丰富?

详细的 5 分钟和 15 分钟窗口化交易量和钱包计数仅在变体市场快照报告 metricsSource 为 clickhouse_trades 时才存在。对于来自 Birdeye 等提供商的资产,你只能获得 1 小时和 24 小时的粒度。脚本会检测这一点,并回退到将 1 小时速度与 24 小时基线进行比较,以便时机裁定仍然有效。

资源

我们 ❤️ 反馈!

请告诉我们 如果你有任何反馈或对新主题的建议。我们期待你的意见。

  • 原文链接: quicknode.com/guides/sol...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~

相关文章

0 条评论