
What jev-trader does
jev-trader is a block-by-block trading bot for the Kuru MON-USDC order book on Monad. On every new block, it reads the live order book, asks a model whether to buy or sell, and creates a post-only limit-order intent on the selected side.
The order is priced QUOTE_INSIDE_TICKS ticks inside the current touch. If the spread is too narrow, the price is clamped to the touch. The bot cancels its previously resting orders and posts the replacement in a single Kuru batchUpdate.
Because the order is post-only, it is intended to rest on the book rather than execute immediately. A fill occurs when another participant submits a taker order that crosses its price. This allows the strategy to earn the spread instead of paying it, although execution, inventory, gas, and market risks still apply.
Key features
- One decision per Monad block: The bot processes the newest block approximately every 300 ms.
- Live Kuru order-book data: A custom reader decodes
getL2Bookand merges the AMM vault into the book. - Two model options: The default mock model uses a momentum heuristic, while
MODEL=jevenables the TypeSafe Jev model. - Safe dry-run behavior: Without
PRIVATE_KEY, the bot uses the real book and real market prints but simulates its own orders and fills. - Post-only order replacement: Each update cancels known resting orders and submits a new post-only order in one batch.
- Position and P&L tracking: Events report position, realized and unrealized results, gas costs, fill counts, and other totals.
- HTTP and SSE monitoring: A small Bun server exposes the latest snapshot, up to 1,000 historical block events, and a live event stream.
- Asynchronous confirmations: Live transactions are submitted on the hot path, while receipts and related checks are handled later.
Project architecture
The source tree separates block ingestion, market access, model evaluation, trading logic, and monitoring:
src/config.tsloads environment configuration.src/chain.tsreceives WebSocketnewHeadsupdates, uses polling as a backstop, and keeps only the newest block.src/book.tsreads and decodes the order book with oneeth_call.src/market.tshandles Kuru book reads, margin deposits, local nonces, batch updates, and asynchronous confirmations.src/model.tsdefines the model interface and providesJevModelandMockModel.src/trader.tsruns the trading loop and tracks positions and P&L.src/server.tsserves snapshots, history, and Server-Sent Events withBun.serve.
Installation and setup
1. Prepare the environment file
From the repository directory, copy the example configuration:
cp .env.example .envReview the resulting .env file before starting the application. The most important behavior is controlled by the presence or absence of PRIVATE_KEY.
Leave
PRIVATE_KEYunset for your first run. The application will dry-run against real market data without broadcasting its own orders.
2. Install dependencies
The project uses Bun for dependency management and execution:
bun install3. Start the trader
bun run startWith the default configuration, the mock momentum model acts as an inference stand-in. The server then makes the trader state and event stream available through its HTTP endpoints.
Choosing the decision model
Use the default mock model
The default mock model is suitable for testing the complete pipeline without calling Jev. It makes decisions using a momentum heuristic and lets you validate book reads, simulated orders, fills, accounting, and event streaming.
Enable the Jev model
To use Jev, configure these values in .env:
MODEL=jev
TYPESAFE_AI_API_KEY=your_api_keyEvery block, the selected model is asked about the expected move over HORIZON_BLOCKS. Its default value is 100 blocks, or roughly 30 seconds according to the project README.
The normal decision is either buy or sell. A hold result does not represent a third model strategy: it appears only when the model misses the current block deadline. In that case, decision.late is true and no order is posted for that block.
Running safely in dry-run mode
When PRIVATE_KEY is not configured, jev-trader does not submit live transactions. It still reads the real Kuru book and produces real model decisions.
A simulated quote has status: "sim". It rests for one block and is considered filled only when a real market print crosses its price. The resulting fill has simulated: true.
This mode is useful for verifying:
- Whether book updates are arriving reliably.
- Whether model latency stays within the block budget.
- How frequently quotes would be filled.
- How positions and P&L change under the simulation rules.
- Whether the dashboard or another SSE client handles every event type correctly.
Inspecting the HTTP endpoints
A deployed dry-run instance using the mock model is available at https://jev-trader-production.up.railway.app.
Read the latest snapshot
The root endpoint reports the active model, wallet, dry-run state, and most recent block event:
curl https://jev-trader-production.up.railway.app/Read recent history
The history endpoint returns the last 1,000 block events:
curl https://jev-trader-production.up.railway.app/historyFollow the live SSE stream
Use curl -N to keep the connection open and display events as they arrive:
curl -N https://jev-trader-production.up.railway.app/eventsThe stream sends a snapshot event when the client connects, followed by one block event per processed block. Live transaction receipts produce quote updates, and detected executions produce fill events.
Understanding a block event
A block event combines market state, the model decision, the new quote intent, resting liquidity, position information, and cumulative totals. A shortened example looks like this:
{
"block": 105488269,
"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,
"status": "sent",
"capped": false
}
}mid,bestBid, andbestAskdescribe the observed market.spreadBpsexpresses the current spread in basis points.decision.actionis the model’s selected direction.decision.probabilitiespreserves the model’s buy and sell probabilities.upIn10is equal to the buy probability.decision.latencyMsreports model evaluation latency.quotedescribes the order attempted for this block.quote.cancelidentifies previously resting orders included for cancellation.restingrecords the bot’s known bid and ask size after the update.positionreports direction, size, entry price, and unrealized results.totalsaccumulates blocks, decisions, quotes, fills, reversions, late blocks, model cost, gas, and P&L.
Position and funding caps
If a position cap prevents the requested side, or live margin funds are insufficient for it, the quote is placed on the opposite side. In this situation, quote.capped is true. The original model probabilities remain unchanged, allowing consumers to distinguish the model’s call from the side actually quoted.
Understanding live quote confirmations
Live sends are fired and forgotten so the trading loop does not wait for a receipt. Therefore, the initial block event contains an intent with status: "sent".
The reported gasMon is calculated as the gas limit multiplied by the last known base fee plus priority fee. The project notes that Monad charges the gas limit, so this is treated as the real cost whether the transaction succeeds or reverts.
A receipt normally arrives one or two blocks later and produces a separate quote event:
event: quote
data: {
"block": 105488269,
"quote": {
"status": "placed",
"orderId": 100295812,
"gasMon": 0.0357
}
}A live quote can progress to one of these states:
placed: The transaction succeeded and includes an order ID.reverted: The book moved through the submitted price before inclusion, or a requested cancellation targeted an order that had already filled.lost: No receipt was found after 10 blocks.
Understanding fill events
Fills do not occur in the bot’s own placement transaction. They occur when another trader’s taker order hits a resting order. The bot detects the corresponding Trade log through the same eth_getLogs polling process that supplies market information to the model.
event: fill
data: {
"block": 105488271,
"fill": {
"side": "buy",
"size": 200,
"price": 0.022629,
"txHash": "0x…",
"orderId": 100295812,
"simulated": false
}
}The fill’s txHash belongs to the taker transaction. When a fill is processed, the stream also reflects updated position data, realized P&L, and cumulative fill totals.
Moving to live trading
Setting PRIVATE_KEY changes the application from dry-run behavior to live transaction submission. Before doing so, verify the environment configuration, margin funding, position limits, trade size, quote placement, fee cap, and RPC reliability.
Live mode submits real post-only Kuru orders and incurs real gas costs. Test the complete workflow in dry-run mode first and use conservative limits when enabling a funded key.
The primary strategy-related settings described by the project are:
TRADE_SIZE_MONfor the order size.QUOTE_INSIDE_TICKSfor placement relative to the touch.HORIZON_BLOCKSfor the model’s prediction horizon.MAX_FEE_GWEIfor the type-2 transaction fee cap.READ_RPC_URLfor order-book reads.RPC_URLfor transaction submission.
How the bot meets the 300 ms budget
The hot path is designed around exactly two RPC round trips:
- One
eth_callreads the current order book throughREAD_RPC_URL. - One
eth_sendRawTransactionsubmits the signed update throughRPC_URLand returns once accepted.
The loop deliberately avoids several potentially expensive operations:
- No per-block
eth_estimateGas; the gas limit is hardcoded or derived once at startup. - No
eth_sendRawTransactionSync, which waits until the transaction is proposed. - No gas-price lookup on the hot path; transactions use static type-2 fee settings with a 2 gwei priority fee and the
MAX_FEE_GWEIcap. - No receipt waiting in the decision loop.
- No inline vault or fee checks that could delay the order.
The README reports a dry-run mock-model benchmark of roughly 18 ms p50 for the book read and 100 ms p50 for the whole loop, with about 80 ms attributed to the mock inference stand-in.
Validation and benchmarking
Benchmark the custom book reader
Run the included benchmark to compare the custom reader with the SDK for exactness and latency:
bun run scripts/bench-read.tsVerify transaction encoding offline
The dry encoding script signs a buy and a sell without broadcasting them, then checks that the generated calldata matches the SDK:
bun run scripts/dry-encode.tsThis is especially useful before enabling live mode because the performance-oriented market module uses hand-encoded batchUpdate calldata.
Advanced operating tips
- Watch late decisions: Track
decision.lateandtotals.lateBlocks. A late model response means no order was posted for that block. - Separate intent from confirmation: Do not treat
status: "sent"as a placed order. Wait for the laterquoteevent withplaced,reverted, orlost. - Track capped quotes: When
cappedis true, compare the submitted side with the unchanged model probabilities to understand why execution diverged from the signal. - Use the SSE snapshot: New monitoring clients should initialize from the first
snapshotevent and then apply subsequent block, quote, and fill updates. - Measure both RPC paths: Slow reads delay decisions, while slow submissions reduce the chance that a quote lands before the market moves.
- Account for charged gas limits: A reverted transaction can still have a meaningful cost, so monitor
gasMon,gasUsd, and reversion totals rather than evaluating fills alone. - Validate with dry-run prints: Simulated fills depend on real prints crossing an order price, making dry-run mode more realistic than an immediate-fill assumption while still differing from actual queue position and execution.
Conclusion
jev-trader combines a live Kuru book reader, a mock or Jev decision model, optimized Monad transaction submission, position accounting, and an SSE monitoring service. Start without PRIVATE_KEY, inspect the snapshot and event stream, benchmark the critical path, and verify encoding before considering live execution. In production, monitor confirmations, late blocks, caps, reversions, inventory, P&L, and gas costs together.
