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)
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
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
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
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
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
TODOinmatch!).
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.