# JIT Auctions

> Canonical: https://docs.velocity.exchange/developers/market-makers/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

## Auction parameters

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

| Parameter | Description |
|---|---|
| `auctionDuration` | How long the auction runs, in wall-clock units of 400ms (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. |
| `auctionStartPrice` | The price at **the start** 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. |
| `auctionEndPrice` | The price at **the end** of the 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 time passes: 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

The figure uses the worked example of a 10-unit auction (10 x 400ms = 4 seconds) from \$100.00 to \$100.10. The unit is wall-clock time, not a live slot, so the auction lasts the same 4 seconds whatever the current slot duration; see [Slot duration and wall-clock time](/developers/concepts/slot-duration.md).

**For a taker going LONG:**
- Unit 0: auction price = `auctionStartPrice` (e.g., oracle), taker pays at or near oracle
- Unit 5: auction price = midpoint (e.g., oracle + 0.05%)
- Unit 10: auction price = `auctionEndPrice` (e.g., oracle + 0.10%), taker at their limit

**For a taker going SHORT:**
- Unit 0: auction price = `auctionStartPrice` (e.g., oracle), taker sells at or near oracle
- Unit 10: auction price = `auctionEndPrice` (e.g., oracle - 0.10%), taker at their limit

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

## Auction pricing formula

Auction prices interpolate linearly from start to end over the auction duration. The program measures elapsed slots and converts them to wall-clock milliseconds through the live slot duration before comparing them against `auctionDuration`:

```
Auction Price(t) = start_price + (end_price - start_price) x progress

where elapsed_ms  = wall clock elapsed since auction_start_slot
      duration_ms = auction_duration x 400
      progress    = min(1, elapsed_ms / duration_ms)
```

**Example** (long market order, oracle at \$100):
- `auctionStartPrice`: \$100.00 (oracle)
- `auctionEndPrice`: \$100.10 (oracle + 0.1%, the taker's limit)
- `auctionDuration`: 10 (4 seconds)
- At 1.2s elapsed: price = \$100.00 + (\$100.10 - \$100.00) x 0.3 = \$100.03
- At 2.8s elapsed: price = \$100.00 + (\$100.10 - \$100.00) x 0.7 = \$100.07

A maker offering \$100.05 would be eligible to fill from 2 seconds in (when the auction price reaches \$100.05). Makers offering \$100.02 could fill as early as 0.8 seconds in.

## Auction lifecycle

**1. Taker places order**

  
```typescript
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 x 400ms = 4s (may be raised by sanitization)
  auctionStartPrice: velocityClient.convertToPricePrecision(100),   // oracle
  auctionEndPrice: velocityClient.convertToPricePrecision(100.1),   // limit, oracle + 0.1%
});
```
  

**2. Auction starts**: the order enters auction for `auctionDuration` x 400ms of wall clock. 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.

```typescript
// 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 the `auctionDuration` window elapses, remaining size can fill against resting DLOB orders and the AMM, matched by price at each level rather than in a fixed order (see [Orderbook & Matching](./orderbook-and-matching#how-matching-works)).

## Maker participation

To participate in JIT auctions, bots typically:

**1. Subscribe to auction feed**

  
```typescript
import { AuctionSubscriber } from "@velocity-exchange/sdk";

const auctionSubscriber = new AuctionSubscriber({
  velocityClient,
  opts: { commitment: "processed" }
});

await auctionSubscriber.subscribe();
```
  

**2. Filter and price auctions**

  
```typescript
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 the 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);
    }
  }
});
```
  

**3. Risk management**
- **Oracle validity**: Reject if oracle is stale or invalid
- **Position limits**: Skip the fill if it would push the bot past its 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 the maker order onchain
2. Fills against the taker order
3. Settles P&L in a single transaction

The order is credited as the maker side, earning rebates, while the taker is filled in the same transaction.

  
```typescript
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);
```
  

## 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 Auction | DLOB |
|---|---|---|
| **Duration** | `auctionDuration` x 400ms of wall clock | Orders rest indefinitely |
| **Pricing** | Dynamic, interpolates toward oracle | Fixed price set at placement |
| **Commitment** | None until fill, makers choose per-auction | Onchain, orders are committed |
| **Best for** | Active makers, flow-selective strategies | Passive makers, committed liquidity |
| **After the window** | Unfilled size falls through to resting orders and the AMM | The order stays on the book until filled or cancelled |

Once the auction window elapses, whatever is left is matched by price at each level rather than in a fixed source order. See [Orderbook & Matching](./orderbook-and-matching#how-matching-works).

## 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 delay before execution. An `auctionDuration` of 5 to 10 units is 2 to 4 seconds of wall clock
- The price improvement is paid for with that delay: an auctioned order is not an instant fill
- Use market orders for auction participation (limit orders bypass auction if they don't cross spread)

## The vAMM as a competing quote

The AMM isn't only the fallback of last resort. `determine_perp_fulfillment_methods` walks the crossing makers in price order and inserts an `AMM` step **ahead of** any maker the AMM out-prices, capped at that maker's price, before falling back to a residual `AMM` step at the end (see [Orderbook & Matching](./orderbook-and-matching#how-matching-works)).

Two things about it matter when a maker prices against it:

- **It jumps ahead of a maker it beats.** If the AMM's bid or ask (spread and reference price offset included) is better than the maker's resting price, the taker's size hits the AMM first, at the maker's price, not the AMM's own. Being on the book is not enough: a maker has to beat the AMM's quote to see the fill.
- **It also competes inside the match.** When `amm_jit_allowed` holds, the `Match` step itself builds an `AmmJitQuoter` alongside the maker's order and the two split that fill. Those fills are recorded with `OrderActionExplanation.OrderFilledWithAMMJit`.

Whether the vAMM participates at all depends on hard gates checked at fill time:

- `AmmFill` isn't paused for the market (`PerpOperation.AMM_FILL`)
- the AMM's drawdown is inside the limit the program checks on each fill
- the oracle (including the MM oracle, when active and recent) is valid and not too volatile vs. the exchange oracle

If any gate trips, `amm_can_fill_order` returns false and the fill plan contains `Match` steps only. Practically: don't assume the vAMM is always a competing quote. During a pause, drawdown, or oracle-stress event it drops out, and the other makers pick up the full remaining size.

## JIT Proxy library

`@velocity-exchange/jit-proxy` is on npm. It declares two peer dependencies and does not install them for you. Anchor is pinned to `@anchor-lang/core@1.0.1` under the `@coral-xyz/anchor` alias, the same form the SDK uses, so installing `@coral-xyz/anchor` unaliased pulls the wrong Anchor:

```bash
bun add @velocity-exchange/jit-proxy
bun add @coral-xyz/anchor@npm:@anchor-lang/core@1.0.1 @solana/web3.js@1.98.0
```

The client is ported to Anchor 1.0 and built against `@velocity-exchange/sdk`; it exposes the same `JitProxyClient` / `JitterSniper` / `JitterShotgun` API as upstream. Source lives at `packages/jit-proxy` in the `velocity-v1` monorepo, which is not public yet, so npm is the only way to get it.

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.

```typescript
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

- **`auctionDuration` is wall clock, not slots**: the field stores 400ms units of wall-clock time, so `auctionDuration: 10` is 4 seconds and stays 4 seconds as Solana's slot time steps down from 400ms toward 200ms. `get_auction_duration` clamps the sanitized value to the range 1 to 180 (72 seconds) and applies no slot conversion. At fill time the program converts elapsed slots into wall clock through the live slot duration on `State`, so a shorter slot time means more slots inside the same auction, not a shorter auction. Do not multiply `auctionDuration` by the current slot time to get seconds: multiply by 400ms.
- **Requested duration can be raised**: the `auctionDuration` passed in is a floor, not a guarantee. Two separate rules can raise it, so the onchain value may exceed the requested one. Order sanitization raises it to a minimum derived from the requested price range and the market's contract tier: tier A and tier B grant 100 units per 1% of range, lower tiers 60 units, clamped to 1 to 180 units. On top of that, placement raises it to the exchange-wide floor held in `State.minPerpAuctionDuration`; read the live `State` account for that value. Market and oracle orders always get at least that floor. A limit order placed with an explicit `auctionDuration` of `0` keeps `0` and rests immediately, but any non-zero value on a limit order is raised to the floor too, so a requested duration below the floor never survives placement.
- **Partial fills are common**: multiple makers compete for the same auction. A 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`**: a taker reference held too long can point at an order that is already filled or cancelled. Check `order.baseAssetAmount - order.baseAssetAmountFilled` for remaining size.

## Related

- [Orderbook & Matching](./orderbook-and-matching): DLOB and how JIT, resting, and AMM liquidity compete on price
- [Matching Engine](/protocol/trading/how-fills-work.md): Full liquidity priority flow
- [JIT-only MM](./jit-only): Building a JIT market maker bot
- [SWIFT API](./swift-api): see taker orders 100 to 500 ms before they hit the auction
- `@velocity-exchange/jit-proxy`: JIT proxy SDK, on npm
