Case study
Hashgreen DEX
Frontend Engineer · Hashgreen Labs · 2022 – Present
A Next.js order-book exchange for trading Chia CAT tokens, with per-market routes and a live order book.
- Next.js
- Tailwind + daisyUI
- Mobx
- Ably
- Docker

Trading — chart, order book & trade history
500+ CATs, one searchable list
A tabbed, fuzzy-searchable market list that scrolls 500-plus assets while keeping only the on-screen rows mounted.
I led the frontend and owned this list. I refused to trade rendering speed against searchability, so I layered three independent mechanisms — windowing, deferred fetch, fuzzy scoring — each solving one axis. The Fuse threshold is 0.1 on purpose: tickers are short and collision-prone.
TableVirtuoso mounts only the rows in view; MarketStore uses onBecomeObserved/onBecomeUnobserved so a market defers fetching until something observes it; Fuse with threshold: 0.1 scores over currency code and name.
Mirrors src/components/Markets/MarketList.tsx · src/stores/MarketStore.ts · src/utils/fuse.ts
Reading the book without acting on a ghost
A live bid/ask book where clicking a level opens a summary panel, and a level that moves off the book closes it instantly.
I owned an invariant: a trader must never act on a price level that has already moved. A tooltip anchored to a row that gets replaced is a first-class event that closes the popover — not a rendering afterthought.
A reconciliation useEffect keys every level by key(market, price, amount, total) and clears the selection the moment that key stops matching anything in the current book — the same invariant the production UI's side-aware react-popper tooltip relies on to avoid anchoring to a row that no longer exists.
Mirrors src/components/Trade/OrderBook.tsx · src/components/Trade/OfferTooltip.tsx
Fetch once, then stream
Stats and a trade tape that load once over REST, then go live over one subscription — with a dot that mirrors the socket.
I built streaming as one reusable contract. Every live surface hydrates the same way — one REST fetch, then one hook — and every subscription tears down on unmount and re-subscribes cleanly when the channel changes.
A useAbly({ channelName, events, callback }) hook owns the whole subscription lifecycle; ablyIndicator reads Ably's connection state to drive the dot.
- 14:02:11buy 1.243142.50
- 14:02:13sell 1.242818.20
- 14:02:16buy 1.243365.70
- idle — flip Live to subscribe; switch market to see the channel swap.
Mirrors src/hooks/useAbly.ts · src/components/Navbar/ablyIndicator.tsx
One state machine, three wallets
Connect Goby, Hoogii, or Chia over WalletConnect — all resolving to one shortened address and one connected state.
I built this around one source of truth — WalletStageEnum — instead of a scatter of booleans. WalletConnect pairing is its own explicit stage, because pairing can be rejected or time out, and those had to be first-class states, not error handling bolted on afterward.
A small stage machine — Unspecified → Initial → Pairing → Connected — sits over per-wallet Wallet classes; the connected account exposes shortenAddress and iconUrl.
Stage is Unspecified until the user opts in.
- Unspecified
- Initial
- Pairing
- Connected
Mirrors src/stores/WalletStore.ts · src/utils/wallets/*.ts
Patch one row, or refetch the page?
A live order history — paginated and filterable — that decides per event whether to refetch the page or patch one row.
I owned the sync strategy here. The real question is whether a live update still belongs on the page you're looking at. Refetch only when an event changes page membership; otherwise patch the row in place and leave everything else untouched.
A page-keyed cache is fed by an Ably channel scoped to auth.id. Each event runs needRefetch() = checkMarket && checkStatus && checkDateRange; when it's false, checkUserOrderCached() swaps just that row in place.
Mirrors src/stores/OrderStore.ts · src/hooks/useUserOrders.ts

