Appearance
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
| Layer | Technology | Role |
|---|---|---|
| Clients | React + Vite (web/PWA), Expo / React Native (native), Capacitor (mobile), Telegram Mini App | How people reach their agents |
| API | Hono v4 on @hono/node-server, Node ≥ 20 | One app: all routes, middleware, and the scheduler |
| Source of truth | Postgres (pg pool) | Every authoritative byte of state |
| Coordinator | Redis (ioredis) | Money locks, leader election, webhook dedup, rate limits, caches |
| Identity & wallets | Privy | User sign‑in + embedded/server wallets |
| Cards & commerce | Reap (Reap Global) | Virtual Visa cards, KYC, agentic checkout |
| Agent brain | OpenRouter (default deepseek/deepseek-chat) | The tool‑using LLM loop |
| Hosting | Railway (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

There is one product surface rendered across several shells, all served by the same API:
- Web SPA (
web/) — React + Vite, built toweb/distand 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
initDatais 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:
- Body limit — 1 MB by default; 20 MB for the media routes (
/agents/:id/run, transcription). - CORS — an allowlist from
CORS_ORIGINS. In production, an unset allowlist means deny‑all (not permissive); dev keeps a permissive legacy default. - Structured logging — JSON request logs, with an
onErroralert hook. - Public routes — health, config, and the provider webhook receivers.
/api/*— guarded byprivyAuth(); the verified Privy DID becomes theappUserIdfor the request./admin— mounted only whenADMIN_PASSWORDis set (fail‑closed; otherwise it does not exist).- 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:
| Provider | Supplies | Config gate |
|---|---|---|
| Privy | User identity (DID) + embedded/server wallets | PRIVY_APP_ID, PRIVY_APP_SECRET |
| Reap | Virtual Visa cards, KYC orchestration, Agentic Commerce checkout, card auth/clearing webhooks | REAP_API_KEY (+ two webhook secrets) |
| SumSub | The actual identity‑verification flow, reached through Reap's Managed KYC | via Reap (sdkToken) |
| OpenRouter | The agent's LLM brain | OPENROUTER_API_KEY |
| Enso | DeFi intent routing (swap/bridge/stake/lend/LP) | ENSO_API_KEY + ENABLE_WITHDRAWALS |
| Stripe | Subscription billing (Plus / Pro) + merch top‑ups | STRIPE_SECRET_KEY |
| Resend | Each agent's email alias on agents.pocketagent.to | RESEND_API_KEY |
| Pimlico / Alchemy / Tenderly | ERC‑4337 bundler + paymaster; transaction simulation | BUNDLER_RPC_URL, ALCHEMY_API_KEY, … |
| Browserbase / CDP | The agent's real stealth browser | BROWSERBASE_API_KEY, BROWSER_CDP_URL |
| Telegram / VAPID / Expo | Push notifications across surfaces | token / 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:
| Subsystem | What it covers | Deep dive |
|---|---|---|
| Data model & storage | Every Postgres table — identity, agents, the card ledger, money‑op ledgers, approvals, growth analytics | data model → |
| Money movement | Card funding (crypto → card), redeem/cash‑out, wallet transfers and withdrawals, card purchases | money movement → |
| Money‑safety engine | The invariants: one guarded send path, record‑intent‑before‑broadcast, exactly‑once settlement, reconcilers, kill‑switch | money safety → |
| Custody | The non‑custodial refactor — the "no‑license" thesis, the custody guard, and Tracks A/B/C | custody → |
| Reap integration | The Reap SDK, authorization matcher, webhooks (two signing secrets), PAN reveal, Agentic Commerce | Reap integration → |
| Agent runtime & tools | The perceive→plan→act loop, the tool catalog, approvals, personas, autonomous tasks | agent runtime → |
| Gasless & smart accounts | EVM gas sponsorship, Solana gasless, ERC‑4337 (Kernel + Pimlico), the DeFi stack | gasless & smart accounts → |
| Infrastructure & deploy | Railway, health checks, graceful shutdown, observability, COGS, the scaling model | infrastructure → |
| Admin & ops console | The /admin operator panel — read‑all plus freeze / KYC / suspend / kill‑switch, all audited | admin & ops → |
| Feature flags | Every src/config.ts flag, its default, and what it gates | feature 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 EXISTSDDL 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).