A minimal crypto centralized exchange (CEX)-style matching engine with an HTTP API, in-memory order books, a simple market-making loop, and optional ETH transfers against a local dev chain. The project boots an API server, a demo client, and a market maker to continuously seed liquidity and tighten spreads.
-
Go 1.22+ (tested with 1.22.2)
-
Make (optional but recommended)
-
Running JSON-RPC Ethereum node on
http://localhost:8545(Anvil, Hardhat, or Ganache) if you want token and USD transfer flows to execute fully. Without this, blockchain-dependent calls will fail.- Example (recommended) using Anvil:
anvil -p 8545
- Example (recommended) using Anvil:
main.go: Entry point. Starts the HTTP server, creates a demo client, registers users, starts a market maker, and a background market-order placer.api/api.go: Echo HTTP server setup, middleware (CORS), and route registration.handlers/orderbook.go: Request/response types and HTTP handlers for users, orders, books, trades, and best bid/ask.
core/exchange.go: Exchange state: users, order books per market, and user order indexing. ProvidesAddUser,AddOrder, andGetOrders.orderbook.go: Matching engine and data structures. DefinesOrder,Limit,OrderBook,Trade, and matching logic for LIMIT and MARKET orders; token/USD transfer hooks; best bid/ask; trade history; and current price.
auth/user.go:Usermodel with ECDSA keypair and USD balance, utilities to generate dev users, and ETH balance queries.
internals/utils.go: Utilities for ECDSA keys, Ethereum address derivation, unit conversions, RPC client, gas price, and raw ETH transfers via go-ethereum.
client/client.go: Simple HTTP client wrapper for calling the API from Go (used by the market maker and demo flow inmain.go).
market_maker/mm.go: A basic market maker: seeds an initial two-sided book and tightens the spread at an interval using LIMIT orders.
bin/: Build artifacts (make buildoutputsbin/vleho).Makefile: Convenience targets to build, run, and test.
-
On
main.gorun:- Starts the API server on
:3000. - Instantiates a
client.Clientthat talks to the local server. - Registers a few users with initial USD balances.
- Starts a
market_maker.MarketMakerwhich places LIMIT orders around the mid. - Starts a background goroutine that periodically submits MARKET orders to exercise matching.
- Starts the API server on
-
Exchange state:
- Two markets pre-initialized:
BTCandETH(seecore/exchange.go). The demo market maker and flows useETH. - Each
OrderBookmaintains:Asks(ascending by price) andBids(descending by price), both AVL trees of price levels.- Each price level (
Limit) stores FIFO orders keyed by timestamp (implemented as a tree ordered by descending timestamp for efficient head iteration). OrdersMapfor direct order lookups by UUID.- Trade tape (
Trades) and the latest traded price (CurrentPrice).
- Two markets pre-initialized:
-
Matching & settlement (see
core/orderbook.go):- LIMIT orders rest on the book and adjust aggregate bid/ask volume.
- MARKET orders sweep opposite-side limits from best price outward until filled or volume exhausted.
- Post-match, the engine calls settlement hooks to move USD between users and transfer tokens between user and exchange wallets.
- Token and USD flows:
- USD is tracked off-chain in memory (
User.USDandExchange.UsdPool). - Token “custody” is simulated via ETH transfers to/from the exchange/user wallets using the dev chain at
:8545.
- USD is tracked off-chain in memory (
Base URL: http://localhost:3000
-
Users
- POST
/user- Body:
{ "private_key": string (hex) | "", "usd": number } - If
private_keyis empty, a new ECDSA key is generated. Returns{ status, user: <userID> }.
- Body:
- GET
/user/:id- Returns the full user object (including USD; ETH balance is on-chain and not included).
- POST
-
Orders
- POST
/order?user=<userID>- Body:
{ "order_type": "LIMIT"|"MARKET", "price": number, "size": int, "bid": bool, "market": "ETH"|"BTC" } - LIMIT returns
{ status: "success", id: <orderID> }. - MARKET returns
{ status: "success", matches: [...] }or expectation-failed with an error if insufficient volume.
- Body:
- DELETE
/order?id=<orderID>&market=<ETH|BTC>- Cancels a resting LIMIT order by ID.
- GET
/order?userID=<userID>- Returns active orders for the user segregated into
AsksandBids.
- Returns active orders for the user segregated into
- POST
-
Order book & prices
- GET
/orderbook?market=<ETH|BTC>- Returns full book snapshot with
Asks,Bids, and total bid/ask volumes.
- Returns full book snapshot with
- GET
/book/bid?market=<ETH|BTC>→{ price: number }(best bid; 0 if none). - GET
/book/ask?market=<ETH|BTC>→{ price: number }(best ask; 0 if none). - GET
/trade?market=<ETH|BTC>- Returns recent trades recorded by the engine.
- GET
/marketPrice/:id?market=<ETH|BTC>- Returns
{ status, price }representing the last traded price.
- Returns
- GET
-
Build
make build # binary: ./bin/vleho -
Run (starts server, demo client, market maker, and market-order loop)
# Ensure a dev Ethereum node is running on :8545 (see Requirements) make run -
Test
make test
-
Register a user (auto-generate key)
curl -s -X POST http://localhost:3000/user \ -H 'Content-Type: application/json' \ -d '{"private_key":"","usd":100000}'
-
Place a LIMIT bid for 100 units of ETH at price 995
curl -s -X POST 'http://localhost:3000/order?user=<USER_ID>' \ -H 'Content-Type: application/json' \ -d '{"order_type":"LIMIT","price":995,"size":100,"bid":true,"market":"ETH"}'
-
Place a MARKET sell order for 50 units of ETH
curl -s -X POST 'http://localhost:3000/order?user=<USER_ID>' \ -H 'Content-Type: application/json' \ -d '{"order_type":"MARKET","price":0,"size":50,"bid":false,"market":"ETH"}'
-
Get best bid/ask
curl -s 'http://localhost:3000/book/bid?market=ETH' curl -s 'http://localhost:3000/book/ask?market=ETH'
-
Get the order book snapshot
curl -s 'http://localhost:3000/orderbook?market=ETH' -
Cancel a LIMIT order
curl -s -X DELETE 'http://localhost:3000/order?id=<ORDER_ID>&market=ETH'
- Data structures: price-time priority via AVL trees (
github.com/zyedidia/generic/avl). - Matching semantics: MARKET orders walk the book; LIMIT orders rest. After matching, trades are recorded and
CurrentPriceis updated to the last execution price. - Settlement:
- USD ledger: in-memory adjustments between users and the exchange pool.
- Token transfers: ETH transfers through
internals.TransferETHon a dev chain. This requires funded keys and a running RPC node.
- Keys used in the demo:
auth.GenerateMM/main.initMMsinclude static private keys intended for local dev only. Do not use them on public networks.
- Server: listens on
:3000(seeapi/api.go). - Client: uses
http://localhost:3000(seeclient/client.go). - Markets:
ETHandBTCare initialized; the demo usesETH. - Dev chain: expected at
http://localhost:8545(seeinternals/utils.go). - Make targets:
build,run,test.
- This is an in-memory demo service; no persistence or durability.
- If no Ethereum node is running on
:8545or keys are unfunded, ETH transfer calls may fail or cause panics due to unchecked errors in utility calls. Run a local node as described or avoid flows that require token movement. - Not production grade; for learning and experimentation.