Talent Trees
Server-authoritative progression trees: nodes with per-rank costs, AND/OR prerequisites, tier gates, and mutually exclusive branches; XP curves that convert experience events into spendable points; full and partial respecs with refunds; versioned definitions with declarative player migration; and build analytics. Every example on this page was executed against a live server.
When to reach for it: RPG skill trees with exclusive specializations, learning platforms where exams earn points that unlock electives, SaaS power-user feature ladders driven by usage XP, certification tracks with prerequisites.
When not to: customer points with a redeemable reward catalog and no unlock graph belong to Loyalty; one-time unlock badges belong to Achievements; step-by-step objectives with completion rewards belong to Quest.
Concepts
- A tree is data - each node has a
tier,max_rank, multi-currencycost_per_rank, optionalall_of/any_ofprerequisites, an optionalexclusive_group, and optional per-rankgrants. - Tier gates hold a tier closed until points spent in the tree and/or required nodes are met; exclusive groups lock out sibling branches until a respec.
- Points live in one per-user wallet, keyed by free-form currency strings; several trees can spend from the same currency.
- XP curves turn experience into points. XP accumulates per curve; each
level-up credits
points_per_levelplus anybonus_levelsextras. Kinds:linear(level N atper_level * NXP),polynomial(base * N^exponent),table(explicit thresholds). - Allocation is server-authoritative - one atomic script checks rank
cap, prerequisites, tier gate, exclusive group, and balance, each with its
own
409code, so a modified client cannot cheat. - Definitions are versioned - publishing requires a migration plan and players migrate lazily.
Authoring (admin)
snug talent definitions validate --file ./tree.json # dry-run a draft
snug talent definitions create --file ./tree.json
snug talent curves create --file ./curve.jsonCreating, validating, publishing, and deleting definitions and curves needs
a platform admin token; any other caller gets 403 insufficient_permissions
(verified). Reading does not - any authenticated caller can get and list
them. The document shapes, the validator, and how to publish a new version
without breaking existing builds are on
Authoring and versioning.
Earning points
snug talent xp grant --curve-id docs4-curve --amount 350 --idempotency-key grant-1
snug talent walletThe examples use a linear curve with per_level: 100, 2 points per level,
and a +1 bonus at level 3 (document on the authoring page).
350 XP lands at level 3 and credits 7 points; captured:
{ "idempotent": false, "old_level": 0, "new_level": 3, "granted": { "docs4-points": 7 } }Replaying the same idempotency_key is a no-op flagged "idempotent": true;
the replay does not echo the original outcome (granted empty, levels 0).
Spending points
snug talent state --tree-id docs4-tree # annotated view of what you can take
snug talent allocate --tree-id docs4-tree --node-id node_ignite --ranks 2Over HTTP, allocation returns the updated state in the standard envelope; captured live after taking Ignite twice, Firewall, then the tier-2 Inferno:
curl -X POST -H "Authorization: Bearer $SNUG_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{"node_id": "node_inferno"}' \
http://localhost:4000/api/v1/talent/docs4-tree/allocate{
"status": 200,
"msg": "OK",
"data": {
"user_id": "docs4-talent", "tree_id": "docs4-tree", "tree_version": 1,
"ranks": { "node_ignite": 2, "node_firewall": 1, "node_inferno": 1 },
"points_spent": { "docs4-points": 5 },
"exclusive_selections": { "spec": "node_inferno" },
"highest_tier_unlocked": 1
}
}state annotates every node with what a picker UI needs; captured after the
allocations above, with 2 points still in the wallet:
{ "node_id": "node_frostfire", "current_rank": 0, "max_rank": 1, "affordable": true,
"prerequisites_met": true, "tier_locked": false, "excluded_by": "spec" }The annotations are independent: affordable reports only whether the
wallet covers the next rank, so a node can be affordable and still
unreachable because tier_locked or excluded_by is set. Ignore
highest_tier_unlocked - the server writes a constant 1 and never updates
it. Reading another user's state
(GET /api/v1/talent/{tree_id}/users/{user_id}/state) is self-or-admin;
anyone else gets 403 insufficient_permissions ("cannot act on another
user's progression", verified with a second token).
Allocation rules
Every rule violation is a 409 with its own error code; msg spells out
the specifics. Each was reproduced live:
error code | Reproduced msg tail |
|---|---|
insufficient_points | need 1 docs4-points, have 0 |
prereq_unmet | node node_firewall requires 2 rank(s) of node_ignite, have 0 |
tier_locked | node node_inferno is tier 2, which is not yet unlocked |
exclusive_group_violated | group spec already has a selection |
rank_exceeded | node node_ignite has max_rank 3, requested rank 4 |
An unknown node is a 404 node_not_found instead.
allocate and xp grant both take an idempotency_key, and both draw on
one key set per user - not one per tree, not one per operation. Reusing
a key any earlier call consumed makes the new call a silent no-op: verified
by sending an allocation with the key an earlier xp grant had used, which
returned 200 with wallet, ranks, and counters unchanged. allocate
carries no idempotent flag, so a swallowed call looks like a successful
one. Mint a fresh key per operation.
Respec
snug talent respec --tree-id docs4-tree --node-id node_firewall --ranks 1 # partial
snug talent respec --tree-id docs4-tree --full # everythingRefunds follow the policy's refund_ratio (at 1.0 a full respec restored
the entire spent balance, verified). Two more 409 codes, both reproduced:
respec_dependency when removing ranks another allocated node still
requires (node node_firewall depends on node node_ignite), and
respec_not_allowed when the tree's policy omits or disables that respec
kind (full respec is not enabled for this tree). A full-respec policy can
also declare a treasury_fee, charged through the
Treasury service.
Analytics (admin)
snug talent analytics paths --tree-id docs4-tree # per-node take rates
snug talent analytics archetypes --tree-id docs4-tree # build population sharesTwo caveats, both verified. paths counts total_ranks_allocated
cumulatively and never decrements - a node allocated, respecced away, and
allocated again counted twice - so it measures attraction, not currently
held ranks. And both reports are stored per tree version, so publishing
restarts them from zero: counters standing at 6 under version 1 read 0 the
moment version 2 went live. archetypes returns one row per build, as
{"signature": "spec=node_inferno", "population": 1, "share": 1.0} plus the
selections behind it; players holding no exclusive selection group under
no_capstone.
Events
Allocations, respecs, and migrations land on the caller's
Timeline stream talent_{user_id},
tagged ["talent", "{tree_id}"], and on the
PubSub channel
talent.{user_id}.{tree_id} (both captured live). Each carries an event
of talent.allocated, talent.respec, or talent.migrated plus a data
object of user_id, tree_id, tree_version, and a per-kind detail -
for an allocation, node_id, ranks, and ranks_total.
Limits and configuration
Trees are capped at 1000 nodes, 20 tiers, and 8 currencies; a single
allocation at 100 ranks and a single XP event at 10,000,000 XP - all
tunable via the TALENT_* variables in the
Talent CONFIG reference.
Reference
- Talent API - every endpoint, callable
- Related: Treasury for respec fees and currency wallets, Loyalty for points-and-rewards programs, Quest for objectives that feed XP, Achievements for one-time unlocks