Mechanics
Streaks, badges, quests, levels, leaderboards, referrals, catalogue and seasons: what each does, its operator commands and its reads.
Mechanics are kinds of program. /v1/programs is the one place definitions are versioned and
published; the mechanic routes below are typed views and commands over the same programs. Every
change is recorded as immutable evidence, so corrections append rather than rewrite.
Rules feed mechanics. Events reach a streak, badge, quest or level only through a published
rule whose action contributes to it, or through a direct operator command (a referral only
through its own qualifying event); Author a mechanic
has the table of which mechanic is fed how. A program's qualification.sources does not feed it: it describes
which events qualify and under which conditions (for a streak or a leaderboard, a contribution
caused by an event not listed there is refused). A program with sources but no rule never moves. Levels follow the XP currency that points.award
rules fill. Author a mechanic shows a complete, working streak.
The canonical API names never change: streaks, badges, quests, levels, leaderboards,
referrals. A project can relabel them for its users ("missions" for quests, "achievements" for
badges) through PATCH /v1/context terminology; labels change copy only, never routes, keys or
rule actions. Each mechanic can be enabled, hidden (runs, not shown) or off per project.
Which mechanics and rule actions are installed in an environment is answered at runtime by
GET /v1/capabilities (execution_supported, feature_versions). Every mechanic on this page,
with its rule actions, is executable where it is installed; a rule action missing from
execution_supported is refused at publication with rule_invalid.capability_unavailable. The
published /capabilities.json lists every operation and rule action the API
supports. Customer collections for a mechanic that is not available answer with an explicit
availability value, never an empty earned state.
Authoring a mechanic (create, publish a version, pause, archive) goes through change sets. How a
proposal commits follows the environment's autonomy policy: in a test environment or in a
live environment on auto the proposer applies it with POST /v1/change_set_applications; in limits it
applies when the plan is inside the limits; otherwise, and always in confirm, a signed-in owner
or admin confirms it in the dashboard. See Author a mechanic. The
commands below are operational actions on live programs and are audited.
Customer collections list what the customer takes part in. Items of the quest, streak, badge,
level, leaderboard position and referral collections name their program by program.id and by
program_key, the stable key the operator chose. Only quests also list what the customer could
join.
Streaks
Consecutive qualifying days or weeks. A streak counts a period when the customer reaches its minimum (count or sum), allows a bounded grace and lateness, and can be protected by freezes from a capped inventory (granted, or bought with a spendable currency). Milestones award once per lifetime or per episode.
A streak advances only through a rule with streak.contribute (one contribution per qualifying
event; the period counts when the contributions reach the minimum). The calendar decides the
periods: period is day or week, timezone_source is environment (the environment's
timezone, GET /v1/environments/{id}), customer (the customer's) or fixed (with
timezone), and day_start_offset_minutes moves the start of the day. enrollment.mode is
automatic by default, so the first contribution creates the customer's participation; a
customer appears in GET /v1/customer/streaks only from then on. Milestone rewards are
reward.on["milestone:<key>"] actions on the program.
Streak repair. Add repair to the streak definition to let a player buy back a broken
streak: "repair": { "price": { "currency": "gems", "amount_minor": "50" }, "window_periods": 3, "maximum_per_days": { "count": 1, "days": 30 } }. While the newest break is at most
window_periods periods old, POST /v1/customer/streaks/repairs (body program_id,
program_version_id, with an Idempotency-Key) charges the price from the player's balance and
protects the missed periods of that break. The streak continues from its length before the break
plus anything earned since, a streak.restored fact is recorded, and milestones already paid are
not paid again. Without enough balance nothing changes (insufficient_balance). A second repair
of the same break, a break outside the window or a streak without repair is refused
(streak_repair_unavailable); more paid repairs than maximum_per_days allows is refused with
streak_repair_limit_reached. Your backend can repair for a player with
POST /v1/streaks/{id}/repairs (body customer_id, charge): charge: false grants the repair
for free, for example as a subscription perk, and does not count towards the limit. Each item of
GET /v1/customer/streaks has repair: null, or { available, price, currency_id, expires_at, broken_length } so a widget can offer the repair until expires_at.
Weekend protection. Add protected_weekdays to a day streak to name days that never break it,
in the streak's own timezone: 0 is Sunday, 1 Monday and so on up to 6 Saturday (at most six
days). "protected_weekdays": [6, 0] protects weekends. A protected day without the activity is
skipped: it neither counts nor resets the streak, uses no freeze and is never reported at risk. A
protected day with the activity counts as usual.
- Rule actions:
streak.contribute,streak.freeze_grant. - Commands:
POST /v1/streaks/{id}/freeze_grants,POST /v1/streaks/{id}/freeze_returns,POST /v1/streaks/{id}/freeze_cancellations,POST /v1/streaks/{id}/corrections,POST /v1/streaks/{id}/repairs. - Operator reads:
GET /v1/streaks,GET /v1/streaks/{id}/participations,GET /v1/streaks/{id}/participations/{participation_id}/freezes. - Customer:
GET /v1/customer/streaks; buy a freeze withPOST /v1/customer/streaks/freeze_purchases, undo withPOST /v1/customer/streaks/freeze_cancellations; repair a break withPOST /v1/customer/streaks/repairs.
Badges
Ranked recognition (bronze, silver, gold) from a count, sum or distinct-value threshold, or a direct grant backed by evidence. Hidden criteria stay hidden until earned. Recognition is kept on refund by default (see the program's correction policy).
Rarity. Each badge shows how many players hold each rank. GET /v1/badges and
GET /v1/badges/{id} return rarity, one entry per rank: holders (players whose grant of that
rank is not revoked; a restored grant counts again), population (players taking part in the
badge program), rarity_percent (one decimal, as a string) and rarity_tier: common above 50%,
uncommon above 20%, rare above 5%, epic above 1%, otherwise legendary. Each item of
GET /v1/customer/badges has rarity with rank_key, rarity_percent and rarity_tier only.
The numbers are refreshed about once a minute. While fewer than 20 players take part, the share
is null, so a small group cannot be identified. A hidden badge, and so its rarity, reaches a
customer only once they earned it.
- Rule actions:
badge.contribute,badge.grant. - Commands:
POST /v1/badges/{id}/contributions,POST /v1/badges/{id}/grants,POST /v1/badges/{id}/revocations,POST /v1/badges/{id}/restores,POST /v1/badges/{id}/corrections,POST /v1/badges/{id}/backfills. - Operator reads:
GET /v1/badges,GET /v1/badges/{id}/participations. - Customer:
GET /v1/customer/badges.
Quests
Multi-step goals. A quest has up to 32 steps with prerequisites, count, sum or distinct targets, an optional deadline, automatic or explicit enrollment, and auto-collect or claim mode. Rotating quests pick from a pool deterministically, so a retry never rerolls.
- Rule actions:
quest.contribute(with thestepit feeds) andquest.enroll. A rule can feed any step of a quest, so a multi-step quest needs no backend: one rule per step, each on its own event. A rule naming a step the quest does not have is refused at publication. Each step's event must be listed in the quest'squalification.sourcesfor that step.POST /v1/quests/{id}/contributionsstays available for direct contributions. - Commands:
POST /v1/quests/{id}/enrollments,POST /v1/quests/{id}/runs,POST /v1/quests/{id}/contributions,POST /v1/quests/{id}/claims,POST /v1/quests/{id}/corrections. - Operator reads:
GET /v1/quests,GET /v1/quests/{id}/participations(filter one player with?customer_id=cust_...;limitup to 100 andcursorpage the list). - Customer:
GET /v1/customer/quests(runs, and quests the customer can join askindavailable); join withPOST /v1/customer/program_enrollments(If-Match: "<program_revision>"), claim a completed run withPOST /v1/customer/quest_claims. A one-step quest reports progress in a receipt'seffects.quest_progress, a multi-step quest ineffects.graph_quest_progress.
With explicit enrollment, a rule that contributes to the quest is skipped for customers who have not joined, including its other actions; keep awards that every customer should get in a separate rule (Rules).
Daily (repeating) quests
A quest does not reset on a calendar. Its runs are numbered (run:1, run:2, ...), and a
calendar with period: "day" and run_per_period: true does not start a new run each day: keep
the quest's calendar at period: "none", run_per_period: false, as the game starter does. To
offer the same quest every day:
-
Publish it with
definition.repeatable: true. Without it, a new run answersnot_repeatable. -
Once a player's run is finished (
state.statusof their participation iscompleted,claimedorexpired), your server starts the next one withPOST /v1/quests/{id}/runs(secret key or agent token withprograms:write, anIdempotency-Key, andIf-Match: "<state_revision>"), body{ "customer_id": "cust_...", "program_revision": <version>, "operation_key": "daily-<player>-<UTC day>" }.program_revisionis the quest program's top-levelversionfromGET /v1/programs/{id}(orGET /v1/quests/{id}, also sent as itsETag); the response has norevisionoractive_versionfield. A different value answersstale_write. A run that is still in progress answersinvalid_state, so a player who did not finish yesterday's run keeps it. Before the player's first contribution there is no run to roll over (the participations list is empty) and a new run also answersinvalid_state: skip the call that day, and the player's first event startsrun:1(or enroll them first withPOST /v1/quests/{id}/enrollmentswhen the quest needs an explicit enrollment).Where each input comes from:
{id}is the quest program'sprg_...id (GET /v1/questsorGET /v1/programs; the quest routes take only the id, and the program key answers404).GET /v1/quests/{id}/participations?customer_id=cust_...returns the player's current participation: its top-levelstate_revisiongoes inIf-Match, and itsstate.statusandstate.runsay whether the run is finished and which run it is.program_revisionis the program'sversion. -
Call it at the player's first session of the day (or from a daily job). Events then feed the new run through the same rules. There is no player-session route and no rule action that starts a run.
For a goal that only needs one count per day (play once a day), use a streak instead: it is calendar based.
Levels
Levels are XP tiers: strictly increasing thresholds over an XP currency, optionally per season or
with prestige. A level program is a program of kind tier (so GET /v1/programs and a playbook's
plan show it as a tier), and GET /v1/levels is its typed view. XP is a non-spendable currency, so it can never buy rewards. Thresholds are fixed
integers at publication; no formula runs while earning.
A program has up to 1,000 levels. Instead of writing every threshold, add level_curve beside
definition in program.publish_definition: { "curve": { "type": "exponential", "levels": 200, "first_step": "100", "growth": "1.08" }, "title": "Level {n}" }. Curve types are linear
(levels, step), quadratic (levels, first_step, step_increase), exponential
(levels, first_step, growth as a decimal above 1) and table (steps, the XP for each
level after the first). The levels are generated once with keys level_1 to level_N and stored
as explicit thresholds; send the first level only in definition.levels. level_curve exists
only in change sets: publish_program_version (POST /v1/programs/{id}/versions) takes explicit
levels, and every level:<key> reward slot must name a level the definition has. At most 128
levels can carry their own reward.
A level reached by an event is applied before the event request returns, so the response and the
next GET /v1/customer/levels already show it. A scheduled job completes any evaluation that did
not fit in the request.
- Rule actions:
points.awardinto the XP currency; the level follows from the ledger, andPOST /v1/levels/{id}/evaluationsre-checks one customer on demand. - Commands:
POST /v1/levels/{id}/enrollments,POST /v1/levels/{id}/evaluations,POST /v1/levels/{id}/prestiges. - Operator reads:
GET /v1/levels,GET /v1/levels/{id}/participations. - Customer:
GET /v1/customer/levels.
Leaderboards
Ranked scores for a season or period: a sum, max or latest verified score, ascending or descending, global or per segment or group, with prize bands. Standings are built from recorded contributions and settle after the season's late-evidence window.
- Fed by: a rule with
leaderboard.contribute({ "type": "leaderboard.contribute", "leaderboard": "<key>", "value": 10 }) on any event listed in the board'squalification.sources, or the contribution commandPOST /v1/leaderboards/{id}/contributions. One event contributes to a board once: feed a board from one rule per event. Browser events can feed a board whose trust policy allows them (see trust policy); a board with prizes still needs evidence from your backend. - Commands:
POST /v1/leaderboards/{id}/contributions,POST /v1/leaderboards/{id}/corrections,POST /v1/leaderboards/{id}/boards/{board_id}/standing_builds,POST /v1/leaderboards/{id}/boards/{board_id}/correction_generations. - Operator reads:
GET /v1/leaderboards,GET /v1/leaderboards/{id}/boards,GET /v1/leaderboards/{id}/boards/{board_id}/standings. - Customer:
GET /v1/customer/leaderboard_positions,GET /v1/customer/leaderboards/{id}/boards/{board_id}/standings; set the public display name withPOST /v1/customer/leaderboard_profiles.
Score source. definition.score_source says what feeds the score. Omitted (or
{ "type": "contributions" }), the board is fed by rules and the contribution command as above.
With { "type": "currency_earned", "currency": "<code>" } the score is the net amount of that
currency the player earned inside the board's period or season: confirmed earns and positive
adjustments minus their reversals. Spends do not lower it, pending points count once confirmed,
and earnings outside the period never count, so each new period starts at zero. You write no rule
for the board: whatever awards the currency (rules, quests, level rewards, adjustments) feeds it.
The score is on the board when the event request returns; earnings that do not come from an event
(confirmations of pending points, operator adjustments, reversals) arrive with the next scheduled
run. Such a board has one writer: a leaderboard.contribute rule aimed at it is skipped and the
contribution command is refused (invalid_state, reason leaderboard_score_source_managed). It
needs aggregation: "sum", an input range with a negative minimum and a positive maximum (so a
reversal can lower the score), no qualification.sources, no recognition caps, and
trust.allowed_sources that include operator_command. trust.minimum_trust is compared with
the weakest trust of the earnings summed: on a merchant_asserted board, currency earned from
browser events does not count; use customer_claim to count it. currency_balance is reserved
and refused for now. A weekly XP board:
{
"schema": "orishare.program/v2",
"kind": "leaderboard",
"semantics": "native_v2",
"definition": {
"kind": "leaderboard",
"aggregation": "sum",
"input": { "minimum": "-1000000", "maximum": "1000000" },
"order": "descending",
"scope": { "type": "global" },
"rank": "ordinal",
"privacy": { "display": "pseudonymous", "opt_out": true, "anonymous_entries": false },
"prizes": [],
"score_source": { "type": "currency_earned", "currency": "xp" }
},
"qualification": { "sources": [] },
"calendar": {
"schema": "orishare.calendar/v1",
"period": "week",
"run_per_period": false,
"timezone_source": "environment",
"ambiguous_local_time": "earlier",
"nonexistent_local_time": "next_valid",
"event_time": { "merchant": "occurred_at", "client": "received_at" },
"after_finalization": "correction_only"
},
"trust": {
"minimum_trust": "merchant_asserted",
"allowed_sources": ["operator_command"],
"on_insufficient": "ignore",
"liability_caps": [],
"recognition_caps": [],
"allow_legacy_unknown": false
},
"correction": { "spendable": "reverse" },
"reward": { "on": {} },
"display": {},
"enrollment": { "mode": "automatic" }
}Item prizes. A prize band can award items from your economy beside
currencies and rewards, as { "type": "item", "item": "<key>", "quantity": <n> } (1 to 1,000).
The item must exist and be active when the program is published; an unknown or archived item
fails publication with rule_invalid.action_reference. Each frozen prize is granted into the winner's
inventory once, so a retried settlement never grants twice, and the grant emits item.granted
with source_type leaderboard_prize. While the item cannot be granted (archived, or the
player's inventory is at the item's max_quantity), the prize stays owed and is retried, like a
prize whose budget is exhausted. A correction that lowers a player's prize takes the items back
from that grant; units the player already used cannot be taken back, so that correction ends
unrecoverable, as for a redeemed reward. Items carry no monetary value, so no liability cap
applies to them. A winner band paying a trophy and coins:
"prizes": [
{
"key": "winner",
"from": 1,
"through": 1,
"awards": [
{ "type": "item", "item": "gold_trophy", "quantity": 1 },
{ "type": "currency", "currency": "coins", "amount_minor": "500" }
]
},
{ "key": "top_ten", "from": 2, "through": 10, "awards": [{ "type": "item", "item": "silver_trophy", "quantity": 1 }] }
]Referrals
A known customer shares an opaque code; the first valid claim by a new account wins. The reward
waits for trusted qualifying evidence (for example a merchant-sent order.paid above a minimum),
a cooling-off period (cooling_off_seconds) and an approval. A refund within the policy reverses
it.
Approval is set by the definition's optional approval:
{ "mode": "review" }, or noapprovalat all: every settlement waits after its cooling-off for an operator review,POST /v1/referral_settlements/{id}/reviewswithdecisionapproveorreject."mode": "automatic"with three limits (example below): once the cooling-off ends, the background job approves and settles the referral with no review, as long as the referrer (with its merged accounts) has had fewer thanmax_per_referrer_per_periodautomatic approvals, and the whole program fewer thanmax_per_period, in the trailingperiod_seconds(at most 31 days). A referral over either limit is an outlier: it stayspendingwithreview: "pending"until an operator approves or rejects it, exactly as in review mode, and its due work result carries"reason": "review_required"with"limit": "per_referrer"or"per_period". Operator approvals do not count toward the limits. An automatic approval is recorded as a review with the actor{ "kind": "automatic", "by": "approval_policy", "at": "<decision time>" }; the limits count approvals by that decision time.
Both reads say whether a person has to act. The operator settlement read
(GET /v1/referrals/{id}/settlements) carries review and vests_at (end of the cooling-off);
the customer's GET /v1/customer/referrals carries settlement.review_pending and
settlement.vests_at. In review mode a settlement waits for review (review: "pending",
review_pending: true) from qualification until approved or rejected, cooling-off included. In
automatic mode a settlement still cooling off reads review: "none" and review_pending: false:
no one needs to act, it is approved when it vests. Only an outlier (vested and still unapproved,
held by a limit) reads review: "pending" and review_pending: true; for a short moment right
after vesting, until the next background pass approves it, an ordinary settlement can read the
same.
"approval": {
"mode": "automatic",
"period_seconds": 2592000,
"max_per_referrer_per_period": 10,
"max_per_period": 500
}The limits are separate from caps: caps.per_referrer and caps.per_environment bound how many
referrals can ever qualify, while the approval limits only decide which qualified referrals need a
person. A settlement keeps the approval mode of the program version it qualified under.
Item rewards. Either side can be rewarded with items, as
{ "type": "item", "item": "<key>", "quantity": <n> }; the item must exist and be active when the
program is published. Items are granted when the referral settles (after the cooling-off and the
approval), never while it is pending, so a player cannot use a reward that may still be refunded.
A refund recorded before settlement lowers what is granted; a refund after it takes items back
from the grant in proportion, and units the player already used are recorded as unrecoverable.
While the item cannot be granted (archived, or the inventory is full), the settlement waits and
is retried. A referral that gives the referrer a skin and the new player a starter chest:
"rewards": {
"referrer": [{ "type": "item", "item": "referral_skin", "quantity": 1 }],
"referred": [
{ "type": "item", "item": "starter_chest", "quantity": 1 },
{ "type": "currency", "currency": "coins", "amount_minor": "100" }
]
}- Commands:
POST /v1/referral_codes,POST /v1/referral_codes/{id}/revocations,POST /v1/referral_settlements/{id}/reviews,POST /v1/referral_settlements/{id}/corrections. - Operator reads:
GET /v1/referrals,GET /v1/referrals/{id}/settlements. - Customer:
GET /v1/customer/referrals,GET /v1/customer/referral_codes; the referred customer claims withPOST /v1/customer/referral_claims(code only, never ids). - Self-service codes: a signed-in customer creates their own share code from the browser or app
with
POST /v1/customer/referral_codes(publishable key and customer session, Idempotency-Key required), so an app with no backend needs no secret key for it. The body is{ "program_id": "prg_..." }, or{}when exactly one active referral program is visible to the customer (several needprogram_id,400 validation_failed). The response is the code object asGET /v1/customer/referral_codeslists it. A customer holds one active code per program: a second call, even with a new key, returns the same code until it is revoked. An anonymous session is refused (409 invalid_state); a hidden, paused or archived program is404 resource_not_found. Creating a code is included (not metered). Qualification is unchanged: the reward still waits for the trusted qualifying event.
Catalogue
Things customers buy with points: immutable offer versions in catalogues, a quote that pins the
price, then an atomic redemption that spends, reserves stock or a code, and starts fulfilment.
A customer quotes an offer from GET /v1/customer/catalogue_offers with
{ "offer_version_id": offer.id }, then redeems with { "quote_id" }; the quote spends nothing
and lasts until its expires_at, and the redemption starts as pending_fulfillment. Issued
rewards (reward.issue) stay a separate, older path, redeemed with POST /v1/customer/redemptions.
- Rule actions:
coupon.issue,discount.quote. - Commands:
POST /v1/reward_catalogues,POST /v1/reward_catalogues/{id}/offer_versions,POST /v1/reward_offer_versions/{id}/revocations,POST /v1/reward_inventory/{id}/movements,POST /v1/reward_code_pools/{id}/imports,POST /v1/fulfillments/{id}/reconciliations. - Operator reads:
GET /v1/reward_catalogues,GET /v1/reward_offer_versions,GET /v1/fulfillments. - Customer:
GET /v1/customer/catalogue_offers,POST /v1/customer/redemption_quotes,POST /v1/customer/catalogue_redemptions,POST /v1/customer/catalogue_cancellations,GET /v1/customer/coupon_entitlements.
Seasons
A season is a named, versioned calendar interval that leaderboards, levels and badges can pin. Occurrences are the concrete runs of a recurring season. A season accepts late evidence until its recorded deadline, then finalizes; after that only corrections change results.
- Commands:
POST /v1/seasons,POST /v1/seasons/{id}/versions,POST /v1/seasons/{id}/occurrences. - Change set commands (the path for an agent in a live environment, where the direct routes answer
403 confirmation_required, reasonai_gate):season.create(key,name),season.publish_definition(key,definitionin theorishare.season/v1grammar),season.set_scheduling(key,schedulingactiveorstopped) andseason.archive(key). Example in Game economy. - Reads:
GET /v1/seasons,GET /v1/seasons/{id}/versions,GET /v1/seasons/{id}/occurrences.
Pausing a program
Pausing is a change set command (program.pause); program.activate resumes. A paused program
keeps everything customers already earned, and its customer reads keep answering with the stored
state. What stops is new activity. A command a paused program refuses answers 409 invalid_state
with details.reason: "program_inactive".
| Mechanic | While paused | When activated again |
|---|---|---|
| Levels | Nobody is enrolled or evaluated, so no level changes, also for customers who already have a level. XP keeps being recorded in the ledger. | Every balance change made during the pause is evaluated from the ledger, within a few minutes: each customer lands on the level their XP gives, once. |
| Streaks | New enrollments, contributions, freeze purchases and repairs are refused. Time is frozen: a period the pause touches (any part of it, its grace window included) is excused, so it neither breaks the streak nor uses a freeze, and the streak is not shown at risk. Corrections and freeze returns still apply. | Contributions count again from the current period, and the streak continues as if the pause never happened: excused periods neither add to the length nor break it. A period kept before the pause still counts, a period missed before the pause still breaks, and paused periods do not use up a repair window. |
| Quests, badges | New enrollments and new progress are refused. Corrections of earlier activity still apply. | Progress continues from where it stopped. Activity sent during the pause is not replayed. |
| Leaderboards | New entries and new scores are refused. Corrections of earlier activity still apply. | Scores are accepted again. Scores sent during the pause are not replayed. |
| Referrals | No new code is created, no new claim of an existing code is accepted, and a qualifying event qualifies no referral. A claim recorded before the pause is kept, and a referral that qualified before the pause still settles and pays. | Codes can be created and claimed again. A qualifying event that arrived during the pause is not replayed. |
Pausing a streak program is safe for its customers: the paused days are excused and nobody
loses a streak or a freeze because of the pause. GET /v1/customer/streaks reports
paused: true while it lasts.
Corrections and reversal
Every mechanic corrects by appending: a correction command references the original contribution
and records a compensating fact. program.reverse_contribution in a rule takes back a program
contribution (all, a fraction or an amount) by its permanent business fact, so it works after
raw events age out. What a refund does to points, XP and recognition is set per program in its
correction policy, and trust floors per program in its trust policy.
Game economy
Items, a shop, energy and lives, timed boosts, chests and battle passes are a separate domain, not a program kind: see Game economy.
Programs
Currencies and the ledger, tiers, rewards, challenges, segments and historical counters.
Author a mechanic through a change set
A complete, working daily streak: the change set that creates and publishes the program, the rule that feeds it, who confirms what, and when a customer first sees it.