All work
◆ Personal · Live

CallShot

Personal project2025
Built the database, api and interface layers

The problem

Before an IPL season, a friend group wanted to lock in predictions (Orange Cap, Purple Cap, Player of the Tournament) and track who was winning across the whole season. Spreadsheets could hold the picks but not the scoring, and working it out by hand after every match was not practical.

Constraints

  • Opened from a WhatsApp group on a phone, so mobile-first was the only assumption
  • It had to be live before the season started
  • Scoring rules had to stay easy to change
  • Group members join by link, so onboarding had to be near-frictionless

Decisions

Decision 01

Scoring composes rather than branches

Points are positional: 1st place in a category scores 10, down to 2 for 5th, with Player of the Tournament as a 20-point exact match. On top of that sits a strategy-card layer: four cards per player, each usable as a Swap to change a pick mid-season or a Joker to call an exact finishing position for +30. Unused cards are worth 4 points each, so holding onto them still counts for something. One pointsCalculator service folds category results, card effects and per-match points into a single standings array.

Trade-off

A single service that has to be correct for every rule combination, in exchange for scoring rules that live in one readable place.

Decision 02

Two prediction layers, one leaderboard

Season-long category predictions and per-match predictions score independently and merge into one ranking, with a visible split between tournament points and match points so people can see where their lead came from.

Decision 03

Sockets, not polling

JWT-authenticated Socket.IO connections push leaderboard updates, results and a notification feed. People follow the match in a group chat, so the standings need to update without a refresh.

Trade-off

Stateful connections to manage and authenticate, rather than a stateless polling endpoint.

Outcome

  • Shipped before the season and used by a real friend group throughout it
  • Full auth flow: OTP email verification, JWT pair, transparent refresh on 401 with request retry
  • Branding isolated in a single constants file on both client and server, so the whole app can be re-skinned without touching components

What I'd change

Most of the later commits were production fixes: CORS, SPA routing 404s, locking down SMTP, and ranking bugs found by people using it.