Skip to content

Architecture overview ​

A map of how pocket agent is put together — the clients people use, the single API that serves them, the two stores that hold all the state, and the outside providers that supply cards, wallets, a brain, and KYC. If you are new to the system, read this page first; every other architecture page zooms into one box on the diagram below.

The guiding idea is simple to state and strict to enforce: the money path is narrow, guard‑railed, and always ends with the user. Everything you see here is organized so that spending, moving, or redeeming value flows through as few code paths as possible, each of which records its intent before it acts and confirms before it reports success.

One brain, many hands, one safe. The agent can act anywhere on the web, but money, identity, memory, and approvals live in one place the user controls.

The system at a glance ​

Three (plus) kinds of client all talk to one Hono API. That API is the only thing that touches Postgres (the source of truth) and Redis (the cross‑replica coordinator), and the only thing that holds the keys to the external providers.

Everything below is one of those boxes.

The stack ​

LayerTechnologyRole
ClientsReact + Vite (web/PWA), Expo / React Native (native), Capacitor (mobile), Telegram Mini AppHow people reach their agents
APIHono v4 on @hono/node-server, Node ≥ 20One app: all routes, middleware, and the scheduler
Source of truthPostgres (pg pool)Every authoritative byte of state
CoordinatorRedis (ioredis)Money locks, leader election, webhook dedup, rate limits, caches
Identity & walletsPrivyUser sign‑in + embedded/server wallets
Cards & commerceReap (Reap Global)Virtual Visa cards, KYC, agentic checkout
Agent brainOpenRouter (default deepseek/deepseek-chat)The tool‑using LLM loop
HostingRailway (Nixpacks)Build, deploy, health checks, restarts

A note the honest version of this document has to make: production runs the server with tsx, not a compiled bundle. A compiled path exists (build:server → dist/server.js via esbuild, plus start:compiled), but npm start is tsx src/server.ts. Moving prod to the compiled artifact is a known, tracked gap — see Known gaps.

Clients ​

pocket agent home

There is one product surface rendered across several shells, all served by the same API:

  • Web SPA (web/) — React + Vite, built to web/dist and served same‑origin by the API through a static catch‑all. It is a full PWA (service worker + manifest) with Web Push via VAPID. This is the primary surface.
  • Native app (native/) — an Expo / React Native build (EAS), with Expo push. A Capacitor/WebView wrapper was deliberately rejected in favor of a real native port; the app‑store track runs in parallel to the live web product.
  • Mobile wrapper (mobile/) — a Capacitor/PWA packaging path.
  • Telegram — a Mini App (whose initData is HMAC‑verified when linking push) plus a bot that delivers notifications.

Live today: web, PWA, and Telegram. Separate track: the native app stores.

Because the web client is served from the same origin as the API, there is no cross‑origin hop for the common case; third‑party origins are governed by the CORS_ORIGINS allowlist (see below).

The API ​

The entire backend is a single Hono application in src/server.ts (~3,200 lines — the monolith wires every route, webhook handler, and the scheduler in one place; the module comments note the eventual split into packages/ + apps/gateway).

The middleware pipeline ​

Every request descends the same ladder before it reaches a handler:

In order:

  1. Body limit — 1 MB by default; 20 MB for the media routes (/agents/:id/run, transcription).
  2. CORS — an allowlist from CORS_ORIGINS. In production, an unset allowlist means deny‑all (not permissive); dev keeps a permissive legacy default.
  3. Structured logging — JSON request logs, with an onError alert hook.
  4. Public routes — health, config, and the provider webhook receivers.
  5. /api/* — guarded by privyAuth(); the verified Privy DID becomes the appUserId for the request.
  6. /admin — mounted only when ADMIN_PASSWORD is set (fail‑closed; otherwise it does not exist).
  7. Static SPA — the web client, as a catch‑all.

Auth and identity ​

Authentication is Privy. The privyAuth() middleware verifies the bearer token on every /api/* call and resolves the user's Privy DID, which is used verbatim as the primary key app_user_id. Privy also provides the embedded/server wallets the system signs with (how custody of those wallets evolves is the subject of the custody page). A per‑user global rate limit of 300 requests / 60 s (Redis‑backed) sits on top, with tighter per‑route caps where needed.

The response envelope ​

Every API response uses one shape:

json
{ "ok": true,  "data": { } }
{ "ok": false, "error": { "message": "…", "code": "…" } }

Subscription refusals are the notable special case: HTTP 402 with code: "UPGRADE_REQUIRED", which the clients catch to show an upgrade prompt.

State: Postgres + Redis ​

Two stores, two very different jobs.

Postgres is the sole source of truth (src/db.ts). There is no authoritative in‑memory state anywhere, which is what makes the app stateless and horizontally scalable. The pg pool is capped at max = 16 with statement_timeout, query_timeout, and idle_in_transaction_session_timeout all at 15 s. SSL is auto‑enabled for Railway/sslmode=require URLs. DATABASE_URL is required — the app throws on boot without it, with no in‑memory fallback.

One sharp edge worth naming on the overview: schema is applied by ensureSchema() running CREATE TABLE IF NOT EXISTS DDL on every boot of every replica. There is no migration system yet (the planned replacement is advisory‑locked, run‑once migrations). The full schema is catalogued on the data model page.

Redis is the cross‑replica coordinator (src/net/redis.ts, ioredis). It holds everything that must be agreed across replicas but need not be durable:

  • per‑user money locks (money:{userId}) that serialize all money operations,
  • leader election so only one replica runs the scheduler,
  • webhook dedup (a fast path in front of the durable exactly‑once guard),
  • rate limits, caches, and ephemeral state.

Redis has a graceful in‑memory fallback for single‑instance dev, but it is required in production — the app throws on boot without REDIS_URL, because without a shared coordinator two replicas could double‑send or double‑credit.

External providers ​

The API is the only component that holds provider keys. Each provider owns one capability:

ProviderSuppliesConfig gate
PrivyUser identity (DID) + embedded/server walletsPRIVY_APP_ID, PRIVY_APP_SECRET
ReapVirtual Visa cards, KYC orchestration, Agentic Commerce checkout, card auth/clearing webhooksREAP_API_KEY (+ two webhook secrets)
SumSubThe actual identity‑verification flow, reached through Reap's Managed KYCvia Reap (sdkToken)
OpenRouterThe agent's LLM brainOPENROUTER_API_KEY
EnsoDeFi intent routing (swap/bridge/stake/lend/LP)ENSO_API_KEY + ENABLE_WITHDRAWALS
StripeSubscription billing (Plus / Pro) + merch top‑upsSTRIPE_SECRET_KEY
ResendEach agent's email alias on agents.pocketagent.toRESEND_API_KEY
Pimlico / Alchemy / TenderlyERC‑4337 bundler + paymaster; transaction simulationBUNDLER_RPC_URL, ALCHEMY_API_KEY, …
Browserbase / CDPThe agent's real stealth browserBROWSERBASE_API_KEY, BROWSER_CDP_URL
Telegram / VAPID / ExpoPush notifications across surfacestoken / VAPID keys

Almost every capability is config‑gated: if a provider isn't configured, the feature is cleanly absent (and, for agent tools, hidden from the LLM entirely) rather than half‑present. The exhaustive list of flags and what each one turns on lives on the feature flags page.

The subsystems ​

Each box on the system diagram has its own page. Start here, then follow the thread you care about:

SubsystemWhat it coversDeep dive
Data model & storageEvery Postgres table — identity, agents, the card ledger, money‑op ledgers, approvals, growth analyticsdata model →
Money movementCard funding (crypto → card), redeem/cash‑out, wallet transfers and withdrawals, card purchasesmoney movement →
Money‑safety engineThe invariants: one guarded send path, record‑intent‑before‑broadcast, exactly‑once settlement, reconcilers, kill‑switchmoney safety →
CustodyThe non‑custodial refactor — the "no‑license" thesis, the custody guard, and Tracks A/B/Ccustody →
Reap integrationThe Reap SDK, authorization matcher, webhooks (two signing secrets), PAN reveal, Agentic CommerceReap integration →
Agent runtime & toolsThe perceive→plan→act loop, the tool catalog, approvals, personas, autonomous tasksagent runtime →
Gasless & smart accountsEVM gas sponsorship, Solana gasless, ERC‑4337 (Kernel + Pimlico), the DeFi stackgasless & smart accounts →
Infrastructure & deployRailway, health checks, graceful shutdown, observability, COGS, the scaling modelinfrastructure →
Admin & ops consoleThe /admin operator panel — read‑all plus freeze / KYC / suspend / kill‑switch, all auditedadmin & ops →
Feature flagsEvery src/config.ts flag, its default, and what it gatesfeature flags →

The product‑facing side of these same ideas is documented in Agents, Cards, Wallet, Autonomous tasks, and Security & control.

Design principles ​

Five ideas recur across every subsystem; the overview is the right place to state them once.

1. Postgres is the only truth ​

No handler trusts a cached balance for a money decision, and no authoritative state lives in a process. This is what lets the system run N identical replicas behind one load balancer.

2. One guarded send path ​

Every on‑chain send — a wallet withdrawal, card funding, a transfer, a redeem — routes through a single function, executeWithdraw (src/provider/withdraw.ts). It takes the per‑user Redis money lock, reads a fresh balance, blocks in‑flight double‑sends, records intent before broadcasting, and confirms before reporting success. The code comment is blunt: calling the lower‑level send directly bypasses all of this — never do that. See money safety.

3. Record intent before you act; settle exactly once ​

Money ledgers (fund_ops, redeem_ops, transactions) write a pending row before any broadcast, so a crash leaves a reconcilable record rather than a mystery. Settlement flips status and mutates balance in one transaction, using the amount recorded on the row (never a caller‑supplied value), guarded by a processed_events insert so a redelivered webhook can never double‑credit.

4. Config‑gated by default, fail‑closed in production ​

Features switch on only when their provider and flag are present. The master money gate, ENABLE_WITHDRAWALS, is false by default and gates every on‑chain send. In production, the boot sequence is fail‑loud: it throws without Privy, OpenRouter, or Redis, and refuses dangerous flag combinations.

5. The LLM is not a security boundary ​

Anything irreversible the agent proposes — a card purchase over its limit, a withdrawal, any DeFi action — becomes a pending approval the user taps, executed through the guarded paths. Untrusted web/email content is fenced; autonomous runs have irreversible tools hard‑blocked at execution time, not merely hidden.

Scaling model ​

Stateless app + Postgres source of truth + Redis coordinator ⇒ run as many replicas as you like, with the scheduler elected to exactly one leader so autonomous tasks and reconcilers fire once.

The practical ceiling today is the Postgres connection pool: with max = 16 per replica and no PgBouncer in front, horizontal scaling is comfortable to roughly 5–6 replicas before connection pressure, which is tracked as an infrastructure item. Details, including health checks and graceful shutdown, are on the infrastructure page.

Known gaps (the honest list) ​

An architecture overview that only lists strengths is marketing. These are real, tracked gaps as of this writing:

  • Production runs on tsx, not a compiled bundle. The compiled path exists but isn't the prod entrypoint yet.
  • No migration system. Schema is applied as boot‑time CREATE TABLE IF NOT EXISTS DDL per replica; advisory‑locked run‑once migrations are the planned replacement.
  • Kill‑switch / suspend coverage is partial. They cover the wallet‑withdraw path (executeWithdraw) today, but not redeem / transfer / fund. Per‑tx, velocity, and daily limits exist for card spend but not for wallet sends. OFAC/sanctions and geo gating, and a treasury solvency reconciler, are not yet built. See money safety.
  • KYC PII is a stub. The current create‑user call hardcodes placeholder identity data and a generated phone number; the real SumSub path exists but has not been run against production Reap. See Reap integration.
  • The non‑custodial refactor is scaffolding, not yet wired. The "no‑license" target (one isolated treasury signer, zero agent on‑chain money tools) is not met — see custody for the current baseline and the three tracks.
  • No PITR / backup runbook and no PgBouncer in front of Postgres yet.
  • Waitlist purchases, balances, and NFTs are animated demos — no real funding, mint, or fulfillment behind them.

For the full production go/no‑go picture, the engine is assessed as production‑grade with real‑money launch gated on two external items (Reap production access and a money‑transmission / custody legal determination).

be everywhere — live here.