Skip to main content

Database Model

Architecture

The database stores historical and identity data only. Current state (balances, open positions, open orders, vault TVL) is read directly from the Solana RPC and cached in Redis.

LayerWhat lives thereWho writes
PostgreSQLHistorical events, closed positions, filled ordersIndexer (events) + API
RedisCurrent state cache - balances, open positions, vault stateAPI (on RPC cache miss)
Solana chainSource of truth for all current statePrograms

The indexer listens to on-chain events and writes immutable records to the DB. It never writes current state - that is always read from the chain.


Current State (RPC → Redis)

These are not in the DB. The API fetches them from the RPC on cache miss and stores them in Redis with a short TTL.

DataOn-chain accountRedis key pattern
Account balanceUserAccount PDAaccount:{id}
Open positionsPosition PDA per marketpositions:{wallet}
Open ordersOrderBook slaborders:{wallet}:{market}
Vault state (TVL, NAV, status)Vault PDAvault:{id}
User vault deposit (shares, lockup, depositor count)VaultUserDeposit PDA + getProgramAccountsvault_deposit:{vault_id}:{wallet}, vault_depositors:{vault_id}
Market config (base/settle currency, fees, tick size, margin %, status)Market PDAmarket:{symbol}
Protocol configGlobalConfig accountglobal_config

Models

Diagram


accounts

One row per trading account. Stores identity only, balance is read from the UserAccount PDA on-chain.

ColumnTypeNotes
idIntPrimary key
walletStringManager/owner wallet public key. For vault accounts this is always the manager's wallet (not the Vault PDA), so that WHERE wallet = ? returns all accounts for a user including their vault's trading account
addressStringUserAccount PDA address — unique per account. The indexer uses this to match on-chain events to DB rows (WHERE address = <PDA from event>)
nameString?Optional display name, user-set via API
typeAccountTypeprimary, secondary, or vault — derived from UserAccount.account_type on-chain
created_atDateTime

Indexes: wallet, address

Type values:

  • primaryUserAccount.account_type == Primary on-chain. The API pre-computes the PDA and inserts the row on first auth, before the account exists on-chain
  • secondaryUserAccount.account_type == Secondary on-chain. Created by the API when the user adds a sub-account; PDA is initialised on-chain lazily on first use
  • vaultUserAccount.account_type == Vault on-chain. Created by the indexer on create_vault. The wallet column stores the manager's wallet so the account is queryable alongside the manager's personal accounts

Written by: API creates primary/secondary rows on auth/sub-account creation. Indexer creates vault rows on create_vault events.


markets

Used as FK target for historical tables. Live market config (fees, margin requirements, status) is read from the Market PDA on-chain.

ColumnTypeNotes
idIntPrimary key - internal ID used as FK target
symbolStringUnique, e.g. SOL-PERP. Used for API lookups before fetching from RPC
addressStringMarket PDA address on Solana, used to fetch live config from RPC
created_atDateTime

All other config (base_currency, settle_currency, tick_size, step_size, maker_fee, taker_fee, status, margin requirements) is read from the Market PDA on-chain and cached in Redis under market:{symbol}.

Written by: Indexer - inserted on initialize_market events.


positions_history

Closed and liquidated positions only. Open positions are read from Position PDAs on-chain.

ColumnTypeNotes
idIntPrimary key
market_idIntFK → markets.id
account_idIntFK → accounts.id
walletStringOwner wallet (denormalised for fast queries)
sideStringlong or short
sizeDecimalBase quantity
entry_priceDecimalAverage entry price
exit_priceDecimalPrice at close or liquidation
marginDecimalAllocated margin (USDC)
realized_pnlDecimalTotal PnL at close - price PnL minus fees plus/minus funding
funding_paidDecimalFunding component isolated - positive = received, negative = paid. Captured from the close_position event
statusStringclosed or liquidated, open positions are read from chain, never stored here
opened_atDateTime
closed_atDateTime

Indexes: (wallet, status), (market_id, status)

Written by: Indexer - inserted on close_position or liquidate_position events.


orders_history

Filled and canceled orders only. Open and partially filled orders are read from the OrderBook slab on-chain.

ColumnTypeNotes
idIntPrimary key
market_idIntFK → markets.id
account_idIntFK → accounts.id
walletStringOwner wallet (denormalised)
sideStringlong or short
typeStringlimit or market
priceDecimal?Null for market orders
sizeDecimalOriginal quantity
filledDecimalTotal filled quantity
order_valueDecimalprice * size - pre-computed from the fill event, avoids recomputing at query time
reduce_onlyBooleanWhether the order was flagged reduce-only on-chain
trigger_conditionsString?Human-readable description for stop/TP orders, null for standard limit/market
statusStringfilled, canceled, or expired, open orders are read from chain, never stored here
created_atDateTime
closed_atDateTimeWhen the order was fully filled, canceled, or expired

Indexes: (wallet, status), (market_id, status)

Written by: Indexer - inserted when an order is fully filled (FillEvent with remaining = 0) or canceled (cancel_order).


trades

Immutable record of every fill. Used for public trade feed, per-account trade history, and candle building.

ColumnTypeNotes
idIntPrimary key
market_idIntFK → markets.id
tx_hashStringUnique — Solana transaction signature. Used for deduplication and linking to Solana Explorer
maker_account_idInt?FK → accounts.id. Nullable - counterparty may not be in our system
taker_account_idInt?FK → accounts.id. Nullable - counterparty may not be in our system
maker_walletStringMaker wallet (denormalised for public feed queries)
taker_walletStringTaker wallet (denormalised for public feed queries)
priceDecimalFill price
sizeDecimalFill quantity
sideStringAggressor side (long or short)
maker_feeDecimalFee charged to maker
taker_feeDecimalFee charged to taker
timestampDateTimeOn-chain slot time

Indexes: (market_id, timestamp), maker_wallet, taker_wallet, maker_account_id, taker_account_id

Queries:

  • Public feed: WHERE market_id = ? ORDER BY timestamp DESC
  • Candles: group by time interval, one row per trade (no deduplication needed)
  • Per-account history: WHERE maker_account_id = ? OR taker_account_id = ?

Written by: Indexer - inserted on each FillEvent from onLogs.

Candles: There is no separate candles table. OHLCV data for any interval is derived from trades.


vaults

Stores immutable vault identity. Live vault state (TVL, NAV per share, status, depositor count) is read from the Vault PDA on-chain.

ColumnTypeNotes
addressStringPrimary key - Vault PDA address on Solana. Used as FK target by vault_transactions and vault_stats
account_idInt?FK → accounts.id - the vault's trading account (type = vault)
descriptionString?Free-text description set by the manager, stored here because arbitrary text is expensive on-chain
created_atDateTimeUsed to display vault age without an RPC call

All other config (name, owner, performance_fee, lockup_period, min_deposit, min_owner_share, strategy_type, TVL, NAV, status, depositor count) is read from the Vault PDA on-chain and cached in Redis under vault:{id}.

Written by: Indexer - inserts row on create_vault event. API - updates description when manager edits vault info.


account_transactions

Immutable ledger of every event that affects an account's balance. One row per event per account.

ColumnTypeNotes
idIntPrimary key
account_idIntFK → accounts.id
typeAccountTransactionTypeSee enum values below
assetStringAsset symbol — USDC (default)
amountDecimalAmount (always positive; direction implied by type)
statusAccountTransactionStatuspending, completed, or failed. Only relevant for deposit and withdrawal; all other types are always completed
tx_hashString?Solana transaction signature. Only set for deposit and withdrawal
created_atDateTime

Indexes: account_id

AccountTransactionType enum:

  • deposit — USDC deposited from external wallet into the platform
  • withdrawal — USDC withdrawn from platform to external wallet
  • transfer_in — funds received from another sub-account
  • transfer_out — funds sent to another sub-account
  • fee — trading fee charged on a fill
  • funding — funding rate payment (positive amount = received, negative = paid — use signed amount or separate credit/debit rows)
  • realized_pnl — PnL credited or debited on position close
  • liquidation - liquidation penalty deducted

AccountTransactionStatus enum: pending, completed, failed

Written by: Indexer — inserted on deposit_margin, withdraw_margin, transfer_margin, FillEvent (fee), funding settlement, close_position, and liquidate_position on-chain events.


account_snapshots

Hourly snapshots of each account's equity and cumulative PnL. Used to render the account value and PnL charts over time on the Portfolio page.

ColumnTypeNotes
idIntPrimary key
account_idIntFK → accounts.id
equityDecimalTotal account value at snapshot time - balance + unrealized PnL of open positions
realized_pnlDecimalCumulative realized PnL up to this point
timestampDateTimeSnapshot time (floored to hour)

Unique constraint: (account_id, timestamp)

Indexes: (account_id, timestamp)

Written by: Indexer - upserted on the hourly stats job (reads open position PnL from chain).


leaderboard_stats

Precomputed leaderboard metrics per account. Recomputed periodically by a background job.

ColumnTypeNotes
idIntPrimary key
account_idIntFK → accounts.id
equityDecimalCurrent account value (from chain via Redis)
all_time_pnlDecimalequity - total_deposited - total return since account creation
realized_pnlDecimalSUM(positions_history.realized_pnl)
volumeDecimalSUM(size * price) FROM trades
trade_countIntCOUNT(*) FROM trades
funding_paidDecimalSUM(positions_history.funding_paid) - net funding paid/received
win_rateDecimalCOUNT(realized_pnl > 0) / COUNT(*) FROM positions_history
sharpe_ratioDecimalComputed from account_snapshots.equity series
max_drawdownDecimalPeak-to-trough from account_snapshots.equity series
updated_atDateTimeLast time the job ran for this account

Unique constraint: account_id

Indexes: all_time_pnl - default sort order.

Written by: Indexer leaderboard job - recomputes all rows every X minutes.


vault_transactions

Immutable record of every vault deposit and withdrawal. Captures NAV per share at transaction time for realised PnL calculation.

ColumnTypeNotes
idIntPrimary key
vault_addressStringFK → vaults.address
walletStringDepositor wallet address
typeStringdeposit or withdrawal
amountDecimalUSDC deposited or withdrawn
sharesDecimalLP shares issued or burned
nav_per_shareDecimalNAV per share at transaction time
feeDecimal?Performance fee charged on withdrawal. Null for deposits
pnlDecimal?Realised PnL at withdrawal: (nav_per_share - avg_entry_nav) * shares_burned. Null for deposits
created_atDateTime

Indexes: vault_id, wallet

Written by: Indexer - inserted on vault_deposit and vault_withdraw events.


vault_stats

Hourly snapshots of vault-level metrics. Each row covers exactly one 1-hour bucket. Used to render TVL, share price, and PnL charts over time on the Vault Details page.

ColumnTypeNotes
idIntPrimary key
vault_addressStringFK → vaults.address
tvlDecimalTotal AUM in USDC at snapshot time (point-in-time)
nav_per_shareDecimalNAV per LP share at snapshot time (point-in-time)
aprDecimalAnnualised return computed from the 30-day nav_per_share series: ((nav_now / nav_30d_ago) - 1) * (365 / 30). Precomputed by the Indexer hourly job so the API does not need to aggregate on each request. null if the vault is less than 1 day old
pnlDecimalPnL generated in this 1h bucket
timestampDateTimeBucket start time (floored to hour)

Unique constraint: (vault_id, timestamp)

Indexes: (vault_id, timestamp)

Written by: Indexer - upserted on the hourly stats job (reads vault state from chain).


market_stats

Hourly snapshots of per-market aggregate metrics. Each row covers exactly one 1-hour bucket. open_interest is a point-in-time value (read the latest row or filter by timestamp). All other fields (volume, trade_count, liquidations) are per-bucket flows - sum N rows to get any time window (3h, 24h, 7D).

ColumnTypeNotes
idIntPrimary key
market_idIntFK → markets.id
open_interestDecimalTotal base quantity of open positions at snapshot time (point-in-time)
volumeDecimalUSDC volume traded in this 1h bucket
trade_countIntNumber of fills in this 1h bucket
liquidationsIntNumber of liquidations in this 1h bucket
timestampDateTimeBucket start time (floored to hour)

Unique constraint: (market_id, timestamp)

Indexes: (market_id, timestamp)

Written by: Indexer - upserted on the hourly stats job (reads open positions from chain via getProgramAccounts).


faucet_claims

One row per faucet claim. Used to enforce the 24h cooldown per wallet. Devnet only.

ColumnTypeNotes
idIntPrimary key
walletStringWallet that claimed
claimed_atDateTimeTime of the claim

Indexes: wallet

Written by: API - inserted on each successful /faucet/claim request.