Instead of fetching up to 50k inventory snapshots (~442MB), scope the query to only the time range of the current submission batch with a 15-minute buffer. For a typical 1-hour batch this drops from 50k rows to ~300 rows, well within the 1280Mi pod memory limit. The coalesced_at_idx on intent_inventory_snapshots covers the BETWEEN clause so this remains efficient. Proof: history-writer OOM kills from refreshQuoteOutcomes inventory fetch Assumptions: 15min buffer covers the attribution window for all submissions Still fake: heuristic gap outcomes may attribute trades imprecisely
23 KiB
Current System Architecture
This document explains the current unrip runtime at a high level:
- which components exist
- what responsibilities each component owns
- which signals move through Kafka/Redpanda
- what gets persisted
- which attributes are available in the stored data
- what kinds of analysis and operator workflows the data supports today
The system is intentionally bus-first and data-first:
- components are separate services
- the core trading flow moves through Kafka-compatible topics
- PostgreSQL is the durable analytical and operator history store
- some safety-critical operational state is also persisted in local/PVC-backed JSON files
- the browser reads only the operator dashboard backend, not Kafka or PostgreSQL directly
Design Rules
The current architecture follows these rules:
- venue inputs, strategy, execution, alerting, persistence, and the dashboard are separate services
- the core trading path is event-driven, not direct service-to-service RPC
- raw and normalized events are both preserved
- decisions and execution attempts are auditable after the fact
- execution safety depends on persisted idempotency state, not Kafka consumer groups alone
System At A Glance
flowchart LR
subgraph External Sources
NearWS["NEAR Intents solver relay websocket"]
RefData["Kraken + CoinGecko"]
Bridge["Bridge RPC"]
Verifier["Verifier / NEAR RPC"]
BtcObs["BTC address observer"]
end
subgraph Runtime Services
Ingest["near-intents-ingest"]
RefIngest["market-reference-ingest"]
Liquidity["liquidity-manager"]
Inventory["inventory-sync"]
Strategy["strategy-engine"]
Executor["trade-executor"]
Sentinel["ops-sentinel"]
History["history-writer"]
Dashboard["operator-dashboard"]
end
Bus[("Redpanda / Kafka")]
Postgres[("PostgreSQL")]
Browser["Operator browser"]
NearWS --> Ingest
RefData --> RefIngest
Bridge --> Liquidity
Verifier --> Liquidity
BtcObs --> Liquidity
Verifier --> Inventory
Bridge --> Inventory
Ingest -->|"raw.near_intents.quote"| Bus
Ingest -->|"norm.swap_demand"| Bus
RefIngest -->|"ref.market_price"| Bus
Liquidity -->|"ops.liquidity_action"| Bus
Liquidity -->|"ops.funding_observation"| Bus
Inventory -->|"state.intent_inventory"| Bus
Strategy -->|"decision.trade_decision"| Bus
Strategy -->|"cmd.execute_trade"| Bus
Executor -->|"exec.trade_result"| Bus
Sentinel -->|"ops.alert"| Bus
Bus --> Inventory
Bus --> Strategy
Bus --> Executor
Bus --> Sentinel
Bus --> History
Bus --> Dashboard
History --> Postgres
Postgres --> Dashboard
Dashboard --> Browser
Dashboard -. "/state + /healthz" .-> Ingest
Dashboard -. "/state + /healthz" .-> RefIngest
Dashboard -. "/state + /healthz" .-> Liquidity
Dashboard -. "/state + /healthz" .-> Inventory
Dashboard -. "/state + /healthz" .-> History
Dashboard -. "/state + /healthz" .-> Sentinel
Dashboard -. "/state + /healthz" .-> Strategy
Dashboard -. "/state + /healthz" .-> Executor
Components And Responsibilities
near-intents-ingest
Responsibility:
- connect to the NEAR Intents websocket
- subscribe to quote traffic
- preserve the raw upstream payload
- normalize quote requests into the canonical
swap_demandshape - apply pair filtering before publishing
Produces:
raw.near_intents.quotenorm.swap_demand
market-reference-ingest
Responsibility:
- fetch BTC/EUR reference prices
- prefer Kraken and fall back to CoinGecko
- publish the reference price used for valuation and strategy checks
Produces:
ref.market_price
liquidity-manager
Responsibility:
- track bridge deposit handles
- observe deposits and withdrawals
- observe pre-credit funding activity
- estimate and optionally submit withdrawals
- publish treasury and funding operations signals
Produces:
ops.liquidity_actionops.funding_observation
inventory-sync
Responsibility:
- combine verifier balances with deposit and withdrawal side inputs
- publish the current inventory snapshot
- separate spendable balances from pending inbound and pending outbound funds
Consumes:
ops.liquidity_actionops.funding_observation
Produces:
state.intent_inventory
strategy-engine
Responsibility:
- consume normalized demand
- read the latest reference price and inventory snapshot
- evaluate whether a quote is actionable
- record why it skipped or accepted a quote
- emit an execution command when the strategy is armed and the quote passes checks
Consumes:
norm.swap_demandref.market_pricestate.intent_inventory
Produces:
decision.trade_decisioncmd.execute_trade
trade-executor
Responsibility:
- consume execution commands
- enforce arming state
- enforce command idempotency
- submit a quote response back to the venue
- publish the execution result
Consumes:
cmd.execute_trade
Produces:
exec.trade_result
ops-sentinel
Responsibility:
- watch price freshness, inventory freshness, funding progression, liquidity events, and execution outcomes
- turn those signals into operator alert transitions
Consumes:
ref.market_pricestate.intent_inventoryops.liquidity_actionops.funding_observationexec.trade_result
Produces:
ops.alert
history-writer
Responsibility:
- consume the major event topics
- route each topic into a PostgreSQL history table
- compute portfolio metrics from the durable event history
Consumes:
raw.near_intents.quotenorm.swap_demandref.market_pricestate.intent_inventoryops.liquidity_actionops.funding_observationops.alertdecision.trade_decisioncmd.execute_tradeexec.trade_result
Produces:
- rows in PostgreSQL event tables
- rows in
portfolio_metrics_snapshots
operator-dashboard
Responsibility:
- expose one browser-facing API and WebSocket endpoint
- read durable history and metrics from PostgreSQL
- read live service state from
/stateand/healthz - consume a small live Kafka topic set to avoid polling in the browser
- fan live updates to the frontend
Consumes:
- PostgreSQL
- service control APIs
norm.swap_demandref.market_pricestate.intent_inventoryops.alertexec.trade_result
Produces:
/api/session/api/bootstrap/api/trades/api/control/.../ws
Topic Classes And Signals
The topic naming scheme is meaningful:
raw.*means source-native truthnorm.*means canonical normalized inputsref.*means reference market datastate.*means derived state snapshotsops.*means operational or treasury state transitionsdecision.*means strategy output explaining what the system thoughtcmd.*means requested execution workexec.*means execution outcome
| Topic | Producer | Primary meaning | Main consumers |
|---|---|---|---|
raw.near_intents.quote |
near-intents-ingest |
Raw venue quote frame | history-writer |
norm.swap_demand |
near-intents-ingest |
Canonical demand signal from quote requests | strategy-engine, history-writer, operator-dashboard |
ref.market_price |
market-reference-ingest |
Reference BTC/EUR pricing for valuation and strategy | strategy-engine, ops-sentinel, history-writer, operator-dashboard |
state.intent_inventory |
inventory-sync |
Current spendable and pending inventory state | strategy-engine, ops-sentinel, history-writer, operator-dashboard |
ops.liquidity_action |
liquidity-manager |
Deposit handle, deposit, withdrawal, and treasury actions | inventory-sync, ops-sentinel, history-writer |
ops.funding_observation |
liquidity-manager |
Pre-credit inbound funding observations | inventory-sync, ops-sentinel, history-writer |
ops.alert |
ops-sentinel |
Raised or cleared operator alerts | history-writer, operator-dashboard |
decision.trade_decision |
strategy-engine |
Strategy explanation for skip or action | history-writer |
cmd.execute_trade |
strategy-engine |
Instruction for the executor | trade-executor, history-writer |
exec.trade_result |
trade-executor |
Outcome of the execution attempt | ops-sentinel, history-writer, operator-dashboard |
Signal Flow
This is the high-level flow from upstream quote to operator visibility:
flowchart TD
A["Quote request arrives from NEAR Intents"] --> B["near-intents-ingest"]
B --> C["raw.near_intents.quote"]
B --> D["norm.swap_demand"]
E["Reference price refresh"] --> F["market-reference-ingest"]
F --> G["ref.market_price"]
H["Bridge / verifier / BTC observer state"] --> I["liquidity-manager"]
I --> J["ops.liquidity_action"]
I --> K["ops.funding_observation"]
J --> L["inventory-sync"]
K --> L
L --> M["state.intent_inventory"]
D --> N["strategy-engine"]
G --> N
M --> N
N --> O["decision.trade_decision"]
N --> P["cmd.execute_trade"]
P --> Q["trade-executor"]
Q --> R["exec.trade_result"]
G --> S["ops-sentinel"]
M --> S
J --> S
K --> S
R --> S
S --> T["ops.alert"]
C --> U["history-writer"]
D --> U
G --> U
M --> U
J --> U
K --> U
O --> U
P --> U
R --> U
T --> U
U --> V["PostgreSQL"]
V --> W["operator-dashboard bootstrap reads"]
D --> X["operator-dashboard live consumer"]
G --> X
M --> X
T --> X
R --> X
W --> Y["Browser REST bootstrap"]
X --> Z["Browser WebSocket live updates"]
Sequence Diagrams
Quote To Decision To Execution
sequenceDiagram
participant Venue as NEAR Intents WS
participant Ingest as near-intents-ingest
participant Bus as Redpanda/Kafka
participant Strategy as strategy-engine
participant Executor as trade-executor
participant Sentinel as ops-sentinel
participant History as history-writer
participant DB as PostgreSQL
Venue->>Ingest: quote event frame
Ingest->>Bus: raw.near_intents.quote
Ingest->>Bus: norm.swap_demand
Bus->>Strategy: norm.swap_demand
Bus->>History: raw + norm events
History->>DB: insert raw_near_intents_quotes / swap_demand_events
Note over Strategy: Strategy already tracks latest ref.market_price and state.intent_inventory
Strategy->>Bus: decision.trade_decision
alt actionable and armed
Strategy->>Bus: cmd.execute_trade
Bus->>Executor: cmd.execute_trade
Executor->>Venue: quote_response submission
Venue-->>Executor: ack / response
Executor->>Bus: exec.trade_result
else rejected
Note over Strategy: No command emitted
end
Bus->>Sentinel: exec.trade_result
Sentinel->>Bus: ops.alert (if alert state changes)
Bus->>History: decision / command / result / alert
History->>DB: insert trade_decisions / execute_trade_commands / trade_execution_results / ops_alerts
Dashboard Bootstrap And Live Updates
sequenceDiagram
participant Browser
participant Dashboard as operator-dashboard
participant DB as PostgreSQL
participant Services as internal /state + /healthz
participant Bus as Redpanda/Kafka
Browser->>Dashboard: GET /api/bootstrap
Dashboard->>DB: load portfolio metric, price, inventory, quotes, trades, alerts
Dashboard->>Services: GET /state and /healthz across services
DB-->>Dashboard: durable history + metrics
Services-->>Dashboard: live service state
Dashboard-->>Browser: bootstrap JSON
Browser->>Dashboard: WebSocket /ws
Dashboard-->>Browser: session.ready + initial live state
Bus->>Dashboard: norm.swap_demand
Dashboard-->>Browser: quotes.recent
Bus->>Dashboard: ref.market_price / state.intent_inventory / ops.alert / exec.trade_result
Dashboard-->>Browser: status_bar.updated / alerts.updated
Event Envelope
Every bus event shares the same outer shape:
{
"event_id": "string",
"event_type": "string",
"venue": "string",
"source": "string|null",
"schema_version": 1,
"observed_at": "ISO-8601|null",
"ingested_at": "ISO-8601",
"payload": {},
"raw": {}
}
This matters because it gives every downstream consumer a stable minimum contract:
- unique event identity
- event type
- venue/source attribution
- two timestamps
- the domain payload
- optional raw payload preservation
What Is Persisted
There are three different persistence layers in the current system.
1. Kafka / Redpanda Retention
Kafka is the operational event backbone:
- it retains the event stream for replay and decoupling
- it is not the main analytical store
- it lets multiple consumers read the same event flow independently
What this layer is good for:
- fan-out
- short-horizon replay
- decoupled service design
- live dashboard updates without browser polling
2. PostgreSQL Durable History
PostgreSQL is the main durable queryable history layer for operator views and analysis.
All event tables share the same durable columns:
| Column | Meaning |
|---|---|
event_id |
Unique event id from the envelope |
topic |
Kafka topic name |
venue |
Venue or source family |
source |
Producing service or adapter |
event_type |
Domain event type |
observed_at |
When the upstream thing happened, if known |
ingested_at |
When this service emitted the event |
quote_id |
Quote correlation key when available |
pair |
Pair string when available |
decision_key |
Topic-specific correlation key such as decision id or command id |
payload |
The structured event payload |
raw |
The raw upstream payload when preserved |
raw_near_intents_quotes
Source topic:
raw.near_intents.quote
Key payload attributes:
message: original upstream frame
Useful for:
- schema drift inspection
- replay or re-normalization
- debugging unexpected parser behavior
- checking what fields the venue actually sent
swap_demand_events
Source topic:
norm.swap_demand
Key payload attributes:
quote_idasset_inasset_outpairrequest_kindamount_inamount_outmin_deadline_ms
Useful for:
- quote frequency by time
- pair mix and direction analysis
- size distributions
- exact-in vs exact-out behavior
- quote freshness analysis
market_price_events
Source topic:
ref.market_price
Key payload attributes:
price_idpaireur_per_btceure_per_btcbtc_per_eurbtc_per_euresource_usedfallback_in_usedivergence_pcteure_per_eur_assumptionkraken.pricekraken.observed_atkraken.age_mskraken.healthykraken.errorcoingecko.pricecoingecko.observed_atcoingecko.age_mscoingecko.healthycoingecko.error
Useful for:
- valuation at a point in time
- price freshness tracking
- fallback source usage
- disagreement between Kraken and CoinGecko
- linking strategy decisions to the exact price snapshot they saw
intent_inventory_snapshots
Source topic:
state.intent_inventory
Key payload attributes:
inventory_idaccount_idsynced_atspendablepending_inboundpending_outbounddepositswithdrawalsreconciliation_status
Deposit record attributes:
tx_hashchainasset_idamountaddressstatusdecimals
Withdrawal record attributes:
withdrawal_hashasset_idchainamountstatusaddress
Useful for:
- spendable balance history
- pending transfer tracking
- treasury state over time
- deposit and withdrawal visibility
- linking a decision to the inventory snapshot it used
liquidity_actions
Source topic:
ops.liquidity_action
Key payload attributes:
liquidity_action_idaccount_idaction_typestatuschainasset_iddetails
The details object varies by action type. Common examples include:
- refreshed deposit addresses
- observed deposit records
- tracked withdrawal objects
- changed withdrawal status objects
- submitted withdrawal details
Useful for:
- treasury workflow timelines
- deposit handle rotation history
- withdrawal lifecycle tracking
- identifying when operator actions changed state
funding_observations
Source topic:
ops.funding_observation
Key payload attributes:
funding_observation_idaccount_idasset_idchainfunding_handlesourcetx_hashstatusamountconfirmationsfirst_seen_atlast_seen_atcredited_atbridge_deposit_tx_hashbridge_statuscredit_correlation
Useful for:
- pre-credit deposit detection
- funding aging and stuck funding analysis
- credit lag measurement
- correlation between observed chain transactions and bridge crediting
ops_alerts
Source topic:
ops.alert
Key payload attributes:
alert_event_idalert_codestatusseverityreasonservice_scopepairasset_idraised_atcleared_atdetails
Useful for:
- alert timelines
- mean time to recovery
- stale data and treasury issue analysis
- operator incident review
trade_decisions
Source topic:
decision.trade_decision
Key payload attributes:
decision_idquote_idpairdirectionrequest_kinddecisiondecision_reasonthreshold_pctmax_notional_eurestrategy_armedassumptions.eure_per_eurprice_freshness_msinventory_freshness_msreference_rateimplied_rategross_edge_pctinventory_assetinventory_requiredinventory_availableinventory_idprice_ideure_notionalproposed_amount_inproposed_amount_outpending_inbound
Useful for:
- understanding why quotes were skipped
- measuring how many opportunities reached the actionable state
- analyzing insufficient inventory vs stale data vs threshold rejects
- reconstructing the exact decision context used by the strategy
execute_trade_commands
Source topic:
cmd.execute_trade
Key payload attributes:
command_iddecision_ididempotency_keyexecution_keyquote_idpairasset_inasset_outamount_inamount_outrequest_kindmin_deadline_msquote_output.amount_inquote_output.amount_outproposed_amount_inproposed_amount_out
Useful for:
- seeing exactly what execution was asked to do
- duplicate detection analysis
- comparing command intent to result outcome
- joining strategy decisions to actual execution attempts
trade_execution_results
Source topic:
exec.trade_result
Key payload attributes:
command_iddecision_ididempotency_keyexecution_keyquote_idpairstatusresult_codenotevenue_responseerror
Useful for:
- execution success and failure rates
- venue response analysis
- distinguishing disarmed rejects from submission failures
- command-to-result conversion tracking
portfolio_metrics_snapshots
This table is derived from durable history rather than copied directly from a Kafka event.
Top-level columns:
metric_idcomputed_atbaseline_anchor_atbaseline_statuspayload
Key payload attributes:
metric_versionbaseline_statuscommand_countresult_countcurrent_price.price_idcurrent_price.observed_atcurrent_price.eure_per_btccurrent_inventory.inventory_idcurrent_inventory.synced_atcurrent_inventory.btc_unitscurrent_inventory.btccurrent_inventory.eure_unitscurrent_inventory.eurecurrent_portfolio_value_eurecurrent_btc_mark_value_eurecurrent_eure_cash_value_euretrade_pnl_euremark_to_market_pnl_eureprice_move_pnl_eurebaseline_portfolio_value_eure_at_baseline_pricebaseline_portfolio_value_eure_at_current_pricecurrent_portfolio_value_eure_at_baseline_priceinventory_delta.btc_unitsinventory_delta.btcinventory_delta.eure_unitsinventory_delta.eurebaseline.anchorbaseline.command_atbaseline.pricebaseline.inventory
Useful for:
- current operator PnL display
- separating trade contribution from price movement
- baseline vs current comparison
- current portfolio valuation using the current price snapshot
3. JSON / PVC-Backed Operational State
Some important state is persisted outside PostgreSQL because it is operational control state, not analytical history.
Strategy armed state
Stored in:
strategyStateDir/strategy-engine-control.json
Attributes:
armedupdated_at
Purpose:
- preserve strategy arming/disarming across restarts
Executor armed state
Stored in:
executorStateDir/trade-executor-control.json
Attributes:
armedupdated_at
Purpose:
- preserve executor arming/disarming across restarts
Executor command idempotency state
Stored in:
executorStateDir/trade-executor-commands.json
Attributes per command id:
quote_ididempotency_keyexecution_keystatusupdated_at- result or error metadata when available
Purpose:
- prevent duplicate execution across restart or replay scenarios
Liquidity manager state
Stored in:
liquidityStateDir/liquidity.json
Key attributes:
pausedfunding_observer_pausedwithdrawals_frozendeposit_addressesdepositstracked_withdrawalssupported_tokensfunding_observationsfunding_observations_by_handlefunding_visibility_by_assetuncredited_funding_total_by_assetcredit_correlationobserver_healthlast_refresh_atlast_funding_observation_atfunding_observer_last_refresh_atfunding_observer_last_errorlast_errorlast_withdrawal_requestlast_withdrawal_resultpublish_countfunding_publish_count
Purpose:
- preserve treasury and observer operational state across restarts
- back service
/stateresponses
What The Data Lets You Do
With the currently persisted data, you can already answer questions like:
- How many quotes arrived, at what sizes, and in which direction?
- Which quotes were rejected because of stale price, stale inventory, insufficient inventory, pending deposits, unsupported pair, or max notional?
- Which decisions became actual commands?
- Which commands became successful submitted results?
- What was the reference price and inventory snapshot seen by the strategy when it made a decision?
- How often did the pricing source fall back from Kraken to CoinGecko?
- How much pre-credit funding was visible before bridge crediting happened?
- How long did funding remain in a pending or stuck state?
- What alert transitions happened and how long did they remain raised?
- What is the current portfolio value, and how much of the change looks like price move vs trade contribution?
Current Limits
A few important limits are worth naming plainly:
- Kafka retention is not the long-term historical warehouse; PostgreSQL is the main durable query surface
- raw venue schemas can drift, so raw quote storage matters for parser changes
- execution results currently represent submission outcomes, not full realized settlement economics
- fee coverage is not yet complete enough to treat all PnL numbers as full realized net profit
- some safety-critical state lives in JSON/PVC-backed operational stores, not only in PostgreSQL
Recommended Reading Order
If you want to trace the system from code:
src/lib/config.mjssrc/venues/near-intents/ws.mjssrc/apps/strategy-engine.mjssrc/apps/trade-executor.mjssrc/apps/history-writer.mjssrc/core/history-records.mjssrc/lib/postgres.mjssrc/apps/operator-dashboard.mjs