# Trading volume and rewards

Build trading rewards from immutable source facts, a stable customer mapping, and an approved calculation. This page uses synthetic data to show one trader with two accounts, separate FX and metals units, and a separately defined comparable USD turnover input.

> Illustrative configuration only
>
> The point rates and field names below are examples, not Returning.AI defaults or a client rate card. The broker and Returning.AI owners must approve the symbol map, volume conversion, eligibility, destination field, rounding, and correction model before launch. The browser should display an authoritative result, not calculate entitlement.

Trading facts to a visible reward

The aggregate is formed before additive delivery, and each asynchronous checkpoint is verified separately.

1. [Extract immutable close-deal facts](#worked-example)

   Broker data owner

   Read complete history for the approved UTC window and retain account, symbol, volume, event time, and source record ID.
2. [Map accounts to one customer](#identity-and-lineage)

   Broker identity owner

   Resolve ACCOUNT_2001 and ACCOUNT_2002 to canonical CUSTOMER_1001 without using an email as the dedupe boundary.
3. [Aggregate by customer, window, and class](#aggregation)

   Broker data owner

   Create one delivery row with separate FX and metals totals, comparable turnover, source lineage, and a versioned aggregate ID.
4. [Deliver through the agreed transport](https://docs.returning.ai/broker-integrations/sqs.md)

   Broker transport owner

   Send the approved aggregate using the configured SQS, Event Ingest, file, or other provisioned path.
5. [Process the configured reward](#reward-calculation)

   Returning.AI workflow owner

   Check eligibility and apply the approved rule once. Do not credit both raw facts and their derived aggregate.
6. [Read back and refresh](#corrections-readback)

   Returning.AI and portal owners

   Check workflow and user-update results, read the authoritative value, then refresh the widget for the same canonical user.

## Canonical user and account lineage

A trading account is not the loyalty user. Use a durable account key made from platform, server, and account login, then map each account to the canonical broker customer and its enrolled Returning.AI user.`CUSTOMER_1001` is the broker's external identifier, not the platform user ID returned by enrollment. Keep both IDs and the mapping version and effective dates when ownership can change. See the [enrollment guide](https://docs.returning.ai/broker-integrations/users.md).

| Canonical customer | Trading account key | Account source | Lineage to retain |
| --- | --- | --- | --- |
| `CUSTOMER_1001` | `mt5 / broker-live-1 / ACCOUNT_2001` | Broker account map | Mapping version, account status, source event IDs |
| `mt5 / broker-live-1 / ACCOUNT_2002` | Broker account map | Mapping version, account status, source event IDs |  |

Hold an unmapped account without awarding points. Resolve the mapping and replay through the approved process. Do not merge users because an email or display name happens to match.

## Processing model

This worked example chooses raw close-deal activity as the source model and a user/window aggregate as the delivery model. The collector first stores the raw facts, then groups all eligible rows for one canonical user and UTC window before sending one aggregate row. A field such as`aggregate_id` keeps the derived row tied to its source set.

> Choose one credit boundary
>
> Do not send raw deals and the derived aggregate into the same additive reward workflow unless one path is explicitly non-crediting lineage. A source fact and its aggregate are two representations of one activity, not two rewards.

## Worked input: two accounts and two asset classes

Every row below is individual closed-deal activity in the UTC window from `2026-09-07T00:00:00Z` inclusive to`2026-09-08T00:00:00Z` exclusive. Adjacent windows share no boundary event. The USD amount is a separately defined comparable turnover input for this example. It is not a conversion of the FX and metals lot columns, and it is not used by the illustrative lot-rate calculation below.

| Source event ID | Account | Symbol and class | Raw close activity | Close time (UTC) | Comparable turnover input | Eligible? |
| --- | --- | --- | --- | --- | --- | --- |
| `deal-2001-fx-001` | `ACCOUNT_2001` | `EURUSD.a` / FX | 1.20 standard lots | 2026-09-07 10:15 | 100,000 USD | Yes, closed and mapped |
| `deal-2002-fx-002` | `ACCOUNT_2002` | `EURUSD.a` / FX | 0.80 standard lots | 2026-09-07 11:20 | 75,000 USD | Yes, closed and mapped |
| `deal-2001-metals-003` | `ACCOUNT_2001` | `XAUUSD.a` / metals | 0.50 metal lots | 2026-09-07 12:05 | 75,000 USD | Yes, closed and mapped |
| **Total separately defined comparable turnover** | **250,000 USD** | Comparable input only |  |  |  |  |

The two FX rows total **2.00 standard lots**. The metals row totals **0.50 metal lots**. Do not report one combined lot total: the units and contract rules are different. The USD turnover total is valid here only because it was separately defined as a comparable USD measure and supplied for every included row. Each`source_event_id` is the stable replay identity for its raw row; the aggregate has its own stable `aggregate_id`.

## One canonical-user and window aggregate

Retain the full account list, class totals, source IDs, and aggregate version in one canonical-user/window row. The exact field names below are a synthetic mapping for the example. The configured workflow decides the actual payload contract.

`customer-window-aggregate.json`

```json
{
  "aggregate_id": "agg:CUSTOMER_1001:2026-09-07:v1",
  "canonical_customer_id": "CUSTOMER_1001",
  "window_start": "2026-09-07T00:00:00Z",
  "window_end": "2026-09-08T00:00:00Z",
  "accounts": [
    { "platform": "mt5", "server": "broker-live-1", "account_login": "ACCOUNT_2001" },
    { "platform": "mt5", "server": "broker-live-1", "account_login": "ACCOUNT_2002" }
  ],
  "volumes": [
    {
      "asset_class": "fx",
      "normalized_volume": 2.0,
      "volume_unit": "standard_lot",
      "source_event_ids": ["deal-2001-fx-001", "deal-2002-fx-002"]
    },
    {
      "asset_class": "metals",
      "normalized_volume": 0.5,
      "volume_unit": "metal_lot",
      "source_event_ids": ["deal-2001-metals-003"]
    }
  ],
  "comparable_turnover_usd": 250000,
  "aggregation_version": "trading-aggregate-v1"
}
```

| Asset class | Account contributions | Aggregate | Source IDs retained |
| --- | --- | --- | --- |
| FX | ACCOUNT_2001: 1.20 + ACCOUNT_2002: 0.80 standard lots | **2.00 standard lots** | `deal-2001-fx-001`, `deal-2002-fx-002` |
| Metals | ACCOUNT_2001: 0.50 metal lots | **0.50 metal lots** | `deal-2001-metals-003` |
| Comparable turnover | 100,000 + 75,000 + 75,000 USD | **250,000 USD** | Same three source rows |

## Illustrative points calculation

Assume the approved example configuration says 10 points per FX standard lot and 20 points per metal lot. Round half-up to the nearest whole point after each class calculation. These rates are synthetic and do not claim a Returning.AI or client default.

| Class | Volume | Illustrative rate | Calculation | Class points |
| --- | --- | --- | --- | --- |
| FX | 2.00 standard lots | 10 points / lot | 2.00 × 10 | **20** |
| Metals | 0.50 metal lots | 20 points / lot | 0.50 × 20 | **10** |
| **Total points** | **20 + 10 = 30** |  |  |  |

Trading volume is a measurement. Points are an illustrative reward output. Coins, XP, points, and a custom volume field are separate destinations; a configured workflow must say which one is written. The 250,000 USD turnover input is also separate and must not be silently converted into points or combined with lot totals.

## Eligibility, launch cutoff, and history

Use the close time and the approved UTC launch cutoff, not the time a poller happened to see a row. In this synthetic example the cutoff is`2026-09-07T00:00:00Z`; the integration owner must replace it with the approved value for the real campaign.

| Case | Safe handling before reward delivery | Owner to resolve |
| --- | --- | --- |
| First-ever trade | Check complete accessible history. The first row observed after the extractor starts is not proof of the trader's first trade. | Broker data owner and reward owner |
| Before enrollment | Exclude or hold unless the approved rule grants retroactive credit. Define whether eligibility starts at broker registration or Returning.AI enrollment; do not infer one from the other. | Identity and campaign owners |
| Before launch cutoff | Quarantine or exclude with a recorded reason unless the approved campaign explicitly backfills it. | Campaign owner |
| Unknown symbol | Hold without reward until the asset-class mapping and conversion are approved. | Broker and reward owners |
| Unmapped account | Hold without reward. Do not guess the canonical customer or use an email merge. | Identity owner |
| Late data | Apply the documented late-window and correction policy. Do not assume automatic retroactive credit. | Data and reward owners |
| Open position | Keep it out of a closed-deal reward model unless the approved rule explicitly includes open exposure. | Reward owner |

## Snapshot or delta

Decide whether the delivery represents a complete snapshot or a new delta before choosing retry behavior. Do not alternate between them for the same aggregate identity.

| Model | Meaning | Replay and correction concern |
| --- | --- | --- |
| Window snapshot | The row contains the complete approved value for one customer and window. | Requires a configured overwrite or reconciliation path. Do not assume an additive workflow replaces it safely. |
| Window delta | The row contains new contribution since the previous checkpoint. | Needs a stable aggregate or source identity before retry; replay can add the points twice. |

## Partial success, corrections, and readback

1. Record the baseline authoritative value, aggregate ID, source count, and expected class totals before delivery.
2. Check transport acceptance, Event Ingest persistence, and workflow execution as separate checkpoints. A queue acknowledgement is not a reward result.
3. If a queued user update has per-row results, preserve successful users and retry only failed users through the approved path. Do not replay the whole additive batch to repair one failure.
4. Read back the authoritative field, points, coins, or ledger value using the operation and permission supplied for the environment. Then compare it with the expected aggregate and source lineage.
5. Replay the same aggregate identity and verify that it does not add a second reward when the configured contract promises that behavior.
6. For a changed fact, pause and use the approved correction or compensation flow. A new batch or aggregate ID alone is not a safe financial correction.
7. Refresh the widget for `CUSTOMER_1001` only after the saved state is authoritative. A stale widget does not prove the workflow failed.

## Next step

Use the [SQS data feed guide](https://docs.returning.ai/broker-integrations/sqs.md) to package bounded JSON messages or review a provisioned file contract. Use the [workflow webhook guide](https://docs.returning.ai/broker-integrations/workflow-webhooks.md)for registration, KYC, and other lifecycle events. Once enrollment and readback are complete, open the [widget quickstart](https://docs.returning.ai/widget-sdk.md).

For onboarding progress, connect [custom fields and milestones](https://docs.returning.ai/broker-integrations/user-fields.md). Before enabling real rewards, complete the [launch checklist](https://docs.returning.ai/broker-integrations/launch-checklist.md).
