4 Things Claude Code Built for My Project That I Couldn’t Have Built

A split-flap is the flip board you’d find in an airport or train station, where each character sits on a rotor that clacks through the alphabet until it reaches the right letter. I’ve been building a software version and writing about it as I go: a Windows app that paints the board on a monitor or TV, plus a control app where you enter messages, connect data feeds, and schedule what appears.

You already know the setup if you’ve been following along. I’m not a developer. I know what the product should do, and Claude Code works out how to build it. By this third stretch of the project, the route had become familiar: I could describe the destination, and Claude Code would head into territory beyond my map. Four parts of the project made that especially clear.

I assumed this was the easy part. You spin some letters until they land. How hard can that be?

The animation turned into a physics problem

Rather than choose a rendering method based on a guess, Claude Code built what developers call a spike, a temporary test used to answer a technical question before committing to an approach. It created two working prototypes. One used Avalonia’s transform animation engine, while the other drew the flap geometry by hand on every frame. It then measured both versions on my machine at several scales.

Our initial target was a standard monitor with six rows and 15 columns of flip tiles. One test had 15 tiles moving at once, roughly one line of text changing. At the higher end, all 90 tiles could be flipping continuously on a 32-inch screen. The monitor size, tile size, number of rows, and number of columns are all configurable.

At 22 active tiles, the transform version ran at 58.6 frames per second compared with 38.1 for the hand-drawn version, while also using less CPU. Then, at 132 tiles, both prototypes fell apart.

The drawing method was not the problem. Each tile had its own animation clock, and 132 clocks ticking independently swamped the CPU regardless of how efficiently the pixels were painted. The board now owns one clock and updates every active tile in a single batch. Individual tiles never schedule themselves. The decision is recorded in an architecture decision record, so nobody quietly reverses it six months from now, possibly me.

The part I cared about most was making the movement feel mechanical, and four smaller decisions made most of the difference.

Each tile moves forward through a fixed 57-character wheel and wraps back to the beginning when it reaches the end. Moving from A to Z takes 25 flips. There is no shortest-path shortcut because a physical rotor doesn’t have one. Without this rule, the board looks like text changing. With it, the board looks like machinery.

Blanking is the exception. A tile showing A is 47 flips away from blank, so when a shorter message replaces a longer one, an entire row may need to clear at once. Letting every tile travel the full distance creates a lot of frantic movement nobody is meant to read. Blank reversion is capped at three flips, using the final three steps of the real route rather than the first three. The tile still settles into blank as though it had completed the entire sequence.

The final flip is also slower. This was one I raised myself because a physical rotor slows as it reaches the stop bracket rather than moving at normal speed and halting instantly. A normal-speed flip followed by a pause looked like a hitch. Lengthening the final movement looked more like the rotor settling. The multiplier is 1.75 and applies to every sequence, even a one-step change where the first flip is also the last.

The tiles also do not all begin at exactly the same moment. When a row fills from blank, every tile starts from the same character, so without some variation they move in lockstep and look like one synchronized wave.

My first idea was to give each tile a random starting character. Claude Code talked me out of it, correctly. Doing so would break the rule that a tile filling from blank travels through the real sequence, and a physical rotor’s starting position would not suddenly become random either. The implementation leaves the character path alone and varies only the start time, giving each tile a random delay of up to 150 milliseconds before it begins.

Two apps that talk through a file

The display and control panel run as separate processes. The obvious way to connect them would be a socket or a small local HTTP API, but they communicate through a plain JSON file instead. Part of the reason is support. The only on-call engineer for this project is a non-technical product owner who cannot diagnose a silently frozen display. That would be me.

The control app compiles the contents of its database into a versioned playback manifest. It writes the file atomically, first creating a temporary version and then renaming it, so the display app never catches it half-written. The display watches the manifest and renders whatever it contains.

The display has no access to the database and does not even reference the projects that can reach SQLite. It knows only the shared contract types needed to read the manifest. A rendering problem cannot corrupt the data, and a database problem stays out of the renderer. That separation gives me fewer places to look when something goes wrong.

The surprise was FileSystemWatcher. On Windows, it can miss events during rapid writes or when its buffer overflows. Who knew? I’m sure a lot of you did. I did not.

The watcher is paired with a polling fallback. Every second or two, the display checks the file’s timestamp and checksum whether an event has fired or not. Without the fallback, one missed event could leave the display showing old content with no visible error and no obvious cause.

About 20 lines of polling save me from an entire category of support problem I would experience as, “The sign stopped, and I don’t know why.” Not that I expect anyone besides me will want this thing.

Weather without an API key

I wanted live weather without creating another account, entering a card number, or studying a commercial-use license before I could display the temperature. Open-Meteo met those requirements. It is free, requires no API key, and gives the application the raw WMO weather codes it needs. The app translates those codes into something readable on the board, so 0 becomes “Clear sky” and 99 becomes “Thunderstorm with hail.”

The more interesting problem appeared in the location search. Open-Meteo matches place names literally and does not understand a search such as “Portland, Oregon,” even though its own results are formatted as “Portland, Oregon, United States.”

That is a perfectly natural thing to type, especially when there are several cities named Portland. It also returns no result and gives no indication of what went wrong. The app now makes one simple retry. When a query containing a comma returns no matches, it tries again using only the text before the comma.

There is no fuzzy matching system or chain of increasingly creative guesses. It retries once in a predictable way. If the network request fails, the user sees “no matches found” rather than a raw HTTP error. The latitude and longitude fields remain editable, so I can still enter the coordinates by hand and move on.

A paid weather provider could be added later without rebuilding the feature. Any provider requiring a key would use the same secret store already built for the stock feed. For now, though, Open-Meteo does what I need without turning a weather display into another account-management project.

The quote was the easy part

I wanted near-real-time stock quotes and settled on Twelve Data because it offers a free tier. The API integration came together in two passes. The problem that sent me looking in the wrong direction appeared later and had nothing to do with the quote service.

Twelve Data requires an API key, but the key never goes into a configuration file. Windows DPAPI encrypts it for the machine, and the app pulls it from the secret store when it requests a quote. I do not have to keep track of a key sitting in a text file or worry about accidentally copying it with the rest of the configuration.

The application does not depend on a Twelve Data software kit either. The rest of the stock feature talks to a general quote-source interface, and the Twelve Data connector sits behind it. If I change providers later, the work is limited to writing another connector rather than rebuilding the feature around a new vendor.

One bad ticker cannot take down the whole refresh. If a symbol times out, returns bad data, or has been delisted, the app marks that row as failed and continues with the rest.

The character wheel also had to change. A quote needs to show direction, and writing “UP 2.4%” wastes valuable tiles, so up and down arrows were added directly to the wheel. An arrow is now a real rotor position. A tile can land on it or flip through it on the way to another character, just as it does with $ or ,.

The part that sent me down the wrong path came when the ticker started showing only the first few symbols. Rate limits and malformed responses seemed like the obvious suspects, so naturally I started looking at the API.

The actual cause was in the playback system. The manifest regenerated every 60 seconds, and each regeneration reset the content rotation to index zero. Symbols appearing later in the list never stayed in the rotation long enough to get their turn. The quote provider was working correctly. The schedule kept sending playback back to the beginning.

Finding the cause required following the data through the provider, the manifest generator, and the playback coordinator. Left to myself, I’d have spent a week in HTTP logs.

Looking back, the stock ticker is probably the clearest example of what I could not have done on my own. I knew something was broken, but I was looking in the wrong part of the application and would not have known when to stop.

Somewhere along the way, the project also accumulated 26 architecture decision records, a documented review process, and a standing rule that anything writing data on a timer has to explain how old rows are removed before it ships. I did not set out to create a written memory for the project, but that is what those records have become.

I asked for code. What I got was a codebase that can explain itself to whoever opens it next, including me.

Click for Article 1

Leave a Reply

Discover more from Jeff Stivers

Subscribe now to keep reading and get access to the full archive.

Continue reading