Quote Expiration: Why DeFi Prices Cannot Be Treated Like Static API Responses

Cache a weather API response for thirty seconds and nothing bad happens. Cache a swap quote for thirty seconds and you can end up building a transaction against a price that's no longer true. The difference isn't a technicality โ€” it changes how the entire integration has to be built.

Most developers coming into DeFi from traditional web development carry a mental model that quietly breaks the first time they build a swap feature: a price is data, and data can be fetched, stored, and reused for a little while before it needs refreshing. A product listing, a currency conversion rate, a weather forecast โ€” all reasonably treated as short-lived but essentially static once returned. A DeFi quote looks like the same shape of object โ€” a JSON payload with a number in it โ€” and that resemblance is exactly what causes bugs. It isn't a cacheable fact. It's a live, competitive, time-boxed offer from a specific counterparty, and Omniston's own API is built around that distinction explicitly rather than leaving it implicit.

๐Ÿ’ญ The bug I've seen most often isn't exotic โ€” it's a developer calling a quote endpoint once, storing the number in state, and building the transaction from that stored value minutes later. Nothing in a REST-shaped response warns you not to do that. The protocol has to make the time-boxed nature structural, not just documented.

โœ… Key Takeaways

  • A DeFi quote is a live offer from a specific resolver or pool state, not a fetched fact โ€” it degrades in accuracy the moment it's issued.

  • Omniston models this by returning quotes as a subscribable RFQ stream, not a single request/response call, so re-quoting is the default behavior, not an opt-in refresh.

  • Quotes carry explicit time-boxing fields โ€” validity windows and trade-start deadlines โ€” that a transaction must respect to guarantee the quoted price.

  • Treating quote expiration as a UI afterthought (a toast that says "price changed") produces worse UX than designing the interface around expiration as an expected, routine state from the start.

  • Building a transaction from a stale quote isn't just risky โ€” it can fail outright, since expired quotes aren't meant to settle at all.


๐ŸŒ Why the REST Mental Model Breaks Here

A typical REST API response represents something that was true at request time and stays reasonably true for a while โ€” a product's list price, a city's current temperature. The implicit contract is: fetch once, trust for some duration, refetch when you need fresher data. Nothing about that contract requires the client to actively track an expiration deadline as part of correctness โ€” staleness is usually a UX nuisance, not a source of financial loss.

A swap quote breaks that contract in a specific, structural way: it represents a price a counterparty โ€” a pool's current reserves, or a resolver's live inventory โ€” is willing to honor right now, and that willingness has a real, finite shelf life measured in seconds, not minutes. Unlike a stale weather reading, a stale quote doesn't just become "a bit inaccurate" โ€” building and submitting a transaction against it can fail outright, or in worse designs, execute at a materially different price than the user actually saw and approved.

๐Ÿ”„ How Omniston Encodes This Structurally: A Stream, Not a Call

Rather than exposing quotes as a single GET /quote request, Omniston's requestForQuote() returns an observable stream that a client subscribes to:

import { Omniston, Blockchain, SettlementMethod } from "@ston-fi/omniston-sdk"; const omniston = new Omniston({ apiUrl: "wss://omni-ws.ston.fi" }); const quoteStream = omniston.requestForQuote({ settlementMethods: [SettlementMethod.SETTLEMENT_METHOD_SWAP], offerAssetAddress: { blockchain: Blockchain.TON, address: "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs", // USDT }, askAssetAddress: { blockchain: Blockchain.TON, address: "EQA2kCVNwVsil2EM2mB0SkXytxCqQjS4mttjDpnXmwG9T6bO", // STON }, amount: { offerUnits: "1000000" }, // 1 USDT settlementParams: { maxPriceSlippageBps: 500, // 5% max slippage maxOutgoingMessages: 4, }, }); quoteStream.subscribe((quoteEvent) => { // A fresh quoteEvent can arrive multiple times over the stream's lifetime โ€” // resolvers and pools re-quote as conditions change, not just once });

This is a deliberate design choice, not an incidental one. A WebSocket-backed stream means the default behavior is continuous re-quoting as market conditions shift, rather than a client having to remember to poll again. An integration that subscribes and simply keeps the most recent event is automatically working with a reasonably fresh price, without needing to build its own refresh-timer logic on top of a request/response API.

โฑ The Fields That Make Expiration Explicit, Not Implicit

Underneath the SDK's convenience layer, Omniston's protocol-level messages carry explicit timing fields that turn "this quote is time-limited" from a documentation note into something a client can programmatically check:

message QuoteRequestedEvent { string rfq_id = 1; // Quote request ID sint64 request_timestamp = 4; // When the request was made sint64 quote_validity_timeout = 5; // How long the quote should remain valid sint64 resolve_timeout = 7; // Max time allowed to complete the trade } message UpdateQuoteRequest { string rfq_id = 1; uint64 trade_start_deadline = 4; // Quote expiration timestamp }

quote_validity_timeout and trade_start_deadline aren't cosmetic metadata โ€” they're the actual contract a resolver is offering: this price, if and only if execution starts before this deadline. A transaction built and submitted after that deadline isn't just "risking a worse price" the way slippage tolerance handles ordinary market movement โ€” it's outside the terms of the quote entirely, and settlement logic is built to reject or fail it rather than silently execute at a different rate.

๐Ÿงฉ What This Looks Like When Actually Building the Transaction

Because the quote is time-boxed, building a transaction from it means carrying that deadline forward into the transaction itself โ€” not just displaying a price and hoping the user confirms quickly:

// Conceptual shape โ€” building a transaction needs to reference // both the specific quote and the window it's valid within const tx = await omniston.buildTransfer({ quote, // the exact quote object from the stream above sourceAddress, destinationAddress, // The settlement layer enforces the quote's own deadline โ€” // a transaction submitted after trade_start_deadline is expected to fail // rather than silently settling at a different price });

The important design principle here: the deadline isn't something the client application invents on its own (like an arbitrary "5 minute cache" rule borrowed from REST habits) โ€” it's a value the resolver itself set when it made the offer, carried through the quote object, and enforced at settlement. A client can't extend it by simply waiting longer or retrying with the same stale data.

๐ŸŽจ The UX Problem This Actually Creates

Handling expiration correctly at the protocol level solves half the problem. The other half is interface design, and it's where a lot of otherwise well-built integrations still fall short:

  • Treating expiration as an edge case, not a routine state. If a user takes 15 seconds to review a quote before confirming, hitting an expired quote shouldn't feel like an error state โ€” it should feel like a normal, expected part of using the app, handled by quietly re-quoting rather than surfacing a raw failure.

  • Not showing any indication that a quote is time-limited at all. A price displayed with no visual cue that it's a live, decaying offer trains users to treat it like a static number โ€” the same misconception that causes the caching bug in the first place, just moved from the developer to the end user.

  • Blocking confirmation entirely without a graceful re-quote path. The better pattern is subscribing to the stream continuously in the background so the displayed price updates itself, and confirmation simply uses whatever the most recent valid quote happens to be at the moment of the click.

๐Ÿ’ญ The best version of this UX is almost invisible: the number on screen just quietly stays current, and the user never consciously notices that "quote expiration" was ever a problem being solved underneath. The worst version throws an error message after the user already clicked confirm.

๐Ÿ›ก Why This Matters More for Cross-Chain and RFQ-Sourced Quotes

Expiration windows aren't uniform across every kind of quote. A quote priced against a deep, stable AMM pool tends to have more forgiving timing, since the underlying reserves move relatively slowly. A quote sourced from an RFQ resolver โ€” especially for a cross-chain trade settling through paired HTLCs โ€” is a live commitment from a specific market maker, often with a tighter validity window, since the resolver is exposing its own capital to price risk for the duration of that offer. Cross-chain flows add a further wrinkle: resolve_timeout governs not just how long the quote is valid to start, but how long the overall trade has to complete across both chains before the settlement window closes โ€” a distinctly different timing concern from same-chain execution.

An integration that treats every quote as having the same generous shelf life, regardless of settlement method, is likely to either annoy users with unnecessarily aggressive re-quoting on stable pairs, or โ€” worse โ€” attempt to execute stale RFQ-sourced quotes that were never going to be honored past their deadline.

๐Ÿงญ Conclusion

Quote expiration isn't a quirky edge case bolted onto DeFi APIs โ€” it's a direct consequence of what a quote actually represents: a live, time-boxed commitment from a specific counterparty, not a cacheable fact about the world. Omniston's design reflects that at the protocol level, with a subscribable RFQ stream in place of a single request/response call, and explicit validity and deadline fields carried through to settlement rather than left to client-side assumption. The remaining responsibility sits with whoever builds the interface on top: designing around expiration as a routine, invisible background process, not an error state the user discovers after the fact.

โ“ Frequently Asked Questions

Can a client just increase how long it waits before treating a quote as stale? No โ€” the validity window and trade-start deadline are set by the resolver or protocol issuing the quote, not by the client. A client can request a fresh quote at any time, but it can't extend the validity of an existing one past what was actually offered.

Does every quote on Omniston expire at the same rate? No. Quotes sourced from a deep, stable AMM pool tend to tolerate slightly more delay than RFQ-sourced quotes from an individual resolver, since the resolver is exposing its own capital to price risk for as long as the quote remains valid.

What happens if a transaction is submitted after a quote's deadline has passed? It's expected to fail at settlement rather than silently execute at a different price โ€” the deadline is enforced as part of the quote's terms, not treated as a soft suggestion.

Is subscribing to a quote stream instead of polling actually necessary, or just a nice-to-have? It's closer to necessary for correctness. Polling on a fixed interval risks either showing stale prices between polls or hammering the API with unnecessary requests; a subscription model delivers updates exactly when the underlying price actually changes.

$GRAM

GRAM
GRAMUSDT
1.374
-0.21%