DEV Community

Cover image for How to Match Orders in 100 Lines of Ruby
Stefan Buhrmester
Stefan Buhrmester

Posted on

How to Match Orders in 100 Lines of Ruby

Order matching has dropped in Shitcoin Swap in ~100 lines of Ruby.

Most crypto exchanges reach for an existing matching engine or a Uniswap-style AMM. We wrote our own — not because we're smarter, but because the problem is simpler than people think, and understanding every line of your matching logic pays off if things go sideways at 3 AM.

The data model

Two tables, one idea:

  • Account — holds a balance of one asset. Each user gets one account per asset.
  • Order — says "I want to sell X of asset A to buy Y of asset B." Tracks how much is funded, how much remains, and whether it's filled.
Account(id, user_id, asset_id, balance)

Order(id, account_id, sell_asset_id, buy_asset_id,
      sell_amount, buy_amount, funded_amount,
      remaining_sell_amount, price, completed, cancelled_at)
Enter fullscreen mode Exit fullscreen mode

Orders are pre-funded at creation time — the funded_amount is automatically set to the minimum of your sell_amount and your account's available balance (which subtracts funds already locked in other active orders):

def available_balance
  balance - orders.active.sum(:funded_amount)
end

def fund!
  self.funded_amount = [account.available_balance, sell_amount].min
  save!
end
Enter fullscreen mode Exit fullscreen mode

You can place an order for any amount you want. But if your account can't cover it, funded_amount gets capped at what's available — and an order with no funding won't match anything. It'll just sit there until you deposit. No rejection, no error, just waiting.

The matching algorithm

When an order is created, it funds itself, then searches for a counterparty.

Step 1 — find matching orders:

def matching
  result = Order.active.where(
    sell_asset_id: buy_asset_id,
    buy_asset_id: sell_asset_id
  )

  if price
    result = result.where("price IS NULL OR price >= ?", 1.0 / price)
  end

  result
end
Enter fullscreen mode Exit fullscreen mode

Two orders match when their asset pairs are flipped. If the order has a limit price, we filter out counterparties whose price would give us less than we asked for — a single WHERE clause that handles the unit conversion implicitly.

Step 2 — agree on a price and execute:

def match!(other)
  return if completed? || remaining_sell_amount <= 0 || funded_amount <= 0

  # Negotiate price
  if other.price
    price = [self.price, 1.0.to_r / other.price].compact.min
  elsif self.price
    price = self.price
  else
    return  # both market orders — can't determine fair rate
  end

  amount_affordable = funded_amount.to_r / price
  amount = [amount_affordable, other.funded_amount.to_r].min.to_r
  return unless amount > 0

  # Execute
  self.remaining_sell_amount -= price * amount
  self.funded_amount -= price * amount
  other.remaining_sell_amount -= amount
  other.funded_amount -= amount

  self.completed = true if remaining_sell_amount <= 0
  other.completed = true if other.remaining_sell_amount <= 0

  [other, self].each(&:save!)
end
Enter fullscreen mode Exit fullscreen mode

Price discovery has three cases:

This order Other order Result
Limit Limit Trade at min(price(this), 1/price(other)) — satisfies both
Limit Market Trade at this order's price
Market Limit Trade at other order's price (taker accepts market price)
Market Market Skip — no fair rate determinable

The trade amount is the minimum of what we can afford and what the counterparty has. Both sides debit proportionally. Either side hits zero remaining → marked complete. Partial fills happen naturally.

Two details worth mentioning

Rational numbers. You'll see to_r everywhere. Floating-point is fine for 50000 / 1 but not for 1 / 3 repeated across dozens of conversions. Ruby's Rational gives us exact arithmetic during matching; we cast to decimal only for database storage.

Pessimistic locking. Two concurrent matches on the same order would double-spend the funded amount. process! locks both rows with SELECT ... FOR UPDATE:

def process!
  self.lock!
  matching.lock.each do |other|
    match!(other)
    break if completed?
  end
end
Enter fullscreen mode Exit fullscreen mode

The database serializes access. With SQLite that means one writer at a time — fine for now. PostgreSQL would give row-level granularity when needed.

What's still missing

  • Account settlement: We debit order state but haven't hooked up balance transfers yet (that's the TODO in match!).

The takeaway

An order matching engine doesn't need to be complex. Find counterparty, negotiate price, debit both sides — it fits in ~100 lines. The hard parts aren't algorithmic; they're concurrency, numeric precision, and making sure the accounting never drifts by a single satoshi.


Shitcoin Swap is a work in progress. Follow along or contribute at github.com/shitcoinsociety.

Top comments (0)