dean kerr

I build tooling for people who work directly with LLMs and inference.

Everything below is built solo, in TypeScript — edge infrastructure, streaming runtimes, protocol libraries, and the analysis tooling on top. I observe the model catalog, build client runtimes against it, and operate inference in production.

OpenRouter Capability Analysis

ORCA

May 2025 - present

On OpenRouter, what you actually call is a provider endpoint, and the same model is often served by half a dozen providers at different prices, quantizations, context lengths, and feature support. Across a catalog this large, those differences can matter more than the model itself.

The data grid puts every endpoint side by side: a dense, filterable comparison across pricing, capabilities, modalities, and supported parameters, on TanStack Table and Virtual so it stays responsive over the whole catalog. There’s deliberately no by-model lookup — the endpoint is the unit that matters.

Monitor adds the dimension the catalog never shows: time. Endpoints appearing and vanishing, prices moving, and capabilities like tool use or structured outputs switching on and off — activity that’s otherwise invisible.

Discord alerts deliver a personalised Monitor for subscribed model-id patterns, generated and dispatched entirely server-side. A public API exposes the curated data as JSON.

ORCA endpoints grid showing OpenRouter models, providers, pricing, modalities, features, and context windows
ORCA monitor timeline showing recent pricing changes and new endpoints across providers

stack

  • Next.js 16 / React 19 on Vercel: the frontend, in a Turborepo monorepo
  • Convex: backend, database, and cron scheduling in one; the crawl pipeline, change tracking, Discord bot, and public API all live server-side
  • TanStack Query / Table / Virtual: the data grid stays interactive across thousands of endpoint rows
  • json-diff-ts: field-level change detection that feeds Monitor and alerts
  • bun, Zod, Remeda, OXC toolchain

tetra

isomorphic inference client

Mar 2026 - present

Most chat UIs bind the inference stream to the React component lifecycle: switch sessions or navigate between views and the in-flight request stalls or dies, because component state owns the conversation. tetra inverts that. The runtime writes streaming snapshots into TinyBase, a reactive in-memory database, and every UI is a pure reader.

The same engine drives the browser SPA, the desktop app, and the CLI unchanged. Because the store is mergeable (CRDT), one shape serves local persistence, tab sync over BroadcastChannel, and opt-in cross-device sync through a Cloudflare Durable Object. A session in the terminal and the same session on the desktop converge on identical state.

Underneath, a session is a durable tree of messages rather than a linear transcript, so branching, regeneration, and transcript editing are derived views instead of bolted-on features. Run configuration is a shared recipe every surface reads, and tools run over the same primitives.

OpenRouter is the sole inference provider, and the intent is to lean into it fully rather than treat it as one more OpenAI-compatible endpoint. The runtime has had the attention so far; that deeper integration is next.

github ↗
tetra answering a Node.js workers question with a code block and a comparison table
tetra chat session with an AI-generated pixel-art planet beside a settings panel of model, tool, and provider options

stack

  • TinyBase: the load-bearing choice. Local, durable, reactive state between runtime and UI, with synchronous reads and CRDT merge
  • tinydb: a typed layer over it, built for the project. Zod-derived tables, inferred queries, and React hooks as a mapped-type transform of the read surface
  • Bun monorepo, 10 workspaces: UI-agnostic core, typed schemas, sync worker, and CLI as separate packages
  • AI SDK + OpenRouter: inference and the tool-execution loop
  • React 19 / TanStack Router / Vite: wrapped by Tauri for desktop
  • Cloudflare Workers + Durable Objects: WebSocket sync with SQLite storage
  • Zod, Remeda, OXC toolchain

ig

inference infrastructure

Jan 2026 - present

ig is inference infrastructure, not an end-user product: submit a generation request, get an artifact back, through one API regardless of model or modality. Every artifact keeps the input parameters, model, seed, timing, and cost that produced it, so provenance is never lost. R2 is cheap, so ig stores everything and asks questions later.

Each request’s lifecycle is owned by a Durable Object as the source of truth, progressively projected into D1 as the generation advances; finished outputs land in R2, and domain events publish to a Cloudflare Queue so consumers react without polling. Async by default, with a sync mode for fast models.

When a request leaves the output dimensions to ig, a small LLM on the Workers AI binding classifies the prompt’s intended orientation.

It runs in production on Cloudflare, serving web apps, a Discord bot, and CLI tools. The whole stack (three Workers, D1, R2, Durable Objects, Queues, KV, secrets, custom domains) is declared in one ~140-line Alchemy file and provisioned with a single deploy, with two named production stages forming a promotable blue-green arrangement.

github ↗
ig artifact detail view showing a generated PSX-style kaiju image beside its prompt, model, seed, cost, and duration
ig artifact feed: a dense grid of AI-generated images

stack

  • Cloudflare end to end: Workers, D1, R2, Queues, Durable Objects, KV, Workers AI
  • Alchemy: infrastructure-as-code for every resource, binding, and secret; reproducible from the repo
  • Hono + oRPC: one API surface exposed as both REST and type-safe RPC, documented via OpenAPI
  • Drizzle over D1: generations, artifacts, and tags as a relational schema at the edge
  • Runware: the image-generation provider behind the orchestration layer
  • React 19 + TanStack: admin console for browsing artifacts and submitting requests
  • Turborepo + bun, Zod, OXC toolchain

e-suite

origin · 2024

Oct 2024 - Mar 2025

A multi-modal AI content platform from late 2024: threaded conversations where any message can trigger a generation run. Text and speech flow through LLM and TTS providers, images through fal.ai; outputs are tracked, organised into collections, and produced through reusable patterns that parameterise a run. The concerns the later projects are built on first took shape here: inference orchestration, artifact tracking, reusable run configuration.

My early, self-hosted take on LLM artifacts, begun November 2024, roughly five months after Anthropic popularised the concept: the renderer itself, not a consumer of someone else’s API. When a model returns a fenced HTML, SVG, or Mermaid block, the UI can open it as a live artifact instead of printing the source.

HTML renders in a sandboxed iframe (allow-scripts only, a CSP, no referrer), and errors thrown inside the frame are caught there and surfaced back to the host over postMessage. Extraction is automatic: any code block gets a save-as-artifact affordance, with the HTML <title> pulled out for naming.

The backend is Convex-native throughout: mutations, queries, actions, crons. Entities are modelled as a graph of edges and indexes, and images live in Convex file storage with blur placeholders. It’s parked now, frozen part-way through a UI migration; what it proves is where the ideas started, not current polish.

github ↗
e-suite conversation with a live-rendered HTML artifact, a retro terminal interface, beside the chat
e-suite model picker filtered to "flash", listing available models with per-token pricing

stack

  • Next.js + React on Vercel: the frontend, in a Turborepo monorepo
  • Convex: backend, database, file storage, and scheduling; convex-ents for entity edges and scheduled deletion
  • fal.ai: image generation; LLM/TTS providers for text and speech
  • Radix UI + Tailwind: the component layer; Jotai for artifact state
  • Sandboxed iframe + postMessage: the artifact renderer's isolation and error channel

iirc

plugin-driven event streaming

Jan 2026 - present

The server does two things: stream a persistent event log down to clients, and broadcast commands up to plugins. Everything protocol-specific (connections, state, how a message renders) is plugin-defined, so a new protocol is another plugin rather than a server change. IRC is the one implemented today; the server assumes nothing about it.

Events flow down: a plugin emits, the server persists to SQLite and pushes to every client over SSE. Commands flow up: the client sends one, the server broadcasts it to all plugins, and each self-selects on an opaque context string like irc:localhost/6697:#dev. Commands are fire-and-forget; the event stream is the sole source of truth.

That opacity is the boundary. Reconnection, buffer routing, self-echo: everything IRC-specific lives in the plugin, one layer above the iirc-lib protocol library. The SolidJS web client renders it all in a multi-buffer layout, deriving view state rather than syncing it.

stack

  • Bun + Hono + oRPC: typed server, shared endpoint contracts, SSE publisher
  • SQLite via Drizzle: the persistent event log
  • SolidJS + Tailwind: fine-grained web client, derived state over synced state
  • Turborepo, TypeScript, Zod, OXC toolchain
iirc web client: a buffer sidebar beside two channel panels, a names list and a live #iirc-client conversation
github ↗

@deankerr/iirc-lib

IRC protocol library

IRC is a 35-year-old protocol, loosely specified; every server implements it a little differently. The library’s job is normalization: it turns that inconsistent wire behaviour into one coherent, typed event stream you narrow by command, without pretending the protocol is simpler than it is. The raw line is always there underneath.

You bring the transport (any Duplex stream, a real socket or an in-memory mock) and it speaks the protocol; it never opens the connection or hides the wire. The surface is kept deliberately small: built-in behaviour is just functions on the same event stream a consumer uses, and a session is one runtime, one transport, one connection. No reconnecting, no routing. That’s policy, and it stays with the consumer.

stack

  • TypeScript: transport → typed events → runtime
  • Zero runtime dependencies: ESM only, Node ≥ 22 or Bun
  • Published on npm: built with tsdown
  • Bun test: 175 tests, table-driven against protocol fixtures, no network needed

other work

advekt

structured argumentation

Feb 2026

github ↗

Design knowledge is fragile. It lives in conversations that expire and documents that drift, and when humans and AI collaborate the assumptions accumulate invisibly. But correction is cheaper than generation, for both humans and AI, so instead of chasing consensus, advekt grows a design through friction. It’s a tree of typed claims that gets stronger by being agitated: statements, and three kinds of challenge (contradiction, assumption, question).

Two adversarial roles constrain the tree: the Architect makes canonical statements and shapes it; the Challenger can only attach challenges, which are always leaves. The tree’s shape becomes the signal. Dense branches are settled, sparse ones need work, challenge-heavy ones are the frontier.

One Bun process serves three surfaces from a single oRPC procedure layer: an MCP endpoint so agents are first-class participants, an HTTP API, and a React UI. The graph is in-memory Graphology with the domain invariants wrapped around it; persistence is event-sourced without a database, each tree a JSON log of actions replayed on startup.

stack

  • Bun: one server, one port, three surfaces
  • fastmcp: the MCP endpoint that makes agents first-class participants
  • oRPC: one procedure layer serving MCP, RPC, and the web UI, with Zod validation at the boundary
  • Graphology: the in-memory directed graph (two node types, two edge types)
  • React + TanStack Query: the web frontend
  • JSON action logs: event-sourced persistence with no database

omux

process management

Jan-Mar 2026

github ↗

Dev servers, watchers, and builds need to outlive an AI agent’s many short-lived turns, and the agent and the developer need to observe and control the same processes. Plain tmux makes that awkward. Session names are arbitrary, there’s no project scoping, and a crashed process just vanishes, so an agent can’t reliably find “the dev server for this project” without bespoke plumbing. omux removes the friction by making the command itself the session name.

Sessions are deterministic and project-scoped, so omux run 'bun run dev' is safe to call repeatedly: it no-ops if the process is up, respawns it if it died, and the agent tracks nothing to find it again. The developer can read or attach to the same process without touching tmux internals. It all runs on an isolated tmux socket so it never collides with your own sessions, a crashed process stays visible with its exit status instead of disappearing, and every run streams to a clean per-project log.

stack

  • TypeScript on Bun: every tmux call goes through Bun's shell API (Bun.$)
  • tmux: the only external dependency; omux adds naming, scoping, and failure visibility rather than reinventing the multiplexer
  • No runtime libraries: one source file, one test file

durian

durable agents

Feb 2026

github ↗

durian is built around a working theory of the durable agent: a persistent, multi-context entity, rather than the usual stateless responder where a request arrives, a loop runs, and everything evaporates. Durable meaning an identity that outlives any single session, an awareness of who wrote each message and which channel it’s scoped to, and serial processing, so participants never see contradictory output from concurrent inference.

The actor model is that theory’s execution substrate. Each agent and each channel is an independent RivetKit actor holding its own state, indexed by a singleton registry — so the unit of isolation is the actor, not a row in a database or a job on a queue, and persistent identity and channel scoping fall out of it. A Discord bot and a management UI sit on top of the same actor server.

A working vertical slice shipped — isolated actor state, streaming with tool calls, across the bot and the UI — but the durable core, long-term memory and identity, are still on the roadmap.

stack

  • RivetKit: actors as the unit of isolation; agent, channel, and registry actor definitions
  • OpenRouter: the sole inference provider. One API, any model
  • Elysia on Bun: the actor server
  • TanStack Start + React: the management frontend
  • Discord.js v14: a gateway bot as a thin translation layer over actor RPC
  • Turborepo: five workspaces, with typed env validation shared across server, web, and bot

orbstack-sandbox

agent sandboxing

Feb 2026

github ↗

OrbStack runs lightweight Linux VMs on macOS, but it mounts the host filesystem into every VM with full read/write access: home directories, SSH keys, and credentials, exposed to any process inside. Fine for trusted use, and dangerous the moment a coding agent is running unattended in a skip-permissions mode.

orbstack-sandbox closes the gap with two shell scripts: one provisions an Ubuntu VM with a full dev toolchain, the other creates a sandboxed user for whom the macOS mount points appear as empty tmpfs directories, with no sudo inside. The trick is the login shell, which is really a wrapper that builds a private mount namespace with unshare --mount, overlays the host paths with empty tmpfs, then drops to the unprivileged user with setpriv before handing over to the real shell. The admin user keeps full access to everything, including the sandbox homes, through POSIX ACLs rather than privilege escalation.

It’s a seatbelt for your agent, rather than a jail. It guards against accidental host damage and credential access from a well-meaning agent, not a determined attacker.

stack

  • POSIX shell: two scripts, ~225 lines, no application code or dependencies
  • OrbStack: driven entirely through the orb CLI
  • Linux primitives: unshare mount namespaces, tmpfs overlays, setpriv privilege dropping, POSIX ACLs

archi

codebase tooling

Apr 2026

github ↗

Getting a bird’s-eye view of an unfamiliar TypeScript codebase (what exports what, how modules connect) usually means reading files one at a time or leaning on IDE features that show one hop at a time. archi points at a tsconfig.json and extracts the whole project’s structure from the type-checked source via the TypeScript compiler API: every file’s exports, imports, and re-exports, resolved back to concrete files.

Extraction and rendering are deliberately decoupled: the extraction is renderer-agnostic, so the graph is just one thing the data could drive. The default output is one HTML file, the structure embedded as JSON with an interactive dependency graph on top, shareable with no server and no build step. It’s an early exploration and dormant. The extraction works; the graph layout is still rough.

stack

  • ts-morph: the TypeScript compiler API, doing the extraction from type-checked source rather than regex or import-parsing heuristics
  • Cytoscape.js + dagre: the interactive dependency graph and its layout, embedded in one HTML file (the libraries load from a CDN)
  • commander: the CLI surface
  • Bun: runtime and tooling; two runtime dependencies total