The $2 Bill That Made Me Self-Host My Memory
How I unified Claude Code, Cowork, and claude.ai into one shared brain — and the OAuth detour I didn't see coming.
TLDR
I got a $2 bill from OpenAI for one month of mem0 fact extraction — small money, but the wrong direction. mem0 was calling GPT-4o on every add to extract facts, and Sonnet wasn't much cheaper.
I swapped the extractor to Gemini 2.5 Flash Lite (free tier, 1000 req/day), kept Pinecone Serverless as the vector store, and the whole memory layer now runs at $0/month across Claude Code, Cowork, and claude.ai. Semantic search still works cross-lingual (CZ ↔ EN), dedup still works, latency is the same.
Here's the config, why I didn't go fully local (Render has no GPU, self-hosted Ollama doesn't pencil out), and the one quota gotcha that almost killed the swap.
Friday evening. I open my Anthropic Console out of habit. There’s a $2 spike from the day before on a key labeled drippery-app.
That’s odd. drippery-app is the API key for Drippery — my email drip SaaS. It legitimately calls Sonnet for AI features (email generation, series creation). But I check Drippery’s logs: zero AI calls that day. No tenant triggered anything. The spike came from somewhere else.
I grep my codebase for the key and find it in two places. One is Drippery itself (legit). The other is ~/.claude.json — the config file for Claude Code’s MCP servers — under the mem0 entry’s environment variables.
mem0 is the persistent semantic memory I plug into Claude Code, so it remembers decisions across sessions. Every time I tell it “remember that Drippery uses Pinecone,” it calls an LLM to extract structured facts and dedup against existing memory.
That LLM call had been billed to drippery-app for weeks. The Console sees one key burning Sonnet tokens; I see Drippery and assume customers are using AI features. Wrong assumption. It was me, talking to Claude Code, paying for mem0 to remember.
That was the cheap fix. Pull the Anthropic key out of the mem0 server, point it at Gemini 2.5 Flash Lite (free tier), done. ~30× cheaper, separated billing. Forty minutes of work.
The fix that actually changed things came from a different question:
Why was my memory local in the first place?One brain, three surfaces
Hi, I’m Daniel — a software engineer, solo builder, and the person behind Digital Craft Workshop.
I’ve spent ten years writing TypeScript, .NET, and React for a living, and the last few of them building things alongside the day job: B2B SaaS, AI tools like Article Forge, internal infrastructure, and a Unity 2D narrative game. Some shipped. Some I killed. Both are useful.
Digital Craft Workshop is for developers, AI builders, and indie founders who’d rather understand the craft than ride the hype — the kind of reader who likes Domain-Driven Design, distrusts gurus, and wants AI that’s cheap, practical, and accountable to one engineer in a workshop.
Each essay is something you can read in one sitting and use the same week: an architecture pattern, an AI workflow a single developer can actually run, a build log, or a postmortem on a project that didn’t make it. Built carefully. Explained plainly. Owned by you.
I run three flavors of Claude:
Claude Code — terminal CLI for actual coding, deep flows, long sessions.
Cowork — Anthropic’s desktop agent app, also for coding but more interactive, runs in the background.
claude.ai — the web/desktop chat for brainstorming, drafting, and quick lookups.
Each one is good at its niche. None of them shared memory.
I’d decide on Claude Code at 9 PM (“we’ll use Pinecone, not Qdrant”), and the next morning in claude.ai, I’d ask “what did we land on for the vector store?” — blank. Different surface, no recall. I had a brain split across three windows.
mem0 fixed that for Claude Code. Cowork sometimes, too, when I bothered to mirror the config.
But claude.ai chat? Out of luck. Local stdio MCP servers can’t be reached from a browser tab. (Aside: Claude Code skills live in the same MCP world — same protocol, different shape.)
The architecture I needed:
[claude.ai] ─┐
[Claude Code] ─┼──→ mem0 lib ──→ Pinecone (one shared store)
[Cowork] ─┘
One vector store, three frontends. To get there:
Move mem0’s storage from local Chroma to Pinecone. Free Starter tier, 2 GB, more than enough for personal memory.
Re-point Claude Code’s mem0 server at Pinecone. Trivial config change.
Build a hosted HTTP version of the mem0 server that claude.ai can connect to as a Custom Connector.
Wire up auth so the URL isn’t wide open.
Steps 1–2 took an hour. Step 3 ate the night.
The MCP server I didn’t expect to write
claude.ai’s Custom Connectors to expect a remote MCP server speaking streamable HTTP.
The MCP Python SDK supports this — FastMCP(stateless_http=True) plus streamable_http_app() gives you a Starlette app you mount under uvicorn. Same four tools as the local stdio version (mem0_add, mem0_search, mem0_get_all, mem0_delete), now reachable over the wire.
from mcp.server.fastmcp import FastMCP
from starlette.applications import Starlette
mcp = FastMCP(”mem0-remote”, stateless_http=True)
# ... register mem0_add, mem0_search, mem0_get_all, mem0_delete ...
app = Starlette()
app.mount(”/mcp”, mcp.streamable_http_app())
Wrap it in a Dockerfile, push to a private GitHub repo, deploy to Fly.io with one warm machine in Frankfurt. ~$3.89/month — predictable, no cold starts, no surprise bills.
End-to-end test passes. claude.ai connector dialog opens. I paste the URL.
The dialog has two fields: OAuth Client ID and OAuth Client Secret.
I had built the server with a static bearer token. claude.ai doesn’t do bearer.
OAuth, but small
Custom Connectors in claude.ai support either:
No auth at all (open endpoint, security through URL obscurity — bad)
OAuth 2.0 (real flow, real tokens)
Static bearer? Not on the menu.
So I implemented a minimal OAuth 2.0 server — about 200 lines of Python on top of the MCP wrapper. Standard endpoints:
Authorization Server Metadata at
/.well-known/oauth-authorization-serverDynamic registration at
/oauth/registerAuthorize at
/oauth/authorizeToken exchange at
/oauth/token
Nothing exotic.
First version: open Dynamic Client Registration with auto-approve. claude.ai walks the OAuth dance, gets a token, calls /mcp. Works on the first try.
Then I noticed: anyone who finds the URL can do the same thing. Certificate Transparency logs index every fly.dev subdomain.
An adversary scrapes mem0-remote.fly.dev. Hits /oauth/register. Gets their own client credentials. Walks the same auto-approved flow. Reads every memory I’ve ever stored.
OAuth on paper. Wide open in practice.
The fix took another twenty minutes:
Disable Dynamic Client Registration (
/oauth/registerreturns 403).Provision a single static client via Fly secrets —
OAUTH_CLIENT_ID,OAUTH_CLIENT_SECRET. claude.ai stores them after I paste them once.Whitelist redirect URIs to
https://claude.ai/api/mcp/auth_callbackand a couple of variants. An attacker with stolen credentials still can’t redirect codes anywhere they control.Mandate PKCE (
code_challengerequired on every authorize call).
Bearer middleware on /mcp validates OAuth-issued tokens. A static MCP_AUTH_TOKEN escape hatch stays around for CLI smoke tests.
It’s not enterprise-grade. It’s “single user, threat-model-aware, defense in depth.” Plenty for a personal memory.
What it actually feels like now
This morning I asked claude.ai chat: “Status archieve-concierge?” It pulled mem0_search, found:
POC done, deployed live on Fly.io. Web app for AI-powered archive navigation over 75+ articles (Medium, Substack, dev.to). RAG over pgvector backend.
Five facts back about a project I last worked on in Claude Code three days ago. The chat had never seen those decisions before today — but mem0 had, and now claude.ai can read it. (That project is Archive Concierge, if you want the backstory.)
Second example, two hours later. I started a Cowork session to wire up a new email template in Drippery. First thing Cowork did before writing a line of code: hit mem0_search for “Drippery email rendering”. It came back with a note I’d left in Claude Code last week — “templates use MJML compiled at write-time, not at send-time, because of Resend’s 250 KB payload cap.” Cross-surface handoff. No manual context-paste, different repo, same memory.
Tonight I’ll write a decision in Claude Code; tomorrow I’ll ask Cowork about it from a different folder. Same answer.
The bill and the gaps
What I’m paying for:
Pinecone Starter — free, no card, 2 GB cap I’ll never approach.
Gemini 2.5 Flash Lite — free tier, 1000 req/day. Even on heavy days, I touch maybe 50.
Fly.io shared-cpu-1x / 1 GB / Frankfurt — $3.89/month. One warm machine, no cold starts. (For more on the infra side, see my Fly.io infra dashboard post.)
My time — one Friday night and a Saturday morning. Maybe six hours.
The bills I cancelled — those quiet $2 days on drippery-app — already covers the Fly cost ten times over.
What it didn’t replace: mem0 is for atomic facts and decisions — the things you semantic-search later. It’s not the only memory layer.
me.md— my identity profile, auto-loaded into every Claude Code / Cowork session via@-reference. Always-on baseline. Not searched, just present.Notion — long-form documents, plans, brainstorms. Browseable, hierarchical, reachable via the Notion connector.
mem0 — atomic recall. “What did I decide about X?”
Three layers, three jobs. mem0 didn’t replace the others — it filled the missing one.
The thing I keep thinking about
The MCP protocol is quietly becoming the thing every AI assistant plugs into.
Once you host your own server with OAuth, you stop being the client. You start treating Claude as a frontend. Any future MCP-aware client is just another window onto your own data.
Memory is the obvious first target. After that: notes, code search, custom domain APIs, whatever your daily work touches.
The protocol is small, the SDK is decent, and OAuth feels like overkill — until someone scrapes your fly.dev subdomain.
I built mine for $3.89/month. Worth every cent of the Friday night.
If you’ve ever watched a small API line item creep up and thought “I could just host this myself” — restack with one sentence about what you’d self-host first. That’s how others building the same way find this.
Or drop it in the comments — I want to see what other solo makers have on the self-host shortlist.





![[Shop Notes: Library] Stop Scrolling My Archive. Ask My Librarian.](https://substackcdn.com/image/fetch/$s_!lbPB!,w_140,h_140,c_fill,f_auto,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F5fdff190-d42b-4d7e-a742-b01476fe9be0_1484x1060.png)
![[Build Log] I asked Claude to build my infra dashboard. Three numbers surprised me.](https://substackcdn.com/image/fetch/$s_!wSUk!,w_140,h_140,c_fill,f_auto,q_auto:good,fl_progressive:steep,g_auto/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe9fc353f-8cbd-4085-a996-47708cd32a07_1484x1060.png)
