How I Built OMLA by Directing AI Agents Instead of Writing the Code
- Category
- AI & Local LLM
- Posted
- July 11, 2026
- By
- Jacob Lloyd — written with AI assistance, post-project
- Read time
- 9 min read
In plain terms: The true story of how one person, with no programming team, directed AI assistants to build a real online licensing platform for a nonprofit. It explains the working method: clear written plans, layers of double-checking, and a rule that nothing goes live without human approval. A practical blueprint for building serious software with AI help.
Here's the exact loop I used to get AI coding agents to build OMLA, a nonprofit's royalty-licensing backend, almost single-handedly. Steal the process. The finished backend is just proof it works.
tl;dr
- What it is: the workflow that got Claude Code and a local AI agent swarm to build a real nonprofit backend, with me as architect and gatekeeper, not typist.
- What it costs: basically nothing new. A coding-agent subscription, maybe a spare GPU for second opinions. The real cost is your own review time.
- What you need: a coding agent, real written specs instead of vibes, and a hard rule that nothing deploys until a human types the word.
- What you end up with: a working backend with a paper trail. Layered audits, tests against a real local stack, and a deploy script that refuses to ship without a human "GO."
What you end up with
Before any how-to, here's what came out the other end: OMLA, the Open Model Licensing Association. A royalty-licensing backend and public site for open AI models.
| What | The result |
|---|---|
| Status | Organizing as a Washington nonprofit, 501(c)(3) pending. Site is badged "Beta 0.9.0," on purpose. |
| The deal | Free for personal, research, and educational use. Commercial use owes 30% of whichever is bigger: attributable revenue or what the model runs cost. |
| Database | 12 core tables, Postgres 17 on Supabase, row-level security everywhere, money math in integer cents (never floats) |
| Identity | Hybrid ed25519 + ML-DSA-65 signatures, classical plus post-quantum, so a future quantum break doesn't undo the system. No verified signature, no royalty statement. |
| Audit trail | Append-only, SHA-256 hash-chained, checked hourly, with a daily "anchor" of the chain tip stored outside the database entirely |
| Frontend | About 30 plain static HTML pages, no framework, in 13 languages, on ordinary shared hosting |
| Who built it | One human directing traffic, Claude Code writing the code, a local open-model agent swarm doing review passes and busywork |
| Where it stands | Fully staged and tested. Deploy is dry-run-only until I personally type "GO." |
Rough timeline, in case you think this happened over a weekend:
- Late May 2026 — pivot from a personal chatbot side project to a licensing nonprofit, plus the initial 12-table schema
- Within days — post-quantum signature hardening, the money-path edge functions, and the "no-custody" rewrite (more below)
- Early June — backend pushed to production (the database, not the public frontend)
- June 16 — legal rewrite done, License v1.0 takes effect
- July 2 — full site/backend refresh and a 13-locale retranslation, staged and tested, still waiting on my "GO" as of this writing
Funny origin note: OMLA originally stood for "Open Machine Learning Assistant," a personal chatbot project. The infrastructure choices carried straight over to the licensing pivot; the chatbot got demoted to a scope-locked docs helper that's deliberately switched off in production. Turning a hobby chatbot into licensing infrastructure is a normal amount of scope creep, right?
The rule that made the rest possible
One design decision did most of the heavy lifting, before any code got written: OMLA never moves, holds, or transmits money. It computes what's owed and publishes the payee's self-provided wallet or payment details. The commercial user pays the creator directly, peer to peer. No payment processing, no escrow, no "OMLA balance." Wallet identifiers are routing info, not accounts.
The license wording says it plainly: "OMLA publishes; OMLA does not pay."
Why bother? Being a money transmitter is its own regulatory nightmare on top of the licensing one. Skip custody, and the KYC/sanctions/tax burden shifts to the two parties actually transacting, which is where it legally belongs anyway. This one decision is why a solo hobbyist could even attempt this project.
It wasn't even the original design. Mid-build, the schema still implied custody, so a table literally named payments got renamed to royalty_statements — a whole migration of columns, indexes, triggers, RLS policies, and enums, just to make the database stop lying about what it does. The old wording still came back to bite me later (see the gotchas).
Here's the path a dollar of royalty takes without OMLA ever holding it.
Step 4 is the gate: no verified signature back at step 1, no royalty statement, ever. OMLA publishes the number and the wallet, then it's between two other parties — same as me telling you a friend owes you twenty bucks and letting you two sort it out.
The actual loop: human, agents, verify, deploy
The workflow isn't complicated. It's just strict about who does what, and it repeats every time something changes.
The point isn't the boxes, it's that nothing skips a box. Every change starts with me writing down what should exist and why, not "make it better." Agents draft against that spec, other agents audit the draft before I ever see it, and tests run against infrastructure that behaves like production. Only then do I read the diff.
The whole backend runs locally first: a real Supabase stack, Postgres 17 plus edge functions, on my own machine in rootless containers. Production is a deploy target, never the source of truth. If local and live disagree, live is wrong until I ship.
The quality bar lives in the project config, not just my head: "Audit-ready by default: ship work that can withstand external audit." Everything assumes an independent reviewer, AI or human, will try to poke holes in it later.
Testing against a real stack, not a mock
None of the above matters if the tests are lying to you, so nothing here ships against a mock database. Everything runs against a real, disposable local stack. Three layers, all green before anything moves toward deploy:
deno test # 13 unit tests for the edge functions
./test/run.sh # isolated scratch DB per run, migrations w/ ON_ERROR_STOP,
# smoke tests (PQ signatures, wrong-key rejection, audit
# hash-chain, payout gate), RLS cross-tenant isolation,
# an adversarial suite, then rollback + reapply
./test/integration.sh # the full money path against the live local stack:
# register → tampered payload rejected → wallet verify →
# splits → usage report → statement published → replay
# (asserts zero duplicate writes) → admin gate → compliance
That "replay asserting zero duplicate writes" line matters more than it sounds. Usage reports come from other people's systems, so eventually one gets resubmitted by accident; the test checks that replaying the same report produces zero new statements, not just that nothing crashed. The suite was later hardened to assert outcomes instead of logging them — pedantic-sounding until you're debugging a test that "passed" while doing nothing.
Audits grading other audits
One AI reviewing its own work is a rubber stamp. So audits are layered on purpose: local swarm agents do a first fix pass, then a stronger model runs as a final, independent audit gate.
That final gate isn't ceremonial. On one pass it verified the local agents' fixes and still found a search-path bypass plus column, signature, and lineage gaps the first pass missed. That became its own hardening migration: twelve numbered findings, each shipped with a regression test so it can't quietly come back.
Scale matches the change. Routine stuff gets a 3-agent review panel. The full frontend and i18n overhaul got a 24-agent swarm pass, which caught that the site had drifted (still describing the old chatbot product, not the licensing backend it had become) and forced a rebuild to match reality.
The deploy gate: dry-run until I type it
The deploy runner has one job: make sure nothing above this line reaches production by accident. Dry-run by default, every time; production only gets touched with an explicit flag:
./deploy.sh # dry run (default) — shows exactly what WOULD happen
./deploy.sh --go # only a human runs this, only when ready
A guided wrapper goes one further: you literally type "GO" before any production-mutating step. And the standing agent rule, written down so it stops being a suggestion: never add --go without an explicit human instruction.
Other guarantees baked into the runner, not just policy on paper:
- Frontend sync never uses
--delete, so it can't nuke files on the host it doesn't manage - A server-side backup is taken before any frontend overwrite
- The backend refuses to act unless the linked project matches the expected production project — a hard guard against pushing to the wrong database
- Only additive migrations ever run against remote, never a reset
- The documentation chatbot function is excluded from every deploy, on principle
- Secrets live only in a mode-600 file outside the repo; a plain-text check confirms the service-role key never appears in what ships
Honestly, the deploy log for this project is mostly a diary of me chickening out: dry run after dry run while a fully staged redesign sat waiting for the word. That's not a bug. That's what "gated" is supposed to feel like.
Gotchas
The parts that hurt:
- The zero-width byte. Migration SQL has to be pure ASCII. One invisible non-ASCII byte can split a
$$dollar-quote token and silently merge two SQL statements. The fix became ritual: grep for non-ASCII bytes (want zero) and count$$tokens (want an even number), every time. - Percent signs bite. In a
RAISE EXCEPTIONformat string,%%is a literal percent and every value needs exactly one%. This bit me once, which is the entire reason it's written down now. - SECURITY DEFINER needs a pinned search_path. Every definer and trigger function pins its search path so nobody can shadow a table in
pg_tempand feed it the wrong data. The adversarial test counts the pinned functions and fails if the count drops. - The flaky superuser. After an unclean restart, the local Postgres role sometimes comes back without superuser privileges. The fix is a clean stack stop and start, not troubleshooting. Written down so the agents stop rediscovering it.
- i18n quietly overrides HTML. The translation layer overrides page copy entirely, so "fixing" the HTML can still serve stale wording if the locale file wasn't fixed too. Exactly how old custody-era copy kept reappearing after it was supposedly removed.
- The audit chain can be fooled, on purpose, so we know it. An attacker with full database control could drop the hash-chain trigger and rebuild a self-consistent fake history; the internal check alone wouldn't catch it. So a daily job anchors the chain's tip hash outside the database entirely, and the test suite proves both halves in one run: internal check fooled, outside anchor still catches the rewrite.
- The live site isn't the staged site. As of this writing, the public site still shows some pre-realignment copy (an old license draft, a stale payment-rails card) because the redesign is staged, not deployed. Also, the public
/aboutURL is a bare directory index; the AI-disclosure page lives one level down.