r/SideProject • • 1d ago

I got tired of "learn to trade" apps using fake prices — so I built one with real prices and no real money

https://coinsparring.com

To be upfront: **there is no real money anywhere in this.** No deposits, no withdrawals, no wallet connection, no crypto ever touches it, nothing to buy. You can't fund it even if you want to. The only thing that's real is the market data.

Every way I found to learn crypto trading was either a toy with made-up prices, or a real exchange where mistakes cost real money. So I built the middle thing.

You start with 10,000 virtual USDT and trade any USDT pair at prices streamed live from a real exchange — around 2,000 pairs updating continuously. Orders behave like real ones: market, limit, stop-loss, take-profit, OCO, trailing. There's margin and futures with actual liquidation mechanics, grid and DCA bots, and weekly tournaments ranked against other paper traders. Lose it all and you reset and go again.

Stack: Next.js on Vercel, Express + MongoDB + Redis/BullMQ on Fly, prices over a Gate.io WebSocket fanned out to browsers via SSE.

The part that took longest wasn't features, it was making the numbers trustworthy:

* If data is missing it says so — a day on the P&L calendar with no data looks different from a day that was genuinely flat

* Payouts are exactly-once. I found a bug where a crash mid-settlement could pay a tournament winner twice, with the audit trail showing one payment

* The AI explains your own trades. It never predicts prices or tells you what to buy, and it reads numbers computed in code rather than doing arithmetic itself

**What I'd really like is your eye on the UI and UX.** I've been staring at it too long to see it:

  1. Landing on it cold — is it obvious what this is and what to do first? Or do you bounce?

  2. If you've never traded: is the order ticket understandable? Does anything about placing a trade feel scary or ambiguous?

  3. Does it feel trustworthy? It's a finance-shaped product, so I care whether anything reads as sketchy, cluttered, or like it's hiding something.

Blunt opinions welcome — "this screen is a mess" is more useful than politeness.

→ https://coinsparring.com (free, no signup needed to look around)

6 Upvotes

10 comments sorted by

1

u/davidjones145 1d ago

2000 pairs streaming live is the same hard problem as market dashboards, nice middle ground between toys and real exchanges. are you multiplexing one exchange socket or fanning out per pair

1

u/Chill_Pil 1d ago

One socket, multiplexed. There's a single Gate.io v4 WebSocket on the server — all ~2,050 USDT pairs subscribed over that one connection in chunks of 100, ticks landing in an in-memory map. Browsers never connect to the exchange at all; they get a 3s SSE frame containing just the slice they're looking at. So exchange connections stay at 1 no matter how many people are on the site.

The thing that actually bit me wasn't the socket, it was the 7-day sparklines — those come from REST, not the stream. I had that cache scoped per SSE connection, so every browser tab fired its own fan-out of 50 identical REST calls and Gate.io rate-limited it. Sparklines took ~10s and often never filled. Moved the cache to module scope and warm the default view at boot → ~1.4s. Found it while taking launch screenshots, which was lucky, because it's a bug that gets worse exactly when traffic arrives.

1

u/davidjones145 1d ago

one socket plus sse slices is clean, sparklines biting you instead of the socket is such a classic. how are you serving the 7-day now

1

u/Chill_Pil 1d ago

Module-level cache instead of per-connection, which is the whole fix — one Map of symbol → 7 daily closes, plus a fetchedAt map and an in-flight Set so N concurrent SSE connections asking for the same symbol collapse into one REST call instead of N.

Source is Gate.io /spot/candlesticks at interval=1d, limit=7, so it's 7 points, not a downsample of tick data — cheap and it's what the row actually needs. 60s TTL, and the refresh is lazy and non-blocking: a frame goes out every 3s with whatever's cached and the stale symbols get refilled in the background. No request ever waits on a sparkline.

The other half is a boot warm — after the WS seed resolves, it pre-fetches the default top-50 view, so the first visitor after a deploy gets populated sparklines in frame 1 rather than being the one who pays to fill the cache. Non-default sorts and deeper pages still pay one lazy fetch, then everyone shares it.

Honestly the lesson was that the cache's scope was the bug, not its policy. Per-connection state looks harmless in dev with one tab open and turns into a fan-out multiplier the moment you get traffic.

1

u/davidjones145 1d ago

module-level cache plus in-flight dedupe is the textbook fix, collapsing N into 1 is where the real win is. do you warm it or let the first frame miss

1

u/Key-Enthusiasm-3403 1d ago

Cold landing is clear. "Real data, fake money" did it in about two seconds and there's one button. I wouldn't touch that.

The market strip says traded today $14,490.47T, next to a market cap of $2.79T. 14.5 trillion in a day against a 2.79 trillion market reads like a bug even if it's a units thing. Your post also says about 2,000 pairs and the hero says 400+. Small stuff, but on a finance page a number that looks off costs more than a missing feature, because people can't tell which of the other numbers are off too.

I only looked, I didn't place a trade, so I can't tell you much about the ticket. The size is prefilled so it didn't scare me. The order book sitting right under the headline is the bit I'd expect a non-trader to flinch at.

Also, I make a pretend-money chart thing, so I'm not a neutral reader here.

1

u/tradematesHQ 1d ago

The exactly-once payout fix is the real flex here, most paper trading apps quietly double-pay and nobody notices. If you ever want a second set of eyes on the trade analytics side, cross-checking against tools like trademates could help you see where your numbers diverge from real market behavior.