你的机器人认为自己持有 1,000 个代币。链上说是 987。 两个数字都不是通常意义上的 bug —— 它们是在每一个单独步骤都正确的情况下分岔的。
★链是唯一的权威。你的账本是它的一份缓存,而每一份缓存都会漂移。★
漂移从哪里来
| 原因 | 方向 |
|---|---|
| ★记录了意图,而不是结果★ | ★账本偏高★ |
| ★转账费被扣下★ | ★账本偏高★ |
| 滑点 —— 成交得比预期差 | 账本偏高 |
| ★revert 的交易被记成成交★ | ★账本偏高★ |
| 没记录的手工交易 | 账本偏低 |
| ★你放弃之后才落块的重试★ | ★账本偏低★ |
★注意大部分原因都朝同一个方向推。★ 一个记录"意图"而不是"结果"的机器人,会累积出一个乐观的仓位 —— 而那是更糟的错误:你会去卖你根本没有的代币。
记录结果,不是意图
// ★错:签名只意味着 RPC 接收了字节。★
const sig = await connection.sendRawTransaction(raw);
positions.add(mint, expectedAmount);
// ★对:读实际动了什么。★
const outcome = await resolve(sig, lastValidBlockHeight);
if (outcome.status !== "success") return;
const tx = await connection.getTransaction(sig, { maxSupportedTransactionVersion: 0 });
const delta = tokenDelta(tx.meta, myTokenAccount); // ★来自 pre/post 余额★
positions.add(mint, delta);
★preTokenBalances 和 postTokenBalances 是"什么变了"的权威记录★,而且它们自动涵盖一切 —— 滑点、转账费,以及 CPI 内部做的任何事。
用它们一次性消除掉六个漂移原因里的四个,因为你不再预测,而是开始读取。
如果你的记账是对的、交易还是会错过,免费领个 BoltTx key 改一行就能测测提交路径。
三终态规则在这里同样适用
switch (outcome.status) {
case "success": applyDelta(readDeltaFromMeta(sig)); break;
case "reverted": recordFeeOnly(sig); break; // ★仓位不变★
case "expired": recordNothing(); break; // ★从没执行★
}
★一笔 revert 的交易落了块、花了手续费,但没有改变任何仓位。★ 把它记成成交,是漂移最常见的单一来源 —— 而且它会在很多笔交易上静默复利。
过期的交易是相反的陷阱 —— 它从没执行,所以把它记成失败是对的,但前提是你确认了过期,而不是从一个超时推断出来的。
按计划对着链上核对
async function reconcile(connection, wallet) {
const [classic, t22] = await Promise.all([
connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_PROGRAM_ID }),
connection.getParsedTokenAccountsByOwner(wallet, { programId: TOKEN_2022_PROGRAM_ID }),
]);
const onChain = new Map();
for (const { account } of [...classic.value, ...t22.value]) {
const info = account.data.parsed.info;
onChain.set(info.mint, BigInt(info.tokenAmount.amount)); // ★字符串,精确★
}
for (const [mint, believed] of ledger) {
const actual = onChain.get(mint) ?? 0n;
if (actual !== believed) await flagDiscrepancy(mint, believed, actual);
}
}
★有两个细节决定它是"正确"还是"大致正确"。★
两个代币程序都要查。 只查一个程序会静默漏掉 Token-2022 的余额,而那读起来像是一个丢失的仓位,而不是一次不完整的查询。
用 bigint 在原始单位上比较。 用 uiAmount 会在 2^53 以上引入浮点误差,在大额余额上产出幽灵差异,同时掩盖真实的差异。
差距的方向指向哪里
★差异的正负号告诉你该往哪看,而这比它的大小更有用。★
账本高于链上: 你记录了一件没发生的事。 去找被记成成交的 revert 交易,或者"在结果之前就记录了意图"。
账本低于链上: 发生了一件你没记录的事。 ★去找一个你放弃之后才落块的重试,或者一笔由你的密钥签名、而你的机器人没有发起的交易。★
第二种情况值得一个告警,而不是一次修正。 一个你没创建过的仓位有一种良性解释 —— 迟到的重试 —— 和一种不良性的,而对账正是你搞清楚是哪一种的地方。
SOL 要单独对账
原生 SOL 需要它自己的处理方式,因为手续费从同一份余额里出:
const lamports = await connection.getBalance(wallet);
const expected = lastKnown - feesSpent + inflows - outflows;
★一个和你的手续费开销恰好吻合的 SOL 差异,不是差异★ —— 那是你的记账没有把手续费减掉。把手续费作为单独一行追踪,这样交易变动和手续费变动才可区分。
另外记得 wrapped SOL 是一个代币账户,不属于这份余额。一个会包装和解包的机器人需要两者都追踪,而且要知道它们不同步变动。
出现差异时该怎么办
★永远不要用链上的值静默覆盖你的账本。★ 那修好了数字,却销毁了"它为什么错"的证据。
保住信息的处理顺序:
1. 把两个值和时间戳都记下来。 差异的历史,是你找到系统性原因的方式。
2. 尝试归因。 取最近的签名,找一笔你的账本不知道的交易。
3. 归因不了就告警。 ★一个你解释不了的差距,和一个你能解释的,是两个不同的问题。★
4. 然后再修正,以链为权威,并留下这次修正的记录。
5. 差距很大就停掉那个仓位的交易。 对着一个你无法核实的仓位交易,正是小的记账错误变成大错误的方式。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
★可预测的落块,缩小了"一笔交易命运不明"的那个含糊窗口。★ 大多数记账漂移,都始于一笔结果不明的交易 —— 不明得足够久,久到有人记下了一个猜测。
BoltTx 在这里的位置
我们负责提交。仓位追踪和对账完全留在你的代码里。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前、传输途中观察不到。
你在本地签名。我们不托管资金、不代签、不修改交易内容 —— ★这也是为什么"你钱包历史里一笔你没发起的交易"不可能来自我们。★ tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
我机器人的仓位为什么和链上不一样? 通常是因为它记录的是意图而不是结果。滑点、转账费、revert 的交易,都会让实际结果和预期不同。
怎么记录一笔交易实际做了什么?
从交易 meta 里读 preTokenBalances 和 postTokenBalances。它们反映真实结果,包括 CPI 影响、滑点,以及转账时被扣下的任何费用。
拿到签名时该更新账本吗? 不该。签名只意味着 RPC 接收了字节。 要在收敛到终态、并读出实际余额变化之后再更新。
记账里该怎么处理 revert 的交易? 记手续费,仓位不变。 把 revert 记成成交是漂移最常见的单一来源,而且它会静默复利。
多久该对着链上核对一次? 频繁到能在几分钟而不是几天内发现差异。 任何持续交易的系统,每几分钟查一次都是有益的。
我的对账为什么漏掉一些代币? 多半是因为你只查了一个代币程序。Token-2022 的余额需要单独查,而它们的缺席读起来像一个丢失的仓位。
余额该按数字还是字符串比较?
按原始金额字符串转成 bigint。 用 uiAmount 会在 2^53 以上引入浮点误差,制造幽灵差异并掩盖真实差异。
账本高于链上意味着什么? 你记录了一件没发生的事。 去找被记成成交的 revert 交易,或者"在提交时而不是在结果时"更新的仓位。
账本低于链上意味着什么? 发生了一件你没记录的事。 通常是一个你放弃之后才落块的重试,但也可能是一笔由你的密钥签名、而机器人没有发起的交易。
该自动修正差异吗? 不要静默修正。 先把两个值都记下来、尝试归因、解释不了就告警,然后再以链为权威修正,并留下记录。
什么时候差异该停掉交易? 当它大到"继续交易可能放大这个错误"的时候。 对着一个你无法核实的余额交易,会把小问题变成大问题。
我的 SOL 余额为什么从来对不上? 因为手续费和交易从同一份余额里出。把手续费开销作为单独一行追踪,否则每一笔手续费看起来都像一个无法解释的差异。
wrapped SOL 算在我的 SOL 余额里吗? 不算,它是一个独立的代币账户。两者不同步变动,所以会包装和解包的机器人必须分别追踪。
转账费怎么影响仓位追踪? 到账的比发出的少,所以一个记录"发出金额"的账本会永久偏高。读取交易后的余额,能自动处理这一点。
能只靠链上历史重建仓位吗? 大部分可以,用签名和余额差额 —— 但只限于已落块的交易。关于意图、以及过期掉的尝试,只存在于你自己的日志里。
最有价值的对账告警是什么? 你的账本解释不了的链上活动。 它有一种良性成因和一种不良性成因 —— 而那正是值得把人叫起来的那个区分。