如何使用 Tokens API 研究 Solana 代币交易
本文介绍了如何使用 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 流动性池)有基本了解。
你还需要:
- Node.js v22 或更高版本
- 一个 Tokens API 密钥。登录 ( https://app.tokens.xyz/) 创建一个
- (可选)一个 Quicknode 账户,带有 Solana 主网端点,以便扩展此工具用于执行交易
本指南使用以下依赖项:
| 依赖项 | 版本 |
|---|---|
| chalk | 5.3.0 |
| tsx | 4.19.0 |
| typescript | 5.5.0 |
什么是 Tokens API?
Tokens.xyz(“Solana 上的代币”)是一个针对 Solana 上链上资产的代币搜索和流动性聚合器,由 Solana 基金会作为公共产品构建和托管。它覆盖了大约 300 种规范资产,涵盖 包装代币、代币化股票、ETF、金属和大宗商品,并为每个链上表示形式提供实时价格、交易量、流动性和风险数据。
在编写代码之前,需要理解的核心概念是规范资产和变体之间的区别:
- 一个 规范资产 有一个简洁的 slug,如
bitcoin、usd或tesla。它代表底层概念,无论其在链上如何持有。 - 一个 变体 是一个特定的链上铸币,由 Solana 代币地址标识。一个规范资产可以有多个变体。例如,
usd解析为 USDC、USDT 和 PYUSD,每个都带有其kind(如native、wrapped、bridged或stablecoin)、liquidityTier(tier1流动性最强,直至tier3)、trustTier和issuer。
这种模型使本指南中的比较成为可能。你不必将 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 打印章节标题,tierColor 和 gradeColor 根据质量对文本着色。
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,并返回资产的name和category。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(例如 crypto、stablecoin 或 equity),后续步骤会复用它们。
发现链上选项
此步骤揭示了代币碎片化问题。一次调用 variant-top-markets 即可返回规范资产的每个链上变体,并在同一响应中附加每个变体的顶级 DEX 市场。这一调用替代了原本需要三到四个单独请求来获取变体列表、市场数据和池地址的操作。
GET /assets/{assetId}/variant-top-markets 每个变体返回:
mint:Solana 代币地址symbol、name、label:标识字段kind、liquidityTier、trustTier、issuer:分类topMarket.liquidity、topMarket.volume24h:池深度和 24 小时交易量topMarket.source:DEX 名称topMarket.name:交易对topMarket.address:池地址topMarket.trade24h、topMarket.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,值为success、warning或danger。GET /assets/{assetId}/risk-details?mint={mint}增加了按因素分解的功能,每个因素都包含value和benchmark,因此可以解释为什么某个代币获得了该等级,而不仅仅是报告等级。GET /assets/{assetId}/profile返回规范资产的外部简介(website、twitter、marketCap、supply)。
首先,打印每个变体的详细信息。在 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 同时返回 liquidity、volume24h、trade24h 和 uniqueWallet24h,你可以一眼看穿它。
添加推荐代码块,该代码块按执行分数排序,并将获胜池链接到 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}返回一个实时缓存的快照:price、priceChange1hPercent、priceChange24hPercent、lastTradeAt(Unix 秒数)和一个metricsSource。当metricsSource为clickhouse_trades时,你还会获得窗口化的交易量和计数(volume5mUSD、volume1hUSD、volume24hUSD、trade24h、uniqueWallet24h等)。当来源为其他时,你只有 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}(间隔从1m到1W)为推荐的变体渲染蜡烛图历史,或使用GET /assets/{assetId}/price-chart获取规范价格视图,该视图会自动选择最佳可用来源。 - 批量估价钱包持仓。 将 Quicknode Solana 端点(用于读取钱包的代币账户)与
POST /assets/market-snapshots(每次调用最多 250 个铸币)配对,以 获取 Solana 钱包持有的所有代币账户,并在一次往返中将每个铸币映射到其规范资产。 - 构建观察列表。 使用
GET /assets/curated(按 24 小时交易量排序,使用groupBy=asset或groupBy=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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~