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
NATIVEThere 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
NATIVENo 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
MCPA 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 DBState 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.
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.
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.
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.
| Component | Technology / location | Function |
|---|---|---|
| Portfolio Manager (orchestrator) | main Claude Code session | Reads 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/*.md | Four least-privilege specialist roles, loaded when Claude Code starts. None of them hold order tools. See The Desk Team. |
| Broker connector | robinhood-trading MCP · .mcp.json (HTTP) | The single path to the Robinhood Agentic account; authenticates via OAuth in-session. See MCP & the broker. |
| Guardrails & permissions | CLAUDE.md + .claude/settings.json | The PM's operating contract plus a deny → ask → allow permission gate (orders gated, options denied, reads allowed). See Guardrails. |
| Strategies & risk rules | strategies/*.md | Written caps and entry/exit rules the Risk Manager reads and enforces. See Strategies. |
| Dashboard (read-only) | ui/, Vite + React, type: module | Mirrors desk-state.json; it cannot place orders. See Dashboard. |
| Backtester (offline) | backtest/, Node ESM, no dependency | Sanity-checks strategy logic against historical bars with plain node, no npm install. See Backtesting. |
| Audit log (JSONL) | logs/ + tools/desk-log.mjs | Append-only structured trail of desk runs, verdicts, previews, approvals, fills, and injection alerts. See Logging. |
| Documentation | docs/ (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.
{
"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_orderandplace_equity_orderare 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.jsonholds 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 viatools/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
/loopor a Claude-session cron, the session is the scheduler.
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 existsThe 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/withtools:/model:frontmatter that defines each role's least-privilege tool set and model. - Broker
- The
robinhood-tradingMCP 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 plainnode, nonpm install. - UI format helpers
ui/src/format.jsprovidesusd,pct,num,signClass, andtimeAgofor rendering.
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 component | Reality 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 orchestrator | None. Orchestration is native to Claude Code. |
| Celery / Redis job queue | None. Scheduling uses /loop or a Claude-session cron. |
| PostgreSQL / any database | None. State is desk-state.json plus append-only JSONL logs. |
| Docker / Docker Compose | None. It runs as local files and plain node. |
| Blockchain / RPC / wallet / token | Not 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.
node and a Vite dashboardThe 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
USEConnect, fund, compose an order, read the vault's verdict, sign. Open
The Vault
CUSTODYERC-4626 over USDG; previewTrade() names the rule an order would break; exits that always work. Open
Session Keys
AUTHZExpiring, budgeted, ticker-scoped sessions; a refusal spends nothing. Open
Oracle & Execution
PRICE5-minute Uniswap V3 TWAP with a deviation bound; typed adapter, no router. Open
Contracts
REFAddresses, ABIs, build and deploy. Open
Risks
READ FIRSTWhat can go wrong, ranked, with what limits it today. Open
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| Contract | Address |
|---|---|
| RWAVault (vSPHYNX) | 0x510Af4fC7fA571e5549258541a9374dE3D894F28 |
| SessionKeyExecutor | 0x87a6F83D1375401e1BfFca9e8055228033788713 |
| GuardrailConfig | 0x7Ec7A870361E75A44E5549b57Cd437742e509be2 |
| UniswapV3Oracle | 0xBf4fbd55eB70DC6424d839B9F6fDbc693A63cCe5 |
| UniswapV3Adapter | 0x18bdc0EE9C2d33eeAbC5fe422126310cf3df13bC |
| DeskRegistry | 0x685915EB0226757bFeae58c5f5BdD3f5F493Eac4 |
Robinhood Chain mainnet, chainId 4663, block 26,015,235. Mirrors onchain/deployments/latest.json.