[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"readme:jev-trader":3},"\u003Ch1>jev-trader\u003C\u002Fh1>\n\u003Cp>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.\u003C\u002Fp>\n\u003Ch2>Run\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>cp .env.example .env\nbun install\nbun run start\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>With no \u003Ccode>PRIVATE_KEY\u003C\u002Fcode> it dry-runs: real book, real decisions, simulated fills. Set \u003Ccode>MODEL=jev\u003C\u002Fcode> and \u003Ccode>TYPESAFE_AI_API_KEY\u003C\u002Fcode> to use Jev; the default \u003Ccode>mock\u003C\u002Fcode> is a momentum heuristic stand-in.\u003C\u002Fp>\n\u003Ch2>Endpoints\u003C\u002Fh2>\n\u003Cp>Deployed (dry run, mock model): \u003Ca href=\"https:\u002F\u002Fjev-trader-production.up.railway.app\" rel=\"nofollow ugc noopener\">https:\u002F\u002Fjev-trader-production.up.railway.app\u003C\u002Fa>\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ccode>GET \u002F\u003C\u002Fcode> snapshot: model, wallet, dryRun, latest block event\u003C\u002Fli>\n\u003Cli>\u003Ccode>GET \u002Fhistory\u003C\u002Fcode> last 1000 block events\u003C\u002Fli>\n\u003Cli>\u003Ccode>GET \u002Fevents\u003C\u002Fcode> SSE: \u003Ccode>snapshot\u003C\u002Fcode> on connect, then one \u003Ccode>block\u003C\u002Fcode> event per block, plus a \u003Ccode>fill\u003C\u002Fcode> event whenever a live order's receipt lands\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>Every event (see \u003Ccode>src\u002Ftrader.ts\u003C\u002Fcode> for types):\u003C\u002Fp>\n\u003Cpre>\u003Ccode>{\n  \"block\": 105488269, \"ts\": 1789593630676,\n  \"mid\": 0.022636, \"bestBid\": 0.022628, \"bestAsk\": 0.022644, \"spreadBps\": 7.07,\n  \"decision\": { \"action\": \"buy\", \"probabilities\": { \"buy\": 0.77, \"sell\": 0.23, \"hold\": 0 }, \"upIn10\": 0.77, \"latencyMs\": 81, \"late\": false },\n  \"quote\": { \"side\": \"buy\", \"price\": 0.022629, \"size\": 200, \"txHash\": \"0x…\", \"gasMon\": 0.0357, \"cancel\": [100295801], \"status\": \"sent\", \"orderId\": null, \"capped\": false },\n  \"fill\": null,\n  \"resting\": { \"bidMon\": 200, \"askMon\": 200 },\n  \"position\": { \"side\": \"short\", \"size\": 200, \"entryPrice\": 0.022633, \"unrealizedUsd\": -0.0006, \"unrealizedMon\": -0.027 },\n  \"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 }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Every block the model is asked about the move over \u003Ccode>HORIZON_BLOCKS\u003C\u002Fcode> (default 100, ~30 s) and answers \u003Ccode>buy\u003C\u002Fcode> or \u003Ccode>sell\u003C\u002Fcode>. \u003Ccode>quote\u003C\u002Fcode> is the order that block put on the book: a post-only limit order of \u003Ccode>TRADE_SIZE_MON\u003C\u002Fcode> on that side, \u003Ccode>QUOTE_INSIDE_TICKS\u003C\u002Fcode> inside the touch (clamped to the touch when the spread is too tight), in one \u003Ccode>batchUpdate\u003C\u002Fcode> that also cancels everything we had resting (\u003Ccode>cancel\u003C\u002Fcode>). \u003Ccode>hold\u003C\u002Fcode> appears only with \u003Ccode>decision.late: true\u003C\u002Fcode>, 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 \u003Ccode>capped: true\u003C\u002Fcode> and \u003Ccode>probabilities\u003C\u002Fcode> still show the model's call. \u003Ccode>resting\u003C\u002Fcode> is our size known to be on the book after this block. \u003Ccode>upIn10\u003C\u002Fcode> equals the buy probability.\u003C\u002Fp>\n\u003Cp>Live sends are fired and forgotten, so the \u003Ccode>block\u003C\u002Fcode> event carries the \u003Cstrong>intent\u003C\u002Fstrong>: \u003Ccode>status: \"sent\"\u003C\u002Fcode>, \u003Ccode>gasMon\u003C\u002Fcode> is \u003Ccode>gasLimit x (last known base fee + priority)\u003C\u002Fcode>. 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:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>event: quote\ndata: { \"block\": 105488269, \"quote\": { …, \"status\": \"placed\", \"orderId\": 100295812, \"gasMon\": 0.0357 } }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>status\u003C\u002Fcode> becomes \u003Ccode>placed\u003C\u002Fcode> (with the order id) or \u003Ccode>reverted\u003C\u002Fcode> (the book moved through the price before the tx landed, or a cancelled order had already filled). No receipt after 10 blocks gives \u003Ccode>lost\u003C\u002Fcode>. 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 \u003Ccode>eth_getLogs\u003C\u002Fcode> poll that feeds the model. Each block with fills gets its own SSE event, and \u003Ccode>position\u003C\u002Fcode>, \u003Ccode>realizedUsd\u003C\u002Fcode> and \u003Ccode>fills\u003C\u002Fcode> update then:\u003C\u002Fp>\n\u003Cpre>\u003Ccode>event: fill\ndata: { \"block\": 105488271, \"fill\": { \"side\": \"buy\", \"size\": 200, \"price\": 0.022629, \"txHash\": \"0x…\", \"orderId\": 100295812, \"simulated\": false } }\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>txHash\u003C\u002Fcode> is the taker's transaction. In a dry run the quote is \u003Ccode>status: \"sim\"\u003C\u002Fcode>: the order rests for one block and a real print crossing its price fills it (\u003Ccode>simulated: true\u003C\u002Fcode>).\u003C\u002Fp>\n\u003Ch2>Layout\u003C\u002Fh2>\n\u003Cpre>\u003Ccode>src\u002Fconfig.ts   env\nsrc\u002Fchain.ts    block\n\u003C\u002Fcode>\u003C\u002Fpre>\n",1789861589599]