A place for my photos, videos and thoughts.

Documenting Bucky
Documenting Bucky
jordy
jordy
August 14, 2026

Prototyping

In the summer of '22, when NYC saw its first lightening up of COVID-19 restrictions, I put together a Terminal program to aid in my volunteer job as the team statistician for my brother's team at their annual amateur basketball tournament. The program was an interactive Node.js script. Using only the file system for data persistence (no database), and contained completely in one directory, it was something very basic, and looked something like this (See repo here):

Then, after Google’s Gemini came out to the public during the summer of '23, I experimented its feasibility for stat-keeping as that year's tournament neared. The experiment looked something like this:

I was amazed. Initially, I was looking for nothing more than an efficiency improvement for myself, but seeing the idea in action immediately convinced me that I needed to share this with the world in the form of a product.

Fast-forward to last summer, summer of '25... I put together a prototype that started with a backend concept then evolved to include a frontend as well.


The Work

If what occurred until now were hors-d'œuvres, then what came after were the meat and potatoes. Unseasoned, overcooked, yet necessary for sustenance. Being a web app, it first and foremost needed Authentication, making access reserved for registered users, protected from the public. Then came a flood of features (list of 50+ items) that came pouring in from the App Gods. That is to say, they were features obvious for a basic web app to function. UI/UX development was another large part of this effort. For the non-engineer folks, this section might be a snooze.

Authentication

Access to the backend was authenticated using an API key, but all actions that required API access was routed through the an extra network hop that was responsible for permitting API requests only if it verified that a login session was active. The flow of logging in and getting game data is illustrated below.

I followed the guide at this link and used a cloud Redis instance where it needed to createDatabaseClient.

UI/UX

Staying with the meat and potatoes analogy, the UI/UX portion was the sauce you drizzled on an otherwise bland situation. There was joy in not only rediscovering my affinity towards working with CSS, but also in the exercise of thinking about what I wanted in the look and behavior. In parallel, I was working through a course on animations which further enforced my frontend-iness. Having full reign over what the app could look like was liberating and made me feel like an individual painter or songwriter, not an engineer. Some of the choices I made were:

  • Mimicking the UI of iMovie for organizing and opening game artifacts.
  • Taking inspiration from Baccarat "roads" when designing the snippet results in the recorder tray.
  • Leveraging ease-out animations wherever possible and applying what I learned in the animations course.

Scope Creeps

Aliases - "What if I wanted to track stats by someone's name?", was a common question I was getting early on, and something I was going back and forth on implementing. I knew that once I opened this can of worms, the worms would spill out every which way. Can multiple aliases converge into one player? Can aliases map to players and numbers? Can aliases cause an infinite loop? How do you create/edit/remove aliases? Are they handled by the frontend or backend? Can I save common aliases for my teammates/players? It spiraled out of control but because of the regularity of this piece of feedback, I opened the can. But it didn't stop there. Can I view stats per player? Can players have profiles? Can the actual persons own their profiles? This was where I had to draw the line.

Leagues and Tagging - Having a user tied to a single league was quite limiting. Thinking about hypothetical customers/users of this service, like a 50-team league in NYC with four divisions that also hosts tournaments, last minute changes were paramount. Although it was going to push back my timeline a bit, I changed the logic to a many-to-many relationship between users and leagues, as well as added functionality for adding other members to leagues with high and low privilege levels. Tagging was also added to allow for a customized way of organizing games for leagues with large game counts.

Notifications - I deliberately left out the work of making the service be a replacement for league websites. Existing website builders were perfectly capable of providing the tools to market a basketball league and advertise upcoming games in a way that could rival professional leagues. However, to take it the extra mile and reach full NBA-like experience, building a way to send/receive push notifications about the games sounded very appealing. So when you subscribe to your friends' and families' teams, you will get push notifications, for example, that a game is about to start along side alerts about Victor Wembanyama's battle with Jalen Brunson.

Managing scope creeps gave me tremendous respect for Project Managers. Ultimately, though, it is the owner that needs to be certain about their vision and purpose. Had I, as the owner, decided this project would be nothing more than an experiment or a research project, I would not have had built out any of the above scope creeps. The owner is the be-all and end-all. The product goes where the owner allows it to go. Whether it is the idea of an app that sends money to friends and acquaintances, or a betting app that lets you wager on anything with other strangers, two people can have the same idea, but what determines its path is where the owner wants to take it.

Architecture

The system architecture diagram.

Complexity

Regarding algorithmic complexity, I never had to come up with anything clever or sophisticated. When dealing with plays in a basketball game, there never more than 500 plays and never more than 20 players. Programming with low constants like these becomes very straightforward, requiring nothing more than Lists, Maps, and Sets to achieve robust and efficient functions. For example, here is the function for flattening plays into a box score lines. It's so computationally light that I can call this method every time the page loads without feeling any guilt towards the CPU.

def flatten_plays(plays, game_session) -> list[PlayerLine]:
	player_map = {}

	for p in plays:
		side_int = 1 if p.side == 'away' else (0 if p.side == 'home' else 2)
		num = p.player_no
		if (side_int, num) not in player_map:
			player_map[coalesced] = PlayerLine(side=side_int, num=num)
		line = player_map[coalesced]

		polarity = -1 if p.is_delete else 1
		match p.play:
			case PlayType.TwoPoints.value:
				line.pts = line.pts + 2 * polarity
				line.fga = line.fga + 1 * polarity
				line.fgm = line.fgm + 1 * polarity
			case PlayType.ThreePoints.value:
				line.pts = line.pts + 3 * polarity
				line.fga = line.fga + 1 * polarity
				line.fgm = line.fgm + 1 * polarity
				line.tha = line.tha + 1 * polarity
				line.thm = line.thm + 1 * polarity
			case PlayType.TwoPointsMiss.value:
				line.fga = line.fga + 1 * polarity
			case PlayType.ThreePointsMiss.value:
				line.fga = line.fga + 1 * polarity
				line.tha = line.tha + 1 * polarity
			case PlayType.FreeThrow.value:
				line.pts = line.pts + 1 * polarity
				line.ftm = line.ftm + 1 * polarity
				line.fta = line.fta + 1 * polarity
			case PlayType.FreeThrowMiss.value:
				line.fta = line.fta + 1 * polarity
			case PlayType.OffRebound.value:
				line.oreb = line.oreb + 1 * polarity
				line.reb = line.reb + 1 * polarity
			case PlayType.Rebound.value:
				line.reb = line.reb + 1 * polarity
			case PlayType.Block.value:
				line.blk = line.blk + 1 * polarity
			case PlayType.Assist.value:
				line.ast = line.ast + 1 * polarity
			case PlayType.Turnover.value:
				line.tov = line.tov + 1 * polarity
			case PlayType.Steal.value:
				line.stl = line.stl + 1 * polarity
			case PlayType.Foul.value:
				line.fls = line.fls + 1 * polarity
			case _:
				print('unknown playtype ' + str(p))

	lines = []
	for line in player_map.values():
		lines.append(line)

	return lines

When the backend virtual server is fully occupied with two workers in parallel processing audio snippets, the CPU usage spikes up to about 50%, and when idle stays close to 0%. Given that the machine I'm using (provided by DigitalOcean) is the free tier one with the lowest specs (1 vCPU / 1 GB RAM / 25 GB Disk), there is a lot more room to scale this machine up vertically before looking into horizontal scaling options. Similarly, with the ChatGPT portion, I looked for the oldest model that could handle the problem of turning text into "plays", and landed on 4o-mini. I expect the token usage to be so minimal that I'd be worrying about costs for domain registration before tokens. Not needing to be too concerned about complexity was definitely a plus.

Using AI

I did not pay a single cent to an AI service to help with the coding. But I sure did leverage the free tier! Only having a handful of messages to work with across the AI services, I tried to squeeze as many questions as possible into one message, which I think inadvertently improved the quality of my questions. I learned that a big part of incorporating AI into the workflow is cash. Offline agents are a different story, and it is something I'm eager to learn more about.


With that, I hope you have the time to check out bucky.live. Join the discord (here) if you want to leave any feedback, ask questions, and stay updated about Bucky!

Also, a quick note about Hoop Union: it's a secondary website I made where Bucky games are enshrined. Inspiration credit goes to MetaTft.com. Hoop Union can be found at beta.hoopunion.org.




Continue to other posts below or go to the All Posts page.