04, Architecture

Architecture

How Sphynx is put together: a Claude-Code-native desk with no backend and no Python orchestrator, one main session, a folder of sub-agents, one MCP broker connection, a JSON snapshot instead of a database, and a separate on-chain module on Robinhood Chain mainnet that layers session scoping, custody-layer caps, and an append-only registry — unaudited, deposit-capped, no timelock yet.

Sphynx's architecture is Claude-Code-native. There is no backend server and no Python orchestrator. The Portfolio Manager (PM) is not a separate process, it *is* the main Claude Code session you talk to. It orchestrates a small team of sub-agents, reaches the broker through a single MCP server, and writes its results to a plain JSON file. That's the whole system.

The sub-agents are just Markdown files in .claude/agents/, loaded when Claude Code starts. The broker lives behind one connector, the robinhood-trading MCP server declared in .mcp.json. State is a snapshot file plus append-only logs, not a database. Everything you'd normally stand up as services, queues, an ORM, a job scheduler, a container, is deliberately absent. This page maps each piece to where it actually lives in the repo.

Claude-Code-native by design

The design goal is that nothing about the desk is aspirational plumbing. Every component is a file you can open and read: an agent is Markdown, a guardrail is a contract, a strategy is a note, state is JSON. Four architectural choices follow from that.

No backend server

NATIVE

There is no FastAPI/Uvicorn process and no service to deploy. The PM is the live Claude Code session; orchestration happens inside it.

No external orchestrator

NATIVE

No LangGraph or LangChain. The PM fans out to sub-agents and routes their findings through the Risk Manager using Claude Code's own dispatch.

One broker connection

MCP

A single robinhood-trading MCP server (.mcp.json, HTTP transport) is the only path to the Agentic account. Auth is OAuth 2.0, in-session.

One state snapshot

NO DB

State is ui/public/desk-state.json plus append-only JSONL logs. No PostgreSQL, no Redis, the dashboard mirrors the snapshot.

How a run flows through the system

A single request travels from you, through the PM, out to the sub-agents and the broker (read-only), and back to a preview card. Steps 1–6 are research and produce no order, the desk stops at the preview and waits. The full lifecycle, with every tool call and gate, is documented in The Desk Run.

One request, end to end (steps 1–6 place no order)
YOU ──▶ PORTFOLIO MANAGER  (main Claude Code session · only role that can order)
   1. SENSE     ─▶ read portfolio + positions        (Agentic account, read-only)
   2. SCREEN    ─▶ Technical Analyst runs scans       ─▶ candidate shortlist
   3. RESEARCH  ─▶ Fundamental · Technical · Macro/News
                    (dispatched in parallel · read-only · news is injection-isolated)
   4. SYNTHESIZE─▶ propose one trade tied to a rule in strategies/
   5. RISK      ─▶ Risk Manager (opus) ─▶ APPROVE / CHANGES / VETO   (veto stops here)
   6. PREVIEW   ─▶ review_equity_order ─▶ build a preview card   ◀── steps 1-6: NO order
   7. APPROVAL  ─▶ present preview to YOU and wait                ◀── the desk stops here
   8. EXECUTE   ─▶ only on your "yes": place_equity_order  (still gated by the 'ask' rule)

The desk's standard output is the preview card at step 7. It never proceeds to place_equity_order on its own, see Guardrails for the permission gate.

How the pieces are wired

Everything hangs off the one session. The PM loads the sub-agents from a folder, reads its contract and permissions from two files, enforces written strategy rules, and emits a snapshot the dashboard polls. The broker sits behind a single MCP hop.

The desk, mapped
drag to rotate

Drag to rotate. The Portfolio Manager is the only role with order tools; everything else is read-only or advisory. The wiring in full is below.

Component wiring
CLAUDE CODE  (host runtime)
  │
  └─ PORTFOLIO MANAGER  = main session · only role with order tools
        │
        ├─ .claude/agents/*.md ──▶ 3 analysts (sonnet) + Risk Manager (opus)
        │                          least-privilege · loaded at startup · no order tools
        │
        ├─ robinhood-trading MCP ──HTTP──▶ https://agent.robinhood.com/mcp/trading
        │                          OAuth 2.0, in-session · Agentic account only
        │
        ├─ CLAUDE.md + .claude/settings.json ──▶ operating contract + deny→ask→allow gate
        │
        ├─ strategies/*.md ──▶ written caps + entry/exit rules (Risk Manager enforces)
        │
        ├─ writes ──▶ ui/public/desk-state.json ──▶ ui/ dashboard (Vite+React, read-only)
        │                                            polls the snapshot every ~5s
        │
        └─ appends ──▶ logs/*.jsonl  (via tools/desk-log.mjs · append-only audit trail)

Main components

Every component below is a real path in the repo. Nothing here is a running service you have to manage, they are files the session reads, writes, or dispatches.

ComponentTechnology / locationFunction
Portfolio Manager (orchestrator)main Claude Code sessionReads the account, dispatches sub-agents in parallel, synthesizes a proposal, presents a preview, and, after your approval, places the order. The only role that can order.
Sub-agents (analysts + risk).claude/agents/*.mdFour least-privilege specialist roles, loaded when Claude Code starts. None of them hold order tools. See The Desk Team.
Broker connectorrobinhood-trading MCP · .mcp.json (HTTP)The single path to the Robinhood Agentic account; authenticates via OAuth in-session. See MCP & the broker.
Guardrails & permissionsCLAUDE.md + .claude/settings.jsonThe PM's operating contract plus a deny → ask → allow permission gate (orders gated, options denied, reads allowed). See Guardrails.
Strategies & risk rulesstrategies/*.mdWritten caps and entry/exit rules the Risk Manager reads and enforces. See Strategies.
Dashboard (read-only)ui/, Vite + React, type: moduleMirrors desk-state.json; it cannot place orders. See Dashboard.
Backtester (offline)backtest/, Node ESM, no dependencySanity-checks strategy logic against historical bars with plain node, no npm install. See Backtesting.
Audit log (JSONL)logs/ + tools/desk-log.mjsAppend-only structured trail of desk runs, verdicts, previews, approvals, fills, and injection alerts. See Logging.
Documentationdocs/ (TEAM, SETUP, TRIGGER, LOGGING)Team roles, OAuth setup, the optional "Run desk" trigger, and the logging schema.

Faithful to design.md §3.2, each row is a real location in the repo.

The broker connection

There is exactly one way for the desk to touch the market: the robinhood-trading MCP server. It is declared project-scoped in .mcp.json with an HTTP transport pointed at Robinhood's Agentic endpoint. Nothing else in the repo talks to the broker.

.mcp.json
{
  "mcpServers": {
    "robinhood-trading": {
      "type": "http",
      "url": "https://agent.robinhood.com/mcp/trading"
    }
  }
}
  • Transport: HTTP, the connector is a remote MCP endpoint, not a local process.
  • Auth: OAuth 2.0, completed in-session (desktop + mobile verify). No token lives in the repo.
  • Reach: the Agentic account only. Other Robinhood accounts are read-only context; nothing outside Agentic is ever traded.
  • Order tools live only on the PM. review_equity_order and place_equity_order are held by the main session, never by a sub-agent. Full tool map in MCP & the broker.

State without a database

Sphynx keeps no database. Its entire state is two kinds of file: a single snapshot the PM overwrites after each run, and an append-only log of what happened. That's enough to drive a live dashboard and a full audit trail without a server.

Current state, the snapshot
ui/public/desk-state.json holds the latest account view, positions, candidate verdicts, the proposed trade/preview, recent orders, and any injection alerts. The PM overwrites it after every run; the real file is gitignored and only *.example.* is committed. Schema lives in `ui/README.md`.
History, the logs
Append-only JSONL under logs/, written via tools/desk-log.mjs. One JSON object per line captures desk runs, analyst and risk verdicts, previews, approvals, fills, and injection alerts. See Logging.
Scheduling
There is no Celery/Redis job queue. Recurring runs use /loop or a Claude-session cron, the session is the scheduler.
Data flow: snapshot → dashboard
PORTFOLIO MANAGER ──runs the desk, writes──▶ ui/public/desk-state.json
   (Claude Code session)                        (gitignored: real account state)
                                                       │
                                                       │  polled every ~5s (cache-busted)
                                                       ▼
                                              ui/ dashboard (Vite + React, read-only)
                                              falls back to desk-state.example.json
                                              when no live snapshot exists

The dashboard is a mirror, not a controller, approval and execution happen only in the Claude Code session. See Dashboard.

Technology stack (the real one)

The running stack is small and unglamorous on purpose. Here is exactly what is in play, layer by layer.

Host runtime
Claude Code, the main session acts as the Portfolio Manager.
Sub-agents
Markdown files in .claude/agents/ with tools: / model: frontmatter that defines each role's least-privilege tool set and model.
Broker
The robinhood-trading MCP server over HTTP (.mcp.json), authenticated with OAuth 2.0 in-session.
Models
Claude, the three analysts run on sonnet; the Risk Manager runs on opus.
Dashboard
Vite + React in ui/, ES modules ("type": "module"), strictly read-only.
Offline tooling
The backtester (backtest/) and the logger (tools/desk-log.mjs) are pure Node, ESM, with no dependencies, they run with plain node, no npm install.
UI format helpers
ui/src/format.js provides usd, pct, num, signClass, and timeAgo for rendering.
Claude CodeMCP over HTTPOAuth 2.0Markdown sub-agentssonnet analystsopus riskVite + ReactNode ESM · no depsJSON snapshot · no DBRead-only dashboard

What Sphynx deliberately does not use

An earlier v1.0 design draft imagined a much heavier system. None of it ships. This matters for reading the code: if you expect a server, a queue, or a wallet, you will look for files that do not exist. The table below maps each assumed component to its real status.

Assumed componentReality in the running system
Python backend (FastAPI / Uvicorn)None. The PM is the Claude Code session; there is no server process to deploy.
LangGraph / LangChain orchestratorNone. Orchestration is native to Claude Code.
Celery / Redis job queueNone. Scheduling uses /loop or a Claude-session cron.
PostgreSQL / any databaseNone. State is desk-state.json plus append-only JSONL logs.
Docker / Docker ComposeNone. It runs as local files and plain node.
Blockchain / RPC / wallet / tokenNot in the running desk. A separate module lives in onchain/ (ERC-4626 vault + guardrails-as-code + attestations). It is now deployed to Robinhood Chain mainnet (chainId 4663), still unaudited and deposit-capped, and it is not in the equities trading path. The $SPHYNX token is not built.

Faithful to design.md §5.3, components removed from the aspirational v1.0 draft.

What a heavier draft assumed
A backend service to deploy and monitor
An orchestration framework to wire agents
A queue and a database to hold state
Containers to reproduce the environment
What Sphynx actually is
One Claude Code session as the PM
Sub-agents as Markdown files
A JSON snapshot + append-only JSONL
Plain node and a Vite dashboard

The separate on-chain module

Everything above is the equities desk. The onchain/ module is a different system with a different trust model: a Foundry project where the risk caps written in strategies/ are enforced by a contract instead of read by an agent. Enforced, not promised. As of 2026-09-20 it is deployed to Robinhood Chain mainnet, chainId 4663, against real USDG and the real Uniswap V3 stock-token pools.

It is wired to the desk in neither direction: the desk's orders go through the Robinhood MCP and never touch a chain, and the vault's orders go through a Uniswap V3 pool and never touch a brokerage. Every desk order still needs your explicit in-session approval.

Trading Guide

USE

Connect, fund, compose an order, read the vault's verdict, sign. Open

The Vault

CUSTODY

ERC-4626 over USDG; previewTrade() names the rule an order would break; exits that always work. Open

Session Keys

AUTHZ

Expiring, budgeted, ticker-scoped sessions; a refusal spends nothing. Open

Oracle & Execution

PRICE

5-minute Uniswap V3 TWAP with a deviation bound; typed adapter, no router. Open

Contracts

REF

Addresses, ABIs, build and deploy. Open

Risks

READ FIRST

What can go wrong, ranked, with what limits it today. Open

Three layers of least privilege
AGENT KEY ──execute(trade)──▶ SessionKeyExecutor   expiry · per-trade · budget · count · side · ticker
                                     │
                                     ▼
                              RWAVault (vSPHYNX)    previewTrade() == None or revert GuardrailBreach(rule)
                                     │              caps from GuardrailConfig · owner-changeable within hard ceilings
                                     ▼
                              UniswapV3Adapter      one registered USDG pool per token · no router · minAmountOut
                                     │
                              Uniswap V3 pool       also the price source: 5m TWAP, 3% spot-deviation bound

DeskRegistry                  append-only attestations · refusals and vetoes go on the record
ContractAddress
RWAVault (vSPHYNX)0x510Af4fC7fA571e5549258541a9374dE3D894F28
SessionKeyExecutor0x87a6F83D1375401e1BfFca9e8055228033788713
GuardrailConfig0x7Ec7A870361E75A44E5549b57Cd437742e509be2
UniswapV3Oracle0xBf4fbd55eB70DC6424d839B9F6fDbc693A63cCe5
UniswapV3Adapter0x18bdc0EE9C2d33eeAbC5fe422126310cf3df13bC
DeskRegistry0x685915EB0226757bFeae58c5f5BdD3f5F493Eac4

Robinhood Chain mainnet, chainId 4663, block 26,015,235. Mirrors onchain/deployments/latest.json.