Orishare
Concepts

Friends and teams

Friend codes, requests, blocks, nudges and high-fives, friend standings, friend quests, friend streaks, teams and team quests, with the privacy rules and what anonymous sessions can do.

Social features let players see and help each other: add friends by code, nudge or high-five them, compare league and leaderboard scores, share a weekly friend quest or a daily friend streak, and join a team with a shared team quest. Everything a player does goes through their customer session (publishable key plus customer token); your backend and operator tools use the operator routes.

Operator routes need customers:read or customers:write for friends, blocks and teams, and programs:read or programs:write for friend quest, friend streak and team quest definitions. Customer routes need the session capabilities customer:read and customer:write. Customer social writes are naturally idempotent (a repeat returns the current state with replayed), so they take no Idempotency-Key.

Privacy rules

  • No customer id ever reaches another player. Other players are named by their friend code (a 10 character handle) and shown by display name, or by a stable pseudonym such as "Player 4821" when they have none.
  • Profile visibility. PATCH /v1/customer/social_profile sets display_name, avatar_url and visibility: public, friends (the name is shown only to accepted friends; everyone else sees the pseudonym) or hidden (always the pseudonym). Display names are 1 to 40 characters, with no URLs or @. An operator can hide a profile with POST /v1/customers/{id}/social_profile_moderations.
  • Blocking wins everywhere. When either side blocked the other there is no request, no friendship (a block removes it), no nudge or high-five, no friend quest or streak pairing (an active one is cancelled or ended), and neither appears in the other's lists or friend standings. Messages already queued about the other player are suppressed with blocked.
  • Anonymous sessions have no social features. Every social read and write answers 403 forbidden with details.reason social_unavailable_for_anonymous. Anonymous players still take part in leagues under a pseudonym. Sign the player in (and merge the anonymous customer) to unlock friends and teams.
  • Nudge and high-five context is a short machine key (lesson:12, streak), never free text, so players cannot message each other through it.

Friends

ActionRoute
Read my friend codeGET /v1/customer/friend_code
Rotate my friend codePOST /v1/customer/friend_code
Send a request by codePOST /v1/customer/friend_requests with { "code" }
Accept or decline (by the friend's code)POST /v1/customer/friend_requests/{friend}/accept, POST /v1/customer/friend_requests/{friend}/decline
List friends, incoming, outgoingGET /v1/customer/friends?status=accepted (incoming, outgoing)
Remove a friendDELETE /v1/customer/friends/{code}
Block a playerPOST /v1/customer/blocks with { "code" }
List my blocksGET /v1/customer/blocks
UnblockDELETE /v1/customer/blocks/{handle}

A request to a player who already requested you becomes a friendship at once. A player has at most 200 friends and pending requests together; past that the answer is 409 invalid_state with details.reason limit_reached. A blocked or deleted player is refused the same way (details.reason blocked, customer_deleted), and your own code is 400 validation_failed.

Blocks are listed by handle. GET /v1/customer/blocks returns { "data": [{ "handle": "blk_...", "display_name", "created_at" }], "has_more" }, newest first (at most 200). A handle is derived for this viewer only: it is not a customer id and does not work for anyone else. DELETE /v1/customer/blocks/{handle} lifts the block (a second delete of the same handle is 404); the blocked player's current friend code also works in place of the handle and is safe to repeat.

Nudges and high-fives. POST /v1/customer/nudges and POST /v1/customer/high_fives with { "code", "context"? }: friends only, one of each kind per sender, recipient and day (a repeat the same day returns the first with replayed: true). The recipient gets a friend.nudged or friend.high_fived event, which can become a push or email (Notifications). GET /v1/customer/reactions lists sent and received ones.

Friend standings. GET /v1/customer/leagues/{id}/friend_standings ranks the player and their friends by this period's league score across cohorts; GET /v1/customer/leaderboards/{id}/friend_standings does the same over a leaderboard program's current board.

Your backend can import an existing friend graph with POST /v1/customers/{id}/friendships ({ "other_customer_id", "status": "accepted" }) and remove one with DELETE /v1/customers/{id}/friendships/{other}; it reads GET /v1/customers/{id}/friends and GET /v1/customers/{id}/blocks.

Friend quests

Two friends share one target for the week (or day): "complete 20 lessons together".

{
  "key": "weekly_duo",
  "name": "Weekly duo quest",
  "config": {
    "schema": "friend_quest/v1",
    "period": { "kind": "week", "start_weekday": 1, "timezone": "environment" },
    "target": 20,
    "reward": [
      { "currency": "xp", "amount": 50 },
      { "type": "item", "item": "chest_small", "quantity": 1 }
    ]
  }
}

Create with POST /v1/friend_quest_definitions, then activate, pause, resume or archive with POST /v1/friend_quest_definitions/{id}/activate (and /pause, /resume, /archive), each { "expected_revision" }. The reward list has 1 to 5 entries, currency or item.

  • A player starts the quest with a friend: POST /v1/customer/friend_quests with { "definition": "<definition id or key>", "friend_code" }. A player is in at most one pair per definition and period. A player whose experiment variant withholds this friend quest is refused (403 forbidden, details.reason withheld_by_experiment).
  • Progress comes from a rule action friend_quest.contribute (frontend-only apps) or from your backend: POST /v1/friend_quest_definitions/{id}/contributions with { "customer_id", "amount", "dedupe_key" }.
  • When the two sides together reach the target, the quest completes and both players are paid, exactly once each, and both get friend_quest.completed. A quest that is not complete by the end of its period expires; blocking or unfriending cancels it with no reward.
  • Reads: GET /v1/customer/friend_quests (each definition with this period's quest, both sides' progress and the friend's display name), and for operators GET /v1/friend_quest_definitions/{id}/quests.

Friend streaks

A pair's day counts when both friends were active that day; the streak grows by one per such day and resets when a day is missed.

{
  "key": "duo_streak",
  "name": "Friend streak",
  "config": {
    "schema": "friend_streak/v1",
    "timezone": "environment",
    "milestones": [
      { "length": 7, "reward": [{ "type": "item", "item": "streak_freeze", "quantity": 1 }] },
      { "length": 30, "reward": [{ "currency": "xp", "amount": 300 }] }
    ]
  }
}

Create with POST /v1/friend_streak_definitions and manage it like a friend quest definition. Activity comes from a rule action friend_streak.activity or from your backend: POST /v1/friend_streak_definitions/{id}/activities with { "customer_id", "at"? } (idempotent per day). Every friend who was also active that day extends the pair's streak. Milestone rewards are paid once per pair, length and player. After 18:00 in the streak's zone a pair with a streak of 2 or more where one friend has not played yet gets friend_streak.at_risk. GET /v1/customer/friend_streaks lists the player's streaks with length, longest, extended_today, at_risk and the next milestone.

Teams and team quests

A player is in at most one team. Teams have a name, a join_policy (open, invite or closed) and a capacity (2 to 500, default 30).

ActionRoute
My team, role and membersGET /v1/customer/team
Create a teamPOST /v1/customer/teams with { "name", "join_policy"?, "capacity"? }
Find open teamsGET /v1/customer/teams?query=<name prefix>
JoinPOST /v1/customer/team_memberships with { "team_id" } (open) or { "invite_code" }
LeaveDELETE /v1/customer/team_membership
Edit, new invite codePATCH /v1/customer/team, POST /v1/customer/team/invite_codes
Kick or change a rolePOST /v1/customer/team/members/{handle}/kick, POST /v1/customer/team/members/{handle}/role

Members are named by a per-team handle, never a customer id. The owner and admins manage the team; an owner who leaves passes ownership on, and the last member leaving disbands it. Blocks do not apply inside teams. Operators read GET /v1/teams, GET /v1/teams/{id} and GET /v1/teams/{id}/members, hide an offensive name with POST /v1/teams/{id}/moderations (shown as "Team " and 4 digits) and remove a member with POST /v1/teams/{id}/member_removals.

A team quest gives every team the same target for the period:

{
  "key": "team_week",
  "name": "Team week",
  "config": {
    "schema": "team_quest/v1",
    "period": { "kind": "week", "start_weekday": 1, "timezone": "environment" },
    "target": 500,
    "reward": [{ "currency": "xp", "amount": 100 }],
    "min_contribution": 10
  }
}

Progress comes from the rule action team_quest.contribute or from POST /v1/team_quest_definitions/{id}/contributions ({ "customer_id", "amount", "dedupe_key" }). When a team reaches the target, every current member who contributed at least min_contribution is paid once and gets team_quest.completed. A member withheld by an experiment that targets this team quest is recorded but paid nothing. Reads: GET /v1/customer/team/quests, GET /v1/customer/team_quest_boards/{definition} (teams ranked by progress), and for operators GET /v1/team_quest_definitions/{id}/board and GET /v1/team_quests/{id}/contributions.

Pausing

Pausing a friend quest, friend streak or team quest definition freezes it. While it is paused nothing starts, no progress or activity is recorded, and nothing expires or breaks; rewards already earned are still paid. When you resume, every quest that was running when the pause began gets its end moved later by the time the definition was paused, so players keep the time they had left. For friend streaks, every day the pause touched (in the streak's time zone) is excused: it neither breaks the streak nor counts toward it, so the streak continues where it was. The customer reads (GET /v1/customer/friend_quests, GET /v1/customer/friend_streaks, GET /v1/customer/team/quests) keep showing paused definitions with paused: true.

Experiments

Friend quest and team quest definitions can be experiment targets, named by key: { "type": "friend_quest", "key": "weekly_duo" } and { "type": "team_quest", "key": "team_week" }.

Changing definitions through change sets

In a live environment without auto autonomy an agent proposes definition changes as change-set commands (POST /v1/change_sets): friend_quest_definition.create, friend_streak_definition.create and team_quest_definition.create take the create body; .update takes key, expected_version and the PATCH fields; .activate, .pause, .resume and .archive take key and expected_version. Archiving is irreversible.

Feeding social mechanics from a frontend-only app

Rules can drive all three from customer events, so an app with no server of its own (Firebase, Supabase, React Native) needs only the publishable key and a customer session:

{
  "schema": "orishare.rule/v2",
  "trigger": { "kind": "customer_event", "event": "lesson.completed" },
  "actions": [
    { "type": "friend_streak.activity", "friend_streak": "duo_streak" },
    { "type": "friend_quest.contribute", "friend_quest": "weekly_duo", "amount": 1 },
    { "type": "team_quest.contribute", "team_quest": "team_week", "amount": 1 }
  ]
}

The event id is the contribution's dedupe key, so a retried event never counts twice. A player without a friend quest or a team, an anonymous player and an inactive definition are skipped, not errors. A browser event (customer_claim) cannot feed a definition that pays a monetary currency (the environment's monetary_currencies, or every currency when the trust policy lists none): pay those definitions in items or in a non-monetary currency such as XP, and list your real-money currencies with PATCH /v1/trust_policy. The full walk-through is the recipe Duolingo-style social app.

The hosted widget's friends and team surfaces show all of this without UI work (Hosted widget).

On this page