All articles
Architecture

One agent per user: how penpal is built on Maritime

penpal gives every learner a private OpenClaw agent on Maritime: 24 personas, one call to provision, and a friendship that lives on the agent's own disk.

Maritime Team·August 28, 2026·3 min read

penpal is a language app: you pick one of 24 characters, and they text you through the day in the language you are learning. They remember your job, your dog, and the interview you were nervous about last week. Corrections never interrupt; they arrive at night as a short note.

Every user gets their own dedicated agent on Maritime. Not one shared model with a user id in the prompt: a private OpenClaw agent on its own machine, with its own disk, doing one job for one person.

One developer took it from first commit to a live product with accounts, phone verification, SMS in and out, nightly lessons, streaks and 24 personas in nine days. The company-side stack is one Next.js app and one Postgres.

The product: a friend who texts in the language you are learning, running on a dedicated agent per user
The product: a friend who texts in the language you are learning, running on a dedicated agent per user

Why one agent per user

Most AI products multiplex one model across every customer and keep memory as rows to be retrieved and re-injected into the prompt. For a tool, that is correct. Nobody wants a dedicated machine to summarize a PDF.

For a relationship, it fights the shape of the problem. A friendship is state: the running joke, the ongoing argument about where to eat, the fact that you said the interview was Thursday and it is now Friday. Reassembling that from a vector search on every request is a lot of work to approximate something a computer with a disk gets for free.

So each friend is an OpenClaw agent in its own container or microVM, and the memory of the friendship lives in that agent's state directory. penpal never manages context windows or embeddings for personality continuity. It sends the agent a message.

One model, memory as rowsUser AUser BUser COne processuser id inthe promptmemoryrowsthe person is reassembled on every requestOne agent per userUser Aagent + volumeUser Bagent + volumeUser Cagent + volumethe person is what this machine already remembers
Two ways to give a user a persistent personality. Only one of them survives a restart without being rebuilt

A friend is one API call

const agent = await maritime.agents.provision({
  template: 'openclaw',
  externalId: `penpal-${user.id}`,
  instructions: buildInstructions(user.name, user.settings),
})

That is the whole provisioning path. The externalId makes it safe to retry, so a signup that fails halfway never leaves anyone with two Sofías. After that, penpal talks to its friends with agents.chat and lets the platform own the lifecycle: agents sleep between messages and wake when texted, with a 674 ms median restore, so nobody has to think about capacity.

The one thing the app keeps for itself is the clock. A tick every ten minutes decides who is due for a check-in and whose nightly recap is ready, because a schedule that lives inside a sleeping process does not fire.

What you give up

Two things, honestly. Every signup provisions an agent, so there is a per-user cost floor that multiplexing does not have, and sleep makes that floor small rather than zero. And nothing is shared between agents: a phrasing that lands well in one friendship does not propagate on its own, it ships with the next update to the instructions. Both are the price of the thing that makes the product work, which is that the state belongs to one person.

The takeaway

Per-customer agents stop being exotic once an agent costs nearly nothing while idle and provisioning is one idempotent call. The shape is not language specific: anything where the product is an ongoing relationship rather than a one-off task wants a machine per customer.

penpal is live at textpenpal.com, and the API behind it is documented here. One developer, nine days, one agent per user.