Overview

Agentbox

A general-purpose shared inbox for all my agents. Local agents (Claude Code, Codex) connect over a CLI; remote agents (ChatGPT, claude.ai) connect over MCP, and everything lands in one database of threads, messages, and attachments. Built so I can move a whole ChatGPT brainstorm into my coding agents without being the clipboard.

GoTypeScriptReactNext.jsPostgreSQLCLIMCP
Cloudflare R2
Agentbox

Most of my projects start the same way: a conversation with ChatGPT in a browser tab. It’s the fastest way for me to look something up, check whether two ideas are related, or pressure-test a hunch, and before long we’ve fleshed out something I actually want to build. The problem is everything I need lives in that tab: the reasoning, the decisions, the files I uploaded, the diagrams ChatGPT generated. To start building, I have to drag all of it over to my coding agents by hand. I become the clipboard.

Agentbox is the message bus that gets me out of the middle. It’s a general-purpose shared inbox connecting all of my agents, wherever they run, through one database of threads, messages, and attachments. Now I just tell ChatGPT, “make a thread in Agentbox and dump everything from this conversation into it.” Then I open Claude Code and say, “open Agentbox, grab the latest thread, let’s build.” The context moves itself. It’s the piece that ties Zodex and goldengoose into one workflow: the inbox my remote and local agents hand work through.

One inbox, every agent

The point of Agentbox is that it doesn’t care where an agent lives; it only cares that the agent can reach the same threads. So it speaks two languages at once. Local agents talk CLI: a single agentbox binary lets Claude Code, Codex, or any local agent list and search threads, pull one down with every attachment, and post results back. Remote agents talk MCP: ChatGPT or claude.ai connects to a hosted MCP URL with a scoped key and gets list_threads, search_threads, get_thread, create_thread, and post_message as native tools. Both ends read and write the same threads, so a brainstorm that started in a browser is instantly readable from a terminal, and vice versa.

The domain model underneath is deliberately tiny: threads hold messages, messages hold assets, and every surface is a projection of it. That’s not an accident of size; it’s the design constraint the whole codebase is built around.

One service, many faces

The classic failure mode for a product like this is drift: the REST route, the MCP tool, the CLI command, and the dashboard each grow their own slightly different business logic until they’re four subtly different products. Agentbox avoids that structurally. All product rules, including thread existence, message content-type resolution, upload finalization, key authentication, and error codes, live in one service layer, and REST, MCP, CLI, and the dashboard proxy all call into it. The service depends on exactly two interfaces, a repository and an asset store, which keeps the core independent of Postgres, R2, and every delivery surface.

That boundary is also what makes the whole thing testable without infrastructure. There’s an in-memory repository and a fake asset store, so the test suite, around 1,600 lines of Go, exercises real cross-surface flows: the HTTP API creating threads and posting multipart assets, MCP tools returning their fallback payloads, CLI commands running against an actual test server, the profile reader parsing configs written by the old TypeScript CLI. Integration products break at the seams, not inside pure functions, so that’s where the tests live.

Errors are part of the same contract: every failure carries a stable machine-readable code (THREAD_NOT_FOUND, PERMISSION_DENIED, RATE_LIMITED) that’s identical across HTTP and MCP. Agents are the primary consumers of this API, and an agent can’t do anything useful with a prose error message. It can retry, re-auth, or give up based on a code.

MCP for hosts as they are, not as the spec imagines them

The MCP server returns every tool result twice: once as structuredContent, per spec, and once as the same JSON serialized into the plain-text content block. That’s deliberate, and it came from a real debugging session: Agentbox worked perfectly in ChatGPT and then flatly didn’t in claude.ai. Same server, same tools, but one host’s model just couldn’t see the results. It turned out some MCP hosts surface only the text content to the model and quietly drop the structured payload, so a spec-shaped server whose text block says “Fetched thread.” works in one host and is useless in the other. Making the text block self-sufficient JSON means every client works; better clients just get a nicer version. Protocol compliance is table stakes; compatibility with hosts as they actually behave is the real work.

Reverse-engineering ChatGPT’s file model

This is the part I’m quietly proud of. The OpenAI Apps SDK has a file-ID mechanism designed for the forward direction: a user uploads a document in ChatGPT, and ChatGPT passes an opaque file ID (file_abc123) to an MCP server so a ChatGPT app can pull the bytes. I reverse-engineered that flow and ran it backwards, using the same runtime feature to get files out of ChatGPT and into my own system. post_message accepts that file ID directly and Agentbox resolves it into stored R2 assets, turning a feature built for app-ingestion into a general export channel nobody designed it to be.

That inverse flow unlocked something I didn’t expect: ChatGPT has generous image-generation limits, so I started automating image work right inside it, saying “generate these images and save them to Agentbox,” and bulk-generating large batches that land in R2 and pull straight down from the CLI. Effectively free image generation at volume, riding on a flow I’d already built for context handoff.

Files that never touch the server

Attachments are first-class, and the bytes never proxy through the application. Downloads were always direct: the API hands back short-lived signed R2 URLs, so when a local agent pulls a thread’s assets the bytes go straight from R2 to the machine, and the serverless function only brokers URLs, cheaply and unbothered by file size.

Uploads needed more design, because Vercel caps request bodies well below the file sizes agents actually produce. Small files go through ordinary multipart, with the limit derived from what the host platform can genuinely accept rather than an advertised number the platform would reject. Large files go around the server entirely: the client asks for an upload intent, Agentbox records a pending upload and returns a presigned R2 PUT URL, the browser uploads directly to R2, and the eventual post_message references the pending uploads. At that point the service finalizes them into real message assets, atomically with the message itself. The message model stays clean (“a message has assets”), and no file byte ever squeezes through a serverless request body.

Go, because the CLI had to be good

Agentbox started as a single TypeScript app on Vercel, and it was clean. But once local agents became half the product, the CLI stopped being a side feature, and Go is the best language I know for CLIs: type-safe, concurrency built in, and it compiles to a small self-contained binary (under 5 MB) that runs on a fresh machine with no Node or Python installed. A Bun-compiled binary is enormous by comparison, and “install Node first” is exactly the kind of setup friction this tool exists to remove. Go is just as happy as a serverless function, so I moved the whole service over. The same language, and in fact the same core, now runs the API and MCP server on Vercel and the CLI in the terminal. npm install -g @amxv/agentbox still works because the npm package is just a delivery vehicle that unpacks the right native binary per platform.

The migration itself was invisible by design: JSON stayed snake_case, error envelopes stayed identical, validation messages deliberately mimic the old Zod output, and the profile reader accepts every config shape the TypeScript CLI ever wrote while always writing back one normalized shape. It’s liberal in what it reads, strict in what it writes. Nothing consuming Agentbox could tell the runtime changed underneath it.

The Go side itself is deliberately boring: plain net/http with manual routing instead of a framework, purpose-specific interfaces instead of generic abstractions, idempotent schema setup plus checked-in SQL migrations instead of a migration framework. An inbox-scale product doesn’t earn heavyweight infrastructure, and every dependency not taken is one that can’t break an agent’s workflow.

A CLI built for the failure modes that actually happen

Integration software fails in setup, auth, and environment configuration far more often than in CRUD, so that’s where the CLI effort went. agentbox init handles setup end to end: creates a local key and a ChatGPT key in Postgres, saves the profile, prints the MCP URL ready to paste into ChatGPT. agentbox doctor checks the entire chain (profile resolution, health, authenticated API access, signed download URLs, MCP URL construction), so “why can’t my agent see the inbox” is a one-command diagnosis instead of twenty minutes of guessing. Named profiles mean my laptop, a Zodex Sprite, and a CI job each carry their own identity. And deploy vercel deliberately doesn’t run anything: it prints the exact command sequence instead, because a setup tool for self-hosted infrastructure shouldn’t surprise-mutate someone’s Vercel account, especially when the thing running it is often an agent.

Attribution runs through the same key system: every API key is a named actor (chatgpt, codex-local, zodex-agent), so a thread reads like a conversation between named participants, and any agent can be handed a dedicated, revocable identity. Admin access, including key management and the viewer, is a separate credential from actor access, so the key that can configure the system is never the key doing the talking.

The human is a participant, not just a spectator

The web dashboard is the human’s seat at the table. Sign in once with the admin key (held in localStorage and sent as a header, never in a URL) and every thread and every file my agents have been passing around is browsable, but it stopped being read-only. I can reply on any thread and create threads myself, with my own attribution, the same way any agent does. That turns out to matter more than it sounds: when I want all my agents to see something, such as a spec, a screenshot, or a stray text file, I paste it into the inbox myself instead of asking one agent to relay it. On the board, I’m just another actor, and the thread history shows exactly who said what, human included. Messages render Markdown, including tables, fenced code, syntax highlighting, and Mermaid diagrams, but only when the content actually earns it: the format detection is deliberately conservative, because agent output is full of characters that look like Markdown, and a shell command mangled into a bulleted list is worse than plain text. Strong signals render; ambiguous short messages stay verbatim.

Threads live in Postgres, files in R2, the dashboard stays Next.js with thin proxies to the Go backend, and the whole thing self-hosts on Vercel with one init command to go from deployed to connected.

Use and to navigate