Skip to main content

Sidecar Query Server (SQS)

The Sidecar Query Server is a Go service that runs alongside an Osmosis full node and exposes high-performance HTTP endpoints for the queries that are too expensive or too slow to run directly against the chain: most importantly swap routing, batched pool data, and token price lookups.

It is the query path used by app.osmosis.zone in production. If you are building a swap interface, a price oracle, an arbitrage bot, or any UI that needs current pool state without per-block chain queries, you should be reading SQS first and falling back to RPC/REST only when SQS does not expose the data you need.

Why SQS exists​

Routing a multi-hop swap on Osmosis requires knowing the balances, fees, and pool type of every relevant pool. Doing that onchain or through stateless RPC is too slow to drive a frontend. SQS subscribes to a full node, ingests pool state at the end of every block, holds it in memory, and serves routing answers in single-digit milliseconds.

The trade-off is that SQS is eventually consistent with the chain (by one block, in practice) and it does not cover every Cosmos SDK query. Use SQS for the operations it owns (routing, pool batch, prices) and use RPC/REST/gRPC for everything else.

Production endpoints​

EnvironmentHostChain
Mainnethttps://sqs.osmosis.zoneosmosis-1
Testnethttps://sqs.testnet.osmosis.zoneosmo-test-5

The mainnet host is served by several nodes behind a load balancer, which rate-limits requests per client IP. Every endpoint documented on this page is reachable on the mainnet host except the three noted as not publicly served in the passthrough and lookup section (/pools/ticks/{id} and the per-pool router lookups). The testnet deployment serves the same routing, pool and price endpoints and is the right environment for high-volume exploration without consuming the mainnet rate-limit budget; it does not serve the trade endpoints or orderbook depth.

Swagger reference: https://sqs.osmosis.zone/swagger/index.html.

Get a swap quote​

GET /router/quote returns the best route SQS can find for a swap, including any split routes that improve execution. The same endpoint serves both quote directions; the canonical direction is exact-in (out-given-in), and exact-out (in-given-out) is a separate request shape.

Exact-in (canonical)​

Specify the input amount; SQS returns the maximum output it can produce.

curl "https://sqs.osmosis.zone/router/quote?tokenIn=1000000uosmo&tokenOutDenom=uion" | jq .
{
"amount_in": { "denom": "uosmo", "amount": "1000000" },
"amount_out": "1822",
"route": [
{
"pools": [
{
"id": 1100,
"type": 2,
"balances": [],
"spread_factor": "0.005000000000000000",
"token_out_denom": "uion",
"taker_fee": "0.008000000000000000",
"liquidity_cap": "1805"
}
],
"has-cw-pool": false,
"out_amount": "1822",
"in_amount": "1000000"
}
],
"tokens": [
{ "denom": "uosmo", "liquidity_cap": "1839019" },
{ "denom": "uion", "liquidity_cap": "10811" }
],
"liquidity_cap": "1805",
"liquidity_cap_overflow": false,
"effective_fee": "0.008000000000000000",
"price_impact": "-0.005455176823085256",
"in_base_out_quote_spot_price": "0.001846767994347477"
}

Exact-out​

Specify the output amount; SQS returns the minimum input required.

curl "https://sqs.osmosis.zone/router/quote?tokenOut=1000000uion&tokenInDenom=uosmo" | jq .

The response has amount_in as a string (the required input) and amount_out as an sdk.Coin (the target output). Do not invert an exact-in quote to estimate exact-out: call /router/quote with the exact-out parameter pair instead. Exact-out is a separate routing search internally and the optimal route may differ.

Parameters​

  • tokenIn (exact-in): input amount as an sdk.Coin string, e.g. 1000000uosmo. The amount must be a positive whole number of the denom's smallest unit: a registered display unit is converted (1.5osmo is quoted as 1500000uosmo), but zero or a fraction of the smallest unit (1.5uosmo) is rejected with tokenIn amount must be a positive integer. The same rule applies to tokenOut.
  • tokenOutDenom (exact-in): chain denom of the desired output token.
  • tokenOut (exact-out): output amount as an sdk.Coin string, e.g. 1000000uion.
  • tokenInDenom (exact-out): chain denom of the input token.
  • singleRoute (optional): set to true to disable split-route quoting. Defaults to false.
  • humanDenoms (optional): set to true to pass human-readable denoms (OSMO, USDC) instead of chain denoms (uosmo, ibc/...).

Fees​

amount_out is the post-fee amount you would receive (for exact-in) or amount_in is the post-fee amount you must pay (for exact-out). Per-pool spread_factor and taker_fee are returned for transparency; do not subtract them again client-side. The top-level effective_fee is the taker fee compounded across the whole route; it does not include the spread factor (in the example above, 0.008 is pool 2's taker fee alone, while its 0.005 spread factor is already reflected in amount_out), price_impact is the route's price impact, and in_base_out_quote_spot_price is the spot price in base/quote terms. Each pool's taker_fee is read from chain state for that specific pool and can differ from (and exceed) the chain-wide default.

When SQS cannot compute a spot price along the route, the quote is still returned, with price_impact set to "0" and price_impact_unknown: true. Treat that as unknown rather than zero impact, for example by asking for confirmation before a large swap. The field is left out when the impact is known.

Quote a specific route​

If you already know which pool(s) you want to route through (for example, when quoting against a contract-specific pool that the general router has filtered out), use GET /router/custom-direct-quote:

curl "https://sqs.osmosis.zone/router/custom-direct-quote?tokenIn=1000000uosmo&tokenOutDenom=uion&poolID=2" | jq .

This endpoint bypasses the router's minimum-liquidity filter, so it returns a quote as long as the pool exists onchain and is not excluded from routing. A pool that is excluded is rejected with validation failed: pool <id> is excluded from routing.

Pools excluded from routing​

The mainnet deployment excludes the alloyed transmuter pools whose contract is paused from routing and pricing, so no quote or price goes through them. The current list is under Router.ExcludedPoolIDs in GET /config:

curl -s "https://sqs.osmosis.zone/config" | jq .Router.ExcludedPoolIDs

Discover available routes​

GET /router/routes enumerates every route SQS has cached between two tokens, without actually pricing a quote.

curl "https://sqs.osmosis.zone/router/routes?tokenIn=uosmo&tokenOutDenom=uion" | jq .
{
"Routes": [
{
"Pools": [
{ "ID": 1100, "TokenInDenom": "uosmo", "TokenOutDenom": "uion" }
],
"IsCanonicalOrderboolRoute": false
},
{
"Pools": [
{ "ID": 2, "TokenInDenom": "uosmo", "TokenOutDenom": "uion" }
],
"IsCanonicalOrderboolRoute": false
}
],
"UniquePoolIDs": { "1100": {}, "2": {} },
"ContainsCanonicalOrderbook": false
}

UniquePoolIDs is a set serialized as a JSON object: the keys are the pool ids referenced across all routes and the values are empty objects. Read the keys, not the values.

Useful for previewing what the router will consider before committing to a quote, or for surfacing every pool that connects two tokens.

Fetch pool state​

GET /pools returns hydrated pool data (chain model, balances, spread factor, pool type) for either a batch of pool IDs or all pools.

# Specific pools (returns a bare array)
curl "https://sqs.osmosis.zone/pools?IDs=1,2,1066" | jq '.[0]'

# All pools (returns a paginated { data, meta } wrapper)
curl "https://sqs.osmosis.zone/pools" | jq '.data[0]'

Two response shapes: ?IDs=… returns a bare array of pool objects; the no-IDs path returns { "data": [...], "meta": {...} } with pagination metadata. The all-pools response is large; cache it client-side rather than fetching it per request.

This is the preferred way to populate a frontend's pool list. It is much faster than iterating osmosis.gamm.v1beta1.Query.Pools or its osmosis.poolmanager.v1beta1 equivalent and it returns balances in the same payload, saving a separate cosmos.bank.v1beta1.Query.AllBalances per pool.

Canonical orderbook lookup​

When multiple orderbook contracts exist for the same base/quote pair, SQS tracks which one has the highest liquidity cap and promotes it to "canonical" for that pair. Two endpoints expose this view:

GET /pools/canonical-orderbook?base=<denom>&quote=<denom> returns the canonical orderbook pool id and contract address for a single pair:

curl "https://sqs.osmosis.zone/pools/canonical-orderbook?base=factory/osmo1z6r6qdknhgsc0zeracktgpcxf43j6sekq07nw8sxduc9lg0qjjlqfu25e3/alloyed/allBTC&quote=factory/osmo147h5x9pcj7lm0cttlaefx6sqq5vdfnmwfcqxkmjd7exqm9gc7grqhr75m0/alloyed/allUSDC" | jq .
{
"base": "factory/osmo1z6r6qdknhgsc0zeracktgpcxf43j6sekq07nw8sxduc9lg0qjjlqfu25e3/alloyed/allBTC",
"quote": "factory/osmo147h5x9pcj7lm0cttlaefx6sqq5vdfnmwfcqxkmjd7exqm9gc7grqhr75m0/alloyed/allUSDC",
"pool_id": 3525,
"contract_address": "osmo1w4w564csdwlfdrzten924u2kthg53zmva6qjkmzz8759kvp7wv8qdpt6nl"
}

GET /pools/canonical-orderbooks returns the canonical orderbook for every supported pair as an array of the same shape.

These endpoints are the right entry point for any orderbook UI or bot that needs to know which contract address to talk to for a given trading pair. Non-canonical orderbooks for the same pair still appear in /pools and can be queried directly by pool id; they just are not the default routing target.

Orderbook depth​

GET /passthrough/orderbook-ticks?poolID=<id> returns the full tick map of an orderbook pool as SQS holds it, for drawing a depth chart without reading every tick from the contract:

curl -s "https://sqs.osmosis.zone/passthrough/orderbook-ticks?poolID=3525" | jq '.ticks[0]'
{
"tick_id": 22000000,
"tick_state": {
"ask_values": {
"total_amount_of_liquidity": "0",
"cumulative_total_value": "0",
"effective_total_amount_swapped": "0",
"cumulative_realized_cancels": "0",
"last_tick_sync_etas": "0"
},
"bid_values": {
"total_amount_of_liquidity": "12500000000",
"cumulative_total_value": "...",
"effective_total_amount_swapped": "...",
"cumulative_realized_cancels": "...",
"last_tick_sync_etas": "..."
}
}
}

Ticks are sorted by ascending tick_id, and a tick with no liquidity on either side is left out. total_amount_of_liquidity is the resting amount on each side of the tick, in minimal units; convert tick_id to a price with the orderbook tick math. An empty ticks array means the book is empty. Missing data is never reported as an empty book: an unknown pool returns pool <id> not found, a pool that is not an orderbook pool <id> is not an orderbook, and an orderbook whose ticks SQS has not ingested yet returns ticks for pool <id> have not been ingested yet. Don't treat either as zero depth.

Read token prices​

GET /tokens/prices returns spot prices for a list of base denoms.

curl "https://sqs.osmosis.zone/tokens/prices?base=wbtc,dydx&humanDenoms=true" | jq .
{
"factory/osmo1z0qrq605sjgcqpylfl4aa6s90x738j7m58wyatt0tdzflg2ha26q67k743/wbtc": {
"factory/osmo147h5x9pcj7lm0cttlaefx6sqq5vdfnmwfcqxkmjd7exqm9gc7grqhr75m0/alloyed/allUSDC": "80210.000000000000000000000000000000000000"
},
"factory/osmo1zem8r6dv6u38f6qpg546zy30946av8h5srgug0s4gcyy6cfecf3seac083/alloyed/allDYDX": {
"factory/osmo147h5x9pcj7lm0cttlaefx6sqq5vdfnmwfcqxkmjd7exqm9gc7grqhr75m0/alloyed/allUSDC": "0.103138768064016994908621000000000000"
}
}

Each base maps to a quote-denom map whose value is the spot price, keyed by chain denoms. The quote is always USDC, under its full factory/osmo147h5x9pcj7lm0cttlaefx6sqq5vdfnmwfcqxkmjd7exqm9gc7grqhr75m0/alloyed/allUSDC denom. Set humanDenoms=true to pass wbtc instead of the chain denom.

If a price cannot be determined onchain, SQS falls back to CoinGecko. A denom that still has no price is left out of the response rather than priced at zero: it is unknown or unlisted, it has no route through pools above the minimum liquidity, or CoinGecko has no price for it. Check that a key is present before reading it, and treat a missing one as unpriced. Because onchain prices only route through pools above a minimum liquidity, a token traded only in very thin pools has no price.

Recent trades and volume​

SQS records every swap and orderbook fill on the pools it routes (GAMM weighted and stable, concentrated liquidity, orderbook and alloyed transmuter pools) from each block's token_swapped events, and serves them for a pool or an asset.

GET /trades/list returns trades newest first:

curl -s "https://sqs.osmosis.zone/trades/list?coinMinimalDenom=uosmo&limit=1" | jq .
{
"trades": [
{
"timestamp": 1793018527,
"txHash": "8BE484636F4A80FEA08363CC0C0B44FFCD0434A2819D2508C431DDCBDD6BF347",
"wallet": "osmo1xzphwrc9aahmq7smwaggukwx5w2w8mmtqf5mk0ky0v6hh75paxsszfnlu5",
"blockHeight": 73220921,
"txIndex": 0,
"eventIndex": 32,
"poolId": 1111,
"coinMinimalDenomIn": "uosmo",
"coinMinimalDenomOut": "ibc/B9E0A1A524E98BB407D3CED8720EFEFD186002F90C1B1B7964811DD0CCC12228",
"inAmount": "13668",
"outAmount": "172548786",
"usdValueAtIngest": "0.000696217734618997502352000000000000",
"direction": "buy",
"source": "swap"
}
],
"nextCursor": "..."
}
  • Filter with poolId, coinMinimalDenom (traded in or out) or both; at least one is required. wallet narrows to one sender.
  • limit is 1 to 100 (default 50). Pass the response's nextCursor as cursor for the next page; it is null on the last page.
  • A multi-hop swap appears as one trade per hop, with the same txHash and a different eventIndex. Orderbook fills have source: "orderbookFill".
  • wallet is the swap message sender, not the fee payer; for a swap routed through a contract it is the contract.
  • direction is relative to the pair's quote asset: an orderbook's own quote denom, and otherwise the first of USDC, USDT and OSMO in the pair. buy means the trader paid the quote asset and received the other. It is null when neither side is a quote asset, and for stableswap and alloyed transmuter pools, which have no natural quote.
  • usdValueAtIngest is the trade's USD value at the spot price when SQS read the block, not at the trade's own height, and null when the asset could not be priced. Amounts are in minimal units.

GET /trades/aggregates returns rolling 24 hour and 7 day totals:

curl -s "https://sqs.osmosis.zone/trades/aggregates?coinMinimalDenom=uosmo" | jq .
{
"24h": {
"buys": 21610,
"sells": 23069,
"buyVolumeUsd": "824107.494399922065569994493385997926599724",
"sellVolumeUsd": "703975.166796858714405634829049728295506816",
"distinctBuyers": 266,
"distinctSellers": 256
},
"7d": { "buys": 35415, "sells": 40242, "...": "..." }
}

With coinMinimalDenom, a buy is a trade that received that denom and a sell one that gave it up. With only poolId, buys and sells follow each trade's direction, and trades without one are not counted. Volumes are sums of usdValueAtIngest and are null when no trade on that side was priced. windows=24h or windows=7d returns one window; wallet narrows to one sender.

Trades are held in memory, for 8 days on mainnet, from the time each SQS node started. A window therefore only covers trades since the node serving the request started, and two requests served by different nodes can differ slightly. Until a node has ingested its first block, both endpoints answer trades have not been ingested yet. For full trade history, use an indexer.

Token metadata​

GET /tokens/metadata?denoms=<chainDenom>[,<chainDenom>...] returns SQS's view of asset metadata for one or more chain denoms.

curl "https://sqs.osmosis.zone/tokens/metadata?denoms=uosmo,uion" | jq .
{
"uosmo": {
"name": "Osmosis",
"symbol": "OSMO",
"coinMinimalDenom": "uosmo",
"decimals": 6,
"preview": false,
"coingeckoId": "osmosis"
},
"uion": {
"name": "Ion DAO",
"symbol": "ION",
"coinMinimalDenom": "uion",
"decimals": 6,
"preview": false,
"coingeckoId": "ion"
}
}

The response is keyed by the chain denom passed in. SQS reads this metadata from the generated frontend assetlist; the assetlist remains the canonical source if you are not running SQS yourself.

Passthrough and lookup endpoints​

Beyond routing and pool state, SQS serves account-level passthrough queries (it fans out the underlying chain queries and aggregates the result for you) and a few smaller lookup endpoints.

Active limit orders​

GET /passthrough/active-orders?userOsmoAddress=<address> returns a user's open limit orders across the canonical orderbooks.

curl "https://sqs.osmosis.zone/passthrough/active-orders?userOsmoAddress=osmo1..." | jq .
{ "orders": [], "is_best_effort": false }

An address with no open orders returns an empty orders array. is_best_effort is true when SQS could not reach every orderbook and the list may be incomplete.

Portfolio assets​

GET /passthrough/portfolio-assets/{address} returns an address's aggregated holdings, bucketed by category (total assets, staked, pooled, in locks, unclaimed rewards) with a per-coin breakdown and a USD capitalization for each.

curl "https://sqs.osmosis.zone/passthrough/portfolio-assets/osmo1..." | jq .
{
"categories": {
"in-locks": { "capitalization": "0.031304057153332185", "is_best_effort": false },
"pooled": { "capitalization": "0.000000000797905905", "is_best_effort": false },
"staked": { "capitalization": "0.000558723852586202", "is_best_effort": false },
"total-assets": {
"capitalization": "0.039646246920034655",
"account_coins_result": [
{
"coin": { "denom": "ibc/072E5B3D...", "amount": "550940834" },
"cap_value": "0.015699962323825378"
}
],
"is_best_effort": false
}
}
}

(account_coins_result truncated to one coin.) This is the query behind the frontend's portfolio page; it saves you a balance query, a staking query, a lock query, and a price lookup per denom.

Concentrated-liquidity ticks​

GET /pools/ticks/{id} returns the tick-level liquidity (depth) data for a concentrated-liquidity pool id, as an array of { liquidity_amount, lower_tick, upper_tick } ranges:

{
"ticks": [
{ "liquidity_amount": "44445455285122705.656039576085736780", "lower_tick": -108000000, "upper_tick": -107999800 }
]
}

This route is not served publicly. Self-host SQS to use it, or read the same data from the chain over LCD with /osmosis/concentratedliquidity/v1beta1/liquidity_per_tick_range?pool_id={id}.

Per-pool taker fee​

GET /router/taker-fee-pool/{id} returns the taker fee for each denom pair in the given pool:

[
{ "Denom0": "ibc/27394FB0...", "Denom1": "uosmo", "TakerFee": "0.002000000000000000" }
]

Like /pools/ticks, this route is not served publicly. Self-host SQS, or query the chain per denom pair with /osmosis/poolmanager/v1beta1/trading_pair_takerfee?denom_0=<denom>&denom_1=<denom> over LCD.

Per-pool spot price​

GET /router/spot-price-pool/{id}?baseAsset=<denom>&quoteAsset=<denom> returns the spot price of the base asset in terms of the quote asset within a single pool, as a bare decimal string (e.g. "47.416244090626802924000000000000000000"). Both baseAsset and quoteAsset are required; omitting one returns {"message": "baseAsset is required"}. This route is not served publicly either. Self-host SQS, or use the chain's poolmanager spot price query over LCD, /osmosis/poolmanager/v2/pools/{id}/prices?base_asset_denom=<denom>&quote_asset_denom=<denom>.

Unlike /tokens/prices, which prices across all pools with fallbacks, this endpoint prices against one specific pool's state.

Pool-denom metadata​

GET /tokens/pool-metadata returns, for every denom SQS has seen in a pool, its total pooled liquidity, liquidity cap, and price:

curl "https://sqs.osmosis.zone/tokens/pool-metadata" | jq .
{
"uosmo": {
"total_liquidity": "...",
"total_liquidity_cap": "...",
"total_effective_liquidity_cap": "...",
"price": "..."
}
}

The response covers every pooled denom and is large; cache it rather than fetching per request.

Fee tokens​

GET /chainregistry/fee_tokens returns the chain-registry view of accepted fee tokens with suggested gas-price tiers:

curl "https://sqs.osmosis.zone/chainregistry/fee_tokens" | jq .
{
"fee_tokens": [
{
"denom": "uosmo",
"fixed_min_gas_price": 0.03,
"low_gas_price": 0.03,
"average_gas_price": 0.1,
"high_gas_price": 0.16
}
]
}

(Truncated to the first entry.) For the onchain whitelist itself, query the txfees module directly; see Fees and Gas.

Health, metrics, and version​

EndpointUse
GET /healthcheckReturns 200 if the upstream node is reachable, not syncing, and the in-memory cache is within threshold of the chain tip. Wire this into your uptime monitoring.
GET /metricsPrometheus-format metrics.
GET /versionServer version.
GET /configEffective configuration, including router parameters.

When to use SQS vs RPC, REST, or gRPC​

NeedUse
Quote a swap (best route, with splits)SQS /router/quote
Quote a swap through a specific poolSQS /router/custom-direct-quote
List pools and their current stateSQS /pools
Spot prices for an arbitrary basketSQS /tokens/prices
Recent trades, 24h and 7d buy and sell volume for a pool or assetSQS /trades/list, /trades/aggregates
Orderbook depthSQS /passthrough/orderbook-ticks
Read a single account balanceREST /cosmos/bank/v1beta1/balances/{address}
Subscribe to new blocks or txsRPC websocket
Sign and broadcast a transactionRPC /broadcast_tx_sync
Query historical state by block heightArchive RPC / REST
Module queries not in the SQS surfacegRPC (grpc.osmosis.zone:443, TLS)

In short: SQS for cross-pool aggregation and routing, RPC/REST/gRPC for everything else. See RPC, REST, and gRPC for those paths.

Adding a custom CosmWasm pool to SQS​

If you operate a custom CosmWasm pool that you want SQS to handle, there are two integration paths. Background: SQS ingests CosmWasmPoolModel per block and routes against in-memory pool state, so any pool type that participates in route search must be cheap to quote in Go. See the SQS architecture note on CosmWasm pools for the model.

  1. Implement a dedicated pool type in SQS. Best when the pool's quote and spot-price logic is simple enough to mirror in Go. SQS gets enough state from the ingester each block to compute quotes in-process. See routable_cw_alloy_transmuter_pool.go (alloyed transmuter) or routable_cw_orderbook_pool.go (orderbook) as reference implementations. Requires a PR against osmosis-labs/sqs adding the new pool type alongside an entry in the relevant code-id list (transmuter-code-ids, alloyed-transmuter-code-ids, orderbook-code-ids).
  2. Register your code ID as a generalised CosmWasm pool. Best when the pool logic is too complex to mirror in Go. Add your code ID to pools.general-cosmwasm-code-ids in the SQS deployment config (mainnet's runtime config is maintained by the SQS team; see config-testnet.json for the schema). Pools registered under general-cosmwasm-code-ids are deprioritized in route search: they are excluded from split quotes, and when any route without a generalized pool exists, the router filters the generalized routes out of /router/quote results. Only when no other route connects the pair does SQS fall back to the best generalized route. Because SQS queries the contract over gRPC at quote time for these pools (instead of computing in memory), quotes through them are slower; /router/custom-direct-quote against a known pool id always works regardless of route-search priority.

After the PR merges, the SQS team will deploy the updated config to production.

Self-hosting​

SQS is open-source at osmosis-labs/sqs. To run your own deployment you need an Osmosis full node with is-enabled = "true" under the [osmosis-sqs] section of app.toml. See the repo's README.md for the development setup, ingest configuration, and Docker images.

A few settings matter for the endpoints above:

  • Trades are off unless trades.enabled is true, which also needs grpc-ingester.enabled. SQS reads each block's swap events from the node's /block_results, so the node must keep block results (discard_abci_responses = false under [storage] in config.toml); otherwise the trade endpoints never leave the not-ingested state. trades.retention-hours (default 192) must cover the 7 day window.
  • Excluded pools: router.excluded-pool-ids lists pool ids to leave out of routing and pricing.
  • Debug endpoints are off by default: pprof-enabled turns on /debug/pprof, and debug-endpoints-enabled the router and token state dump endpoints.

Reference​