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

# Progress and rewards

> How to integrate the Almedia SDK — synced from the source repository.

A linked player earns coins on Freecash when they complete tasks in your game. The SDK keeps the latest view of that progress. It also tells you when the server reports an event. You build one progress UI and get the same state and events on iOS and Android.

The surface is one accessor, three events and some plain data classes. You do not start anything. Progress travels with the message stream that also carries notifications and grants.

## The snapshot

`Almedia.Progress` is the latest snapshot, or `null` when the SDK has none. `OnProgressUpdated` fires each time the accessor changes: with the newer snapshot, or with `null` when the snapshot is cleared. The accessor is already current when the event fires. Render from the argument or from the accessor, and hide the panel on `null`:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
void OnEnable()
{
    Render(Almedia.Progress);                     // a scene that loads late still sees current state; null before the first snapshot
    Almedia.OnProgressUpdated += Render;
}

void OnDisable() => Almedia.OnProgressUpdated -= Render;

void Render(AlmediaProgress progress)
{
    if (progress == null) { panel.SetActive(false); return; }

    panel.SetActive(true);
    nameLabel.text    = progress.Username ?? "Player";      // null when the server sent no username
    balanceLabel.text = $"{progress.Balance.Coins:N0} coins";
    earnedLabel.text  = FormatMoney(progress.Earned.InPlayerCurrency);

    list.Clear();
    foreach (var task in progress.Pending)
        list.AddPending(task.Title, task.Progress, task.Reward);   // Progress is null for a yes-or-no task
    foreach (var done in progress.Completed)                        // newest first
        list.AddCompleted(done.Task.Title, done.CompletedAt, done.ActualReward);
}
```

A snapshot is never partial. A newer snapshot replaces the previous one completely. Do not keep state from a previous snapshot. `Username` follows the same rule. If a newer snapshot has no username, `Username` is `null`. Clear a name you showed before.

The snapshot lives in memory only. The native SDK clears it together with its message stream token, for example when the account changes or the server refuses the token, and `OnProgressUpdated` then fires with `null`. It is also `null` after `Initialize` with a different configuration. A `null` snapshot means "no progress to show", both before the first snapshot and after a clear.

## Rewards that change

The reward of a burning task (`AlmediaTaskKind.Burning` or `AlmediaTaskKind.BurningPurchaseBonus`) decreases on a schedule. `RewardDropsAt` is the time of the next decrease, or `null` when none is scheduled. Show a countdown from it. You do not need to check `Kind`:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
if (task.RewardDropsAt is DateTimeOffset dropsAt)
{
    var left = dropsAt - DateTimeOffset.UtcNow;
    countdownLabel.text = left > TimeSpan.Zero ? $"Reward drops in {(int)left.TotalHours}h {left.Minutes}m" : "";
}
```

After a decrease, the task keeps its `Id`. The next snapshot carries the lower `Reward` and the next `RewardDropsAt`. The paid reward is the amount at completion. `AlmediaCompletedTask.ActualReward` reports it, so it can be lower than the reward the game showed earlier.

`Kind` groups similar tasks. [`AlmediaTaskKind`](https://docs.almedia.dev/technical/api-reference/almedia/models#almediataskkind) lists the known values. The set is open: the server can add a kind without a package update, so show an unknown value as `AlmediaTaskKind.Main`.

## Task completions and balance changes

Use `OnTaskCompleted` and `OnBalanceChanged` for effects: a celebration, a coin animation, a sound. Each event carries the server's message id. `OnTaskCompleted` carries the completion and the reward it paid. `OnBalanceChanged` carries the balance after the change and the signed change:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
Almedia.OnTaskCompleted += completion =>
    Celebrate(completion.Task.Task.Title, completion.Task.ActualReward.Coins);

Almedia.OnBalanceChanged += change =>
    AnimateCoins(from: change.Balance.Coins - change.Change.Coins, to: change.Balance.Coins);
```

The events describe things that happened. Delivery is best-effort. Design for three consequences:

* **The events do not update the snapshot.** `Progress` changes only when a newer snapshot arrives. A snapshot can arrive before an event, after an event, or without a matching event. Render state from the snapshot. Use the events for effects.
* **An event is not always in the snapshot.** The server limits the `Completed` list, so a completed task can be missing from it. A balance in an event can differ from the balance in the snapshot. Do not look up one in the other.
* **The SDK can replay an event.** This can also happen after the SDK resets its stream token. A replay carries the same `Id`. Keep a short list of recent ids if a repeated effect is a problem:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
private readonly HashSet<string> _celebrated = new HashSet<string>();

Almedia.OnTaskCompleted += completion =>
{
    if (!_celebrated.Add(completion.Id)) return;
    Celebrate(completion.Task.Task.Title, completion.Task.ActualReward.Coins);
};
```

## AlmediaMoney

Coins are the canonical unit. Each `AlmediaRewardPoints` also carries two display values, `InPlayerCurrency` and `InUsd`. Select the value that fits the game. `AlmediaMoney.Amount` is a `decimal`. The SDK parses it from the wire string, so it is exact and independent of the device locale. `AlmediaMoney.Currency` is the ISO 4217 code. The SDK does not format money. Format it with your own symbols, decimals and fonts:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
string FormatMoney(AlmediaMoney money)
{
    var culture = CultureInfo.GetCultureInfo(money.Currency == "EUR" ? "de-DE" : "en-US");
    return money.Amount.ToString("N2", culture) + " " + money.Currency;    // "12,50 EUR" / "12.50 USD"
}
```

Each SDK version supports a closed set of player currencies. This release supports `USD`, `EUR`, `GBP`, `CAD`, `AUD`, `PLN`, `CHF`, `KRW`, `JPY` and `SEK`. You can write your formatting against this list. If the player's currency is not in the list, `InPlayerCurrency` is the USD value with the `"USD"` code. The code above then gives a correct label.

## Testing without a backend

`AlmediaEditorMock.EmitProgress` applies a snapshot that you build with the public constructors, or clears the snapshot when you pass `null`. `EmitTaskCompleted` and `EmitBalanceChanged` fire the two events. Emit two snapshots, the second without a username, to see the name clear. Emit `null` to see the panel hide. Emit one completion twice with the same id to test your deduplication:

```csharp theme={"theme":{"light":"github-light","dark":"night-owl"}}
var points = new AlmediaRewardPoints(12500, new AlmediaMoney(11.50m, "EUR"), new AlmediaMoney(12.50m, "USD"));
var task = new AlmediaTask("t-1", AlmediaTaskKind.Burning, "Reach level 10", points, new AlmediaTaskProgress(3, 10),
    DateTimeOffset.UtcNow.AddHours(2).ToString("o"));                        // RewardDropsAt in two hours
var done = new AlmediaCompletedTask(task, "2026-09-14T09:00:00.000Z", points);

AlmediaEditorMock.EmitProgress(new AlmediaProgress("s-1", "2026-09-14T10:00:00.000Z", "demo_player",
    points, points, new[] { task }, new[] { done }, new AlmediaTask[0]));
AlmediaEditorMock.EmitProgress(new AlmediaProgress("s-2", "2026-09-14T10:01:00.000Z", null,
    points, points, new[] { task }, new[] { done }, new AlmediaTask[0]));    // Username is now null
AlmediaEditorMock.EmitProgress(null);                                    // Progress is now null

AlmediaEditorMock.EmitTaskCompleted("m-1", done);
AlmediaEditorMock.EmitTaskCompleted("m-1", done);                         // must celebrate once
AlmediaEditorMock.EmitBalanceChanged("m-2", points, new AlmediaRewardPoints(-100, new AlmediaMoney(-0.09m, "EUR"), new AlmediaMoney(-0.10m, "USD")));
```

In the editor, the mock delivers a sample snapshot after the simulated link flow and with each `FetchNotifications()`. A progress panel shows data as soon as the player links, without test code.

***


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.