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
| Environment | Host | Chain |
|---|---|---|
| Mainnet | https://sqs.osmosis.zone | osmosis-1 |
| Testnet | https://sqs.testnet.osmosis.zone | osmo-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 ansdk.Coinstring, e.g.1000000uosmo. The amount must be a positive whole number of the denom's smallest unit: a registered display unit is converted (1.5osmois quoted as1500000uosmo), but zero or a fraction of the smallest unit (1.5uosmo) is rejected withtokenIn amount must be a positive integer. The same rule applies totokenOut.tokenOutDenom(exact-in): chain denom of the desired output token.tokenOut(exact-out): output amount as ansdk.Coinstring, e.g.1000000uion.tokenInDenom(exact-out): chain denom of the input token.singleRoute(optional): set totrueto disable split-route quoting. Defaults tofalse.humanDenoms(optional): set totrueto 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>"e=<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"e=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.walletnarrows to one sender. limitis 1 to 100 (default 50). Pass the response'snextCursorascursorfor the next page; it isnullon the last page.- A multi-hop swap appears as one trade per hop, with the same
txHashand a differenteventIndex. Orderbook fills havesource: "orderbookFill". walletis the swap message sender, not the fee payer; for a swap routed through a contract it is the contract.directionis 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.buymeans the trader paid the quote asset and received the other. It isnullwhen neither side is a quote asset, and for stableswap and alloyed transmuter pools, which have no natural quote.usdValueAtIngestis the trade's USD value at the spot price when SQS read the block, not at the trade's own height, andnullwhen 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>"eAsset=<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>"e_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
| Endpoint | Use |
|---|---|
GET /healthcheck | Returns 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 /metrics | Prometheus-format metrics. |
GET /version | Server version. |
GET /config | Effective configuration, including router parameters. |
When to use SQS vs RPC, REST, or gRPC
| Need | Use |
|---|---|
| Quote a swap (best route, with splits) | SQS /router/quote |
| Quote a swap through a specific pool | SQS /router/custom-direct-quote |
| List pools and their current state | SQS /pools |
| Spot prices for an arbitrary basket | SQS /tokens/prices |
| Recent trades, 24h and 7d buy and sell volume for a pool or asset | SQS /trades/list, /trades/aggregates |
| Orderbook depth | SQS /passthrough/orderbook-ticks |
| Read a single account balance | REST /cosmos/bank/v1beta1/balances/{address} |
| Subscribe to new blocks or txs | RPC websocket |
| Sign and broadcast a transaction | RPC /broadcast_tx_sync |
| Query historical state by block height | Archive RPC / REST |
| Module queries not in the SQS surface | gRPC (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.
- 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) orroutable_cw_orderbook_pool.go(orderbook) as reference implementations. Requires a PR againstosmosis-labs/sqsadding the new pool type alongside an entry in the relevant code-id list (transmuter-code-ids,alloyed-transmuter-code-ids,orderbook-code-ids). - 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-idsin the SQS deployment config (mainnet's runtime config is maintained by the SQS team; seeconfig-testnet.jsonfor the schema). Pools registered undergeneral-cosmwasm-code-idsare 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/quoteresults. 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-quoteagainst 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.enabledistrue, which also needsgrpc-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 = falseunder[storage]inconfig.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-idslists pool ids to leave out of routing and pricing. - Debug endpoints are off by default:
pprof-enabledturns on/debug/pprof, anddebug-endpoints-enabledthe router and token state dump endpoints.
Reference
- Repository: https://github.com/osmosis-labs/sqs
- Production swagger: https://sqs.osmosis.zone/swagger/index.html
- Go client:
osmosis-labs/sqs-go-client, a first-party Go SDK wrapping the routing and price endpoints.