jev-trader README

The project's README from github.com/jarrodwatts/jev-trader. As one markdown file: README.md.

jev-trader

One decision every Monad block. A TypeSafe Jev model watches the Kuru MON-USDC order book and answers buy or sell every ~300 ms. Every block posts a real post-only limit order on that side, one tick inside the touch, replacing the last one. Fills happen when a taker hits it, so the bot earns the spread instead of paying it. A small server streams every block to the dashboard.

Run

cp .env.example .env
bun install
bun run start

With no PRIVATE_KEY it dry-runs: real book, real decisions, simulated fills. Set MODEL=jev and TYPESAFE_AI_API_KEY to use Jev; the default mock is a momentum heuristic stand-in.

Endpoints

Deployed (dry run, mock model): https://jev-trader-production.up.railway.app

Every event (see src/trader.ts for types):

{
  "block": 105488269, "ts": 1789593630676,
  "mid": 0.022636, "bestBid": 0.022628, "bestAsk": 0.022644, "spreadBps": 7.07,
  "decision": { "action": "buy", "probabilities": { "buy": 0.77, "sell": 0.23, "hold": 0 }, "upIn10": 0.77, "latencyMs": 81, "late": false },
  "quote": { "side": "buy", "price": 0.022629, "size": 200, "txHash": "0x…", "gasMon": 0.0357, "cancel": [100295801], "status": "sent", "orderId": null, "capped": false },
  "fill": null,
  "resting": { "bidMon": 200, "askMon": 200 },
  "position": { "side": "short", "size": 200, "entryPrice": 0.022633, "unrealizedUsd": -0.0006, "unrealizedMon": -0.027 },
  "totals": { "blocks": 3, "decisions": 3, "quotes": 3, "fills": 1, "reverted": 0, "lateBlocks": 0, "jevUsd": 0.000004, "gasMon": 0.107, "gasUsd": 0.0024, "realizedUsd": 0, "pnlUsd": -0.003, "pnlMon": -0.13, "pnlPct": -0.003 }
}

Every block the model is asked about the move over HORIZON_BLOCKS (default 100, ~30 s) and answers buy or sell. quote is the order that block put on the book: a post-only limit order of TRADE_SIZE_MON on that side, QUOTE_INSIDE_TICKS inside the touch (clamped to the touch when the spread is too tight), in one batchUpdate that also cancels everything we had resting (cancel). hold appears only with decision.late: true, when the model missed the block and nothing was posted. When the position cap (or, live, margin funds) blocks a side, the quote goes on the other side with capped: true and probabilities still show the model's call. resting is our size known to be on the book after this block. upIn10 equals the buy probability.

Live sends are fired and forgotten, so the block event carries the intent: status: "sent", gasMon is gasLimit x (last known base fee + priority). Monad charges the gas limit, so that is the real cost whether the order lands or not. The receipt arrives a block or two later as its own SSE event:

event: quote
data: { "block": 105488269, "quote": { …, "status": "placed", "orderId": 100295812, "gasMon": 0.0357 } }

status becomes placed (with the order id) or reverted (the book moved through the price before the tx landed, or a cancelled order had already filled). No receipt after 10 blocks gives lost. Fills are not in our own transactions: someone else's taker order hits our resting one, and the Trade log for it arrives via the same eth_getLogs poll that feeds the model. Each block with fills gets its own SSE event, and position, realizedUsd and fills update then:

event: fill
data: { "block": 105488271, "fill": { "side": "buy", "size": 200, "price": 0.022629, "txHash": "0x…", "orderId": 100295812, "simulated": false } }

txHash is the taker's transaction. In a dry run the quote is status: "sim": the order rests for one block and a real print crossing its price fills it (simulated: true).

Layout

src/config.ts   env
src/chain.ts    block feed (WebSocket newHeads + polling backstop, newest block only), raw RPC
src/book.ts     one-eth_call order book reader (decodes getL2Book, merges the AMM vault)
src/market.ts   Kuru: read book, hand-encoded batchUpdate (cancel + post-only place), margin deposits, local nonce, async confirmation
src/model.ts    Model interface, JevModel (AI SDK experimental_evaluate), MockModel
src/trader.ts   the loop: one in flight, hold when late, position and P&L accounting
src/server.ts   Bun.serve: snapshot, history, SSE

The 300 ms budget

A decision and an order have to fit in one block, so the hot loop makes exactly two RPC round trips: one eth_call for the book (~18 ms on the public RPC, READ_RPC_URL) and one eth_sendRawTransaction (RPC_URL), which returns as soon as the tx is accepted. Nothing else is on the path — no eth_estimateGas (Monad charges gas on the limit, so the limit is hardcoded or derived once at startup), no eth_sendRawTransactionSync (it blocks until the tx is Proposed), no gas price lookup (static type-2 fees: MAX_FEE_GWEI cap, 2 gwei priority; the effective price is base + priority). Receipts, the fee estimate and the vault check run off the hot path on later blocks. Measured in a dry run with the mock model: read p50 18 ms, whole loop p50 100 ms (80 ms of it the mock's inference stand-in).

bun run scripts/bench-read.ts     # book reader vs the SDK: exactness and latency
bun run scripts/dry-encode.ts     # signs a buy and a sell offline, asserts the calldata matches the SDK

jev-trader (jarrodwatts/jev-trader)

Searching Database…

这是什么东西?一个运行在 Monad 上的自主加密货币交易机器人:AI 模型实时盯住 Kuru 的 MON-USDC 订单簿,每个区块(约 300 毫秒)决定一次买入还是卖出,并每次都提交一张 post-only 限价单。
这是需要注册使用的在线服务,还是我自己在本地运行的程序?是自己本地运行的:一个 Bun 服务器,既负责交易,也提供一个小型仪表盘,机器人、它的 API 和实时事件流都在你电脑上的同一个进程里运行。
怎么把它跑起来?依次执行 cp .env.example .env、bun install、bun run start。如果没有配置 PRIVATEKEY,它会以 dry-run 模式启动:用真实的订单簿、做真实的决策,但成交只是模拟的——所以你可以零风险地先试试。
使用它需要 AI 的 API 密钥吗?只需要 Bun 和一个 Monad RPC 节点。默认模型是一个模拟动量策略的启发式规则,所以不需要 AI 密钥;如果想用真正的 Jev AI 模型,设置 MODEL=jev 并配置 TYPESAFEAIAPIKEY 即可。
实盘运行有什么要注意的坑吗?Gas 费用:Monad 按设置的 gas 上限收费,所以每提交一笔订单都要付 gas,不管最终能不能成交。另外,如果你的交易还没上链,行情价格就先穿过了你的挂单价,报价交易也可能回退失败。
300 毫秒真的够用来读订单簿、做决策再提交订单吗?够,这是刻意设计的。热循环只做两次 RPC 往返:一次用 eth_call 获取订单簿(在公共 RPC 上 p50 为 18 毫秒),一次发送原始交易。在实测的 dry-run 中,整个循环 p50 为 100 毫秒,其中 80 毫秒是模拟模型占位的推理耗时。
如果模型在一个区块内来不及算完会怎样?那个区块它什么都不提交:此时 decision.late 会被设为 true。此外,如果行情价格先穿过了你的挂单价,回执也可能变成 reverted(回退);如果 10 个区块内还没等到回执,状态会标记为 lost(丢失)。
成交是别人的交易,它是怎么知道自己成交了的?成交是通过同一个 eth_getLogs 轮询检测的——这个轮询同时也在给模型喂数据。吃单方的交易哈希会随成交一起返回,而持仓、已实现盈亏和成交次数则通过一个专用的 SSE 事件来更新。
Size
0.1 MB
Stars
3K
Updated
CommitOct 13
Age
1mo
Commits · 90 Days