What this system is: JayAI

One laptop in Michigan runs our websites, our AI agents, a radio station, a phone camera monitor and the dashboards that watch all of it. This is how it fits together, and the principles that keep it ours.

Written October 17, 2026, from what is actually installed and running on the machine.

In one sentence: JayAI is a self-hosted platform where coding agents (Claude Code, Codex and Kimi) build and run small Ruby services on one machine, every service is exposed through a single tunnel, every service reports to one local dashboard, and every outside provider sits behind an adapter we can swap.

The machine

Everything runs on a single Lenovo ThinkPad X1 Extreme laptop: 16 CPU threads, 30 GiB of memory, a 4 GiB NVIDIA GTX 1650 and a 1 TB SSD, on Ubuntu. There is no cloud server and no container platform underneath the core. Each service is a plain systemd user unit running as an ordinary user, not root, and starts at boot without anyone logging in.

Nothing listens on the open network. Every service binds to localhost, and the only way in from the internet is an outbound ngrok tunnel. That one choice removes a whole class of problems: no open ports, no firewall rules to get wrong, no public IP to defend.

The orchestration layer: agents as the workforce

The point of the machine is to be a place where AI agents do real work. Three agent CLIs are installed side by side, each signed in with its own account:

Next to them sit local models served by Ollama (small Qwen and vision models on the GPU) and a local whisper.cpp speech-to-text server. The hosted agents do the heavy reasoning and writing; the local models handle the steady, private, high-volume jobs like transcribing radio or describing camera frames, where sending data out would be slow, costly or simply not ours to send.

Having three providers is deliberate. Our rules ask for at least two swappable providers for anything important, especially AI models, so we can compare them on price and quality and survive any one of them changing terms or going away. Agents share one rulebook and one repository, take ownership of a task, work in an isolated copy, and hand off with evidence; that protocol is written up in How our agents work together, and a proposal for a threaded orchestrator that dispatches work to them is on bada.im.

One rulebook for every agent

Every agent reads the same AGENTS.md before touching anything. It is published live on ruby.im, so the rules we give our agents are the same rules anyone can read. In short:

Small habits back the rules up. Every public change gets a dated, one-sentence changelog entry in the same change, written by a tiny bin/changelog script, so each site's history is readable without git.

Websites: one server, many domains

All our public sites are served by one small Rack app on Puma. It looks at the Host header of each request and serves the matching folder, so adding a website means creating a folder with an index.html. A file watcher notices the new folder and the tunnel picks up the domain on its own. There is no build step: pages are plain HTML with inline CSS, and a saved file is live on the next load.

SiteWhat it is
jaycorp.usJayCorp's home, research notes and how we run things (this page).
ruby.imEngineering notes for people and agents, and the published AGENTS.md.
aol.pmSecurity notes: how systems fail and how to defend what we run.
bada.imAdversarial research on models and agents.
michigan.imA live signal control panel for Southeast Michigan radio.

The server adds the shared "More sites" footer to every page as it sends it, gives every file an ETag so edits show up immediately, and keeps its own first-party visitor and scan log instead of an outside analytics service (see Visitor and scan logging). A site can also send some of its paths to an app, which is how the dynamic pieces below appear under ordinary website addresses.

Apps: things that run, not just pages

Dynamic apps live in their own folders, each with its own Gemfile, README, systemd unit and port.

Radio. Two software-defined radios, an SDRplay RSP1 and a HackRF One, are plugged into the laptop. Hand-written Ruby and small C programs split one slice of the 2 m amateur band into fifteen channels at once, detect who is talking, stream the active repeater to every listener as one shared MP3, and transcribe each transmission with the local Whisper server. The page at michigan.im/radio shows it live, with a waterfall explorer, a transcript archive, an APRS view and a status page that says plainly whether the receiver is healthy. Other presets tune NOAA weather, airport ATIS and broadcast FM. In the background the radios run a continuous spectrum survey (the HackRF from 24 MHz to 6 GHz), record a nightly amateur traffic net into a public archive at michigan.im/traffic, and retune themselves each night from what they have learned. Everything is receive-only and limited to what is lawful to receive.

Live. A phone opens a web page and streams its camera and microphone over a WebSocket. ffmpeg splits the stream into clips and frames, whisper.cpp transcribes the audio, a local vision model describes the scene, and the results flow back to the phone within seconds. When a session ends, a language model writes a summary through a small adapter whose default is the local model and whose second provider is Claude. It is private and token-protected.

Media. A self-hosted media server and its supporting tools run in Docker Compose on the same machine, outside the main repository, and are registered for monitoring like everything else.

Observability: see everything in one place

Every service, site, scheduled job and the machine itself is listed in one registry file, observability/services.yml, with its unit, health check and source. That list drives the monitoring, so registering a service is how it becomes visible. Telemetry uses OpenTelemetry, the open standard, into a local stack:

apps, sites, agents ─OTLP─▶ OpenTelemetry Collector ─▶ Prometheus  (metrics)
host, GPU, journald ──────▶                         ─▶ Loki        (logs)
health checks ────────────▶                         ─▶ Tempo       (traces)
                                     Grafana reads all three

The Ruby apps send traces through the standard OpenTelemetry gems, Claude Code sends its usage and cost, and the host reports CPU, memory, disk, network and GPU, so a slow page can be told apart from a busy laptop. All of it stays on our own disk, and because it is OTLP, the backend could be swapped without touching the apps. Our rule for "done" is that a new service shows up healthy on the dashboard and an error from it can be found there.

Secrets: one door

Code never reads a key from a hard-coded path. It asks one adapter, Secrets.fetch("NAME"), which reads the project's git-ignored .env file today and can switch to Bitwarden Secrets Manager with one setting. Agents may list secret names but never print, log or commit a value, and they never type a person's password: the person stores it, and the agent reads it through the adapter.

The principles, together

The system is young and still one machine. What makes it a platform rather than a pile of scripts is that each new piece arrives the same way: a folder, a unit, a registry entry, a changelog line, and an agent following the same rules as the last one. For the operator's view, with every command and file, see the Handoff README.

Provided as is, for information only, with no warranty and no liability. Not legal or professional advice. Security and radio material is for authorized, lawful use only. Full disclaimer.

Visits are logged on our own server: IP address, browser and device details, and pages viewed. We use them to understand traffic and spot attacks, and never share or sell them.