Programs
Currencies and the ledger, tiers, rewards, challenges, segments and historical counters.
Currencies and the ledger
Every award is a ledger entry in integer minor units. Entries are lots with a remaining balance;
spends and expiries consume lots oldest-expiry first. Pending awards (confirm_after) count in
pending until the confirmation job moves them to available. Manual adjustments are
POST /ledger_entries with a reason; negative adjustments spend FIFO and respect the currency's
negative_balance policy.
Tier programs
{ name, currency, basis, levels[], downgrade_check }. basis is lifetime_earned,
earned_rolling_days or balance; levels are ascending thresholds starting at 0. Rules upgrade
customers with tier.evaluate; the downgrade job re-evaluates at each local day or month
boundary; operators can set a level directly (audited). Removing a level customers hold needs
reassign_to; changing the basis needs reevaluate: true.
Rewards
{ name, cost, currency, availability, expiry_after }. Rules issue them; customers redeem an
issuance (POST /reward_issuances/{id}/redeem), which spends the cost in the same transaction and
fails with insufficient_balance otherwise. Cancelling refunds the cost by a reversal entry and
releases availability; expiry does not refund.
Challenges
{ qualifying: { event, conditions? }, target, period, contribution_key?, enrollment, repeatable, completion_actions[] }.
Each qualifying event contributes once per contribution_key value; reaching target completes
the period exactly once and applies the completion actions in the same transaction. Explicit
enrollment gates progress; revoke and reevaluate are audited operator actions.
Segments
Static segments have explicit members; dynamic segments are a condition over
customer.attributes.* evaluated live at rule time. Campaign audiences and rule conditions
reference segments by name, so a referenced segment cannot be renamed or deleted.
Historical counters
{ name, event, window, filter? } counts events per customer and window and is read as
customer.counters.<name>. A definition registered after events exist starts backfilling;
rules that read it skip with counter_not_ready until POST /counter_definitions/{id}/backfill
(inline for small histories, a job otherwise) marks it ready.