System Architecture
Trade the Spread.
Simulated Perpetual Futures for Interest Rate Differentials

A full-stack trading platform where users long or short the net carry between DeFi yield rates and borrow costs — powered by live Pendle and Aave feeds, simulated at 1200× speed.

username demo
password demo
or register your own in 5 seconds

High-Level Architecture

Three-tier system: React SPA → Hono backend → External DeFi data sources. Real-time data flows via Server-Sent Events from a single persistent stream.

Frontend · React 19 + Vite + Tailwind v4
AuthPageJWT guard
MainLayoutshell
SpreadChartrate viz
PositionsP&L
Zustand Storesauth · market · rates · order
rateStreamSSE client
HTTP REST + SSE · JWT Auth
Backend · Hono + Node.js + Drizzle ORM
/api/authlogin / register
/api/marketslist
/:id/rates/streamSSE endpoint
/:id/orderplace / close
RateServicepoll 30s
MarketServiceskew / OI
FundingServicehourly calc
OrderServiceliquidation 1s
PostgreSQLDrizzle ORM
HTTPS · GraphQL · REST
External Data Sources
Pendle Finance APIPT-sUSDe-7May26 implied APY
Aave V3 GraphQLUSDe variable borrow APY
Aave V3 GraphQLWETH variable borrow APY

Project Structure

Turborepo + Bun workspace with three packages: shared types at the base, API and frontend as consumers.

packages/api
Hono · Node.js

Backend server — handles auth, markets, real-time rate streaming, and order execution with a 1s liquidation loop.

routes/auth · markets · orders · rates/stream
services/rateService · marketService · fundingService · orderService
db/Drizzle schema · PostgreSQL connection
middleware/JWT auth guard
packages/frontend
React 19 · Vite 7

Single-page app — AuthGuard routing, live spread chart, position management, all state via Zustand + SSE.

App.tsxAuthGuard → MainLayout | AuthPage
store/auth · user · market · rates · funding · order · ui
services/SSE rateStream client
lib/API fetch wrapper with JWT
packages/shared
Types · Constants

Shared contract between API and frontend — types and market config consumed by both packages.

types.tsOrder · Market · OracleState · Position
constants.tsMARKET_CONFIG · ORACLE_CONFIG
turbo.jsonparallel build orchestration
railway.jsondeployment config
.envDATABASE_URL · JWT_SECRET

External Rate Feeds

All external fetching happens server-side. The frontend never calls DeFi APIs directly — data arrives via SSE.

Source What's Fetched Freq Market
Pendle Finance API Implied APY for PT-sUSDe-7May26 ~30s usdc-susde-aave
Aave V3 GraphQL Variable borrow APY + utilization for USDe ~30s usdc-susde-aave
Aave V3 GraphQL Variable borrow APY + utilization for WETH ~30s eth-steth-aave

Schema Overview

PostgreSQL via Drizzle ORM. Four tables covering auth, market state, and positions. Key constraint: one active position per user per market.

users AUTH
id uuid · PK PK
username text · unique
passwordHash text bcrypt
balance numeric USD
Accounts with bcrypt-hashed passwords and demo USD balance.
markets CONFIG
id uuid · PK
baseAsset text
quoteAsset text
protocol text
isActive boolean
Market definitions. Currently two markets: usdc-susde-aave and eth-steth-aave.
marketState OI TRACKING
marketId uuid · FK
longNotional numeric
shortNotional numeric
phantomOI numeric baseline
Open interest tracking per market for funding rate calculation and skew signal.
orders POSITIONS
side long | short
leverage numeric
entrySpread numeric
status filled · closed · liquidated
pnl numeric USD
Position records. Unique index: one filled order per user per market.

Key Data Flows

Two main flows drive the platform: the real-time SSE stream, and the order lifecycle from placement through liquidation.

Real-Time Rate Stream LIVE SSE
1
Client connects via EventSource
GET /api/:marketId/rates/stream?token=JWT
2
rates event (~30s)
borrowRate · yieldRate · utilization from Pendle & Aave
3
tick event (every 1s)
rates · marketState · fundingRate · live P&L for open positions
4
Zustand stores update
Dispatched to rates, market, funding, order stores — React re-renders chart + positions
Order Lifecycle ATOMIC TX
1
Place Order (atomic)
Check balance → deduct collateral → insert filled order → update OI
2
Position open — P&L accrues
Each tick: P&L = collateral × leverage × netCarry × elapsedYears
3a
User Close (atomic)
Calc final P&L → set closed → credit balance → reduce OI
3b
Auto-Liquidation (1s loop)
if collateral + pnl < size × 0.5% → set liquidated · pnl = −collateral → reduce OI
P&L Formula
pnl = collateral × leverage × netCarry × elapsedYears
netCarry = spread − platformFee − fundingCost
elapsedYears = realElapsedMs × 1200 / (1000 × 365.25 × 86400)
Trading Parameters
Leverage Range3× – 100×
Entry Fee0.025% notional
Platform Fee0.02% × (lev−1) APR
Maint. Margin0.5% of size
Time Multiplier1200× (1s = 20 min)
Position TypesFloating · Fixed
Funding Rate Mechanism
F_hourly
W_σ × skewAdjustment + W_U × sigmoid(10 × (utilization − 0.8))
F_APR
F_hourly × 24 × 365 — capped at ±5% APR
Skew
(longOI − shortOI) / (longOI + shortOI) — longs pay when positive
Util.
Sigmoid-shaped kick-in above 80% Aave utilization

Build & Deployment

Turborepo orchestrates parallel builds. Production on Railway — backend serves the frontend SPA as static files with fallback routing.

Commands
install bun install
dev bun run dev
API :3001 · frontend :5173
build bunx turbo build
start node packages/api/dist/index.js
Railway Deployment
https://spread-perps.up.railway.app
Build Command bun install && bunx turbo build
Start Command node packages/api/dist/index.js
Static Files packages/frontend/dist/
SPA Routing fallback enabled

Tech Stack Summary

Lean, modern full-stack: Bun runtime throughout, Hono for edge-ready API, Zustand for lightweight client state.

MONOREPO
Turborepo
+ Bun runtime
BACKEND
Hono
Node.js · TypeScript
DATABASE
PostgreSQL
+ Drizzle ORM
AUTH
JWT HS256
+ bcrypt
FRONTEND
React 19
+ Vite 7
STYLING
Tailwind v4
+ Radix UI primitives
STATE
Zustand 5
8 stores
REAL-TIME
SSE
Server-Sent Events
HOSTING
Railway
Single service · API serves SPA · PostgreSQL managed