evotechno
A genetic algorithm that evolves techno while you listen. You vote, the genome moves, and the music never stops to think about it.
- Role
- Solo — I designed it; AI agents implemented it
- Method
- Specification-driven
- Status
- Runs locally
evotechno plays continuous techno that evolves in response to whether you like it. An eight-gene genome describes a section of music. Every 32 bars the genome is mutated or crossed with something from an elite archive, Claude Haiku renders the new genome into Strudel pattern code, and the browser plays it. If you vote a section up it becomes likelier to father the next one; skipping counts against it; sitting through one without touching anything is weak approval.
How it was built
I wrote PLAN.md before any code existed — the genome, the
operators, the cost model, and fourteen decisions I did not want re-litigated
halfway through the build — and AI agents implemented against that
specification. The document is explicit that it is the spec and that
implementing agents should follow it rather than re-derive it.
The work worth talking about is what the spec had to settle. That the GA owns the search and the model only renders. That a credential being present is not consent to spend money. That a guard which only runs when a key exists is not a guard at all. None of those were arrived at by typing code, and two of the three came from the build going wrong first and the rule being written down afterwards so it could not happen again.
The architectural rule
The GA owns the search; the LLM is only a renderer. The genome is a typed eight-gene vector. Claude receives the vector and returns Strudel pattern code. Claude never mutates, never selects, never invents genes.
That line is the whole design, and everything else follows from it. It's tempting to hand a language model the search itself — ask it for "something like that but darker" and let it figure out the rest. But then there's no genome, no heritability, and no way to say what a vote actually changed. A genetic algorithm with an opaque mutation operator isn't a genetic algorithm; it's a chat log.
Keeping the model strictly downstream means the search stays inspectable. Mutation
moves one gene 80% of the time and two the rest, Gaussian with a σ of 0.15 on the
continuous genes, with bpm restricted to ±1–4 and ramped across
the section boundary so the tempo shift is felt rather than heard as a jump.
Crossover is uniform on categorical genes and BLX-α on continuous ones, with no
coherence filtering at all — incoherent offspring are the entire point of
running the thing.
Never stopping
Playback is continuous, which is a harder constraint than it sounds. Section N+1 has to be generated while section N is still playing and swap in exactly on a bar boundary. There is never a gap, so the model call has to finish inside the duration of a section — and when it doesn't, something still has to play. A fallback path and a genome-to-code cache exist purely as deadline safety nets, never as the normal path.
What it cost to make it cheap
Every section is a fresh model call, so the economics are load-bearing. The prompt is built as a large cached prefix — the Strudel cheatsheet and the enumerated sample list — with a small volatile tail carrying the genome. Getting that working turned up the most useful thing I learned on this project: Haiku's minimum cacheable prefix is 4,096 tokens, and if you fall short of it the API doesn't cache and doesn't tell you. No error. It just bills the entire prefix as ordinary input and returns zeros for both cache counters.
The counter-intuitive consequence is that a bigger prompt is dramatically cheaper. At roughly 5,200 tokens cached, the prefix costs about $0.0005 per section. At 4,070 tokens — just under the threshold, uncached — the same prefix costs about $0.0041. Eight times more, for a smaller prompt, with no error to tell you why. Padding the prefix past the line was the optimization.
Three rules the tests enforce
Each of these is written into the project as an invariant rather than a convention, because each one is something that went wrong first.
npm test must never make a billable API call. Live
tests require an explicit environment flag in addition to a credential. A
credential being present is not consent to spend money — a suite that bills
whenever a key happens to exist will bill on every future run, and in watch mode
or CI it bills continuously. I found this the way you'd expect: a key-gated live
test fired on a routine full-suite run.
Anything under src/ must bundle for the browser,
verified with esbuild rather than assumed. The test suite runs in Node and is
structurally blind to this, so a Node-only crypto import once shipped
as a hard blocker behind a completely green 41-test suite.
A guard that only runs when a credential exists is not a guard. Every credential-gated assertion needs an always-on coarse tripwire beside it, or the protection silently disappears in CI and on fresh clones — which is exactly where you needed it.