Skip to main content
This page explains how to compute the total rewards a wallet has earned on a Conduit from on-chain data. The approach has one central idea: value what the wallet holds, treat every unsettled operation as an explicit claim — what was committed minus what has been delivered so far — and net everything against the assets that flowed in and out. Handled this way, the result stays correct through every stage of an asynchronous deposit or redemption.
This is exactly how the Railnet REST API computes total_rewards_in_asset on its positions endpoint. If you just need the number, call the API; this page is for integrators who want to compute it themselves or understand what the API returns.

What you need from the chain

Everything comes from events the Conduit and its query registry emit, all of which are exposed by the indexer GraphQL API: A deposit pulls assets in and pushes shares out when it settles. A redemption pulls shares in and pushes assets out. Rejected operations push the input back, so refunds take care of themselves.

The formula

rewards=balance value+pending deposits+pending redemptionsassets in+assets out\text{rewards} = \text{balance value} + \text{pending deposits} + \text{pending redemptions} - \text{assets in} + \text{assets out} Where:
  • balance value — the wallet’s share balance at the current share price.
  • assets in — every asset amount ever pulled from the wallet, settled or not.
  • assets out — every asset amount ever pushed out, settled or not, excluding amounts pushed to the fee manager (those are fees, not the user’s money).
  • pending deposits / pending redemptions — one claim per in-flight operation, computed below.
The two pending terms are what make the formula robust. Without them, a redemption in flight reads as a loss: the shares are burnt the moment the redemption is requested, but the payout may arrive later — so the position side drops to zero while the deposit side still stands, reporting a loss that never happened. The money is late, not lost. The claim term carries that in-flight value until real flows replace it.

Valuing an in-flight claim

For each operation that is not yet in a final state: still owed=max(0, committeddelivered)\text{still owed} = \max(0,\ \text{committed} - \text{delivered})
  • For a deposit: committed is the assets pulled in; delivered is the shares minted so far, valued at the current share price.
  • For a redemption: committed is the shares burnt; delivered is the asset tranches paid out so far.
Netting off what has already arrived is what stops a tranche from being counted twice — once as paid out, once as still owed. And because a deposit’s minted shares already sit inside the balance term, subtracting them from its claim keeps them counted exactly once too. Clamp each claim at zero individually, so one over-delivered operation cannot hide another operation’s remainder.
Never filter delivered amounts by the user’s own address. Settlement pays whoever holds the operation at that moment — an operation wrapped as an NFT may have changed hands — and a claim whose payouts you can’t see never nets out. Exclude only the fee manager’s legs.

Pricing the redemption’s shares: use the rate at request time

The one subtle step. The burnt shares of a pending redemption must be valued at: min(share price now, share price when the redemption was requested)\min(\text{share price now},\ \text{share price when the redemption was requested}) A redemption’s value typically crystallizes at the moment the shares are burnt. An Allocation Strategy’s redemption queue, for example, stamps each queued request with a maximum payout computed at the request-time price — a hard cap. If the share price rises while the request waits in the queue, the payout does not rise with it. Valuing the claim at the current price would therefore show yield the user will never receive; it would silently evaporate when the redemption settles. The min handles both directions:
  • Price rose since the request → value at the request-time price. No phantom yield.
  • Price fell since the request → value at the current price. Queues socialise losses into pending requests, so the lower value is the honest one.
Some yield sources price redemptions the other way around — at settlement, not at request — so a queued request keeps earning while it waits. The min still does the right thing: the claim is marked at the request-time price, and the growth surfaces the moment the payout lands, when real flows replace the claim. The books are exact either way; the marking only decides when in-flight growth becomes visible, and it errs on the side of never showing yield before it is guaranteed. The same self-healing covers the case where no request-time price is available at all: fall back to the current price, and settlement trues everything up.

A worked example

A user deposits 5 USDC into a Conduit at a share price of 1.00, and later redeems everything. Two things to notice:
  • Rewards never dip negative while the money is in flight, and never jump from double counting. Every intermediate row reports the same +0.10 the user actually earned.
  • Once the operation settles, the claim term is zero and rewards are computed purely from real flows. Whatever approximation the in-flight marking carried, the final number is exact.
Now suppose the price rises from 1.02 to 1.10 while the redemption waits in the queue. The request was stamped at 1.02, so the payout stays 5.10. Valued at the request-time price, pending stays 5.10 and rewards stay +0.10 throughout — which matches the eventual payout. Valued at the current price instead, pending would read 5.50 and rewards +0.50, and the extra 0.40 would vanish on settlement.

Putting it together

Pseudo-code for one wallet on one Conduit, using the entities served by the indexer GraphQL API. Amounts are minimum units; value(shares, rate) converts shares to assets at a rate expressed as assets per share.
Pseudo-code