Writing

Before the Swarm

Part one of a series on GNostr, vibe-coding, and finding out where the machines break

July 24, 202612 min read#nostr#GNOME#vibe-coding#llm
Before the Swarm

It's somewhere past two in the morning and I'm reading the OpenSSL documentation for a function that doesn't exist.

It existed once. That's the maddening part. The model had produced it with total confidence — clean signature, sensible arguments, error handling and all — and the linker had responded with the software equivalent of a blank stare. undefined reference. I go digging and eventually find it, not in the documentation but in a changelog: deprecated in 1.1.0, taken out behind the shed in 3.0. The thing had been dead for years. The model spoke of it in the present tense, warmly, the way you might speak of a friend you hadn't heard had passed.

I sat back and considered my collaborator: brilliant, tireless, fluent in C, and living several years in the past. I was pair-programming with a ghost. Or possibly for one.

Several weeks earlier (I'm not sure precisely how many), this had all seemed like a much better idea.

The experiment

Nostrc didn't start as a product. It started as a question, and not the question everyone else was asking. By then any fool with a free tier could see that an LLM could write code — it could scaffold your React app, it could explain your regex back to you, it could produce CRUD until the cows came home. That was established. The interesting question was the other one: where does it break?

So the project had two goals, and it's fair to say the combination was deliberately reckless. The first was to bring the primitives of the Nostr protocol to the GNOME desktop — properly, natively, as infrastructure other applications could depend on, not another Electron app wearing a GTK costume. The second was to lean on AI-assisted development as hard as it would go, and keep leaning until something gave.

A word on each, because the choice of battlefield matters.

The Nostr protocol, for the uninitiated, is a decentralized protocol that is simple in spirit and sharp in implementation: events, signatures, relays, subscriptions. Identity isn't a username in someone's database, it's a keypair — your public key is you, cryptographically, which has the pleasing side effect that no one can deplatform you and the less pleasing one that there is no "Forgot Password" link when you make a complete and utter balls-up of it. Building an SDK for it means living inside WebSockets, deterministic serialization, and elliptic-curve cryptography whether you like it or not.

I'd been put onto the protocol by Jamie King, an independent filmmaker you might know from his Steal This Film work, a man with a long and distinguished record of annoying the right people, during an anticensorship project we worked on together. So the sovereign-tech case had my sympathies from the start; I'd spent enough years around activists to know what it costs when your identity lives in someone else's database. But honesty compels me to admit the appeal wasn't only political. Nostr was so radically different from everything else on offer that it amounted to vast greenfields: no incumbents, no gatekeepers, no permission slips — a protocol where anyone with an idea could simply build the thing and find out. It was a playground for just about any idea you could think of, and I could think of quite a few.

GNOME, meanwhile, is not just a toolkit but an ecosystem — GObject, signals, main loops, introspection, a whole civilization of conventions — and deep integration meant playing by its rules rather than squatting on top of it. Integration is harder than features. Anyone can build a standalone client. Primitives that other software depends on are a different sport.

If vibe-coding was real, it should survive that. That was the hypothesis, stated with the arrogance the era demanded: push until it breaks, or until I do.

Copy, paste, pray

The workflow, in the beginning, would embarrass a medieval scribe.

Write a prompt. Copy the output. Paste it into the editor. Compile. Squint at the errors. Explain the errors back to the model, which apologizes with the enthusiasm of a golden retriever and produces a new version containing a fresh, entirely different mistake. Repeat until enlightenment or bedtime, whichever comes first.

Dan Shapiro would later rank the levels of AI-assisted development on a ladder running from spicy autocomplete up to the dark factory. This wasn't on the ladder. This was the spicy clipboard.

This was before IDE integration, before agents, before CLAUDE.md or any of the conveniences that now let a model hold a project in its head between sessions. There was no state. There was only the context window, and the context window was small, and everything outside it simply ceased to exist. The model was a brilliant amnesiac in a locked room, and I was passing notes under the door.

The amnesiac, I should add, was not a fixed quantity. I'd started this whole journey back in ChatGPT's version-3 era and worked through every successor as it shipped — until RepoPrompt eventually introduced me to running several models at once, and model monogamy went the way of most of my engineering principles. Which meant my collaborator kept changing underneath me: lurching forward between versions, occasionally lurching backward, like a colleague replaced overnight by a slightly different twin — sometimes sharper, sometimes inexplicably concussed, never announced. There's a second, unwritten story threaded through this one — the same project, the same developer, and a procession of minds of varying wattage passing through the codebase — as the git log tracks every one of them. But that's for another time.

The amnesia had consequences beyond the obvious. Ask for a struct on Monday and it's gnostr_event; ask for a related function fifteen messages later and it's cheerfully operating on nostr_event_t, a thing it just invented, which is almost — almost — the same. Functions renamed in a refactor would reappear under their old names like zomebies, lurching erratically through the codebase in search of brains. I'd end up with duplicate structs, shadowed functions, two subtly different versions of the same abstraction courteously disagreeing about who owned what memory. Not because the model was incoherent. Because it was forgetful, and continuity was my job — I just didn't know that yet.

The workaround was as crude as it sounds: I started pasting entire header files into the chat. Whole API documents. Slabs of OpenSSL reference, GLib type definitions, my own headers fed back to their author's assistant. I was manually simulating memory for a machine that had read everything and retained nothing. And in doing so I learned the first real lesson of the era, the hard way: context is a scarce resource. Stuff too much in and the signal drowns; too little and the hallucinations bloom. Prompting stopped being about asking good questions and became about packing a suitcase.

The time warp

But the context window was the tractable problem. The real trouble was temporal.

The model knew OpenSSL. It did not know this OpenSSL. Its knowledge had a horizon, and beyond that horizon it didn't go quiet — that would have been manageable — it extrapolated, confidently, in the same well-formatted voice it used when it was right. Deprecated functions. Removed initialization patterns. And, most bracing of all, APIs that had never existed anywhere: features proposed on a mailing list once, or plausible-sounding functions conjured from the statistical vapor of ten thousand similar ones. They looked correct. They very nearly compiled. They were literal ghosts in the shell.

I lost entire days to wild geese. There was a stretch where I designed a whole feature around a capability that, I eventually established, had never shipped in any version of the GOA library — not deprecated, not removed, simply never real, a rumor with a function signature. These weren't syntax errors, which the model almost never made. They were reality errors. The machine wasn't querying the world; it was reconstructing a probable one, an exercise in statistical archaeology, and the reconstruction was perfect right up until you tried to link against it.

Here's the perverse bit: the project probably survived because the model was so good at the surface. The C itself was disciplined — allocation patterns, error handling, function signatures, all idiomatic, all fluent. It felt like working with someone who genuinely knew systems programming, which made the misdirections all the more disorienting. But it also revealed the actual shape of the thing. The model was unreliable about the world and surprisingly reliable about structure — and when an API turned out to be fictional, the architecture around it was usually salvageable. Correct the boundary, keep the design. Once I saw that split, I could work with it. Manual coding wasn't optional in those days, but it stopped being random: the model drafted structure, and I hand-verified every point where the code touched external reality. A division of labor, arrived at by attrition.

Riding the Schnorr elliptic

Which brings us to the cryptography, and to the scene of this chapter's actual crime.

"Never roll your own crypto" is good advice that has curdled into doctrine. Somewhere along the way it stopped meaning don't invent your own cipher, you're not Daniel Bernstein and started meaning don't touch any of it, peasant — the priesthood will handle the keys. And look, the priesthood has its reasons; the graveyard of clever amateurs is genuinely large. But Nostr doesn't offer you the luxury of abstinence. Keys, signatures, event IDs — the cryptography isn't a feature of the protocol, it is the protocol. If you're building an SDK, you're in the vault whether you like it or not.

So: port Nostr's key generation and signing — secp256k1, Schnorr signatures — onto modern OpenSSL. The model's first attempts were technically elegant and completely wrong, fossils from the pre-3.0 world, all long-dead EC calls and initialization rituals the provider model had swept away. Fine. By now I knew the drill. This part was hand-crafted: me, the actual current documentation, and a slow education in EVP abstractions, deriving keys and riding the Schnorr elliptic myself, checking every call against ground truth like a man cracking a safe.

The breakthrough — and I want to be precise here, because this is the moment the whole project turned — wasn't in the implementation. It was in the tests.

I fed the model canonical vecotrs: known private keys and their public keys, real event payloads, signatures verified against live relays. And I asked it for exhaustive unit tests. It produced them with the tirelessness of a machine that has never once been bored — edge cases, malformed inputs, corrupted keys, boundary conditions, invalid signatures, repeated signing runs. Coverage more thorough than most humans would bother with, because most humans have somewhere to be.

Notice the shape of that collaboration. The model was not trusted to write the crypto. It was trusted to prove it — to multiply verification around a core that human hands had checked against reality. Cryptography fails silently, which is worse than crashing; the only honest response is overwhelming evidence. The machine turned out to be a superb engine for exactly that.

And when the suite went green against real-world examples — keys matching, signatures verifying, relays accepting — something shifted that hasn't shifted back. This was not autocomplete. This was leverage of a kind I hadn't had before, aimed at the least forgiving part of the entire stack, and it held.

From vibe to discipline

Somewhere in all this, the workflow quietly grew up.

I stopped asking for snippets and started asking for transformations. Not "write me a function that parses this" but "separate transport from protocol logic in this module"; "make ownership explicit across this boundary"; "introduce an abstraction between relay state and subscription handling." The model stopped being a vending machine and became something more like a junior architect with an eidetic memory of everyone else's codebase and no memory of ours — useful, once you accepted the terms.

And the terms were these: the machine mirrors your thinking. Fuzzy prompt, fuzzy code. State your invariants, pin your versions, declare your threading model and your failure semantics, and something disciplined comes back; wave your hands and you get plausible-looking chaos with excellent variable names. Vibe-coding — the improvisational, flow-state, trust-the-feeling mode the term implies — was real, and it was also insufficient. What it matured into needed a different name. The cool kids were starting to call it compound engineering: leverage that accumulates, feedback loops that improve, intuition translated into structure rather than opposed to it. I'll take the term, with the caveat that at the time it mostly felt like learning to stop treating an oracle as an oracle and start treating it as a probabilistic collaborator with a drinking problem it was very good at hiding.

The maps arrived later, as maps do. In January 2026, in the same month, Dan Shapiro published his five levels of AI-assisted development — spicy autocomplete at the bottom, the dark factory at the top, a black box that turns specs into software with nobody minding the lights — and Steve Yegge published Welcome to Gas Town, with its eight stages of the AI-coding journey, from timid tab-completion all the way up to orchestrating swarms of twenty or thirty agents at once. Reading them was a strange experience, like being handed a freshly printed map of a jungle you'd already crossed. The stages are all there in the nostrc repo, in order — the chat-window era from the first commit in October 2024, then the IDE agent on a leash, the agent off the leash, the parallel agents — a couple of thousand commits climbing a ladder nobody had drawn yet. By the time the maps were published I was most of the way up it, and when GasTown shipped on New Year's Day I had a rig running before the month was out — an enthusiasm that came with a bill attached, though that particular horror story belongs to a later chapter. I mention this not to claim prophecy but because the git log is the one witness in this story with no incentive to embellish. (GasTown itself shows up later in this series, the titular swarm no less.)

By the end of this period I trusted the model about structure, distrusted it about reality, and had built the habits — canonical fixtures, exhaustive tests, hand-checked boundaries — that made the difference workable. The ceiling had moved. And once the ceiling moves, you start eyeing the next one.

Because here's the thing I was starting to suspect, and which the rest of this series will make embarrassingly explicit: the friction was never evenly distributed. Some layers of the software world bent easily under this new leverage. Some barely noticed it. There's a physics to that — a gravity, you might say — and we'll get there.

But first I did something genuinely stupid.

Everything so far — the crypto, the parsing, the relay plumbing — had at least been normal systems programming. The obvious next move was to consolidate, be sensible, build the application. Instead, I decided the real test would be to take the most beloved concurrency model of the modern era — Go's goroutines and channels — and re-implement it from scratch. In C11. With the machine as co-conspirator.

They say when you meet a bear in the woods, you should never try running away from it.

Nobody ever said anything about the other direction.

Next: Running Toward the Bear.