Overview
Autonome is an AI-powered autonomous cryptocurrency trading platform built with a split architecture for optimal deployment and scalability. The application separates the frontend and backend into independently deployable units while maintaining real-time synchronization through oRPC and Server-Sent Events (SSE).
Split Deployment Architecture
The application is divided into two distinct deployable units:
Frontend (Vercel)
- Location:
src/
- Framework: TanStack Start (React 19 with SSR)
- Deployment: Vercel (via Nitro preset)
- Responsibilities:
- Server-side rendering (SSR) for fast initial page loads
- Client-side hydration and interactivity
- Real-time UI updates via SSE subscriptions
- State management with TanStack Query
Backend (VPS)
- Location:
api/src/index.ts
- Framework: Hono (lightweight HTTP server)
- Runtime: Bun
- Deployment: Virtual Private Server (VPS)
- Responsibilities:
- oRPC procedure handlers
- SSE event broadcasting
- Trading schedulers (AI agents, price tracking)
- Exchange simulator lifecycle
- Database operations (PostgreSQL + Drizzle ORM)
The split architecture allows the frontend to scale independently on Vercel’s edge network while the backend runs stateful schedulers on a persistent VPS.
Communication Layer
oRPC over HTTP
The primary communication protocol between frontend and backend:
// Frontend: src/server/orpc/client.ts
import { createORPCClient } from "@orpc/client";
import { RPCLink } from "@orpc/client/fetch";
const link = new RPCLink({
url: getRpcUrl(), // Points to VPS API server
});
export const client = createORPCClient(link);
export const orpc = createTanstackQueryUtils(client);
// Backend: api/src/index.ts
const rpcHandler = new RPCHandler(router);
app.all("/api/rpc/*", async (c) => {
const { response } = await rpcHandler.handle(c.req.raw, {
prefix: "/api/rpc",
context: {},
});
return response ?? c.json({ error: "Not Found" }, 404);
});
Server-Sent Events (SSE)
Real-time updates from backend to frontend:
/api/events/positions - Position updates
/api/events/trades - Trade execution feed
/api/events/conversations - AI chat events
/api/events/portfolio - Portfolio snapshots
/api/events/workflow - Trading workflow events
See Data Flow for detailed SSE architecture.
Core Components
Frontend Components
1. TanStack Router
File-based routing with type-safe navigation:
src/routes/
├── index.tsx # Main dashboard
├── leaderboard.tsx # Model performance comparison
├── analytics.tsx # Trading analytics
├── chat.tsx # AI co-pilot chat
└── failures.tsx # Failed workflow debugging
2. TanStack Query + oRPC Integration
Type-safe data fetching with automatic caching and refetching:
// Component usage
import { useQuery } from "@tanstack/react-query";
import { orpc } from "@/server/orpc/client";
function PositionsList() {
const { data: positions } = useQuery(
orpc.trading.getPositions.queryOptions({ input: {} })
);
// ...
}
3. SSE Event Subscriptions
Components subscribe to real-time updates via EventSource:
import { getSseUrl } from "@/core/shared/api/apiConfig";
const eventSource = new EventSource(getSseUrl("/api/events/positions"));
eventSource.onmessage = (event) => {
const positions = JSON.parse(event.data);
// Update UI or invalidate queries
};
Backend Components
1. oRPC Router
Organized by domain feature:
// src/server/orpc/router/index.ts
export default {
trading: { getTrades, getPositions, getCryptoPrices, getPortfolioHistory },
models: { getModels, getInvocations },
simulator: { placeOrder, getAccount, resetAccount },
analytics: { getModelStats, getLeaderboard, getFailures },
variants: { getVariants, getVariantStats, getVariantHistory },
};
Each procedure follows a standard pattern:
// src/server/orpc/router/trading.ts
import "@/polyfill"; // Required for compatibility
import { os } from "@orpc/server";
import * as Sentry from "@sentry/react";
export const getPositions = os
.input(z.object({ modelId: z.string().optional() }))
.output(z.array(positionSchema))
.handler(async ({ input }) =>
Sentry.startSpan({ name: "getPositions" }, async () => {
return await fetchPositions(input.modelId);
})
);
2. Event Broadcasting System
Centralized EventEmitter-based SSE broadcasting:
// src/server/features/trading/events/positionEvents.ts
import { EventEmitter } from "node:events";
const emitter = new EventEmitter();
let currentPositionsCache: PositionEventData[] = [];
export const emitPositionEvent = (event: PositionEvent) => {
currentPositionsCache = event.data;
emitter.emit(EVENT_KEY, event);
};
export const subscribeToPositionEvents = (listener) => {
emitter.on(EVENT_KEY, listener);
return () => emitter.off(EVENT_KEY, listener);
};
export const getCurrentPositions = () => currentPositionsCache;
3. Scheduler Bootstrap
Schedulers initialize once per server process:
// src/server/schedulers/bootstrap.ts
export async function bootstrapSchedulers() {
if (isBootstrapped()) return;
markBootstrapped();
console.log("🚀 Server-side bootstrap: initializing schedulers...");
if (IS_SIMULATION_ENABLED) {
await ExchangeSimulator.bootstrap(DEFAULT_SIMULATOR_OPTIONS);
}
ensurePortfolioScheduler(); // Price tracking every 10s
ensureTradeScheduler(); // AI decision loop every 60s
console.log("✅ Schedulers initialized");
}
Called from API server startup:
// api/src/index.ts
async function main() {
console.log("🚀 Starting Autonome API server...");
await bootstrapSchedulers();
console.log(`✅ API server running on http://localhost:${port}`);
}
4. ExchangeSimulator Lifecycle
The simulator maintains in-memory state for sandbox trading:
// src/server/features/simulator/exchangeSimulator.ts
export class ExchangeSimulator {
static async bootstrap(options?: Partial<ExchangeSimulatorOptions>) {
if (!globalThis.__exchangeSimulator) {
globalThis.__exchangeSimulator = ExchangeSimulator.create({
...DEFAULT_SIMULATOR_OPTIONS,
...options,
});
}
return globalThis.__exchangeSimulator;
}
private async initialise() {
// 1. Initialize market state (BTC, ETH, SOL order books)
for (const metadata of buildMarketMetadata()) {
const market = new MarketState(metadata, orderApi);
await market.refresh();
this.markets.set(metadata.symbol, market);
}
// 2. Restore open positions from database
await this.restorePositionsFromDb();
// 3. Start polling for price updates and exit plan triggers
this.startPolling();
}
}
Key behaviors:
- Singleton pattern: One simulator instance per process
- DB restoration: Rehydrates positions from
Orders table on startup
- Auto-close triggers: Monitors stop-loss and take-profit conditions
- EventEmitter: Broadcasts trade executions and account updates
Data Flow Summary
- Client requests data via
orpc.*.*.queryOptions() → TanStack Query
- Hono receives request at
/api/rpc/* → routes to oRPC handler
- oRPC Router executes procedure → reads from DB via Drizzle
- Response flows back → TanStack Query caches result
- Schedulers emit events (trades, positions, portfolio)
- SSE Endpoints broadcast events → clients update UI
Port Configuration
Development and production use different port configurations:
Development (.env.local)
PORT=8081 # Backend API server
FRONTEND_PORT=5173 # Vite dev server
VITE_API_URL=http://localhost:8081 # Client-exposed API URL
Production
# Frontend (Vercel)
VITE_API_URL=https://api.autonome.ai # Points to VPS
# Backend (VPS)
PORT=8081
CORS_ORIGINS=https://autonome.vercel.app
Vite Proxy (Development)
Vite proxies /api/* requests to the backend:
// vite.config.ts
export default defineConfig({
server: {
port: FRONTEND_PORT,
proxy: {
"/api": {
target: API_URL,
changeOrigin: true,
},
},
},
});
Database Architecture
Schema (Drizzle ORM)
Key tables with quoted capitalized identifiers:
"Models" - AI trading agents with variant configuration
"Orders" - Single source of truth for positions (OPEN) and trades (CLOSED)
"Invocations" - AI model execution history
"ToolCalls" - Trade decisions (CREATE_POSITION, CLOSE_POSITION, HOLDING)
"PortfolioSize" - Time-series snapshots of portfolio value
Orders Table Design
The "Orders" table serves dual purposes:
-- OPEN orders = active positions (Positions tab)
SELECT * FROM "Orders" WHERE status = 'OPEN';
-- CLOSED orders = completed trades (Trades tab)
SELECT * FROM "Orders" WHERE status = 'CLOSED';
Key fields:
status: OPEN | CLOSED
exitPlan: JSONB with stop/target/confidence
realizedPnl: Populated when closed
closeTrigger: null (manual) | "STOP" | "TARGET" (auto)
Unrealized P&L is calculated live from current prices, never stored. This ensures accuracy and avoids staleness.
Environment Management
All environment variables are typed via src/env.ts using T3Env:
import { env } from "@/env";
// ✅ Type-safe, validated access
const apiKey = env.LIGHTER_API_KEY_INDEX;
// ❌ Never use directly
const bad = process.env.LIGHTER_API_KEY_INDEX;
Server-side variables:
DATABASE_URL, PORT, CORS_ORIGINS
NIM_API_KEY, OPENROUTER_API_KEY, etc.
LIGHTER_API_KEY_INDEX, LIGHTER_BASE_URL
TRADING_MODE: live | simulated
Client-side variables (must have VITE_ prefix):
VITE_API_URL - API endpoint for browser
VITE_APP_TITLE - Optional UI title override
Next Steps
- Tech Stack - Detailed breakdown of technologies and versions
- Data Flow - Request/response flow, SSE streaming, and state management