VeilSwap
← All posts
Analysis2026-11-108 min read

Read the Quote API: rate, validUntil, routes, bps

The quote object has five facts most people never read: the rate, a 120-second validUntil on small orders only, a route count, an advantage score, and a min/max pair hidden in the 400 error. All five change how you time an order.

Why this guide exists. The order page is a convenience layer over one small JSON object: the quote. Everything the page decides for you — whether the rate is good, when your window closes, how many pools are actually competing for this pair — is already visible in the raw response, and none of it is hidden behind auth. On 2026-11-10 between 10:03 and 10:17 UTC I ran the pair USDT→XMR (TRON leg) against the public endpoint with a handful of amounts and deliberately broken inputs, plus one probe each on BTC→XMR and the exit leg XMR→USDT. This is what each field means and what it does not mean, measured, not guessed. (XMR to USDT exit)

The shape. A healthy response is one object: best, routes, advantageBps. Inside best: fromAsset, toAsset, fromAmount, toAmount, rate, validUntil. That is the whole contract — six fields inside one, plus two scalars. No session, no key, no signature. One request round-trip measured 194–604 ms (median ~203 ms over ten calls), so a script that quotes a corridor before every manual decision costs about a fifth of a second. The endpoint is GET with three query parameters: from, to, amount.

rate, and its eight digits. The rate field is toAmount divided by fromAmount, rounded to 8 significant digits. At 10:05:55 a 100-USDT quote returned toAmount 0.16052661 and rate 0.0016052661; a 100.123456789012-USDT quote returned 0.16074732 and 0.0016054911 — the rate kept only 8 significant digits (0.0016054911 has 9 digits before rounding; the API returned 8). Practical consequence: for amounts that differ in the ninth digit, the rate field can be identical even when toAmount differs, because toAmount is computed from the raw amount and then rounded to 8 decimals. Do not compare rates across corridors by eye — invert them. On 2026-11-10, 100 USDT bought 0.16082327 XMR (an implied 621.80 USD per XMR), while 1 XMR sold for 540.097 USDT-TRX with two routes at 192 bps. That is a 12.9% buy-sell gap on the same minute — the spread band between the two directions, not an error. (XMR to USDT exit)

validUntil is conditional. The 100-USDT quote carried a validUntil of 2026-11-10T10:13:55.878Z — measured at +119.8 seconds from the response, consistently, three times in a row (119.8, 119.8, 119.1). The same endpoint returned null for validUntil on every two-route order (110, 130 USDT) and on every BTC quote I sampled. The rule is not the amount per se: it is whether the winning route prices this exact amount with a lock. Small orders get a 120-second guaranteed price; large or two-route orders float until the order page locks them. If you are building a bot or a comparison page: treat null validUntil as "re-quote on display", not as a bug. (XMR to USDT exit)

routes and advantageBps: the depth reading. routes tells you how many pool quotes survived to compete; advantageBps grades the winner against the runner-up in hundredths of a percent. Both on one line: at 10:07 a 100-USDT order came back routes=1, advantageBps=0 (only one quote survived). At 10:12 a 110-USDT order came back routes=2, advantageBps=0 — two quotes, and they agreed to the last digit. At 10:12 a 130-USDT order came back routes=2, advantageBps=895: the two routes were almost nine percent apart, and the cheaper one won by that margin. The exit leg 1 XMR→USDT-TRX read routes=2, advantageBps=192. Reading: on a two-route corridor, a high bps tells you the two pools disagree about the price right now — useful as a signal, not a guarantee. On 2026-11-10 the winner at 130 USDT paid 0.235639 XMR (implied 551.83 USD/XMR) against a spot of 543.39 — a premium of 1.55% to buy XMR that minute. (XMR to USDT exit)

The 400 body is a second API. Two responses worth more than a table. Below the minimum, the 400 body says {"error": "amount below minimum", "minAmount": 18.472402, "maxAmount": 162815.934213}. Above the maximum, the identical pair with a different error string: {"error": "amount above maximum", ...}. Both carry both numbers, so one probe on any corridor returns the usable window without a second endpoint — useful for automation that needs to clamp an amount before quoting. The boundary drifts with the market: in the 10:03–10:06 window the USDT→XMR min moved 18.463823 → 18.472402 across three minutes (about four cents of drift), and the max 162793.649781 → 162815.934213 (about 22 dollars). BTC→XMR boundaries on the same day read 0.0001825 / 2.1850807. Requests with amount=0 or a negative number are rejected earlier with {"error": "amount must be a positive number"}. An unknown asset id returns 404 {"error": "pair not supported"}; a missing from or to returns 400 {"error": "from/to required"}. (XMR to USDT exit)

The catalogue, and how asset ids work. The second public endpoint, /api/currencies, returned 453 assets on 65 networks at 10:03 UTC — 367 tickers, 52 flagged isStable, 6 with hasExtraId (assets that need a memo or destination tag), and the top multi-network tickers were USDC with 13 entries, USDT with 11, ETH with 7, DAI and WBTC with 5 each. Each entry carries id, ticker, network, name, logo, hasExtraId, featured, isStable. The asset id convention is ticker-network, and the quote endpoint is tolerant of legacy aliases: on 2026-11-10 both usdt-tron and usdt-trx returned byte-identical quotes (same toAmount 0.16082327, same rate, validUntil differing only by 0.4 seconds of clock). The catalogue, not memory, is the source of truth: ids come and go as pools add or drop networks, so read the list at runtime instead of hardcoding. (XMR to USDT exit)

The cache changes what a 'fresh' quote is. Polling the same amount every second for 25 seconds produced the identical toAmount 0.16080074 for four consecutive polls, then a new number (0.16079135) on the fifth — the quote cache window measured at about 8 seconds, matching what the weak-network test found in November. Two quotes 20 seconds apart, however, returned identical amounts with different validUntil stamps: the cache also keys on the full response, so a cached quote keeps its original lock window. The rate moves underneath: three quotes spaced 30 seconds apart read 0.0016217233, 0.0016272058, 0.0016291304 — a drift of 0.46% in 60 seconds on a 100-USDT order. The practical rule: if you need a guaranteed price, act on one quote inside its 120-second validUntil; if you are building a page that refreshes, re-quote on display rather than trusting a cached body. (XMR to USDT exit)

Error discipline, and one limit worth knowing before you script against it. Every failure mode observed returns a single JSON object with an error string — no HTML, no stack trace, no silent empty body. Malformed inputs (empty to, zero amount, negative amount) are rejected in order: from/to first, then positivity, then range, then pair existence. That ordering matters for automation: you can probe whether a pair exists by sending an amount you know is valid for most corridors, then read the 400 body for the exact window. One operational limit: hammering the endpoint returns HTTP 429 with a plain {"error": "rate limited"} — observed directly while running probes at roughly one request per second for a few tens of seconds; the block held for at least 4 minutes before requests were admitted again. If you script against this API, treat 429 as back off, not retry-now.

Where the fields stop. What the quote does not tell you: which pools sit behind routes, whether a locked quote can be extended, or the actual network fee you will pay to deliver the deposit — the fee bill is chain-side and quoted in the source asset's native coin, as covered in our fee-reserve piece. What it does tell you is enough to time an order: read the rate inverted for cross-corridor comparison, respect the validUntil when it is present, use routes and advantageBps as a depth-and-disagreement signal, clamp amounts to the min/max pair from a single probe, and back off on 429. The whole guide fits in one curl per decision. Prices above are a snapshot from 2026-11-10 10:03–10:17 UTC; quotes move, so the live order page is the authority. (how Monero fees work)

Frequently asked questions

Why does my quote show validUntil but my friend's does not?

The small-order leg locks the price for a fixed window — on 2026-11-10 the 100-USDT quote carried a validUntil set ~119.8 seconds ahead, while 110 and 130 (two-route orders) and all three BTC quotes returned null. Locking costs the provider something, so it is reserved for orders too small to move the market; larger amounts float with the rate until the order page locks them.

How do I find an order's minimum and maximum without the UI?

Ask for a quote with any amount and read the 400. Below the minimum the body says "amount below minimum" and above the maximum "amount above maximum" — both carry minAmount and maxAmount. On 2026-11-10 USDT→XMR read 18.472402 / 162815.934213 (the numbers drift by cents between minutes), BTC→XMR 0.0001825 / 2.1850807. The amount 0 is rejected before that, with "amount must be a positive number".

What is advantageBps for?

It grades the winning route against the next-best one. routes=1 means one quote won and advantageBps is 0; routes=2 means the best of two quotes won. On 2026-11-10 a 130-USDT order showed 895 bps (the two routes were nearly 9% apart), 1 XMR→USDT-TRX showed 192 bps on two routes, and a 110-USDT two-route order showed 0 — the two routes agreed to the digit. A high bps on your corridor is a hint that the two pools disagree about price; the quote that wins may not win next minute.

Keep reading

All articles →

Ready to swap?

No account · No KYC · No email

Exchange now
Read the Quote API: rate, validUntil, routes, bps — VeilSwap