Orishare
Concepts

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 with POST /v1/customer/streaks/freeze_purchases, undo with POST /v1/customer/streaks/freeze_cancellations; repair a break with POST /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 the step it feeds) and quest.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's qualification.sources for that step. POST /v1/quests/{id}/contributions stays 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_...; limit up to 100 and cursor page the list).
  • Customer: GET /v1/customer/quests (runs, and quests the customer can join as kind available); join with POST /v1/customer/program_enrollments (If-Match: "<program_revision>"), claim a completed run with POST /v1/customer/quest_claims. A one-step quest reports progress in a receipt's effects.quest_progress, a multi-step quest in effects.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:

  1. Publish it with definition.repeatable: true. Without it, a new run answers not_repeatable.

  2. Once a player's run is finished (state.status of their participation is completed, claimed or expired), your server starts the next one with POST /v1/quests/{id}/runs (secret key or agent token with programs:write, an Idempotency-Key, and If-Match: "<state_revision>"), body { "customer_id": "cust_...", "program_revision": <version>, "operation_key": "daily-<player>-<UTC day>" }. program_revision is the quest program's top-level version from GET /v1/programs/{id} (or GET /v1/quests/{id}, also sent as its ETag); the response has no revision or active_version field. A different value answers stale_write. A run that is still in progress answers invalid_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 answers invalid_state: skip the call that day, and the player's first event starts run:1 (or enroll them first with POST /v1/quests/{id}/enrollments when the quest needs an explicit enrollment).

    Where each input comes from: {id} is the quest program's prg_... id (GET /v1/quests or GET /v1/programs; the quest routes take only the id, and the program key answers 404). GET /v1/quests/{id}/participations?customer_id=cust_... returns the player's current participation: its top-level state_revision goes in If-Match, and its state.status and state.run say whether the run is finished and which run it is. program_revision is the program's version.

  3. 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.award into the XP currency; the level follows from the ledger, and POST /v1/levels/{id}/evaluations re-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's qualification.sources, or the contribution command POST /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 with POST /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 no approval at all: every settlement waits after its cooling-off for an operator review, POST /v1/referral_settlements/{id}/reviews with decision approve or reject.
  • "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 than max_per_referrer_per_period automatic approvals, and the whole program fewer than max_per_period, in the trailing period_seconds (at most 31 days). A referral over either limit is an outlier: it stays pending with review: "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 with POST /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 need program_id, 400 validation_failed). The response is the code object as GET /v1/customer/referral_codes lists 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 is 404 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, reason ai_gate): season.create (key, name), season.publish_definition (key, definition in the orishare.season/v1 grammar), season.set_scheduling (key, scheduling active or stopped) and season.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".

MechanicWhile pausedWhen activated again
LevelsNobody 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.
StreaksNew 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, badgesNew 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.
LeaderboardsNew entries and new scores are refused. Corrections of earlier activity still apply.Scores are accepted again. Scores sent during the pause are not replayed.
ReferralsNo 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.

On this page