> ## Documentation Index
> Fetch the complete documentation index at: https://docs.railnet.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Computing position rewards

> How to compute the rewards a wallet has earned on a Conduit, including positions with deposits or redemptions still in flight

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.

<Info>
  This is exactly how the [Railnet REST API](https://docs.api.railnet.org) 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.
</Info>

## 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](/developers/api):

| Input                                      | Source                                                                                                          |
| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| Share balance                              | `ConduitBalance.value` (conduit shares are an ERC20)                                                            |
| Asset and share movements per operation    | The Conduit's `Pulled` and `Pushed` events — `ConduitQuery.flows`, each tagged with the operation it belongs to |
| Whether an operation is settled            | The query's STEAM state: `SETTLED` and `REJECTED` are final, everything else is in flight                       |
| Share price                                | The Conduit's `convert()` — how many assets one share is worth                                                  |
| Share price when the operation was created | `ConduitQuery.creationRate` — the same `convert()`, read at the block the query was created                     |
| The fee manager's address                  | `Conduit.feeManager`                                                                                            |

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

$$
\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:

$$
\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.

<Warning>
  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.
</Warning>

### 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(\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.

| Moment                                  | balance value | pending | assets in | assets out | rewards   |
| --------------------------------------- | ------------- | ------- | --------- | ---------- | --------- |
| Deposits 5 — waiting to settle          | 0             | 5.00    | 5.00      | 0          | **0**     |
| Deposit settles, 5 shares minted        | 5.00          | 0       | 5.00      | 0          | **0**     |
| Yield accrues, price now 1.02           | 5.10          | 0       | 5.00      | 0          | **+0.10** |
| Requests full redemption — shares burnt | 0             | 5.10    | 5.00      | 0          | **+0.10** |
| Queue pays a first tranche of 3.00      | 0             | 2.10    | 5.00      | 3.00       | **+0.10** |
| Final tranche of 2.10 arrives, settled  | 0             | 0       | 5.00      | 5.10       | **+0.10** |

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](/developers/api). Amounts are minimum units; `value(shares, rate)` converts shares to assets at a rate expressed as assets per share.

```python Pseudo-code theme={null}
# Indexer reads
balance  = ConduitBalance(owner: wallet, conduit: conduit).value      # conduit shares
queries  = ConduitQuery(owner: wallet, conduit: conduit) {
               query { mode state }                 # DEPOSIT/REDEEM, STEAM state
               creationRate { assetsPerShare }      # share price at creation block
               flows { flowType asset amount account }
           }
fee_mgr  = Conduit.feeManager.address
asset    = Conduit.asset                            # the deposit asset
price    = convert(1 share)                         # current assets per share

assets_in, assets_out, pending = 0, 0, 0

for q in queries:
    committed_assets = committed_shares = 0
    delivered_assets = delivered_shares = 0

    for f in q.flows:
        if f.flowType == PULLED and f.account == wallet:
            if f.asset == asset:            # deposit input
                assets_in        += f.amount
                committed_assets += f.amount
            if f.asset == conduit:          # redemption input (burnt shares)
                committed_shares += f.amount

        if f.flowType == PUSHED and f.account != fee_mgr:
            if f.asset == asset:            # payout or refund
                assets_out       += f.amount
                delivered_assets += f.amount
            if f.asset == conduit:          # minted shares
                delivered_shares += f.amount

    if q.state in (SETTLED, REJECTED):
        continue                            # settled: flows already tell the story

    if q.mode == DEPOSIT:
        pending += max(0, committed_assets - value(delivered_shares, price))

    if q.mode == REDEEM:
        rate = min(price, q.creationRate or price)   # request-time cap; see above
        pending += max(0, value(committed_shares, rate) - delivered_assets)

rewards = value(balance, price) + pending - assets_in + assets_out
```
