An LMS and CRM at 205,000 Lines of Code, Built by AI Agents: The Real Cost of Running It, Three Lead States, and One \r Character
Two linked production systems — an educational platform LMS (121,853 lines of TS/TSX, 47 routers, 288 procedures) and a CRM with a unified registry of 334 tools (83,251 lines) — were built without a development team using AI agents on top of a single shared PostgreSQL instance. Writing the code took a handful of evenings, but the real cost showed up in getting it to production: 14 critical defects found during adversarial review, 5 more introduced by the fixes themselves, and an invisible carriage-return character \r that silently disabled interactive practice in 138 of 146 lessons. Reliably integrating AI agents into a live business required splitting lead handling into three independent states under FOR UPDATE locking, reconstructing conversation history on the server, and persisting a meeting record before ever calling the external Zoom and Google Calendar APIs.

- Codebase size, built without a development team: 205,104 lines of TypeScript/TSX (121,853 in the LMS across 532 files and 83,251 in the CRM across 367 files)
- API and agent-facade surface: 47 routers and 288 tRPC procedures in the LMS (93 tools) + 334 tools in the CRM registry (257 exposed in the facade)
- Scale of a single invisible CRLF bug: 138 of 146 practice lessons with zero successful submissions, caused by a trailing `\r` character during server-side parsing
- Haul from adversarial review before launch: 14 dangerous vulnerabilities in the first pass + 5 defects introduced by the fixes themselves
Building a large system with AI agents and no programming staff has become a cheap task today. What's expensive is everything that starts the morning after the demo: guarding against a client forging history, database locking against double-clicks from managers, and invisible special characters hiding in secrets.
Both of my systems — the learning platform LMS (121,853 lines of TS/TSX, Next 16.2.6, React 19, Drizzle ORM, 58 migrations, 16 tracks, 211 lessons, and 55 simulators) and the CRM sitting alongside it (83,251 lines, Prisma 7.
6, 48 database models, BullMQ, and four connected AI agents) — run on a single shared PostgreSQL instance. We built our own stack for two reasons: off-the-shelf LMS platforms have no primitive where a neural network first plays out a student's weak prompt and only then coaches against a hidden preamble, and the CRM had to sit on top of an already-working Telegram funnel bot without duplicating databases. As a side note, repainting 289 color variables when forking the CRM took one evening, while scrubbing out inherited demo data (205 test leads, a fallback "38% margin," and fake "7 tasks" badges) stretched out over two months.
How did a single invisible carriage-return character \r break 138
lessons, deduplication, and OAuth?
The most insidious bug in the LMS left no trace in the logs. Students would see a correctly rendered `:::practice` block, click "Submit," and get back "Practice block not found." Database statistics showed that 138 of 146 lessons had exactly zero successful completions. The content had been authored on Windows with `CRLF` (`\r\n`) line breaks: the frontend normalized the text before rendering, but the server-side parser split on `\n`, leaving an invisible trailing `\r`:
```typescript // Without normalization, a trailing \r breaks the $ anchor in JS, // since the dot (.) doesn't match \r, leaving the task field parsed as empty: md = md.replace(/\r\n?/g, "\n"); const match = line.match(/^(task|starter|goal|model|system)\s:\s?(.)$/i); ```
That same `\r` also changed the control hash of the text during chunking, forcing the system to pay for embedding generation all over again, and on a third occasion it ended up appended to a secret in `.env`, which caused Google OAuth to return `invalid_client` (caught by a string-length check, `awk -F= '/^GOOGLE_/ {print $1, length($2)}' .env` — 36 bytes instead of 35).
Why can't you trust data coming from the browser, and how is the CRM's
registry of 334 tools structured?
Adversarial review before launch uncovered 14 severe defects, and checking the patches written against them turned up 5 more. In the first version of the practice feature, the browser itself sent the task text and a `history` array to the server, making it possible to tamper with a reply via DevTools — and 8 of 55 simulators could be passed with a score of 71–90 simply by copying the prompt into the answer. We moved preamble assembly and reconstruction of the last 6 turns strictly onto the server, and raised the grader timeout from 30 to 60 seconds.
In the CRM, the entire architecture is built around a single registry of 334 tools (257 exposed in the agent facade, with 77 system tools locked behind the `facadeExposed: false` flag). Every call runs through a single `invoke()` pipeline: the `acting_user` identity is extracted only from the session (distinguishing `missing` from `invalid` errors when, say, a session ID of `api-...` or a negative group-chat ID comes in), and arguments are scrubbed with the `SECRET_KEY_RE` regex before being written to `audit_log`.
Why split a single lead button into three states, and how do you catch orphaned
Zoom meetings?
In live sales, a rep messages a client directly on personal Telegram while, in parallel, a bot is warming up that same person. That's why a single "Take the lead" button had to be split into three independent states: "Taking ownership" (assigns an owner), "Contacted" (records the fact of first touch), and "Intercepted" (the bot goes silent and hands the conversation to a human). To stop two managers from clobbering a card with a double-click, the CRM finds the oldest record by `telegram_id` and locks it with `SELECT ...
FOR UPDATE`. And when creating meetings in Zoom and Google Calendar, the very first step is always an `INSERT` into our own database with a `creating` status and `zoom_state` / `gcal_state` fields, so that if an external API call fails, no orphaned meetings are left floating in the cloud, unaccounted for.
This machinery of interactive simulators and agent registries runs inside [Hamidun Academy](https://academy.hamidun.com) (a 72-hour trial is open) and within the [Nomad AI](https://nomadnet.ai) / [agent.nomadnet.ai](https://agent.nomadnet.ai) stack, and we break down the architecture of our CRM and LMS in [Vibe Club](https://hamidun.com/club).
- Oct 28, 2026Wired Tested 6 AI Agents in a Day: Dots Leads, Gemini Spark Fails at Lunch
- Oct 28, 2026Anthropic files S-1 for IPO at $2 trillion valuation: 85% margin, profitable Q2
- Oct 28, 2026Meta releases Muse Spark Open: 816B parameters and the lead among US open-weight models
- Oct 28, 2026Switzerland's Apertus 1.5 Gains Vision, Hearing and a 262K-Token Context Window
Key Questions Answered
How did a single invisible carriage-return character \r break 138 lessons, deduplication, and OAuth?
The most insidious bug in the LMS left no trace in the logs. Students would see a correctly rendered `:::practice` block, click "Submit," and get back "Practice block not found." Database statistics showed that 138 of 146 lessons had exactly zero successful completions. The content had been authored on Windows with `CRLF` (`\r\n`) line breaks: the frontend normalized the text before rendering, but the server-side parser split on `\n`, leaving an invisible trailing `\r`: ```typescript // Without normalization, a trailing \r breaks the $ anchor in JS, // since the dot (.) doe
Why can't you trust data coming from the browser, and how is the CRM's registry of 334 tools structured?
Adversarial review before launch uncovered 14 severe defects, and checking the patches written against them turned up 5 more. In the first version of the practice feature, the browser itself sent the task text and a `history` array to the server, making it possible to tamper with a reply via DevTools — and 8 of 55 simulators could be passed with a score of 71–90 simply by copying the prompt into the answer. We moved preamble assembly and reconstruction of the last 6 turns strictly onto the server, and raised the grader timeout from 30 to 60 seconds. In the CRM, the entire a
Why split a single lead button into three states, and how do you catch orphaned Zoom meetings?
In live sales, a rep messages a client directly on personal Telegram while, in parallel, a bot is warming up that same person. That's why a single "Take the lead" button had to be split into three independent states: "Taking ownership" (assigns an owner), "Contacted" (records the fact of first touch), and "Intercepted" (the bot goes silent and hands the conversation to a human). To stop two managers from clobbering a card with a double-click, the CRM finds the oldest record by `telegram_id` and locks it with `SELECT ... FOR UPDATE`. And when creating meetings in Zoom and Go
Build & Ship Production AI Agents Without Boilerplate Code
Start your free 72-hour trial at Hamidun Academy (academy.hamidun.com) — interactive lessons on Claude Code & Cursor, live Tech Radar, and production agent templates.
The AI world, distilled — once a week
Seven stories that actually mattered, hand-picked. No noise, no reposts, no press releases.
Done! Check your inbox for a confirmation.