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 (typically 10 slots ≈ 5 seconds). 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 (highest they’d pay). For a short, it’s the lowest they’d accept.
auctionEndPriceThe price at slot N (end of auction). This is the worst price for the taker, closer to the limit price or oracle.

The key insight: The auction starts at the taker’s best price and deteriorates toward their worst acceptable price. This creates a reverse Dutch auction: early in the auction, makers must offer great prices to fill. As the auction progresses, the bar lowers and more makers can compete.

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 + 0.10%), taker pays above oracle
  • Slot 5: auction price = midpoint (e.g., oracle + 0.05%)
  • Slot 10: auction price = auctionEndPrice (e.g., oracle), taker at their limit

For a taker going SHORT:

  • Slot 0: auction price = auctionStartPrice (e.g., oracle - 0.10%), taker sells below oracle
  • Slot 10: auction price = auctionEndPrice (e.g., oracle), 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.10 (oracle + 0.1%)
  • auctionEndPrice: $100.00 (oracle)
  • auctionDuration: 10 slots
  • At slot 3: price = 100.10+(100.10 + (100.00 - 100.10)x0.3=100.10) x 0.3 = 100.07
  • At slot 7: price = 100.10+(100.10 + (100.00 - 100.10)x0.7=100.10) x 0.7 = 100.03

A maker offering 100.05wouldbeeligibletofillstartingatslot5(whentheauctionpricereaches100.05 would be eligible to fill starting at slot 5 (when the auction price reaches 100.05). Makers offering $100.08 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 auctionStartPrice: velocityClient.convertToPricePrecision(100.1), // oracle + 0.1% auctionEndPrice: velocityClient.convertToPricePrecision(100), // oracle });
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 } 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, }; 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

Makers with better prices get priority. This pro-rata allocation ensures best execution for takers.

Auction vs DLOB

JIT AuctionDLOB
Duration5-10 slots (~2-5 seconds)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 5-10 slot delay (2-5 seconds) 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. // Note: the constructor field is still named `driftClient` even though it's typed // as `VelocityClient` -- @velocity-exchange/jit-proxy hasn't renamed the field yet. const jitter = new JitterSniper({ auctionSubscriber, driftClient: velocityClient, // ... }); // Shotgun: multiple fill attempts across auction slots const jitter = new JitterShotgun({ auctionSubscriber, driftClient: velocityClient, // ... });

Gotchas

  • Auction slots ≠ wall-clock time: auction duration is measured in Solana slots (~400ms each), not seconds. Network congestion can stretch slot times, affecting your timing assumptions.
  • 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