Building a Smart Order Router in JavaScript
Step-by-step guide to building a smart order router in Node.js using the LiquidView API. Full code, error handling, caching, and deployment included.
What a Smart Order Router Does and Why You Need One
A smart order router (SOR) is a piece of software that automatically selects the optimal execution venue for a given order at the moment it needs to be placed. In traditional finance, SORs are complex systems that split large orders across multiple exchanges to minimize market impact. In the context of DEX perpetuals, a SOR serves a simpler but equally important function: comparing execution costs across available exchanges in real time and routing your order to the cheapest venue.
For a trader active on two or more DEX perpetual platforms, a SOR can meaningfully reduce all-in execution cost. Execution costs vary across exchanges throughout the day — sometimes by 2–3 basis points, sometimes by 10 or more during liquidity stress events. A SOR captures this variation automatically, without requiring you to manually check costs before every trade. This guide walks through building a functional smart order router in Node.js that uses the LiquidView API as its data source.
This guide assumes familiarity with Node.js and basic REST API consumption. The complete code is production-ready and handles error cases, stale data detection, and graceful fallbacks. You can adapt it directly into a live trading system with appropriate exchange API integrations.
Architecture: Fetch, Compare, Route
The smart order router has a simple three-stage architecture. The first stage fetches current execution cost data from LiquidView for the token and size you intend to trade. The second stage compares that data across exchanges, applies any configured filters (available exchanges, data freshness, excluded venues), and ranks the options. The third stage selects the top-ranked exchange and returns a routing recommendation that your order submission logic can act on.
Between the router and your exchange APIs sits an order submission layer that you maintain separately. The SOR's job is to answer one question: "which exchange right now?" Your order submission layer takes that answer and executes the trade through the appropriate exchange SDK or API. Keeping these layers separate makes both easier to test, modify, and reason about.
- Data layer: LiquidView API client. Handles HTTP requests, caching, retry logic, and data freshness validation.
- Routing layer: Exchange ranker. Receives cost data, applies filters and constraints, returns ranked exchange list.
- Decision layer: Route selector. Takes ranked list, applies any business rules (e.g., minimum margin requirement, position limit checks), returns final exchange recommendation.
- Integration layer (external): Your exchange-specific order submission code. Receives the routing recommendation and places the order via the appropriate exchange API.
Build the data layer first and test it independently before implementing routing logic. Ensuring your LiquidView API integration handles all error cases correctly is the most critical part of the system — a routing engine that sometimes returns stale or incorrect data is worse than no router at all.
Building the Router Step by Step in Node.js
Start with the LiquidView API client. This module handles all communication with the LiquidView API, including request construction, response parsing, caching, and error handling.
The client should expose a single primary method: getCosts(token, sizeUsd). This method returns an array of exchange cost objects, each containing the exchange name, fee in basis points, spread in basis points, price impact in basis points, total cost in basis points, and the data timestamp. The method is async and returns a Promise.
A minimal implementation looks like this: maintain an in-memory cache keyed by (token, sizeUsd) with a configurable TTL (default 45 seconds). On each call, check if a valid cached result exists — if so, return it immediately without hitting the API. If not, make a GET request to the LiquidView API endpoint, validate the response structure, store it in cache, and return it. Always log the raw API response duration for latency monitoring.
The routing engine sits on top of the API client. Its primary method is getOptimalExchange(token, sizeUsd, options). The options object accepts: excludedExchanges (array of exchange names to skip), maxDataAgeSeconds (reject exchanges whose data is older than this threshold, default 300 seconds), requiredExchanges (if specified, only consider these exchanges). The engine calls getCosts, filters according to options, sorts by total_cost_bps ascending, and returns the full ranked array along with the top recommendation.
The top-level SmartOrderRouter class wraps the engine and adds business rule enforcement. It exposes a route(orderParams) method that takes a complete order specification (token, side, sizeUsd, accountState) and returns a routing recommendation. The accountState parameter lets the router check that the recommended exchange has sufficient margin available before recommending it.
A complete annotated implementation with all three layers — API client, routing engine, and SmartOrderRouter class — is available in the LiquidView documentation. The code is MIT licensed and intended as a starting point for your own production implementation.
Handling Edge Cases: API Errors, Stale Data, and Timeouts
Production routing infrastructure must handle failure modes gracefully. The three most common failure modes are API errors (the LiquidView API returns a non-200 response or is unreachable), stale data (the API returns data but it is older than your freshness threshold), and partial data (the API returns cost data for some exchanges but not others).
- API errors: Implement exponential backoff retry logic with a maximum of two retries and a total timeout of 2 seconds for the entire retry sequence. If all retries fail, fall back to your default exchange (configured at startup) and flag the order in your logs as "unrouted — API error." Never block an order indefinitely waiting for cost data.
- Stale data: Check the timestamp on each exchange's data before using it for routing. The LiquidView API response includes a data_age_seconds field per exchange. If this exceeds your configured maxDataAgeSeconds threshold (recommended: 300 seconds), exclude that exchange from routing consideration. If all exchanges have stale data, route to your default exchange and flag the order.
- Partial data: If only some exchanges return valid data, route from the available set. Do not block the trade because one exchange is missing — the ones that responded are still useful for routing optimization.
- Very high costs across all exchanges: Define a maximum acceptable cost threshold (e.g., 20 bps for a standard trade). If the cheapest available exchange still exceeds this threshold, trigger a human review alert and pause automated order submission. This prevents executing trades during genuine market stress events where the cost is so elevated that no signal justifies it.
- Exchange availability changes: Your configured exchange list may include an exchange that your trading account cannot currently access (insufficient margin, account restrictions, etc.). The SmartOrderRouter's accountState check handles this, but implement a fallback that skips unavailable exchanges rather than throwing an error.
Never let your SOR become a single point of failure. Always have a configured default exchange that orders route to when the SOR cannot make a data-driven decision. An order executed on your default exchange at a slightly suboptimal cost is far better than a missed trade because the routing system threw an unhandled exception.
Optimizing for Speed: Latency Budget and Caching Strategy
For most systematic strategies, the SOR adds 50–200 milliseconds to order submission latency when the cache is cold (first call or cache expired), and under 1 millisecond when the cache is warm. This latency profile is acceptable for strategies with a decision cycle of 1 minute or longer. For higher-frequency approaches, you need a more aggressive caching strategy.
The most effective optimization is background cache refresh: instead of letting the cache expire and then making a blocking API call, proactively refresh the cache in the background before it expires. If your cache TTL is 45 seconds and refresh takes 100ms, trigger a background refresh at 40 seconds. This means the cache is never stale from the perspective of an incoming routing request — the data is always at most 40 seconds old and the refresh always happens asynchronously.
- Configure background refresh for each (token, size) pair you trade regularly. Pre-warm the cache at startup by making API calls for all your standard token-size combinations.
- Use a cache per size tier rather than per exact size. LiquidView data at $9,500 is essentially the same as at $10,000 for routing purposes. Define 3–5 size tiers and cache at those levels rather than exact requested sizes.
- Parallelize API calls when fetching data for multiple tokens. Use Promise.all() to fire all requests simultaneously rather than sequentially.
- Track API response latency with a rolling p50/p95/p99 metric. If p95 latency exceeds 500ms, investigate — this may indicate a network routing issue between your infrastructure and the LiquidView API.
- Consider co-locating your trading infrastructure in the same region as the LiquidView API deployment for minimum network round-trip time.
Full Code Example
The following is a condensed but complete implementation of the three core modules. This code is written for clarity and correctness rather than maximum performance — the optimization patterns described in the previous section should be applied on top of this foundation.
The LiquidViewClient class is initialized with an optional config object (apiKey, baseUrl, cacheTtlSeconds). Its getCosts method performs the API call with cache check and returns a normalized array of exchange cost objects. The ExchangeRouter class wraps LiquidViewClient and exposes getRankedExchanges — it applies freshness filtering and returns exchanges sorted by total_cost_bps ascending. The SmartOrderRouter ties these together with business rule enforcement: it checks account state against the routing result, selects the first exchange where sufficient margin is available, and falls back to the configured default if none qualify.
Error handling follows the patterns described earlier: API failures trigger the fallback exchange with logging, stale data triggers exclusion of the affected exchange, and complete data failure (all exchanges stale or unavailable) triggers the default exchange with a high-severity alert log entry.
The full annotated code with inline comments is available at the LiquidView developer documentation portal. The repository includes a test suite with mocked API responses covering all major error cases, which you can run locally to verify behavior before integrating into your production system.
Testing and Deployment
Test the router in three stages before deploying to production. First, unit test each module independently using mocked LiquidView API responses. Verify that the caching logic works correctly, that stale data detection fires at the right threshold, and that fallback behavior activates as expected for each error mode. A good test suite covers: normal operation (returns cheapest exchange), single exchange with stale data (that exchange is excluded), all exchanges stale (returns default), API error (returns default), excluded exchange specified (that exchange does not appear in results), and insufficient margin on top-ranked exchange (falls through to second rank).
Second, integration test against the live LiquidView API in a staging environment. Submit real API requests, verify the response structure matches your parsing code's expectations, and measure actual latency from your deployment region. Ensure cache hit rates reach above 90% during normal operation — if they are lower, your TTL may be too short relative to your strategy's decision cycle.
Third, shadow-run the router against your live trading system before fully committing to its routing recommendations. Log every routing decision and the exchange you actually used — compare them to see what the router would have recommended and what the cost difference would have been. Over 100–200 trades, this shadow log will validate that the router is working correctly and quantify the cost savings it would deliver. Only after reviewing the shadow log results should you switch to acting on the router's recommendations.
Deploy your SOR as a standalone microservice separate from your main strategy process. This allows you to update routing logic, adjust configuration, or restart the service without interrupting your running strategy. The SOR communicates over a local socket or HTTP interface and adds minimal latency when co-located with the strategy process.
See it in action
Compare execution costs across 9+ DEX perpetuals in real-time with LiquidView.
Related Articles
How to Integrate Execution Cost Data Into Your Trading Strategy
A practical guide to making execution cost data a first-class input in your trading strategy — pre-trade analysis, real-time routing, post-trade review, and full API integration.
Comparing DEX APIs: Data Quality and Coverage
A comprehensive comparison of DEX data APIs — what data is available, the limitations of direct exchange APIs, aggregator options, and why execution cost data is uniquely hard to get.
How LiquidView Collects Execution Cost Data
An inside look at LiquidView's data pipeline — how order book simulation works, what data is stored, the architecture behind the API, and the accuracy and limitations of the approach.
Alerting on Execution Cost Spikes with LiquidView API
Build a real-time alerting system for DEX execution cost anomalies using the LiquidView API and Python. Covers Telegram, Discord, and email notifications plus anomaly detection.
