Skip to Content

JIT Auctions

JIT (Just-In-Time) auctions are Velocity’s price discovery mechanism. When a taker order arrives (market order or aggressive limit crossing the spread), it enters an auction where market makers compete to fill it at better prices before it hits the DLOB or AMM.

Why JIT auctions?

Without JIT, taker orders would immediately execute against resting DLOB orders or the AMM at potentially worse prices. JIT auctions:

  • Improve price execution for takers by giving makers time to offer better prices
  • Reduce adverse selection by letting makers react to toxic flow
  • Increase competition among market makers for the same fill
  • Enable offchain quoting where makers don’t need to rest orders, just respond to auctions

This makes Velocity’s pricing more efficient than traditional limit order books.

Auction parameters

Every taker order that enters a JIT auction has three key parameters that define the auction:

ParameterDescription
auctionDurationNumber of slots the auction runs (a caller-requested value can be raised by order sanitization to a market/tier-derived floor; see the gotcha below). After this, unfilled size falls through to DLOB/AMM.
auctionStartPriceThe price at slot 0 of the auction. For a long, this is the best price for the taker (lowest they’d pay). For a short, it’s the highest they’d receive.
auctionEndPriceThe price at slot N (end of auction). This is the worst price for the taker, at their limit price (closer to, or past, the oracle).

The key insight: For a long, auctionStartPrice must be less than or equal to auctionEndPrice (the program rejects the order with InvalidOrderAuction otherwise); for a short, auctionStartPrice must be greater than or equal to auctionEndPrice. The auction starts at the taker’s best price and ramps toward their worst acceptable price as slots pass: early in the auction, makers must offer a price close to the taker’s best price to win the fill. As the auction progresses, the price moves toward the taker’s limit and more makers can compete profitably.

Auction timeline

Slot 0 Slot 5 Slot 10 (end) | | | Best price ──────────────────► Worst price (for taker) for taker Falls to DLOB/AMM Hardest for Easier for Auction over, makers to makers to remaining size win fills win fills goes to DLOB

For a taker going LONG:

  • Slot 0: auction price = auctionStartPrice (e.g., oracle), taker pays at or near oracle
  • Slot 5: auction price = midpoint (e.g., oracle + 0.05%)
  • Slot 10: auction price = auctionEndPrice (e.g., oracle + 0.10%), taker at their limit

For a taker going SHORT:

  • Slot 0: auction price = auctionStartPrice (e.g., oracle), taker sells at or near oracle
  • Slot 10: auction price = auctionEndPrice (e.g., oracle - 0.10%), taker at their limit

Makers who fill closer to slot 0 are giving the taker a better price (and taking more risk). Makers who wait until later slots get easier fills but at less favorable prices.

Auction pricing formula

Auction prices interpolate linearly from start to end over the auction duration:

Auction Price(slot) = start_price + (end_price - start_price) x progress where progress = min(1, (current_slot - auction_start_slot) / auction_duration)

Example (long market order, oracle at $100):

  • auctionStartPrice: $100.00 (oracle)
  • auctionEndPrice: $100.10 (oracle + 0.1%, the taker’s limit)
  • auctionDuration: 10 slots
  • At slot 3: price = $100.00 + ($100.10 - $100.00) x 0.3 = $100.03
  • At slot 7: price = $100.00 + ($100.10 - $100.00) x 0.7 = $100.07

A maker offering $100.05 would be eligible to fill starting at slot 5 (when the auction price reaches $100.05). Makers offering $100.02 could fill as early as slot 2.

Auction lifecycle

1. Taker places order

import { OrderType, PositionDirection } from "@velocity-exchange/sdk"; // oraclePrice and auction prices are BN, PRICE_PRECISION (1e6) await velocityClient.placePerpOrder({ orderType: OrderType.MARKET, direction: PositionDirection.LONG, baseAssetAmount: size, auctionDuration: 10, // 10 slots (may be raised by sanitization) auctionStartPrice: velocityClient.convertToPricePrecision(100), // oracle auctionEndPrice: velocityClient.convertToPricePrecision(100.1), // limit, oracle + 0.1% });
Method VelocityClient.placePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` / `triggerPrice` / `oraclePriceOffset` (signed) / `auctionStartPrice` / `auctionEndPrice` are PRICE_PRECISION (1e6).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account to place the order for; defaults to the active sub-account.
No
isolatedPositionDepositAmount
any
If set and the order increases the position, a transfer into an isolated-margin position (token-mint precision) is prepended in the same transaction.
No
Returns
Promise<string>

2. Auction starts: the order enters auction for auctionDuration slots. The auction price interpolates from auctionStartPrice toward auctionEndPrice.

3. Market makers compete: makers observe the auction and submit fills at prices within the auction range.

// AuctionSubscriber has no getAuction()/pull-style API -- it's event-driven. // It emits 'onAccountUpdate' with the taker's UserAccount whenever an order changes. auctionSubscriber.eventEmitter.on("onAccountUpdate", async (takerUserAccount, pubkey, slot) => { // Inspect takerUserAccount.orders for the specific order in auction, then: await velocityClient.placeAndMakePerpOrder( makerOrderParams, takerInfo // includes taker's order and user account ); });

4. Auction resolves: best maker(s) fill the taker. If partially filled, remaining size continues through the auction. If unfilled after auctionDuration slots, it matches against DLOB, then AMM.

Maker participation

To participate in JIT auctions, bots typically:

1. Subscribe to auction feed

import { AuctionSubscriber } from "@velocity-exchange/sdk"; const auctionSubscriber = new AuctionSubscriber({ velocityClient, opts: { commitment: "processed" } }); await auctionSubscriber.subscribe();
Class AuctionSubscriberReference ↗

AuctionSubscriber — websocket program-account subscription scoped to `User` accounts that currently have at least one order in its auction window (`getUserWithAuctionFilter`). Used by keepers/fillers to react to auction-eligible orders (JIT-fillable or approaching the end of a Dutch auction) without scanning every user account.

PropertyTypeRequired
velocityClient
any
Yes
opts
any
Yes
resubOpts
any
No
eventEmitter
StrictEventEmitter<EventEmitter, AuctionSubscriberEvents>
Yes
subscriber
any
No
subscribe
() => Promise<void>
Establishes the filtered program-account subscription (idempotent — reuses the existing subscriber if already created) and emits `onAccountUpdate` for each matching account update.
Yes
unsubscribe
() => Promise<void>
Tears down the subscription. No-op if never subscribed.
Yes

2. Filter and price auctions

import { getAuctionPrice, isVariant } from "@velocity-exchange/sdk"; // AuctionSubscriber emits 'onAccountUpdate' with UserAccount data auctionSubscriber.eventEmitter.on("onAccountUpdate", (userAccount, pubkey, slot) => { // Find orders in auction (hasAuction flag is set by the memcmp filter) for (const order of userAccount.orders) { if ( !isVariant(order.status, "open") || order.baseAssetAmount.eq(order.baseAssetAmountFilled) ) continue; // Calculate current auction price at this slot. Pass the market's tick size // (PerpMarketAccount.orderTickSize) so this matches the program's own rounding -- // see Orderbook & Matching's tick-size section for why this matters. const perpMarket = velocityClient.getPerpMarketAccount(order.marketIndex); const oracleData = velocityClient.getMMOracleDataForPerpMarket(order.marketIndex); const auctionPrice = getAuctionPrice(order, slot, oracleData.price, perpMarket.orderTickSize); // Check if your desired fill price is within the current auction range const myFillPrice = calculateMyPrice(oracleData, inventory); if (isProfitable(myFillPrice, auctionPrice, order.direction)) { fillAuction(order, userAccount, pubkey, myFillPrice); } } });
Function getAuctionPriceReference ↗

Dispatches to the correct in-progress auction price for `order` based on its order type: fixed-price auction (`getAuctionPriceForFixedAuction`) for market/triggerLimit/plain-limit orders, or oracle-offset auction (`getAuctionPriceForOracleOffsetAuction`) for oracle-pegged limit/oracle/oracle-triggered-market orders. The result is always standardized to `tickSize`.

ParameterTypeRequired
order
Order
Order whose auction price to compute.
Yes
slot
number
Current slot.
Yes
oraclePrice
any
Use `MMOraclePriceData` source for perp orders, `OraclePriceData` for spot; PRICE_PRECISION (1e6).
Yes
tickSize
any
Market's order tick size, PRICE_PRECISION (1e6). Defaults to `ONE` (no effective standardization).
No

Auction price at the current slot, PRICE_PRECISION (1e6).

Returns
BN

3. Risk management

  • Oracle validity: Reject if oracle is stale or invalid
  • Position limits: Don’t fill if it exceeds your max position
  • Toxic flow detection: Skip auctions from certain patterns
  • Inventory skew: Adjust participation based on current inventory

Place-and-make pattern

The placeAndMakePerpOrder instruction atomically:

  1. Places your maker order onchain
  2. Fills against the taker order
  3. Settles PnL in a single transaction

This ensures you’re credited as the maker (earning rebates) while filling the taker atomically.

import { OrderType, PositionDirection, PostOnlyParams, OrderParamsBitFlag } from "@velocity-exchange/sdk"; const makerOrderParams = { orderType: OrderType.LIMIT, marketIndex: auction.order.marketIndex, direction: PositionDirection.SHORT, // opposite of taker's LONG price: velocityClient.convertToPricePrecision(myFillPrice), baseAssetAmount: auction.order.baseAssetAmount, // fill entire order postOnly: PostOnlyParams.MUST_POST_ONLY, // Required: the program rejects any place-and-make maker order that // isn't IOC + post-only + limit with InvalidOrderIOCPostOnly. bitFlags: OrderParamsBitFlag.ImmediateOrCancel, }; const takerInfo = { taker: takerPubkey, // PublicKey of taker's user account takerStats: takerStatsPubkey, // PublicKey of taker's UserStats PDA takerUserAccount: takerUserAccount, // decoded UserAccount order: takerOrder, // the specific Order to fill }; await velocityClient.placeAndMakePerpOrder(makerOrderParams, takerInfo);
Method VelocityClient.placeAndMakePerpOrderReference ↗
ParameterTypeRequired
orderParams
OptionalOrderParams
Maker order to place; `baseAssetAmount` is BASE_PRECISION (1e9), `price` is PRICE_PRECISION (1e6). Must have `orderType: LIMIT`, `postOnly` set, and be IOC.
Yes
takerInfo
TakerInfo
The taker account/order to fill against (`takerInfo.order.orderId` must be open).
Yes
txParams
TxParams
Optional compute-unit/priority-fee overrides.
No
subAccountId
number
Sub-account placing the maker order; defaults to the active sub-account.
No
takerEscrow
RevenueShareEscrowAccount
The taker's decoded `RevenueShareEscrow`. Required to attach the escrow when the taker is referred but their order carries no builder code — the builder case is detected automatically from `takerInfo.order`.
No
Returns
Promise<string>

Multi-maker fills

Multiple makers can fill the same taker order:

  • Maker A fills 30% at oracle + 0.03%
  • Maker B fills 50% at oracle + 0.01%
  • Remaining 20% hits DLOB or AMM

Fills are allocated sequentially in price order, not split pro rata: the fill plan walks the price-sorted maker list and fills each maker in turn, with ties broken by DLOB arrival order.

Auction vs DLOB

JIT AuctionDLOB
Duration5-10 slots (~2-4 seconds at the 400ms baseline slot duration)Orders rest indefinitely
PricingDynamic, interpolates toward oracleFixed price set at placement
CommitmentNone until fill, makers choose per-auctionOnchain, orders are committed
Best forActive makers, flow-selective strategiesPassive makers, committed liquidity
PriorityRuns firstFallback after auction

JIT runs first, DLOB provides backup liquidity if auction doesn’t fully fill.

Performance considerations

For makers:

  • Subscribe with commitment: "processed" for lowest latency
  • Use WebSocket or gRPC subscriptions (not polling)
  • Pre-compute oracle prices and risk checks
  • Keep fills under compute budget (400k CU typical)

For takers:

  • Auction adds a 5-10 slot delay (2-4 seconds at the 400ms baseline slot duration) before execution
  • You get better prices but not instant fills
  • Use market orders for auction participation (limit orders bypass auction if they don’t cross spread)

AMM as a JIT maker

The AMM isn’t only the fallback of last resort (see Orderbook & Matching): inside a DLOB match it can also compete as a JIT maker against your resting order for the same fill. Whether it does depends on a set of hard gates checked at fill time:

  • AmmFill isn’t paused for the market (PerpOperation.AMM_FILL)
  • the AMM doesn’t currently have too much drawdown
  • the oracle (including the MM oracle, when active and recent) is valid and not too volatile vs. the exchange oracle

If any of these gates trips, the AMM is excluded from that DLOB match entirely: you’re only competing against other DLOB makers for that fill, not the AMM. This is separate from the auction-timing rules that decide whether the AMM can fill a fresh taker order immediately outside of a DLOB match. Practically: don’t assume the AMM is always a competing quote inside the auction, during a pause, drawdown, or oracle-stress event it drops out, and DLOB-only makers pick up the full remaining size.

JIT Proxy library

Not yet published to npm. @velocity-exchange/jit-proxy isn’t on the npm registry yet. Build it from packages/jit-proxy in the velocity-v1 monorepo  until it is. The client is vendored in-repo, ported to Anchor 1.0, and built against @velocity-exchange/sdk; it exposes the same JitProxyClient / JitterSniper / JitterShotgun API as upstream.

The package provides higher-level abstractions for auction participation:

  • JitterSniper: waits for the optimal auction slot before submitting a single fill transaction. Best for precise pricing with lower compute costs.
  • JitterShotgun: submits fill transactions at multiple auction slots simultaneously. Higher fill rate but uses more compute and SOL for fees.

The JitMaker bot in keeper-bots-v2 demonstrates both strategies and includes market volatility checks, position sizing, and DLOB-aware pricing.

import { JitterSniper, JitterShotgun, PriceType } from "@velocity-exchange/jit-proxy"; // Sniper: one precise fill attempt. const jitter = new JitterSniper({ auctionSubscriber, velocityClient, // ... }); // Shotgun: multiple fill attempts across auction slots const jitter = new JitterShotgun({ auctionSubscriber, velocityClient, // ... });

Gotchas

  • Auction slots ≠ wall-clock time: auction duration is measured in Solana slots, not seconds. Slot duration is a configurable State field that steps down from a 400ms baseline toward 200ms over time (so 10 slots is roughly 4 seconds today but shortens as the schedule advances), and network congestion can stretch slot times further on top of that.
  • Requested duration can be raised: auctionDuration you pass is a floor, not a guarantee. Order sanitization can raise it to a market/tier-derived minimum based on the requested price range, so the on-chain value may exceed what you set.
  • Partial fills are common: multiple makers compete for the same auction. Your fill may be partial; handle baseAssetAmountFilled < baseAssetAmount gracefully.
  • Compute budget for place-and-make: these transactions are heavier than simple order placement. Budget 400-800k CU (the JitMaker defaults to 800k). Under-budgeting causes silent failures.
  • Stale takerInfo: if you hold a taker reference too long, the taker’s order may already be filled or cancelled. Check order.baseAssetAmount - order.baseAssetAmountFilled for remaining size.
Last updated on