# Reward players

> Pay players in $LIA from your app's reward pool, from your server. A daily bonus as the example, safe to retry and within your limits. From the Lia developer docs: https://docs.lialabs.org/guides/reward-players

Your app needs a reward pool, funded by the Lia team (see [Reward pools](/concepts/reward-pools)). Rewards only ever come from your server, with your key.

## A daily bonus

Each player can claim 2 LIA once a day, after finishing a level.

```js title="server.js"
import { lia } from "./lia.js";

app.post("/api/daily-bonus", async (req, res) => {
  const playerId = await playerFrom(req.body.session); // see Verify on your server
  if (!playerId) return res.status(401).end();
  if (!(await db.finishedALevelToday(playerId))) return res.status(403).json({ error: "play_first" });

  const day = new Date().toISOString().slice(0, 10); // 2026-11-01
  try {
    const result = await lia("POST", "/rewards", {
      playerId,
      amount: 2,
      key: `daily:${day}:${playerId}`, // one a day per player, however often this runs
      reason: "Daily bonus",
    });
    res.json({ paid: result.status === "paid", amount: result.amount });
  } catch (error) {
    if (error.data?.error === "pool_empty") return res.status(503).json({ error: "bonus_paused" });
    if (error.data?.error === "daily_limit") return res.status(429).json({ error: "come_back_tomorrow" });
    throw error;
  }
});
```

In the page:

```js
const { paid, amount } = await api("/api/daily-bonus", {});
if (paid) {
  Lia.haptic("success");
  Lia.toast("+" + amount + " LIA");
}
```

## Choosing keys

The key is what makes a reward safe. The same key never pays twice, so make it name the reason for the reward, not the request:

| Reward | Key |
| --- | --- |
| A daily bonus | `daily:2026-11-01:p_3f9c…` |
| A tournament place | `tournament-44:place-1:p_3f9c…` |
| A finished quest | `quest-7:p_3f9c…` |
| A watched video | `ad:ad_8e1c…` |

A retry with the same key after a timeout answers `{ "status": "duplicate" }` and pays nothing more, so retrying is always safe. Never retry with a new key.

## Keep an eye on the pool

```js
const { balance, dailyCapPerPlayer } = await lia("GET", "/pool");
if (balance < 100) alertTheTeam(`Reward pool is down to ${balance} LIA`);
```

When the pool can't cover a reward, `POST /rewards` answers `409 pool_empty`. Tell players kindly and ask the Lia team to top it up. When a player has had your app's daily limit, it answers `429 daily_limit` (the limit is per player per UTC day, set by the team).

## In test

With your test key, rewards are pretend: recorded, with their webhook, but no $LIA moves, and the pool and daily limit aren't used. Try your keys and your error handling there first.

## Make rewards fair

- **Decide on the server.** What someone earned should come from what your server saw (a finished game, a verified video), not from the page saying so.
- **Mind your own limits** too, per player and per day, so one player can't drain the pool.
- **Say why.** The `reason` shows in the player's activity under your app's name.
