> For the complete documentation index, see [llms.txt](https://docs.fermi.trade/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.fermi.trade/11-matching-engine.md).

# 11 matching engine

The matching engine is the body of `Orderbook::new_order` (`programs/fermi-v1/src/state/orderbook/book.rs:43-328`). It runs during every `perp_place_order` instruction, before any of the remainder is posted to the book.

This page traces what happens to your order, in order.

## Inputs

`new_order` receives:

* The decoded `Order` (side, max\_base\_lots, max\_quote\_lots, client\_order\_id, reduce\_only, TIF, self\_trade\_behavior, params).
* A handle to both `BookSide` accounts and the `EventQueue`.
* The taker `FermiAccount`, the `PerpMarket`, current `oracle_price`, `now_ts`, `now_slot`.
* A `limit: u8` — max number of opposing orders to scan before bailing out (compute-budget protection).

## The pipeline

{% stepper %}
{% step %}

### Step 1 — IOC fee penalty

If the order's params are `Market` or `ImmediateOrCancel`, the market's `fee_penalty` is debited from the taker immediately (`book.rs:66-70`). This is independent of whether the order fills or not.
{% endstep %}

{% step %}

### Step 2 — Initialize state

```
remaining_base_lots  = order.max_base_lots
remaining_quote_lots = order.max_quote_lots
limit                = order.limit
events_emitted       = 0
out_events_left      = 5         // cap on opportunistic OutEvents per call
```

{% endstep %}

{% step %}

### Step 3 — Walk the opposing book

Loop with these per-iteration steps (`book.rs:86-209`):

{% stepper %}
{% step %}

#### Pull the next best opposing order

Pull the next best opposing order via the merge cursor over `Fixed` and `OraclePegged` trees. If empty, exit loop.
{% endstep %}

{% step %}

#### Compute the effective price

Compute the effective price of that order. For OraclePegged that's `oracle_price_lots + offset`.
{% endstep %}

{% step %}

#### Check expiry / peg validity

If the resting order is expired, emit an `OutEvent` (if the budget allows), remove it, and continue. If a pegged order is outside its peg\_limit this tick, skip it (don't remove — it might be valid next tick).
{% endstep %}

{% step %}

#### Check price compatibility

* For Bid: `remaining > 0 && best_opposing_price ≤ order_price`.
* For Ask: `remaining > 0 && best_opposing_price ≥ order_price`.
* Market and IOC orders use their internal price ceiling/floor.

If incompatible, exit the loop — done matching.
{% endstep %}

{% step %}

#### Compute match size

```
match_base = min(remaining_base_lots,
                 floor(remaining_quote_lots / best_opposing_price),
                 opposing.quantity)
```

If `match_base == 0`, exit (can't even match 1 lot at the price).
{% endstep %}

{% step %}

#### Self-trade check

If `opposing.owner == taker.owner`:

* `DecrementTake` → match for `match_base` but **emit a fill with zero fees** for both sides. (Self-trades carry no fee revenue.)
* `CancelProvide` → emit `OutEvent` to remove your own resting order, then continue (don't take it).
* `AbortTransaction` → return error; tx reverts.
  {% endstep %}

{% step %}

#### Emit FillEvent

Emit `FillEvent` with maker, taker, price, quantity, fees, and timestamps. Push to `EventQueue`.
{% endstep %}

{% step %}

#### Decrement state

```
remaining_base_lots  -= match_base
remaining_quote_lots -= match_base × best_opposing_price
opposing.quantity    -= match_base
limit                -= 1
```

{% endstep %}

{% step %}

#### Mark maker\_out when fully consumed

If `opposing.quantity == 0` → mark `maker_out = true` in the fill event; the leaf is removed and slot returned to maker on event consume.
{% endstep %}

{% step %}

#### Exit conditions

If `limit == 0` or `remaining == 0` → exit loop.
{% endstep %}
{% endstepper %}
{% endstep %}

{% step %}

### Step 4 — Post the remainder

If after the match loop there are still `remaining_base_lots > 0` and the order is `Fixed`, `OraclePegged`, or `PostOnly`-class, the matcher attempts to post (`book.rs:256-321`):

* **Market / IOC orders never post** — the remainder is just cancelled.
* **PostOnly orders only post if no match occurred at all** — otherwise the remainder is dropped, no fill, no rest.
* **PostOnlySlide orders** that *would* have crossed are re-priced to one tick *behind* the best opposing price and posted at that new price. (Or to the original price if no opposing order exists.)
* **Limit and OraclePegged limit orders** post the remainder.

Posting allocates a fresh leaf, computes the composite key with `gen_order_id` (which bumps `PerpMarket.seq_num`), and inserts into the appropriate tree. If the tree is full, the worst-priced existing order is evicted (an `OutEvent` is emitted for it, freeing its owner's slot when consumed).
{% endstep %}

{% step %}

### Step 5 — Update taker account

`book.rs:454-488` (`apply_fees`):

* `market.fees_accrued += taker_fees + maker_fees` (these are realized on the fee side immediately, even though the maker account doesn't see the credit until `consume_events`).
* `taker.taker_volume += quote_filled`.
* `taker.taker_base_lots`, `taker.taker_quote_lots` track fills already settled into the book but not yet consumed by the event queue (these are reserved for safety in health checks).

The taker's `bids_base_lots` / `asks_base_lots` are also bumped if the order posted, so future health checks correctly count the posted exposure.
{% endstep %}
{% endstepper %}

## When the maker is updated

The matcher only updates the **taker** synchronously. The maker's account is mutated lazily by `perp_consume_events`. Until then:

* The fill is *publicly visible* (the `FillEvent` is in the event queue and the `FillLogV3` was emitted).
* The maker's `quote_position_native` and `base_position_lots` have *not* yet moved.
* The maker's `bids_base_lots` / `asks_base_lots` have **already** been decremented in the matcher (so health stays consistent).
* `consume_events` settles maker fees into `quote_position_native`, emits `PerpMakerTradeLog`, and frees the perp open order slot.

`consume_events` processes up to 8 events per call (`perp_consume_events.rs:51`). Anyone can call it; in production the keeper service runs it in a tight loop.

## Evidence on chain

Every `new_order` call emits:

* One `PerpTakerTradeLog` per match group (cumulative quote/base/fees).
* One `FilledPerpOrderLog` per fill (price, quantity, taker\_oo\_slot, maker\_oo\_slot).
* For each pulled-out resting order: one `OutEvent` in the event queue and a corresponding entry in `PerpMakerTradeLog` once consumed.

These are the canonical receipts for off-chain accounting; the fanout SSE stream re-broadcasts them in real time.

## Failure modes a trader will see

| Symptom                           | Cause                                                                        |
| --------------------------------- | ---------------------------------------------------------------------------- |
| Order didn't post, nothing filled | PostOnly that would have crossed; or fully filled before reaching post step. |
| Quantity smaller than expected    | `limit` reached, or `max_quote_lots` exceeded by the price.                  |
| Self-trade aborted                | `AbortTransaction` SLT and a self-collision happened.                        |
| Order silently disappeared        | TIF expired between place and the next matcher / consume.                    |
| Eviction                          | Tree was full and a higher-priority order evicted yours.                     |

Because every state transition emits an event, the `/trace/sequence/{market}/{seq}` endpoint is the fast way to see exactly what happened to a specific order.


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter, and the optional `goal` query parameter:

```
GET https://docs.fermi.trade/11-matching-engine.md?ask=<question>&goal=<endgoal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is optional and describes the broader end goal you are ultimately trying to accomplish on behalf of the user. GitBook uses it to tailor the answer towards what is most useful for that goal.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
