← back

spawnlabs

2026

agent platform for domain operators

a platform where people who are good at a kind of work turn their method into agents, tools and automations that do the work the way they would, and share them with anyone. it covers any kind of knowledge work.

fastapi backend, modal sandboxes, redis streams, nango-held oauth

python · fastapi · postgres · asyncpg · redis · modal · claude-agent-sdk · mcp · nango · oauth2 · websockets · nextjs · react · docker

the problemEvery firm buying the same tool gets the same output

A professional services firm sells its judgment, like how a particular recruiter screens candidates or how a contractor prices a bid. If every firm in a category buys the same AI tool, they all produce the same output and lose that edge.

The obvious build is a system prompt and a few API keys. That breaks in three places, and none of them can be fixed with a better prompt, so very little of this repo is the model call itself.

three things a system prompt doesn't solve
  • accessa live token per user, per provider
  • durationa bid runs for two weeks
  • executioncustomer code on real files

the ideaAn agent is a durable object

The main design decision is that an agent is an object that persists between conversations, and each turn wakes it once. A turn can start from a websocket message, a trigger, a schedule, an email to the agent's own address, or a Slack channel it has joined.

Skills are the operator's method stored as files on the agent's volume, and the Claude Agent SDK finds them on disk instead of having them pasted into a prompt. That mattered more than any prompt engineering I did, because a change to the operator's method is now a change to a file.

mind
a JSON blob added to the prompt at the start of a wake, and updated by a tool at the end
skill
a SKILL.md on the volume, looked up in the database first and the filesystem second
what an agent is made of
  • the rowprompt, skills, allowed integrations
  • the volumefiles and artifacts, on modal
  • the mindgoals, state, decisions, questions

how it runsThe backend never runs an agent

FastAPI on eight workers on one machine, deployed blue-green behind Caddy. It never runs an agent itself. It authenticates and authorises requests, writes a JSON line to a sandbox's stdin, and forwards whatever comes back.

That puts all state shared between workers in Redis, which is the right constraint, because with no sticky sessions a deploy can switch slots in the middle of a live turn. A cold Modal sandbox takes five to eight seconds to boot, so new sessions take a pre-booted one in about 200ms instead.

the layers, top to bottom
  1. webNext.js 15 · React 19 · separate auth app on a shared cookie
  2. apiFastAPI · 8 uvicorn workers · blue-green behind Caddy
  3. stateNeon Postgres via asyncpg · Upstash Redis streams + pub/sub · Pinecone for memory
  4. executionModal sandboxes · Claude Agent SDK · persistent volumes
  5. credentialsself-hosted Nango · Fernet columns · GCP Secret Manager

credentialsNothing secret goes into the sandbox

The sandbox runs code an operator wrote and a model extended, so it's the least trusted process in the system. It gets one HMAC session token, naming a session, a user and a scope, plus three proxy URLs. A credentialed call goes through the backend, which verifies the token, checks again that the connection belongs to that user, and only then has Nango supply the access token.

One detail I like: a 429 with none of Anthropic's rate-limit reset headers is probably the load balancer throttling, so in that case it retries the same credential once instead of locking a healthy key out of the pool.

in postgres
a connection id and a status, but no token, so a database dump leaks nothing
known weak spot
the fernet columns have no key version, so rotating the key needs a backfill
what the sandbox holds
keys in the sandboxa token in the sandbox
the model key in envan hmac token as the api key
vendor keys in envthe base url points at the proxy
the user's oauth token in envno oauth token, ever
one escape leaks everythingone escape leaks one session

the hard partsThe two hardest problems

The eight tenancy bugs were the same mistake made eight times. In four of the five worst cases, the correct check already existed elsewhere in the repo for the same value. Code review kept missing it because each diff looked exactly like the code around it.

The second was billing. The Claude Agent SDK reports cost as a running total for the life of a client, not the cost of the turn that just ended, and every code path that saved it added that total to the session. So the charged cost grew with the square of the number of turns.

the fix
one function reads the running total and swaps the old baseline for the new one in a single statement
cost of the suite
about nine seconds, and it can't tell you whether a gate is the right one
the hour-long one
an old second VM kept winning the Redis leader election, so a correct sweep never ran
the ownership contract test
  1. 1import appevery real route
  2. 2lift idspath, body, query
  3. 3taint walkover the handler ast
  4. 4match gate
  5. 5fail on new9 open, 13 allowed
one 33-turn session, before the fix
  • billed to the customer$291.29
  • real model spend$19.43

where it standsWhat works, and what's still wrong

It works. Operators build agents, attach skills and connections, and publish them, and other people clone them. A clone copies the volume, then deletes every reference folder not marked as a template, so the operator's personal material isn't copied. An agent can reach 102 platform tools, 28 MCP servers, 151 Nango action scripts and 61 mapped providers.

Production has been on emergency infrastructure since a billing lockout, and the runbook I wrote has gone out of date in an expensive way. Two VMs are billing, DNS points at the clone, and their disks have diverged for three months. Getting back means a migration, and changing DNS alone won't do it.

open, in order of how much it bothers me
  • impersonationno actor claim, no audit row
  • 9 findingsnamed, not fixed
  • header idsnot modelled by the test
  • key rotationneeds a backfill
  • migration orderstring sort, must be idempotent
  • stale docssay 41 providers, the map has 61