Vibe Coding Update: It’s Gotten a Lot Better, The Ongoing Story of LTHOI.com

Last August I wrote that vibe coding is SOOO rad because I had spent about 20 hours modernizing LTHOI.com before football season. That was the whole update. I dragged an Angular 6 front end forward, fixed the Google sign-in that had rotted, deleted a pile of dead code, and shaved some money off the AWS bill. It felt like a lot at the time, and it was all I had time for. The 2025 season kicked off and the app looked more or less like it had looked since 2019.

This year I went into August with the same amount of free time and a much better set of tools, and the difference is hard to overstate. LTHOI.com is a different application; new cloud provider, new database, new front end, new features that had been on the “someday” list for the better part of a decade and all in the same amount of time.

In this post I’ll describe what changed and then conclude with my learnings about what that means for vibe coding. If you have never heard of LTHOI, it’s a site I use for placing tiny wagers on football with my friends. It’s also the side project I use to make sure my coding skills stay at least a little relevant even though I spend more time in Powerpoint these days and my developers won’t let me near their git repos. It’s technical history is here.

Moving out of AWS

The 2019 version of LTHOI was my serverless showpiece. Three Java Lambdas, DynamoDB, CloudFormation, CodePipeline, an Angular app on S3 behind CloudFront. I was proud of it, and I wrote about it more than once. It also meant that every year the app was one more year behind, and every “small” change required me to remember how five stacks fit together.

This summer I moved the whole thing to Google Cloud. The API is now a single TypeScript service on Cloud Run. The web app lives on Firebase Hosting, which also handles Google sign-in and rewrites every /api request to the service so the site and API share one origin. Cloud Scheduler wakes a planner every twenty minutes during the season; it makes one MySportsFeeds call for the entire schedule, settles any games that just went final, and queues per-game odds refreshes into a Cloud Tasks queue that dispatches one per second. That queue is the rate limiter. There is no rate-limiting code in the app at all, which is the kind of thing I would have written by hand in 2019 and then debugged for a weekend.

Everything is Terraform. Deploys run from GitHub Actions through Workload Identity Federation, so there are no stored cloud keys anywhere.

A rewrite of this level would have taken weeks just a year ago.

The Board

I have said on this blog that I HATE UI development, and I stand by it. What I should have said is that I hate doing UI development. Having it done for me turns out to be fine. Having a cool user interface for LTHOI, is even more fun!

The new interface is modeled on the odds board at the sportsbooks in Vegas. Dot-matrix LED type for everything “on the glass,” a neon script marquee, and the same color code the real boards use: magenta for section headers, amber for teams and spreads, green for totals and money, red for live scores and freezes. It is dark-only, on purpose.

The picks page is one continuous board, not a page of cards. Two rows per game with rotation numbers, and the spread and total cells are the bet buttons. Tap a lit cell to place, tap your solid cell to cancel until the freeze. Settled cells read JACKPOT, BUST, or PUSH.

Every bet in the league is visible to every player, always. My first draft of the new app hid other players’ picks until kickoff, and I reversed that the moment I saw it on screen. Transparency is the game, so each row also shows your net exposure if the home side covers or the over hits. Standings became The Ledger.

The Ledger. Week one, and I am already in the middle of the table, which for me is a hot start.

It fits on a phone. Below phone width the time column folds into a per-game status line and the board fits 390 pixels with no horizontal scrolling, which is where most of my league actually reads it on Sunday morning.

Twelve years of history, finally on the page

Because leagues now span seasons, I could build the thing I have wanted since 2015: a League History tab. It has three views. Forever Standings is every player who ever played, with all-time record, titles, and winnings. Season by Season flips through each year’s final table with the champion tagged. And the first view is a generated narrative that reads the whole history and writes it up like a sportswriter with a grudge.

The Book of The Originals. Quinn H on the throne with three titles. Me in the basement as “the league’s most reliable charitable institution,” which is accurate and unkind.

A live season is treated carefully. No crown, no coronation, and nothing in the record books until the last game goes final. The narrative gets a “Live Board” section instead. The cumulative numbers still include the current year, labeled as live.

Leagues that actually work like leagues

Most of the remaining features are things my commissioners have been asking for since the beginning.

  1. Invite anyone by email. Paste a comma-separated list when you create a league or later from the admin page. Invitees see the invite on their home page and confirm or decline. No more pairing page hack (long-time readers may remember that post).
  2. Weekly rosters. Commissioners can add or remove players between one week’s last game and the next week’s kickoff. Each week’s winnings split over that week’s roster, nothing is retroactive, and the roster locks while a week is in progress. Somebody joining in week six no longer rewrites weeks one through five.
  3. Settings that can change mid-season. Every bet snapshots its dollar amount at placement, so bet amount, bets per week, and freeze minutes can change between weeks without corrupting history.
  4. Renew a league. Creating a league offers to renew a past one, prefilled with last season’s settings and last season’s roster as invites. This is what makes the History tab possible.
  5. Game details. Every game has a page with venue, surface and roof, actual weather plus a kickoff forecast, every sportsbook’s opening and current numbers, injuries and inactives, and the league’s action on that game.

Book-E gets a front door

In 2021 I spent a holiday break building Book-E, a machine-learning player that placed bets through a Lambda wired directly into the database. It worked, in the way that duct tape works.

Now anyone can play with an agent. Create an API key on the Account page, send it as a header, and the full OpenAPI contract is served live from the site. An agent plays with exactly its owner’s permissions: freezes, weekly caps, one bet per market per game, and roster membership are all enforced server-side, so a buggy agent cannot break a league. There is a public developer guide at lthoi.com/agents.md that covers the plumbing and deliberately says nothing about strategy. If you hand an LLM that guide and a key, it can play by the end of the conversation. Book-E has a seat in The Originals this year on the same footing as the humans.

Why this year was different

Here is the part I actually want to write about. Last year’s 20 hours produced one update. This year’s evenings and weekends produced a migration, a re-architecture, a redesign, and a feature list I had been carrying around for a decade. I did not get faster. I did not get more free time. Three things changed.

The intern grew up. When I wrote the Vibe Coding Reflections series, the assistant was knowledgeable but needed watching. This summer I described the target architecture in a conversation, argued about a few choices (the zero-sum formula, weekly rosters, whether to hide bets), and the rewrite went from decisions to a live production deploy in about a day. Then we kept going. Most evenings the work was a design conversation followed by me reviewing what came back.

It can see the whole environment. In March I wrote about giving Claude CLI access and cloud credentials. This is where that paid off. It ran the Terraform, hit the org policy that blocks public IPs on Cloud SQL, found the workaround, loaded the secret, set up the Workload Identity trust, deployed, watched the first sync pull all 272 games, and told me when it was done. When a friend could not sign in from Safari, it read the Firebase error, worked out that the auth handshake was crossing origins, and fixed it. The IDE-trapped version of this tool from a year ago could not have done any of that.

I still read every diff. I still vetoed things. Claude tried to mess up the winnings formula at least 4 times, there were bugs on sign ins, the email addresses and league members were mysteriously not ported well (note that this is why you STILL shouldn’t be letting non-developers do vibe coding), but the amount Claude can do is growing by the day.

The 2026 season is underway. Quinn H is still on the throne. I am still in the basement. But the app finally looks like the idea I had in 2014, and it took one summer of evenings instead of another decade of Augusts.

Comment
Name
Email