jev-trader:Monad 上每个区块做一次 AI 买卖决策的 Kuru MON-USDC 挂单机器人
jev-trader 是一个 TypeScript 项目:用一个 TypeSafe 的 Jev 模型观察 Kuru 的 MON-USDC 订单簿,每隔约 300 毫秒(每个 Monad 区块)回答一次 buy 或 sell,并在该方向以 post-only 限价单挂在最优价内侧一个 tick,同时撤掉上一笔挂单;成交由吃单方主动打到挂单上完成,从而赚取点差而非支付点差。附带的小型服务器通过 SSE 把每个区块事件推送到仪表盘。未配置 PRIVATE_KEY 时以真实订单簿、真实决策、模拟成交的方式干跑。
社区作者 · zZz
它解决什么问题
【项目用途】 jev-trader 在 Monad 链上针对 Kuru 交易所的 MON-USDC 订单簿做自动化挂单交易。核心逻辑是「每个 Monad 区块做一次决策」:模型被问及未来 HORIZON_BLOCKS 个区块(默认 100,约 30 秒)的价格走向,输出 buy 或 sell。
随后在该方向挂出一笔 TRADE_SIZE_MON 大小的 post-only 限价单,价格位于最优价内侧 QUOTE_INSIDE_TICKS 个 tick(点差过窄时被夹到最优价上),并通过一次 batchUpdate 同时撤掉此前所有挂单(cancel 字段)。因为挂的是 post-only 单,成交发生在吃单方主动打过来时,机器人赚取点差而不是支付点差。
hold 只在 decision.late 为 true 时出现,即模型错过该区块、当区块未挂任何单。当持仓上限(或实盘时的保证金资金)挡住某一侧时,报价会挂到另一侧,并标记 capped: true,probabilities 仍显示模型的原始判断。resting 表示该区块之后已知在订单簿上的自方挂单量。upIn10 等于 buy 概率。
【运行方式(来自 README 的 Run 段落)】
步骤 1
cp .env.example .env步骤 2
bun install步骤 3
bun run start没有 PRIVATE_KEY 时即为干跑:真实订单簿、真实决策、模拟成交。设置 MODEL=jev 与 TYPESAFE_AI_API_KEY 才会使用 Jev 模型;默认使用 mock(一个动量启发式替代模型)。
【接口端点】 已部署(干跑、mock 模型):https://jev-trader-production.up.railway.app
- GET / :快照,包含 model、wallet、dryRun、最新区块事件
- GET /history :最近 1000 条区块事件
- GET /events :SSE。连接时先发快照,之后每个区块一条区块事件,每当一笔实盘挂单的收据落地时额外发一条成交流事件
【事件结构】 每个区块事件字段包括:block、ts、mid、bestBid、bestAsk、spreadBps、decision(action / probabilities / upIn10 / latencyMs / late)、quote(side / price / size / txHash / gasMon / cancel / status / orderId / capped)、fill、resting(bidMon / askMon)、position(side / size / entryPrice / unrealizedUsd / unrealizedMon)、totals(blocks、decisions、quotes、fills、reverted、lateBlocks、jevUsd、gasMon、gasUsd、realizedUsd、pnlUsd、pnlMon、pnlPct)。
【实盘发送与回执】 实盘发送是 fire-and-forget 的,因此区块事件携带的是「意图」:status 为 "sent",gasMon = gasLimit ×(最近已知 base fee + priority)。Monad 按 gas limit 收费,所以无论订单是否落链,这都是真实成本。收据在一两个区块后作为独立的 SSE 事件到达,事件名 quote,status 变为 placed(带 orderId)或 reverted(交易落链前价格已穿过,或已撤订单其实已成交)。
超过 10 个区块没有收据则标记为 lost。 成交不在自己的交易里:是别人的吃单订单打到自方挂单上,对应的 Trade 日志通过同一套喂给模型的 eth_getLogs 轮询送达。每个有成交的区块会单独发一条 SSE 事件,此时 position、realizedUsd 与 fills 一并更新,事件中含 fill(side / size / price / txHash / orderId / simulated),txHash 是吃单方的交易哈希。
干跑时 quote 的 status 为 "sim":订单停留一个区块,若有真实成交价穿过其价格则成交,并标记 simulated: true。
【300 毫秒预算】 一次决策加一笔下单必须塞进一个区块,因此热路径只做两次 RPC 往返:一次 eth_call 读订单簿(公共 RPC 上约 18 ms,使用 READ_RPC_URL),一次 eth_sendRawTransaction(RPC_URL,交易被接受即返回)。
路径上没有其它内容——不做 eth_estimateGas(Monad 按 gas limit 收费,因此上限被硬编码或在启动时推导一次)、不做 eth_sendRawTransactionSync(它会阻塞到交易进入 Proposed)、不查 gas price(使用静态 type-2 费用:MAX_FEE_GWEI 作为上限,2 gwei 优先费,实际价格是 base + priority)。收据、费用估算和金库检查都在后续区块的路径之外执行。
干跑加 mock 模型的实测:读取 p50 为 18 ms,整个循环 p50 为 100 ms(其中 80 ms 是 mock 的推理替代)。
【代码布局】 src/config.ts:环境变量;src/chain.ts:区块流(WebSocket newHeads + 轮询兜底,只取最新区块)与原始 RPC;src/book.ts:单次 eth_call 的订单簿读取器(解析 getL2Book,并合并 AMM vault);src/market.ts:Kuru 交互——读簿、手写编码的 batchUpdate(撤单 + post-only 挂单)、保证金存入、本地 nonce、异步确认;src/model.
ts:Model 接口、JevModel(AI SDK experimental_evaluate)、MockModel;src/trader.ts:主循环——同时只有一笔在途、迟到则 hold、持仓与盈亏记账;src/server.ts:Bun.serve 提供 snapshot、history、SSE。
【辅助脚本】
bun run scripts/bench-read.ts :自研订单簿读取器对比 SDK,验证精确度与延迟bun run scripts/dry-encode.ts :离线签名一笔买和一笔卖,断言 calldata 与 SDK 一致【许可证与适用对象】 来源正文未标注许可证,需在仓库中自行核验。适用对象:希望在 Monad 上研究 AI 驱动做市/挂单策略的开发者,熟悉 TypeScript 与 Bun、理解订单簿、post-only 单、gas 计费与链上事件轮询的量化或链上交易实践者;也适合先用干跑模式与 mock 模型观察 SSE 事件流的入门者。
— 本文由 AI 根据公开来源辅助整理,命令、版本与许可证请在使用前到原始页面复核。
安装 / 开始使用
准备环境:安装 Bun 运行时;准备 Monad RPC 访问(读取用 READ_RPC_URL、发送用 RPC_URL);如需实盘还需配置钱包私钥。
安装与首次运行(README 的 Run 段落,按顺序执行):
步骤 1
cp .env.example .env步骤 2
bun install步骤 3
bun run start首次运行说明:没有 PRIVATE_KEY 时进入干跑模式——使用真实订单簿、真实决策,但有模拟成交(quote 状态为 "sim")。要想真正调用 Jev 模型,需设置 MODEL=jev 与 TYPESAFE_AI_API_KEY;不设置时默认使用 mock,一个动量启发式替代模型。
可通过的环境变量(README 中提及):
- PRIVATE_KEY:不配置则干跑
MODEL=jev 与 TYPESAFE_AI_API_KEY:启用 Jev 模型- HORIZON_BLOCKS:预测跨度,默认 100(约 30 秒)
- TRADE_SIZE_MON:每笔挂单大小
- QUOTE_INSIDE_TICKS:挂在最优价内侧的 tick 数
- READ_RPC_URL、RPC_URL:分别用于读簿与发交易
- MAX_FEE_GWEI:type-2 费用上限,优先费固定 2 gwei
验证与基准脚本:
bun run scripts/bench-read.ts —— 用自研读取器对比 SDK 的精确度与延迟bun run scripts/dry-encode.ts —— 离线签名一笔买与一笔卖,断言 calldata 与 SDK 一致常见问题:
- 干跑与实盘的区别:干跑不发真实交易,成交是模拟的;实盘发送为 fire-and-forget,区块事件里的 status 是 "sent",真实结果要等后续区块的 quote 事件变为 placed 或 reverted。
- 订单为何被判 reverted:交易落链前订单簿价格已穿过,或已撤订单其实已经成交。
- lost 状态:收据超过 10 个区块仍未到达。
- hold 的来源:模型错过该区块(decision.late 为 true),当区块不挂单。
- capped: true:持仓上限(实盘时为保证金资金)挡住了该侧,报价改挂另一侧。
- gas 计费:Monad 按 gas limit 收费,因此 gasMon = gasLimit ×(最近 base fee + priority),订单是否落链都要付这笔成本。
- 部署好的干跑实例可直接访问 https://jev-trader-production.up.railway.app 查看 GET /、GET /history 与 GET /events(SSE)。