Weekly league with item rewards
A three-tier weekly league scored by your backend, paying gems and items for top ranks and promotion, with promotion messages.
The result: every week active players are grouped in cohorts of 30, ranked by the score your backend sends, and the top of each cohort moves up from Bronze to Silver to Gold. The top three of each cohort get gems, the winner of a Silver cohort also gets an item, and every promotion out of Silver grants a streak freeze item. See Leagues for every field.
You need: a secret key on your backend (programs:write) and a gems currency. A reward naming
an item that does not exist yet waits as pending and is granted once the item exists, and the
league refuses to activate until every item it names exists, so create the items first.
1. Create the two items
Items are configured through change sets (POST /v1/change_sets with an Idempotency-Key),
one command per change set, each applied with POST /v1/change_set_applications like the league below
(see Economy for every item field):
{
"commands": [
{
"type": "economy_item.create",
"key": "golden_frame",
"item_type": "durable",
"max_quantity": 1,
"display": {
"title": "Golden frame",
"description": "Shown around the league winner's name."
}
}
]
}{
"commands": [
{
"type": "economy_item.create",
"key": "streak_freeze",
"item_type": "consumable",
"max_quantity": 5,
"display": {
"title": "Streak freeze",
"description": "Keeps a streak through one missed day."
}
}
]
}2. Propose the league
In a test environment, or in a live environment with auto autonomy, you can call POST /v1/leagues directly with
the same body. Otherwise propose it (POST /v1/change_sets with an Idempotency-Key):
{
"commands": [
{
"type": "league.create",
"key": "weekly",
"name": "Weekly league",
"config": {
"schema": "league/v1",
"tiers": [
{
"key": "bronze",
"name": "Bronze",
"promote_count": 10,
"rewards": [{ "rank_from": 1, "rank_to": 3, "currency": "gems", "amount": 30 }]
},
{
"key": "silver",
"name": "Silver",
"promote_count": 7,
"demote_count": 5,
"rewards": [
{
"rank_from": 1,
"rank_to": 1,
"reward": [
{ "currency": "gems", "amount": 80 },
{ "type": "item", "item": "golden_frame", "quantity": 1 }
]
},
{ "rank_from": 2, "rank_to": 3, "currency": "gems", "amount": 50 }
],
"promotion_reward": { "type": "item", "item": "streak_freeze", "quantity": 1 }
},
{
"key": "gold",
"name": "Gold",
"demote_count": 5,
"rewards": [{ "rank_from": 1, "rank_to": 3, "currency": "gems", "amount": 100 }]
}
],
"cohort_size": 30,
"period": {
"kind": "week",
"start_weekday": 1,
"start_minute": 0,
"timezone": "environment"
},
"score": { "source": "direct" },
"settle_delay_minutes": 10,
"min_score_to_promote": 1
}
}
]
}Apply it with POST /v1/change_set_applications when your autonomy policy
allows, or hand the change set id to a person to confirm. Then read the league
(GET /v1/leagues, find key weekly) and propose
{ "type": "league.activate", "key": "weekly", "expected_version": <revision> } the same way.
Activation checks that gems exists.
3. Score from your backend
Send a contribution whenever a player does what the league counts. Use your own business id as
dedupe_key so a retry never counts twice:
const API = 'https://api.orishare.com';
async function addLeagueScore(leagueId, customerId, lessonId, xp) {
const res = await fetch(`${API}/v1/leagues/${leagueId}/contributions`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.ORISHARE_SECRET_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': `league-${lessonId}`, // one per intent, reused on retry
},
body: JSON.stringify({ customer_id: customerId, amount: xp, dedupe_key: `lesson:${lessonId}` }),
});
if (!res.ok) throw new Error(`league contribution failed: ${res.status}`);
return res.json(); // { entry: { score, cohort_id, tier_index, ... }, replayed }
}The first contribution of the week puts the player in a cohort of their tier.
4. Show it
From the app, with the publishable key and the player's customer token:
GET /v1/customer/leagues: tier,seconds_left, the player's rank, score andzone, and last week's result (outcome,reward,reward_status).GET /v1/customer/leagues/{id}/standings: the cohort by display name, each withzone.
Or embed the hosted widget with the league surface.
5. Tell players
Create message types for the league events (POST /v1/notification_message_types, or the change
set command notification_message_type.create):
{ "key": "league_promoted", "routes": [{ "channel": "mobile_push", "path": "builtin" }] }Do the same for league_demoted and league_period_started. The catalogue copy is used until you
add templates; see Notifications.
6. Test in the test environment
Contribute scores for a few test customers, then wait for the period to end plus
settle_delay_minutes (a day period makes this quicker while testing). Check
GET /v1/leagues/{id}/periods (status settled), the cohort with
GET /v1/league_cohorts/{id}/standings (outcome per row), the players' balances and the
messages in GET /v1/notification_messages?type_key=league_promoted (test environment messages are sent
by the fake provider and never leave Orishare).
Duolingo-style social app (frontend only)
XP, levels, a weekly league, friends with nudges, friend streaks, friend quests, teams and push, for a Firebase, Supabase or React Native app with no server: publishable key, customer session and rules.
Friend streak with milestones
A daily streak two friends keep together, fed by a rule from app events, with milestone rewards and an at-risk reminder.