04, Architecture

Architecture

How Aelix 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.

Aelix'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

Aelix 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 Aelix 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 $AELIX 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 Aelix 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. The desk is how the rulebook is developed; the vault is how that rulebook becomes code. As of 2026-07-26 it is deployed to Robinhood Chain mainnet, chainId 4663, against real periphery, there are no mocks in the mainnet path.

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 holds no customer money. Deploying it changed what exists on chain, not what the desk is allowed to do, every order still needs your explicit in-session approval.

Three layers of least privilege

The module's shape is least-privilege layering: two independent mechanisms that each have to say yes before value moves, and one record that cannot be edited afterwards. Each layer would still hold if the layer above it failed.

Layer 1 — session scoping (SessionKeyExecutor)
The agent never holds a standing hot wallet. It trades through an expiring session key scoped by expiry, per-trade size, total spend budget, trade count, and a ticker allowlist. A rejected order spends none of that budget — the executor rolls its reservations back when the vault reverts. Sessions are granted and revoked by the owner, the 2-of-3 Safe; there is no per-depositor scoping, the session governs one pooled book.
Layer 2 — custody-layer caps (RWAVault + GuardrailConfig)
The risk caps are compiled into the vault, which reverts any order that breaches them — enforced at the custody layer on every order, changeable only by the 2-of-3 Safe, with no delay yet: there is no timelock, so a cap change the Safe signs takes effect in one transaction. previewTrade() returns the exact rule an order would break before anyone signs.
Layer 3 — the append-only record (DeskRegistry)
Attestations are append-only and chain-stamped: refusals and vetoes go on the record and cannot be pruned. That is why the first number this project intends to publish is how often the vault said no — the only metric honestly accumulable at TVL 0.

There are no fees anywhere in the contracts — no management fee, no performance fee, no carry. The exit fee accrues to remaining holders, not the operator. Shares are always redeemable in kind: a pro-rata slice of cash and tokens, minus the exit fee; cash-only redemption is limited to the vault's USDG on hand.

Deployed addresses

Each address below was confirmed on 2026-07-26 by calling an identifying function on the deployed contract, not copied from a docs table. They mirror onchain/deployments/latest.json.

ContractAddress (chainId 4663)Role
Safe (2-of-3 multisig)0x47b5e2923216f203b7960d8D232215534AF02FF2The owner of the whole stack, two of three signers required for any owner action — with no timelock yet: a change the Safe signs takes effect in one transaction. The handover completed on 2026-07-26, see below.
RWAVault (vAELIX)0x0e500E390cC599055f1e54194e1e611Cf64c5047ERC-4626 vault over USDG for tokenized real-world assets. 12 decimals (6 from USDG plus a 1e6 offset that defeats the ERC-4626 inflation attack). Deposit cap 10,000 USDG — an owner-changeable setting, not structural. Current state: totalAssets 0, totalSupply 0, not paused, 5 tokens allowlisted.
GuardrailConfig0x68cf24994d0363Be7688e96B69dDacC290c766C0The caps as state, not prose: the on-chain store of the risk limits from CLAUDE.md and `strategies/`. Some ceilings (e.g. the sell-tolerance cap) cannot be widened even by the owner; the rest are owner-changeable by the Safe, with no timelock yet.
ChainlinkOracleAdapter0xF6cFcA2024AFDeC14BCb0A9eb7bA402e73b2699APrices the vault reads for NAV and for the trade path, with two-tier staleness bounds, a circuit breaker for splits and corporate actions, and the chain-liveness quorum below.
DeskRegistry0x68cc84d722E2d613cAc36c62167B177656e2C983Append-only, chain-stamped attestation log for a desk. The on-chain counterpart of logs/*.jsonl. Currently empty.
PerfScore0x1CB3df5AAFEb0d2c31277e3e889613bc6F4C9e14Computes a performance summary from attestations in the registry. With nothing attested, there is nothing to score.
UniswapSwapAdapter0x9a8bb5E65f340C4Bf6c7Aa71991EC5D31083b5cfThe vault's only execution surface: a narrow, typed swap over Uniswap V2 Router02, so a manager can never redirect a swap into an arbitrary contract call.
SessionKeyExecutor0xC1C00ED38A41a00Cbbf89be8A4552c1a16706AF7The authorization layer between the AI desk and the vault: expiring session keys scoped by expiry, size, budget, trade count and ticker, instead of a standing hot wallet. A rejected order spends none of the session budget.
AelixAutosave0x5b0778E8561EA31490588D21bd44419803DC709bRecurring, scheduled buys into the vault. It never holds funds beyond the atomic hop.

Robinhood Chain mainnet, chainId 4663. Verified by direct on-chain call.

Stop-loss, precisely: the live vault requires a stop below entry on every buy. The stop-depth cap — a buy's stop must sit within stopLossBps of price, so a nominal $0.01 "stop" fails — is enforced in the current repo code with a regression test and ships with the next deploy. It is not a property of the live vault today.

Real periphery, verified by call

USDG, the 6-decimal original
0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168, decimals() 6, symbol() USDG, name() Global Dollar. An 18-decimal look-alike exists on the same chain, so the deploy gate pins USDG decimals to 6 and rejects the impostor without ever trusting a symbol.
Uniswap V2 Router02
0x89e5db8b5aa49aa85ac63f691524311aeb649eba. Identity cross-confirmed because its WETH() returns the documented WETH for the chain.
Five allowlisted Stock Tokens
NVDA, AAPL, TSLA, GOOGL, SPY, all 18 decimals, all exposing oraclePaused() and ERC-8056 uiMultiplier(). Matching is by contract address, never by symbol, the chain also hosts unrelated tokens with confusable tickers.
One Chainlink feed proxy per token
Every equity feed is 8 decimals, 86400s (24h) heartbeat, 0.5% deviation threshold. The oracle returns live prices, NVDA read $206.37 at deploy time. Wire the proxy, not the aggregator.
Test suite
215 tests passing in onchain/test/, including the regression test pinning the stop-depth cap. Tests are evidence about the code's own invariants, not a substitute for a third-party audit.

No sequencer uptime feed: the chain-liveness quorum

The standard L2 pattern is to read a Chainlink sequencer uptime feed and refuse to price while the sequencer is down. That feed does not exist here: Chainlink's reference directory for Robinhood Chain carries 56 feeds and zero uptime entries, and the chain is absent from Chainlink's L2 sequencer-feeds page.

So the adapter substitutes a chain-liveness quorum (ChainlinkOracleAdapter.livenessRefs): at least two 24/7 crypto feeds act as witnesses. Crypto feeds publish around the clock while equity feeds are 24/5, which is exactly the discriminator a plain staleness check lacks, one fresh witness proves the chain is producing blocks and the oracle network is delivering, so a stale equity feed just means a closed session. If every witness is stale, the vault fails closed with ChainLivenessStale. The gate refuses a deploy that has neither a real uptime feed nor a quorum, and SEQUENCER_FEED stays wired for the day one appears.

Ownership: the two-step handover, complete

The contracts use Ownable2Step, so a transfer only completes when the new owner calls acceptOwnership(). That handover is now finished. On 2026-07-26 the 2-of-3 Safe accepted ownership of the vault, the oracle adapter and the session-key executor in a single batch (tx 0x1ee7a73e7c3df216579554cd3d5993dfeee6be2bd081a68f59700efbd5968cea). Read back by direct eth_call after execution, all three return the Safe from owner() and the zero address from pendingOwner(), so the transfer is settled, not half-done. Per contract:

Contractowner() todayHow it got there
GuardrailConfigthe SafeSafe-owned from construction, it never passed through the deploy key at all.
UniswapSwapAdapterthe SafeSafe-owned from construction, same as above.
RWAVaultthe SafeOwnable2Step transfer, accepted by the Safe; pendingOwner() is now 0x0.
ChainlinkOracleAdapterthe SafeOwnable2Step transfer, accepted by the Safe; pendingOwner() is now 0x0.
SessionKeyExecutorthe SafeOwnable2Step transfer, accepted by the Safe; pendingOwner() is now 0x0.

Ownership as read on chain after the handover, 2026-07-26. Safe = 0x47b5e2923216f203b7960d8D232215534AF02FF2.

The negative control was checked too, not just the positive one. The deployer EOA 0xeC68f3c2f23c11Eb7Ca77322b4E66d23492B5c51 now reverts with OwnableUnauthorizedAccount (0x118cdaa7) on allowToken, setDepositCap and setFeedFrozen, while the same calls from the Safe address succeed. The deploy key retains no authority over any contract in the stack.

The on-chain module remains a direction, gated behind a third-party audit, explorer verification, and legal/securities review. The US-person question is unresolved and the $AELIX token is not built. Scope in full: Safety & Disclaimer.