At Kwickbit we built a fiat price estimator. Although this work was developed under KwickBit’s initial mission — before our recent pivot — this project holds high value for the broader Stellar ecosystem. More details about KwickBit’s evolution will be announced soon, but we’re convinced that this tool, along with others, serves as a strong foundation for developers, particularly for those looking to integrate fiat estimations into wallet solutions.
This post dives into how we developed a reliable USD price estimation API for Stellar assets. You’ll find everything from the architecture we employed to the technical implementation and usage, and the source code is available here.

What This Is About
The Stellar Fiat Price Estimator provides a way to obtain the price of various Stellar assets in fiat currency, specifically in USD (using the USDC stablecoin as an in-network proxy for fiat USD).
- Input: Specify an asset and a single time or list of times.
- Output: An estimated USD price for each time, based on actual swaps.
We achieved this by analyzing swaps occurring across Stellar Classic and Soroswap on Soroban. The approach involves filtering relevant transaction types and aggregating data over defined time windows to calculate accurate estimations. The data is then served by a custom API.
The Data Pipeline

To calculate fiat prices, we process data from two sources:
1 — Stellar Classic transactions
We obtain USDC swaps from different transaction types. Offers
are akin to an exchange order book, and generate swaps when executed. PathPayments
use intermediate steps assets to fulfill the user’s order. For example, the prices at a given moment might favor trading ETH for USDC, USDC for BTC and BTC for XLM rather than ETH-XLM directly. We capture the ETH and BTC swaps for USDC.
2 — Soroswap on Soroban
On Soroban, we obtain swaps from contract events emitted by Soroswap, the decentralized exchange.
Core Process
- Discard invalid swaps (e.g., involving scam addresses or fake XLM);
- Extract swap data from transactions or events: the assets traded and their amounts;
- Aggregate swaps into a weighted average price within a time window.
Here’s the relevant code section:
pub(crate) fn swaps(transaction_results: Vec<TransactionResultMeta>) -> Vec<Swap> {
transaction_results
.iter()
.filter(is_transaction_successful)
.flat_map(swaps_from_transaction)
.collect()
}
pub(crate) fn soroswap_swaps(soroban_events: Vec<ContractEvent>) -> Vec<Swap> {
soroban_events
.into_iter()
.filter_map(soroswap_event)
.flat_map(swaps_from_event)
.collect()
}
These two functions:
- Filter the relevant classic transactions or Soroswap events;
- Return swap information from each source according to its structure.
Once processing is complete here, the swaps are indistinguishable, since we don’t need to store whether they came from classic transactions or Soroswap events.
Another snippet demonstrates how we construct a swap:
impl TryFrom<&SwapData> for Swap {
fn try_from(swap_data: &SwapData) -> Result<Self, String> {
let asset_sold = swap_data.asset_sold.unwrap();
let asset_bought = swap_data.asset_bought.unwrap();
if asset_sold == USDC {
Ok(Swap {
created_at: None,
usdc_amount: swap_data.amount_sold as f64,
floating_asset_code: asset_bought.code.to_string(),
floating_asset_issuer: asset_bought.issuer.to_string(),
price_numerator: swap_data.amount_bought,
price_denominator: swap_data.amount_sold,
})
} else if asset_bought == USDC {
// Similar as above but flipped
} else {
Err("Swap does not involve USDC".to_string())
}
}
}
4. Key Implementation Details
We process swap data and calculate exchange rates using these key steps:
1. Extract Transaction Results
pub(crate) fn extract_transaction_results(result_meta: &TransactionResultMeta) -> Vec<OperationResultTr> {
match &result_meta.result.result.result {
TransactionResultResult::TxSuccess(op_results) => op_results.iter()
.filter_map(|op_result| match op_result {
OperationResult::OpInner(inner_op_result) => Some(inner_op_result.clone()),
_ => None,
})
.collect(),
_ => Default::default(),
}
}
This isolates successful operations, crucial for accurate calculations.
2. Aggregate Swap Data
fn calculate_rates(swaps: Vec<SwapDbRow>) -> ExchangeRateMap {
swaps
.iter()
.fold(HashMap::new(), extract_amounts)
.into_iter()
.map(|(key, (weighted_sum, total_amount))| (key, (weighted_sum / total_amount, total_amount)))
.collect::<ExchangeRateMap>()
}
We aggregate the results by weighting swap amounts to compute accurate exchange rates. Over a given time frame, we accumulate a certain volume of assets being traded (USDC and each other asset swapped) and treat the ratio between these volumes as the reference exchange rate as determined by the market.
5. The Role of Mercury Indexer
This project is built on top of the Mercury Indexer (mercurydata.app), developed by Xycloo Labs. Mercury is a powerful and flexible indexing tool designed for blockchain projects. Here’s why we used it:
- Cloud-based, no deployment required:
Mercury eliminates the complexity of on-premise setups, allowing developers to focus on building without worrying about infrastructure management. - Powerful SQL-like queries:
With a mix of SQL and Rust-based indexing, it offers the best of both worlds: simplicity for common queries and the flexibility of Rust for custom needs. - Zephyr functions for API exposure:
Mercury allows exposing APIs seamlessly, similar to AWS Lambda, making it easy to integrate custom functionalities.
These features made Mercury the ideal backbone for this project.
6. API Usage
Besides our indexer running on every Stellar network ledger close, we have two serverless functions that provide exchange rates. Here’s an example query:
$ curl -X POST https://mainnet.mercurydata.app/zephyr/execute/66 \
-H 'Content-Type: application/json' \
-d '{
"project_name": "kwickbit",
"mode": {
"Function": {
"fname": "get_exchange_rate",
"arguments": "{
\"asset_code\": \"BTC\",
\"asset_issuer\": \"GDPJALI4AZKUU2W426U5WKMAT6CN3AJRPIIRYR2YM54TL2GDWO5O2MZM\",
\"date\": \"2024-12-14T12:05:00\"
}"
}
}
}'
Which gets the response:
{
"data": [
{
"asset_code": "BTC",
"asset_issuer": "GDPJALI4AZKUU2W426U5WKMAT6CN3AJRPIIRYR2YM54TL2GDWO5O2MZM",
"base_currency": "USD",
"exchange_rate": "0.000010506513152552705",
"rate_date_time": "2024-12-14T11:52:02.000000000Z",
"soroswap_certified_asset": true,
"volume": "26913.391331099996"
}
],
"status": 200
}
The example above highlights the important detail that rates are always quoted in reference to 1 USD. We promise our code has not completely tanked the value of BTC to 0.0000105; rather, that’s how much $1 can buy 🚀
Fetching rates for multiple assets and dates
We can go one step further and obtain an exchange rate time series. At KwickBit we use this service in our original product, which includes accounting reports that require knowing what the exchange rate was at a given point in time.
This is what the request and response look like; usage via cURL, similar to the example above, requires simply replacing fname
with get_exchange_rate_history
and properly escaping the quotes in the JSON argument passed to it.
// Request
struct HistoryRequest {
assets: Vec<HistoryRequestTransactions>,
}
struct HistoryRequestTransactions {
asset: HistoryAsset,
transaction_dates: Vec<String>, // Format: "2024-12-14T12:05:00"
unrealized_date: String, // End of the period, for unrealized gains calculation
}
struct HistoryAsset {
asset_code: String,
asset_issuer: Option<String>, // Mandatory unless the code is XLM
}
// Response
struct HistoryResponse {
successful_assets: Vec<SuccessfulAsset>,
failed_assets: Vec<FailedAsset>,
}
struct SuccessfulAsset {
asset: HistoryAsset,
transaction_rates: Vec<TransactionExchangeRate>,
unrealized_rate: TransactionExchangeRate,
}
struct TransactionExchangeRate {
transaction_date: String, // From the request
exchange_rate_date: String, // From the DB; may be in multiple transactions
exchange_rate: String, // Converted from f64 for technical limitations
}
The repository contains shell scripts that make it easier to query the endpoints.
7. Conclusion
Explore the source code here and stay tuned for more updates on KwickBit’s side.
Check our article on Medium: https://medium.com/kwickbit/the-kwickbit-exchange-rate-service-1d87642e0625