大多数交易机器人一开始都是配置文件里一个 RPC URL,然后一路长大。这在机器人还不重要时够用;等它开始重要,两个问题会同时出现:读取撞限流,发送开始输比赛。
这是两个不同的问题、有两套不同的解法,而想用"升个大套餐"同时解决,通常两个都解决不了。
拆分
一个交易机器人用 RPC 做三件事,而它们想要的属性互相冲突:
| 负载 | 想要什么 | 典型调用 |
|---|---|---|
| 读状态 | 吞吐、宽松限额 | getAccountInfo、getMultipleAccounts |
| 流式事件 | 推送、过滤 | onLogs、onAccountChange |
| 发交易 | ★短路径、质押权重★ | sendRawTransaction |
读要的是批量便宜。发要的是单次够快。★为其中一个调优的端点,对另一个就是妥协★ —— 这就是为什么大多数生产环境最后都会拆开。
// 读取与流式:按你的量选一家合适的服务商。
const reader = new Connection(READ_RPC, {
commitment: "confirmed",
wsEndpoint: READ_WS,
});
// 发送:另一个端点,按提交表现来选。
const sender = new Connection(SEND_RPC, { commitment: "processed" });
这两个集成点互相独立,换其中一个不用动另一个。光这一点就值得拆 —— 因为它让你能拿真实流量去测一个新的发送端点,而不用拿读取路径冒险。
如果你已经拆开了、想找个发送端点做基准对比,免费领个 BoltTx key 改一行就行。
读取侧配置
三个比"选哪个套餐"更重要的设置。
批量,别循环。 大多数代码库里读取量降幅最大的一项:
// 贵:N 个请求。
// const infos = await Promise.all(pubkeys.map((pk) => reader.getAccountInfo(pk)));
// 便宜:每 100 个账户一个请求。
const infos = await reader.getMultipleAccountsInfo(pubkeys);
承诺级别按调用设,不要全局设一个。 查价格可以用 processed;任何要记成真相的东西该用 confirmed。
连接复用。 每个请求新建 TLS 连接,意味着数据开始传之前要先握手。复用把它完全去掉,而且热连接上 HTTPS 的成本和纯 HTTP 基本一样。
import { Agent } from "undici";
const agent = new Agent({ keepAliveTimeout: 60_000, connections: 8 });
发送侧配置
这里的设置反直觉,因为默认值对交易场景是错的。
const sig = await sender.sendRawTransaction(raw, {
skipPreflight: true, // ★默认是 false★
maxRetries: 0, // ★默认不是 0★
});
skipPreflight: true。 preflight 拿当前 slot 模拟,要花一次往返。但你会落在之后的 slot、面对不同状态,所以模拟通过什么都预测不了,却花掉了你需要的延迟。真正有用的模拟放在开发阶段。
maxRetries: 0。 RPC 自带的重试按一套你观察不到的节奏跑。如果你也在重试,就是两个组件按不同时钟重发,行为没法推理。重试自己驱动 —— 因为只有你知道 blockhash 什么时候过期。
然后是真正能用的循环:
while (await sender.getBlockHeight("confirmed") <= lastValidBlockHeight) {
const { value } = await sender.getSignatureStatuses([sig]);
if (value[0]) break; // 已上链,看 .err
await sender.sendRawTransaction(raw, { skipPreflight: true, maxRetries: 0 });
await new Promise((r) => setTimeout(r, 400)); // 大约一个 slot
}
重发相同字节是安全的 —— 签名一样,最多被收录一次。
blockhash 的处理
这是代价最隐蔽的一个设置。
// 后台刷新;绝不在热路径上取。
let cached = await reader.getLatestBlockhash("confirmed");
setInterval(async () => {
cached = await reader.getLatestBlockhash("confirmed");
}, 5_000);
★在"我决定交易"和"我提交了"之间取一次 blockhash,是一次你付不起的往返。★ 缓存把它去掉;定时刷新保证它足够新鲜,让你在关键时刻永远不接近过期。
跟踪有效期已经花掉多少 —— 这是几乎没人记录的指标:
log({ blockhashAge: Date.now() - cachedAt });
费用
写死的 priority fee 两个方向都错:网络清闲时浪费,不清闲时不够。
import { ComputeBudgetProgram } from "@solana/web3.js";
// 费用是按账户竞争的,不是全局的。
const recent = await reader.getRecentPrioritizationFees({
lockedWritableAccounts: writableAccounts,
});
const fees = recent.map((r) => r.prioritizationFee).sort((a, b) => a - b);
const median = fees[Math.floor(fees.length / 2)] ?? 0;
instructions.unshift(
ComputeBudgetProgram.setComputeUnitPrice({
microLamports: Math.max(median * 2, 5_000),
}),
ComputeBudgetProgram.setComputeUnitLimit({ units: 200_000 }),
);
★compute unit limit 要设。★ 不设的话会按一个远高于真实用量的默认值计费,恰好浪费在费用高的时候。
该监控什么
三个计数器,分开跟踪。合成一个成功率会把"你到底遇到哪个问题"盖住:
const { value } = await sender.getSignatureStatuses([sig], {
searchTransactionHistory: true,
});
if (!value[0]) metrics.inc("never_landed"); // ★费用、路径、重试、过期★
else if (value[0].err) metrics.inc("landed_failed"); // ★滑点、余额、账户状态★
else metrics.inc("landed_ok");
再加上按网络状况分桶的"提交到落块的 slot 距离"。★清闲时稳、高负载时掉的上链率,指向路由★ —— 而那是唯一没法在应用层代码里解决的。
上链应该是什么水平
我们投递节点上的真实交易:中位确认 336 毫秒 —— 不到一个 slot。
如果你自己的分布明显更宽,先依次排查费用、blockhash 时长、重试行为 —— 然后再看路径。
一份能用的最小配置
// 读取 + 流式
const reader = new Connection(READ_RPC, {
commitment: "confirmed",
wsEndpoint: READ_WS,
});
// 发送
const sender = new Connection(SEND_RPC, { commitment: "processed" });
// 后台刷新 blockhash
let blockhash = await reader.getLatestBlockhash("confirmed");
setInterval(async () => {
blockhash = await reader.getLatestBlockhash("confirmed");
}, 5_000);
// 每次发送:实时算的费用、CU limit、skipPreflight、自己的重试循环
整个形状就是这样。剩下的都是在这个框架内调参。
BoltTx 在这里的位置
我们是发送那一半。不做索引,不做解析历史,不做流式推送。
提交走我们自建的四区域投递节点,带 SWQoS 路由,不暴露公共 mempool —— 交易在落块之前,传输途中观察不到。你现在的读取服务商留着就行,集成点互相独立。
你在本地签名。我们不托管资金、不代签、不修改交易内容。tip 带在交易里、从你自己的钱包链上支付,交易失败时跟着一起 revert —— 这是 Solana 原子交易的机制决定的。只有到达链上的交易才计费。
免费领 API key,没有月费:
const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
常见问题
Solana 交易机器人需要怎样的 RPC 配置? 通常是两个端点:一个负责读取和流式,一个负责发送。两种负载想要的属性不同,而集成点互相独立 —— 换其中一个不用动另一个。
交易机器人该用一个 RPC 还是两个? 等机器人开始重要之后,用两个。读要吞吐和宽松限额;发要一条到出块方、背后有质押的短路径。一个端点同时优化两者,对两边都是妥协。
交易机器人该用哪个承诺级别?
按调用设,不要全局设一个。查价格和检测事件用 processed,任何要记成真相的用 confirmed。一个全局设置必然会在某处强迫你做错误的取舍。
交易机器人为什么该开 skipPreflight? preflight 要花一次网络往返,而且模拟的是当前 slot、不是你会落进去的那个。它可能通过了却什么都没告诉你,同时花掉了延迟。真正有用的模拟放在开发阶段。
为什么要把 maxRetries 设成 0? 因为 RPC 自带的重试按一套你看不见的节奏跑。如果你自己也有循环,两个组件按不同时钟重发会让行为无法预测。只有你的循环知道 blockhash 什么时候过期。
怎么降低交易机器人的 RPC 用量?
用 getMultipleAccounts 批量取代循环调 getAccountInfo、把适合推送的数据从轮询换成订阅、并给程序订阅加服务端过滤。
该缓存 blockhash 吗? 该,而且要后台定时刷新。在"决定"和"提交"之间去取,会在唯一付不起往返的地方加一次往返。另外记得记录有效期已经花掉多少。
交易机器人的 priority fee 该设多少?
用 getRecentPrioritizationFees 按你会写入的账户去算,再按那一刻的竞争程度放大。写死的值在清闲时浪费、在繁忙时不够。
为什么我机器人的成功率会误导人? 因为它把两个无关的问题合并了。把"从未上链"和"上链但失败"分开看:前者的成因在提交侧,可能是费用不够、路径被降级,或者根本没重试到过期;后者的成因在链上,滑点、余额或账户状态都可能。
Solana 交易机器人一定要付费 RPC 吗? 低量读取有时不用。但拥堵时发送,共享公共端点是最糟的情况 —— 它们恰好在你的交易最重要的那些时刻最忙。
怎么测试一个新的发送端点是不是更好? 两边交替发,至少一周,按网络状况分桶,对比上链率和 slot 距离。只在清闲时段测,测的是"所有路径都一样"的那种情况。
compute unit limit 该设多少? 开发阶段模拟一次拿到真实消耗,设一个略高的值。用默认值意味着按一个高得多的数字计费,在费用飙升时白白浪费预算。
连接复用对交易机器人重要吗? 重要。新建 TLS 连接要先握手,字节才开始传。复用之后这笔开销就没了 —— 对持续发送的机器人来说,这是你每一笔都在白付的延迟。
读和发该用同一个承诺级别吗?
不该。发送在确认循环里用 processed 更快;而喂给你数据库的读取该用 confirmed 或更高,因为 processed 的结果可能被回滚。
哪些指标能说明瓶颈在 RPC 配置? 按网络状况分桶的上链率。如果清闲时稳、高负载时掉,而费用是实时算的、blockhash 也新鲜,那剩下的变量就是提交路径。