Velocity ProtocolDevelopers
DevelopersTrading automationKeeper Bots

JIT Maker Bot

This tutorial shows how to run a JIT Maker bot in TypeScript from apps/keeper-bots-v2 in the velocity-v1 monorepo. Velocity has no Python SDK, so there is no Python equivalent.

Market orders go through Just-In-Time (JIT) Auctions where Makers fight to fill orders before the order is allowed to fill against the Velocity AMM.

Running the bot

☠️ This bot requires collateral to run. This tutorial is a developer's guide and holds no responsibility over bot outcomes.

1. Clone the monorepo

The JIT Maker example lives in apps/keeper-bots-v2 inside the velocity-v1 monorepo.

The velocity-v1 monorepo is not public yet. It will be published once the post-fork audit report is final. Until then, ask the team for access to run this bot.

git clone <velocity-v1>
cd velocity-v1
bun install   # run once, at the repo root, this is a Bun workspace
cd apps/keeper-bots-v2

2. Prepare a keypair and Velocity account.

Refer to the bot wallet setup section for how to set up a bot wallet.

3. Prepare the config file.

The jit-maker-config.yaml file is a good starting point. Fill in the following values:

  • global.endpoint: the RPC endpoint (see RPC Providers)
  • global.keeperPrivateKey: the bot private key (alternatively, leave it blank and set the KEEPER_PRIVATE_KEY environment variable)
  • botConfigs.jitMaker.aggressivenessBps: how aggressively the jit maker quotes. If set to 30, the bot will attempt to buy 30 bps above the best bid, and sell 30 bps below the best ask.
  • botConfigs.jitMaker.marketType: use perp. Velocity does not support spot order matching (spot markets are collateral-only), so a spot config produces jit transactions that always revert with SpotOrdersNotSupported.
  • botConfigs.jitMaker.marketIndexes: the list of markets to jit make
  • botConfigs.jitMaker.subaccounts: the subaccount to use for each marketIndex provided

Note: subaccounts and marketIndexes are a direct mapping, so the below config will use subaccount 0 for marketIndex 0, and subaccount 1 for marketIndex 1:

botConfigs:
  jitMaker:
    marketType: perp
    marketIndexes:
      - 0
      - 1
    subaccounts:
      - 0
      - 1

4. Run the bot

Start the bot with:

bun run dev --config-file=jit-maker-config.yaml

Technical Explanation

Strategy overview

This bot uses the JitterShotgun strategy and the jit-proxy program, through the @velocity-exchange/jit-proxy client on npm, to try to fill jit maker fills. See JIT Auctions and JIT-only market making for how the JitMaker strategy fits into the broader JIT auction mechanism. The bot runs the JIT makers lane below:

One taker order through a JIT auction

Takeroff-chainProgramon-chainEvent feedon-chainJIT makersoff-chainKeeperoff-chainAuction opensAuction windowAuction ends unfilledTaker to Program: Order with auction paramsOrder withauction paramsProgram to Event feed: Taker order eventTaker ordereventEvent feed to JIT makers: New auctionNew auctionJIT makers to itself: Price at this slotPrice at thisslotJIT makers to Program: Place and make fillPlace and make fillProgram to Taker: Filled at auction priceFilled at auctionpriceAny maker can fill any part of the order, first come firstserved, so partial fills are normal.Keeper to Program: Fill from DLOB or AMMFill from DLOB or AMMResting orders and the AMM are matched by price ateach level, not in a fixed order. The taker can submit thisfill themselves instead.Program to Taker: Fill, or the order expiresFill, or the orderexpires
Shows the order of events for a single taker order, from placement to a maker fill or to the fallback after the auction. Sources: the JIT FAQ, JIT Auctions, and Matching Engine pages in these docs. The event feed is the on-chain event emitter that makers subscribe to. The auction price ramps from the taker's best price toward their limit as slots pass, so filling early costs a maker more. Durations are counted in Solana slots, not seconds, and a limit order still open when its auction ends rests on the DLOB, where it can then fill as a maker.

The JitterShotgun strategy continuously sends transactions in an attempt to fill orders as soon as it sees one that crosses. The jit-proxy program is a permissionless and stateless program that does the last-mile checks onchain to ensure the fill is within our desired bid/ask price and does not exceed min/max positions. It is deployed under Velocity's own program ID (J1TPRoXCtGuMcWiWFE6RB9eZU8U35PBMETCwNQLCNPhQ), and the client is built against @velocity-exchange/sdk/VelocityClient.

JIT-able orders

Orders with auctionDuration > 0 may be filled by jit makers at any time. This bot finds these orders by using the programSubscribe RPC method, and filtering for users with new orders that meet this criteria.

Final notes and future optimizations

The shipped strategy quotes a fixed offset on both sides of one market and sends until something fills. Places it leaves on the table:

  • Symmetric offsets. One aggressivenessBps sets both the bid and the ask. Splitting it allows skewing quotes against inventory already held.
  • One market per subaccount. The config maps each marketIndex to its own subaccount rather than running a shared book across markets.
  • Untimed sending. JitterShotgun sends without regard to how much of the auction is left or how many transactions it has already spent on the same order.

Troubleshooting

Resubscribing log messages

No ws data from user in 30000ms, resubscribing
No ws data from userStats in 30000ms, resubscribing
No ws data from perpMarket in 30000ms, resubscribing
No ws data from perpMarket in 30000ms, resubscribing
No ws data from perpMarket in 30000ms, resubscribing
No ws data from spotMarket in 30000ms, resubscribing

This is a notification from the Velocity SDK that it is restarting its websocket connection with the RPC due to no messages being received within the set time. This is generally not an error and pretty common for less active markets that don't have much activity.

Running JIT periodic tasks...

[2026-02-27T00:04:31.387Z] Running JIT periodic tasks...
[2026-02-27T00:04:31.389Z] info: (mkt index: JTO-PERP) base to market make (targetLvg=0.95): 1481.8891930588115 = 3476.551044 / 2.228725 * 0.95

This is a normal status message: the bot is running its periodic tasks as expected.

Order does not cross params yet, retrying

Trying to fill 24yfkkuFv849BpCBBo49tV6i6ycfc3aEUY6wWKFsN4SS-177961
SendTransactionError: failed to send transaction: Transaction simulation failed: Error processing Instruction 2: custom program error: 0x1771
  ...
  logs: [
    'Program ComputeBudget111111111111111111111111111111 invoke [1]',
    'Program ComputeBudget111111111111111111111111111111 success',
    'Program ComputeBudget111111111111111111111111111111 invoke [1]',
    'Program ComputeBudget111111111111111111111111111111 success',
    'Program J1TPRoXCtGuMcWiWFE6RB9eZU8U35PBMETCwNQLCNPhQ invoke [1]',
    'Program log: Instruction: Jit',
    'Program log: slot = 250667476 auction duration = 32 ms_left = 6000',
    'Program log: taker order type Oracle auction start -12100 auction end -200 limit price 0 oracle price offset -193',
    'Program log: taker price 2220700 < worst ask 2234257',
    'Program log: AnchorError occurred. Error Code: AskNotCrossed. Error Number: 6001. Error Message: AskNotCrossed.',
    'Program J1TPRoXCtGuMcWiWFE6RB9eZU8U35PBMETCwNQLCNPhQ consumed 39672 of 599700 compute units',
    'Program J1TPRoXCtGuMcWiWFE6RB9eZU8U35PBMETCwNQLCNPhQ failed: custom program error: 0x1771'
  ]

Failed to fill 24yfkkuFv849BpCBBo49tV6i6ycfc3aEUY6wWKFsN4SS-177961
Order does not cross params yet, retrying

This is a failed tx message from the RPC provider when a transaction is sent. It is common for the shotgun strategy to get this message as it sends many transactions at once.

Decoding it further:

  • It is a jit fill attempt on taker 24yfkkuFv849BpCBBo49tV6i6ycfc3aEUY6wWKFsN4SS's orderId:177961
  • the current slot (that the RPC is simulating the transaction in) is 250667476, the auction is 32 units long, and 6000 ms of it remain. auction_duration counts wall-clock 400ms units rather than slots, so 32 units is 12.8 seconds; the program computes ms_left itself and logs milliseconds, not a slot count
  • the taker's order is an Oracle order, with an offset of -12100 (-$0.0121) to -200 (-$0.0020)
  • the taker's order is a buy (since it is comparing with our ask price)
  • the taker's price at that slot is 2220700 ($2.2207), which is below our worst ask 2234257 ($2.2343), so the jit-proxy program threw an error to prevent sending a failing transaction onchain