# Josh Tomaino (copyjosh) — full > Josh Tomaino is a software engineer and engineering manager in Cincinnati who builds side projects in the open. Live tools, honest write-ups, and an open invitation to copy what works. Currently: Building AI coaching at Cloverleaf + shipping side projects from Cincinnati. This is the full machine-readable corpus for copyjosh.com: the site index plus the complete text of every post and the longer bio. The concise index is at /llms.txt. ## Projects Side projects shipped in the open. Most are live and free to try. - [MidRally](https://midrally.com) (live): Find your community a place to meet — without anyone sharing their address. - [VibeBox](https://beardwho.codes) (live): Hand it three tokens, confirm three gates, and an agent stands up a hardened ~$5/mo VPS in about ten minutes. - [HermesBox](https://hermes.beardwho.codes) (live): A 24/7 AI agent that lives on your own VPS. No middleman, no markup. - [StreamLevels](https://streamlevels.app) (live): Co-streamers compare mic levels in a shared lobby before going live. Just the numbers. No audio leaves your machine. - [Drive Smart Academy](https://drivesmart.academy) (live): AI driver's ed with state-aligned lessons and a parent dashboard. Early access in Ohio. - [GitHub Healthcheck](https://github-healthcheck.beardwho.codes) (live): Sign in with GitHub for a security report on your account and repos — and an alert the moment someone clones your work to push malware under your name. ## Elsewhere - [X (@beardwhocodes)](https://x.com/beardwhocodes) - [X (@copyjosh)](https://x.com/copyjosh) - [GitHub](https://github.com/copyjosh) - [LinkedIn](https://www.linkedin.com/in/jotomaino/) ## About - Based in Cincinnati, Ohio. - B.S. Computer Science · Miami University. - Executive Committee · American Wittgenstein Institute (since 2017). - Before code I ran A/V and theatre tech — live, on-stage and off-stage productions, from theatrics and jam-bands to talent shows. - Stage credits: Beauty and the Beast, 42nd Street, The Diary of Anne Frank, Trojan Women, Guys and Dolls. ### Work history - 2021 — now — Engineering Manager, Cloverleaf (https://cloverleaf.me) — Unleashing people to do their best work. ☘️ - 2015 — 2022 — Software Engineering Consultant, Global Teletherapy — Easing the billing and record-keeping burden for therapists. Acquired by Presence Learning. 📚 - 2019 — 2021 — Senior Software Engineer, Kroger Technology — Forever improving the customer experience at Kroger.com. ❤️ - 2018 — 2019 — Software Engineering Consultant, Eliassen Group — Doing what needs to be done, and then some. 💪 - 2016 — 2018 — Web Application Developer, Standard Textile — Internal and external apps for marketing and sales. 📈 - 2015 — 2016 — Full Stack Engineer, The Chesed Fund — Reversed the trend of percentage-taking fundraising — every dollar goes to the people who need it. 💕 - 2014 — 2015 — Meor jInternship, Jerusalem, Israel — Logistics, finance, and turning moments into memories. 📸 - 2012 — 2013 — CRM Systems Analyst, Jenzabar ## Writing Index: https://copyjosh.com/blog/ · RSS: https://copyjosh.com/rss.xml ### skills-test: A/B testing whether a Claude Code skill actually helps URL: https://copyjosh.com/blog/skills-test-ab-test-your-claude-code-skills/ Published: 2026-06-29 Tags: building-in-public, claude-code, ai-agents, ab-testing, side-projects Here's the thing nobody says out loud about Claude Code skills: most of the time you have no idea if they work. You install a skill. You run a task. The diff looks good. You think "nice, that helped." But you ran it once. You have no control arm. You have no idea what the agent would have done *without* the skill on the exact same task. You saw one good outcome and your brain filled in causation. That's not evidence. That's a vibe with a commit hash. I got tired of shipping skills on vibes. So I built **skills-test**. ## What it actually does skills-test is a controlled A/B harness for agentic coding. You give it a task and a thing to test — a skill, a model, or a whole agent CLI. It runs the same task many times with that thing **on vs off**, holds everything else constant (base commit, model, prompt, permission policy), scores every run deterministically, and reports an **effect size with a 95% confidence interval**. So "I think it helped" becomes "+58% on tests-passing, CI clears zero" — or an honest *inconclusive*, which is the answer more often than you'd like. The only thing that differs between arms is whether the skill's `SKILL.md` is present. Each arm runs `k` times, each in its own isolated git worktree, each scored on five metrics: tests pass, lint, build, diff size, cost. The diff is the part you actually came for — the report puts the code each arm wrote side by side, word-level highlighting, so you can see *why* the outcome changed and not just *that* it did. ## The wall I hit The first version filtered the on-runs down to only the runs where the skill actually *fired*. Felt obviously right. Why would you count a run where the skill didn't even activate? Because that's selection bias wearing a lab coat. Whether the skill fires is a *post-treatment outcome*. If you keep only the runs where it fired, you're conditioning on a collider, and your clean little A/B turns into a confounded mess that flatters the skill. I'd built a bias generator and called it a filter. The fix was to commit to the right estimand and never blink. **Primary = intention-to-treat.** Every clean on-run versus every clean off-run, regardless of whether the skill fired. That measures the effect of *shipping* the skill, which is the actual thing you decide when you decide to ship it. Activation rate is still reported, but as a diagnostic off to the side, clearly labeled. There's a per-protocol number too, stamped secondary and biased so nobody quotes it by accident. That was the call that made the whole thing honest. It also made the results less exciting, which is how you know it's working. ## The part that surprised me Cross-CLI comparisons are basically a trap, and I had to build the trap door. You *can* point one arm at `claude` and the other at `codex` and run a head-to-head. People will want to. But that comparison is confounded by construction — you're not testing the skill anymore, you're bundling the CLI binary, its default model, its prompt handling, and a separate login all into one number. So skills-test refuses to give it a confident verdict. It downgrades the result to "suggestive," paints a banner explaining the confound, and leans on the blind judge, which reads only the code each side produced with the labels stripped. I expected to feel annoyed building guardrails against my own feature. Instead it felt like the most honest thing in the repo. A benchmark that won't let you overclaim is worth ten that will. ## One opinion, freely given We are way too comfortable saying "this AI thing made me faster" with zero measurement. I've done it. You've done it. The whole field runs on screenshots of one good run. You don't need a PhD to do better. You need a control arm and the discipline to run it more than once. skills-test is stdlib-only Python, one file, no API key — it runs on your Claude Code subscription via `claude -p`. There's a `demo` command that renders a full interactive report offline, zero cost, so you can see what a result looks like before spending a cent. Run your skill against a no-skill control. You might find it crushes. You might find it does nothing. Either way you'll *know*, and knowing is the whole game. Try it: [github.com/beardwhocodes/skills-test](https://github.com/beardwhocodes/skills-test). If you measure one of your own skills and the number surprises you, [come tell me on X](https://x.com/beardwhocodes) — especially if it came out *inconclusive*. That's the result I trust most. Now copy Josh. ### GitHub Healthcheck: catching the clones shipping malware under your name URL: https://copyjosh.com/blog/github-healthcheck-detect-malware-clones/ Published: 2026-06-22 Tags: building-in-public, security, github, cloudflare-workers, side-projects A while back I read a writeup that genuinely rattled me. Attackers had been cloning real, popular repositories — full commit history, contributor list, README, the works — under throwaway accounts. Not forks. Verbatim copies that read as original. They changed exactly one thing: a single `Update README.md` commit adding a download link to a password-protected ZIP. Inside the ZIP, a LuaJIT loader that pulls SmartLoader and then StealC. The clever part is what makes it nasty. The download *link* scans clean on VirusTotal. Only the extracted ZIP trips antivirus, and it's password-protected so the scanners can't even peek. An estimated ~10,000 of these repos sat undetected for over a year. What stuck with me wasn't the malware. It's that the clone looks *more* legit than your actual project at a glance — inherited history, real contributors, a polished README. And AI coding agents happily recommend them. So I built [GitHub Healthcheck](https://github-healthcheck.beardwho.codes). ## What GitHub Healthcheck actually does You sign in with GitHub and get a security report on three things: your account, your repos, and any clones of your work that are out there impersonating you. It scores every repo you own against the campaign's indicators. It searches GitHub for malicious verbatim copies of *your* repos. It hands the account a 0–100 trust grade — age, 2FA, clustered activity — and a band from `safe` up to `critical`. And because the attack is ongoing, a daily background scan emails you the moment a *new* clone of your work shows up. You can also paste in any repo or account to vet it before you trust it, which is the use I reach for most when an agent suggests some repo I've never heard of. The detection engine isn't fancy ML. It's the boring tells the original researcher used, encoded as rules: README references a binary, a shields badge that links to an archive, password-protected language, a URL shortener or anon file host, "free / cracked / full version" lures, a release asset literally named `loader.exe` or `lua51.dll`. The single clearest signal is almost embarrassing — the latest commit changed *only* the README. That one heuristic does a lot of work. Dumb beats clever here. ## The wall I hit Clone detection is where I lost a weekend. Searching GitHub for copies of your code sounds simple until you actually do it. The rate budget is tiny, code search is fuzzy, and the obvious matches are your own forks and legitimate mirrors. My first pass flagged everything and meant nothing. The fix was to stop being greedy. I cap the search — top-10 most-starred repos, a handful of candidates each — and lean hard on the "verbatim but not a fork" shape, because that combination is the actual fingerprint. Then the daily cron diffs new results against a stored baseline of known clones, so you only ever get emailed the genuinely new one. Nobody wants the same alert every morning until they mute it. ## The design call I'm proudest of One rule, decided on day one and never bent: **the app never touches the payload.** It only ever calls `api.github.com`. It reads README and link *text* — it never downloads an archive, never executes anything, and never fetches a user-supplied URL. Zero SSRF surface. That's a real constraint, not a footnote. A "security scanner" that fetches attacker-controlled URLs is just a new attack surface wearing a badge. So the whole engine is pure text analysis, fully unit-tested, with no network calls in the hot path. Sessions are server-side, the GitHub token is AES-GCM encrypted at rest in D1, and the browser cookie only holds an opaque id. OAuth asks for `read:user` and nothing more unless you opt into private repos. The whole thing is TypeScript end-to-end on Cloudflare — Workers, D1, a Cron Trigger, and Cloudflare Email. One platform for auth, data, scheduling, and the alert mail. No glue servers. ## What surprised me I expected to be defending against scary code. Instead the entire campaign hinges on one social move: make a fake look original. The malware is almost an afterthought. The defense isn't smarter analysis — it's spotting the seam where the disguise is thin. A year of invisibility, undone by "the last commit only edited the README." If you ship on GitHub, [run GitHub Healthcheck on your account](https://github-healthcheck.beardwho.codes) and see if anyone's cloned your work yet. It's open source ([beardwhocodes/github-healthcheck](https://github.com/beardwhocodes/github-healthcheck)) if you'd rather read the rules than trust me. If it catches something, [come tell me on X](https://x.com/beardwhocodes). Now copy Josh. ### New site, same idea: Copy Josh URL: https://copyjosh.com/blog/copy-josh/ Published: 2026-06-07 Tags: meta, building-in-public, side-projects I've been meaning to build this site for years. Classic engineer move: I'll ship a dozen apps for other people and leave my own corner of the internet a parking lot. So here it is — a real home for the work, the write-ups, and the running list of things I'm tinkering with. I wanted it to feel like what it actually is: a workshop, not a storefront. Not a portfolio that's trying to sell you, not a wall of badges — just my name, the things I'm shipping, and a way to reach me. Stripped down: white space, near-black text, one loud orange, bold tight type. It's the same design language I use on my own products, which feels about right — if I'm going to tell you to copy what works, the front door should be one of the things that works. ## What "Copy Josh" means The name is a bit of a joke and also completely sincere. Here's the sincere part. I think the best way to get good at building is to build in the open — to show the messy middle, not just the launch tweet. When you do that, two things happen. You learn faster, because writing it down forces you to actually understand it. And other people get to skip the part where they bang their head against the same wall you already cleared. So "Copy Josh" is the invitation. If a project here is useful, take it. If a write-up saves you an afternoon, that's the win — you don't owe me anything for it. I'd genuinely rather you copy the thing that works than admire it from a distance. That's the loop I'm trying to run: invest in yourself, ship the work, share what you learned, repeat. The world gets a little better every time someone does that, and I'd like to be one of the people doing it out loud. ## What's actually here The projects section is the real list, and most of them are live right now. A quick tour: - **MidRally** turns the far-flung crews you met online into a real plan to meet up in person — one place, one day. - **StreamLevels** lets co-streamers compare mic levels in a shared lobby before going live — no audio leaves your machine, just the numbers. - **Drive Smart Academy** is AI-powered driver's ed with a parent dashboard, in early access for Ohio. - **VibeBox** is a tiny kit of scripts and prompts that stands up a hardened, AI-ready VPS in minutes, for ten bucks. A couple are quieter — an invite-only club, a consulting site, one experiment that's parked while I figure out what it wants to be. I'm leaving the parked one on the list on purpose. Building in public means showing the ones that didn't take off too. ## What the blog will be Short, honest field notes. How I built something, where I got stuck, what I'd do differently, the occasional sharp opinion. I spend my days at [Cloverleaf](https://cloverleaf.me) working on AI coaching, so expect some of that to leak in — what's actually working with these tools, what's hype, and the boring infrastructure stuff nobody writes down. No newsletter funnel, no growth hacks. Just the log. If you build something, [come tell me on X](https://x.com/beardwhocodes) — especially if you copied something from here. That's the highest compliment this site can get. Now copy Josh. ### HermesBox: a 24/7 AI agent on your own VPS, no middleman URL: https://copyjosh.com/blog/hermes-your-own-24-7-ai-agent-on-a-vps/ Published: 2026-06-04 Tags: building-in-public, self-hosting, ai-agents, vps, side-projects Most "AI agents" you can use today live on someone else's server. You rent a seat, you talk to it through their app, and it runs inside their guardrails on hardware you'll never see. That's fine for a lot of things. It bugged me anyway. I wanted an agent that was actually mine: running on a box I control, reachable from my phone, with no company sitting in the middle taking a cut. So I built HermesBox. Hermes is Nous Research's personal AI agent. It's good. But standing it up safely on a server is the kind of job that's easy to get dangerously wrong. You're putting an always-on thing that runs shell commands on a public box, and you have to harden it without locking yourself out. HermesBox is one script that turns a fresh Ubuntu server into a hardened home for Hermes, reachable over Telegram, in about ten minutes. A 24/7 agent that lives on your own VPS. No middleman, no markup. That last bit matters to me. I don't mark up a cent of it. Hetzner gets their eight-ish bucks a month for the box, Tailscale's free tier handles private SSH, your LLM provider gets your key. I'm not in the money path. What you're buying, if you can even call it buying when the whole thing is free and documented, is convenience and a safe default. ## The firewall call I'm proud of Here's the design decision I like most. The box has zero open inbound ports. None. SSH is reachable only over Tailscale, and Telegram works over an outbound long-poll, so the always-on bot never needs to listen for anything. Nothing to port-scan, nothing to brute-force, no inbound surface at all. The obvious risk with "lock the firewall down hard" is locking yourself out of your own server. So there's a deadman timer: arm the lockdown, and if Tailscale isn't actually working, the firewall auto-reverts in ten minutes and you're back in. Belt and suspenders. I'd rather the script assume I fat-fingered something than strand me. The agent also runs as an unprivileged user, not root, and risky commands get bounced back to me in Telegram for a yes/no before they run. It's my box, but it's not a blank check. ## The wall: a venv I owned wrong Field notes are only honest if I include the part where I was wrong. The first version installed Hermes as root. Seemed reasonable. Then the bot would come up, try to talk to Telegram or a provider, and quietly fail to install its own runtime dependencies, because Hermes lazy-installs that stuff at runtime and a root-owned virtualenv was the wrong shape for it. I burned real time staring at `journalctl` before it clicked: the ownership was the bug, not the deps. v0.2 reworks the whole install to be owned by a dedicated `hermes` user, the way a normal local install would be. Re-baked on fresh hardware, the Telegram and Anthropic SDKs both self-installed cleanly and the gateway came up polling. Six separate empirical bugs got caught and fixed this way: wrong Hetzner SKU names, a `curl | bash` exit code, an interactive prompt that hung the headless path, a model default that quietly routed to the wrong provider. None of those show up in a design doc. You only find them by actually running the thing on real hardware until it works. ## What surprised me How fragile the messaging layer is. Hermes v0.15.1 only allows one poller per Telegram bot token. Leave an orphaned poller around and you get `getUpdates` 409 conflicts that look like the bot is just dead. HermesBox runs exactly one clean gateway and documents the webhook alternative, but I would not have predicted that the hardest part of "self-host an AI agent" would be Telegram bookkeeping, not the AI and not the firewall. The boring plumbing is always where the bodies are. If you want an agent that's actually yours, [try HermesBox on your own box](https://hermes.beardwho.codes). Read the script before you run it. I mean that, `less hermes.sh` is in the instructions for a reason. And if you spin one up, [come tell me on X](https://x.com/beardwhocodes). Especially if it locked you out and the deadman saved you. I want to hear it. Now copy Josh. ### VibeBox: a hardened, agent-ready VPS in about ten minutes URL: https://copyjosh.com/blog/vibebox-hardened-agent-ready-vps-in-minutes/ Published: 2026-05-31 Tags: building-in-public, devops, security, ai-agents, side-projects There's a setup that's been making the rounds: levelsio's "vibe coding on a server." You rent a cheap box, SSH in, point Claude Code at it, edit files, refresh your domain. No git push. No deploy step. No CI. The server *is* the dev loop. It's a genuinely lovely way to build small things fast. The problem is the part nobody shows you: standing the box up. Doing it by hand is a minefield. Lock yourself out of SSH and you're starting over. Leave port 22 open to the world and you'll get brute-forced or cryptomined before you've finished your coffee. Forget backups and your SQLite file is one fat-fingered `rm` away from gone. Every one of those is a known failure mode, and every one is easy to hit when you're moving fast. So I built **VibeBox** to make that setup boring. ## What VibeBox actually does Hand it three tokens — Hetzner, Cloudflare, Tailscale. Confirm three gates. About ten minutes later you have a hardened, agent-ready dev server. The shape of it: - **Inbound firewall 100% closed.** Web traffic arrives over a Cloudflare *outbound* tunnel, so there are no open ports to scan. SSH comes in only over Tailscale. - **Claude Code installed on the box.** Code from your laptop, or from your phone with Termius + Mosh + tmux. That last one is dangerously fun on a train. - **PHP + SQLite + nginx starter.** No framework, no build step. - **Litestream backups for SQLite, pre-wired** so you get a sane default instead of assembling one at 1am. You can drive the whole thing with an agent — paste a prompt, it runs Packer + Terraform + the APIs and you never touch the Cloudflare dashboard. Or run `vibebox.sh` by hand on a stock Ubuntu box. Same result. ## The wall: I locked myself out First real end-to-end test, I locked myself out of my own server. The firewall phase armed before Tailscale was actually routing. From the script's point of view Tailscale was "up." In reality the IP hadn't propagated yet. So it slammed the door, dropped inbound, and the only key was on the other side of a network path that didn't exist. Box was unreachable. I nuked it and started over, feeling stupid. That mistake is the most important feature in the whole thing now. There's a **deadman timer**. When the firewall arms, it starts a countdown. You SSH back in over Tailscale and run `bash vibebox.sh confirm` to cancel it. If you *can't* get back in — you do nothing. The firewall auto-reverts in ten minutes and you're back where you started. The safe action when you're locked out is to sit on your hands, which is exactly what a panicking person wants permission to do. The `tailscale_up` phase also got stricter: it now verifies a real Tailscale IP exists *before* the firewall is allowed to touch anything. Order of operations is a security control. ## A design call I'd make again PHP + SQLite + nginx. No framework, no build step. Some people will read that and wince. It's deliberate. It's the stack coding agents make the fewest mistakes on. Plain files, an obvious request lifecycle, no bundler state to get out of sync, no node_modules archaeology. When the thing editing your code is an LLM, "boring and legible" beats "modern and clever" every time. I optimized the starter for the agent, not for my own taste. ## What surprised me Running it on a real Hetzner box caught ten bugs that aren't in any documentation. A Hetzner SKU got renamed. nginx reloads were failing *silently*. apt hooks hung forever over a no-TTY SSH session. Litestream's release asset names had churned. None of that shows up when you read the guide. It only shows up on metal. That's the honest takeaway: infra "works on paper" is worth almost nothing. The value here isn't secret knowledge — every step is documented and free to do by hand. That's literally the point. What you're buying is convenience and the ten footguns I already stepped on so you don't. One more bit of honesty, because it matters: the `c` bypass alias (`--dangerously-skip-permissions`) is for disposable boxes only. `IS_SANDBOX=1` is not real isolation. The actual safety boundary is that this box is one isolated, backed-up project you could lose without crying. ## Try it If you want a secure little server to vibe-code on without the setup tax, [VibeBox is here](https://beardwho.codes). It's field-validated, the script is idempotent, and you can read every line before you run it. Please do read it before you run it. If you spin one up — especially if it saves you an afternoon — [come tell me on X](https://x.com/beardwhocodes). That's the highest compliment this site gets. Now copy Josh. ### Drive Smart Academy: AI driver's ed that won't bluff the law URL: https://copyjosh.com/blog/drive-smart-academy-ai-drivers-ed/ Published: 2026-05-24 Tags: building-in-public, side-projects, ai, driver-ed, ohio There's a teenager in my house inching toward a learner's permit, and the state of teen driver's ed is grim. The official study material is a dense government PDF — the Ohio Digest of Motor Vehicle Laws — written like a tax form. Kids cram it, pass the knowledge test, and forget all of it by the next exit. Meanwhile the parent's job is to supervise 50 hours of driving, 10 of them at night, logged on... what, a sticky note? A glovebox napkin? Nobody actually knows if the kid is ready. They just turn 16 and we hope for the best. That's the gap **Drive Smart Academy** fills. It's AI-assisted driver's ed for Ohio teens, with a parent dashboard, in early access right now. ## What it actually does It's not a chatbot that "teaches you to drive." It's practice grounded in the real source. The whole question bank is built from the Ohio Digest of Motor Vehicle Laws, with explanations that cite the actual law, realistic scenarios drawn as hand-made SVG diagrams instead of cheesy stock photos, and an exam simulator that mirrors the real thing: 25 questions, Ohio's 75% pass line, per-topic results. There's a readiness score, a spaced-repetition queue for the stuff you keep missing, and a study streak, because teenagers respond to streaks. The parent side is the part I'm proudest of. A printable progress report (readiness, per-topic mastery, consistency) plus a supervised-driving log that counts toward Ohio's 50-hour / 10-night requirement. So the answer to "is she ready?" stops being a vibe and starts being a number you can look at. ## The wall: AI should not write traffic law Here's the design call that shaped everything. The obvious move with an "AI" study app is to let a model generate the questions. I tried it. It writes beautiful, confident, plausible questions, some of which are subtly and dangerously wrong about Ohio law. Confidently wrong is the worst failure mode you can ship into a driver's exam. A kid memorizes your mistake and then carries it onto a real road. So I flipped the AI's job. It doesn't author the law. It grades confidence in each question against the source material. Every question carries a record in a `question_confidence` table, and low-confidence ones get flagged for a human (me) to vet against the Digest before they go anywhere near a learner. A content-integrity check runs in CI, so a regression in the safety-critical question bank literally can't merge. The model is a skeptical reviewer, not an author. That inversion is the whole product, honestly. The dumb wall, and there's always a dumb one, was Cloudflare D1 handing JSON columns back as strings instead of parsed objects. Everything worked locally with tidy test data, then quietly returned garbage in production. Half a day gone chasing it. Now every JSON column gets parsed at the service layer. Field note: if your database's "JSON" type is really TEXT in a trenchcoat, find that out before prod does. ## What surprised me I went in thinking the AI would be the star. It isn't. The valuable, boring work was refusing to let it be creative in the one place where creativity is a liability. The best thing AI did on this whole project was tell me which of my own questions to distrust. The other surprise: parents care about the log way more than the quiz. The quiz is for the kid. The dashboard is the thing that lets a parent actually sleep before handing over the keys. I under-built it at first and treated it as a feature instead of the point. Fixing that now. It's early access, Ohio only, and the content bar is high on purpose. This is exam material, not trivia. If you've got a teen staring down the BMV test, kick the tires at [drivesmart.academy](https://drivesmart.academy). And if you find a question that's wrong, you'll genuinely make my week. [Come tell me on X](https://x.com/beardwhocodes). Now copy Josh. ### StreamLevels: matching mic levels before you go live URL: https://copyjosh.com/blog/streamlevels-match-mic-levels-before-you-go-live/ Published: 2026-05-18 Tags: building-in-public, side-projects, web-audio, cloudflare-workers, live-streaming Last month I hopped on a co-stream and spent the first ten minutes sounding like I was broadcasting from inside a fridge. My guest was crisp and loud. I was a distant mumble. Nobody caught it until chat did. Classic. Here's the thing about co-streaming: there's no shared reference for "are we even in the same range?" before you go live. Everyone has different mics, different interfaces, different gain staging. You each glance at your own meter in OBS, decide it looks fine, and hit Live. Then one of you is clipping and the other is a whisper, and you don't find out until it's already on tape. So I built **StreamLevels**. It's dead simple: you spin up a lobby, send the link to whoever you're streaming with, and you all watch each other's mic levels on one screen in real time. Speak normally, nudge your gain until everyone's bars sit in the same zone, then go live. That's the whole app. The one rule I cared about: no audio ever leaves your machine. Only the level number travels. ## The browser was lying to me First version, I got the meters drawing, wired up the lobby, tested it with a second laptop. And it worked... too well. Both meters magically sat at the same level no matter what I did to the gain. Cranked it, dropped it, didn't matter. The bars barely moved. Took me an embarrassingly long time to figure out why. Chrome's `getUserMedia` turns on `autoGainControl` by default. The browser was helpfully normalizing my input level before I ever saw it, which is the one thing a level-matching tool absolutely cannot have. My app was measuring Chrome's idea of a good level, not mine. The fix is three booleans: `echoCancellation`, `noiseSuppression`, and `autoGainControl`, all off. Now the meter shows your actual gain, warts and all. If you're running hot, you look hot. That's the entire point. ## The best feature is the one I didn't build The obvious way to let people "hear each other check levels" is to send audio. WebRTC, peer connections, the whole circus. I almost did it. Then I realized I didn't need to. Co-streamers already have Discord or whatever open for the actual talking. The only thing I needed to move across the network was a single number, your level in dBFS, about thirty times a second. So that's all I send. A tiny JSON payload over a WebSocket. No SDP, no TURN servers, no media negotiation, no "allow this site to record your audio and send it to a stranger." And the privacy line writes itself, because it isn't marketing. It's just true. Your audio never leaves your machine because there is no code path that would carry it. I literally can't see or hear you. I get a number. That constraint made everything smaller. The backend is a Cloudflare Durable Object per room. Rooms hibernate when idle and dissolve the moment the last person closes the tab. No database of recordings to secure, because there are no recordings. No accounts. Refusing to send audio didn't cost me a feature. It deleted half the app and made the rest more honest. ## What surprised me I went in assuming the hard part would be the metering math or the realtime networking. It wasn't. The hard part was getting the browser to hand me an honest signal in the first place. Everything downstream (the smoothed bars, the peak-hold markers, the "possibly muted" nudge when someone's been silent too long) was easy once the input was real. That's the lesson I keep relearning on side projects: the tricky bit is almost never where you brace for it. Budget your worry for the boring layer at the bottom. If you co-stream, try it before your next one. Open [streamlevels.app](https://streamlevels.app), make a lobby, throw the link to your co-host. Takes about fifteen seconds, and you'll never go live mismatched again. And if you do, [come tell me on X](https://x.com/beardwhocodes). Bonus points if you find a bug. Now go match your levels. ### Building MidRally: one place, one day, no addresses shared URL: https://copyjosh.com/blog/midrally-meet-your-online-community-irl/ Published: 2026-05-04 Tags: building-in-public, side-projects, midrally, community-building Every online crew has the same dead conversation. Someone says "we should all actually meet up sometime." Everybody hearts it. Then nothing happens for two years. I've been in a few of those crews — a Discord I've been in since forever, a group of people I only know through replies, a handful of folks I've shipped side projects with and never shaken hands with. Real friendships. Zero square footage in common. And every time someone floated a meetup, it died in the same swamp: where, when, and who has to fly the farthest. That swamp is what MidRally is for. ## The actual problem The problem isn't that people don't want to meet. They do. The problem is that planning it is a negotiation nobody wants to run. Pick a city and you've quietly decided someone is driving six hours while you walk to the venue. Drop a poll and you get fourteen options and zero decisions. And there's a smaller, weirder blocker underneath all of it: to find a fair middle, you sort of need to know where everyone lives. Nobody wants to paste their home address into a group chat with people they met on the internet. So MidRally does one thing. You tell it who's coming, it finds the one place that brings the most of you together with the least hassle, and you pick a weekend. One place, one day. That's the whole product, and keeping it that small was the hard part. ## The wall I hit: the middle isn't the middle My first instinct was the obvious one. Take everyone's location, average it, drop a pin. Geographic center. Done. It's wrong almost every time. The literal midpoint of a scattered group lands in a field with no airport and one motel. Worse, it gets yanked around by whoever lives farthest out. One person in the middle of nowhere drags the "fair" point away from the eight people who'd actually have an easy trip. Distance on a map is not the thing people feel. Hassle is. A 90-minute flight beats a five-hour drive, and "is there even a way to get here" matters more than raw miles. So the middle MidRally finds isn't a geometric average. It's the place that minimizes the pain of the group getting there. And it treats everyone as one vote. Everyone counts once, so no single far-flung person, and no dense cluster all living in one city, gets to hijack the answer. A fly-in reunion and a drive-to meetup are genuinely different problems, and the spot you'd pick for each is different too. ## The design call: no addresses The privacy thing wasn't a feature I bolted on. It was a constraint I started from. You do not need someone's street address to find a fair place for a group to meet. You need roughly where they are. So that's all MidRally asks for. Nobody's exact doorstep goes into a shared plan, because nobody's exact doorstep is anyone else's business. Collecting precise addresses would've been a liability I'd have to defend forever and a thing that makes people hesitate before they join — for a number I don't even need. Easy call. ## What surprised me I went in thinking MidRally was a math project. Optimize the distance, ship it. It's not a math project. The math is the easy part. The real product is removing the moment where one person has to be the one who picks, and then risks looking like they rigged it toward their own couch. When the answer comes from a neutral tool that everyone can see is fair, the awkward part evaporates and people just... go. The constraint of one place, one day isn't a limitation I'm apologizing for. It's the feature. Too much choice is what was killing these trips in the first place. If I added one thing back, it'd be tempting to pile on options. I keep talking myself out of it. The day MidRally has fourteen settings is the day it quietly becomes the group poll it was built to replace. ## Try it If you've got a crew that keeps saying "someday," point it at MidRally and get an actual place to meet: https://midrally.com. And if you plan a meetup with it, [come tell me on X](https://x.com/beardwhocodes). The first real hug is the whole point. Now copy Josh. ## Copy what works The name is an invitation: take the parts that help. The public profile repo is clonable. - `git clone https://github.com/copyjosh/copyjosh.git` - [GitHub profile](https://github.com/copyjosh)