Embers, Ingots & Wallet
For the conceptual overview, see Currency. All endpoints are under /v1/wallet, /v1/embers, and /v1/ingots. Every route requires an authenticated player (guest or registered). The @me token resolves to the caller from the bearer token, mirroring the Users convention.
Combined snapshot
GET /v1/wallet/@me
Returns both currencies plus level info in a single payload. Used by the account dashboard card and SparkUserMenu.
- Auth: authenticated
Response — 200 OK
{
"embers": {
"balance": 25,
"lifetime": 25,
"level": { "ord": 1, "name": "Tinder", "threshold": 0 },
"next_level": { "ord": 2, "name": "Spark", "threshold": 100 },
"progress": 0.25
},
"ingots": {
"balance": 0,
"purchased": 0
}
}
Embers — earned currency
GET /v1/embers/@me
Same shape as the embers slice of /v1/wallet/@me.
Response — 200 OK
{
"balance": 25,
"lifetime": 25,
"level": { "ord": 1, "name": "Tinder", "threshold": 0 },
"next_level": { "ord": 2, "name": "Spark", "threshold": 100 },
"progress": 0.25
}
| Field | Type | Meaning |
|---|---|---|
balance | int | Current spendable Embers. |
lifetime | int | Total ever earned. Drives level. Monotonic. |
level | Level | Current tier (ord, name, threshold). |
next_level | Level|null | Next tier; null at max. |
progress | float | [0, 1] progress toward next_level. 1.0 at max. |
GET /v1/embers/@me/awards
List of awards the caller has earned. Optional ?game=lextris filter.
Response — 200 OK
{
"awards": [
{
"award_id": "id.award.first_word",
"game": "lextris",
"ember_value": 25,
"repeatable": false,
"granted_at": "2026-04-26T21:42:18+00:00"
}
]
}
ember_value is the snapshot at grant time. Editing the codex YAML later doesn't rewrite this number — the audit trail preserves what each player earned.
POST /v1/embers/@me/awards
Grant an award by codex id. Idempotent for non-repeatable awards.
Request
{
"award_id": "id.award.first_word",
"game": "lextris"
}
| Field | Type | Constraints |
|---|---|---|
award_id | string | required, ≤ 255. Codex id like id.award.first_word. |
game | string | required, ≤ 64. Codex game preset (lextris, gear-goblins, katforge, ...). |
Response — first earn — 201 Created
{
"granted": true,
"award_id": "id.award.first_word",
"game": "lextris",
"ember_value": 25,
"granted_at": "2026-04-26T21:42:18+00:00",
"wallet": {
"balance": 25,
"lifetime": 25,
"level": { "ord": 1, "name": "Tinder", "threshold": 0 },
"next_level": { "ord": 2, "name": "Spark", "threshold": 100 },
"progress": 0.25
}
}
Response — already earned — 200 OK
Same shape with granted: false and the original granted_at. wallet reflects the current balance, not a fresh credit.
Errors
| Code | Meaning |
|---|---|
404 | No award with (award_id, game) in the codex. |
422 | Validation failed (missing fields). |
GET /v1/embers/@me/ledger
Most-recent-first paginated ledger of every credit and debit.
Query
| Param | Type | Default | Notes |
|---|---|---|---|
limit | int | 50 | Clamped to [1, 200]. |
before | int | — | Exclusive ledger id from the previous page. |
Response — 200 OK
{
"entries": [
{
"id": 872,
"amount": 25,
"source": "award",
"reason": "First Word",
"ref_type": "award",
"ref_id": "id.award.first_word",
"created_at": "2026-04-26T21:42:18+00:00"
}
],
"limit": 50
}
source for Embers entries: award, spend, bonus, admin. Negative amount is a debit.
Ingots — paid currency
GET /v1/ingots/@me
{
"balance": 0,
"purchased": 0
}
| Field | Type | Meaning |
|---|---|---|
balance | int | Current spendable Ingots. |
purchased | int | Total ever credited via purchase / gift / admin. Never drops on spend. |
GET /v1/ingots/@me/ledger
Same shape and pagination as the Embers ledger; entries have source ∈ {purchase, spend, refund, gift, admin} and ref_type typically stripe, paypal, or gift.
Ingot top-ups land in a follow-up PR alongside the Stripe / PayPal webhook integration. Until then, purchase, spend, refund, and gift are reachable only through IngotService from server-side code or a console command.