Orishare
Recipes

Web game

One prompt: XP, levels, coins, streaks, badges, quests, leaderboards, leagues and referrals in a browser game with a game server. What runs where, the code, and how to test it.

For a browser game in JavaScript or TypeScript (Phaser, PixiJS, three.js, plain canvas, an io-style game) that has a game server: Node, Deno, Bun, a serverless function or any backend that decides who won. Without any server, use the Firebase or Supabase recipe instead.

The prompt

Paste this into your coding agent from the root of the game's repository.

Add a reward system to this web game with Orishare: XP and levels, coins, a daily streak with
repair, badges, a starter quest, a weekly XP leaderboard, a weekly league and friend referrals.
Follow https://docs.orishare.com/docs/recipes/web-game.md and the pages it links, step by step:
sign up for Orishare yourself (no human needed), apply the game starter configuration in the
test environment, integrate the game server and the browser client as the recipe says, test everything in
the test environment, then give me the claim link and a short list of what you built.
Never put a secret key (osk_) or agent token (opat_) in browser code.

What runs where

PieceHoldsDoes
Game serverSecret key (osk_), env var ORISHARE_SECRET_KEYReports what happened in a match (POST /v1/events), mints a customer session for a signed-in player
BrowserPublishable key (opk_), the player's customer session token in memoryReads the player's progress (the /v1/customer/... reads), repairs a streak, makes a referral code, claims a quest

Match results come from the server because the server is the authority: its events are merchant_asserted and never capped. The browser may also report light activity (session.started) with the player's session; those count up to the per-day caps of the game starter.

Steps

  1. Sign up and store the credentials. Follow Start here for AI agents: you get a test and a live environment, keys, an agent token and a claim link. Put the secret key and agent token in the server's environment (a .env the repository ignores).

  2. Apply the game starter in the test environment: Game setup. One change set creates the currencies, levels, streak, badges, quest, leaderboard and referral program; the league is one more call.

  3. Keys. The secret key from the signup reports events and mints customer sessions. A key you create later needs explicit scopes for that (Game setup, session key).

  4. Server: report matches. When a match ends, for each player:

    // server/orishare.js
    const API = process.env.ORISHARE_API ?? 'https://api.orishare.com';
    const KEY = process.env.ORISHARE_SECRET_KEY; // osk_ from the signup
    
    export async function reportMatch({ matchId, playerId, score, won, endedAt }) {
      const res = await fetch(`${API}/v1/events`, {
        method: 'POST',
        headers: { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' },
        body: JSON.stringify({
          // One id per fact: a retry with the same id is answered `duplicate`, never counted twice.
          id: `match-${matchId}-${playerId}`,
          name: 'match.completed',
          customer_id: playerId, // your own player id (the external id)
          occurred_at: endedAt, // when the match ended (ISO 8601): stored with the match, the same
          // on every retry, and never in the future (clamp to now on clock skew)
          properties: { score, won },
          mode: 'sync', // evaluate now and return the effects (XP, coins, badges)
        }),
      });
      // 200 with status "duplicate": already recorded. 409 event_id_conflict: same id, other body.
      if (!res.ok) throw new Error(`orishare ${res.status}`);
      const body = await res.json();
      // Under load sync falls back to 202 with `fallback: true` and no effects: the event is still
      // recorded and evaluated shortly; read the player's state again instead of the effects.
      return body;
    }

    Send level.completed (properties: { level }) the same way when a player clears a level. Retry a failed send with the same id and the same body (occurred_at included: store it with the match rather than computing it per attempt) (a different body under the same id is 409 event_id_conflict); see Stable retry ids.

    /v1/events or /v1/match_results? Both pay the same game starter rules: a match result admits each player as a match.completed event with properties.won (send "won" per player) and the profile's metrics, and the starter's win rules, badge and quest step read won. Use POST /v1/events as above to start: no setup, and mode: "sync" returns the effects (XP, coins, badges). Switch to POST /v1/match_results once you want each result checked against a match profile (metric bounds, duration, player count, rate limits, held reviews): it needs a match_profile.create change set first, and its answer carries each player's admission status, not the XP or coin effects, so read the player's balances and levels afterwards. The Game economy recipe uses match results. Send each match through one of the two, never both, or it pays twice.

  5. Server: give the browser a session. After your own login decides who the player is, resolve the internal customer id and mint a session. Return only the token:

    export async function customerSession(playerId) {
      const auth = { Authorization: `Bearer ${KEY}`, 'Content-Type': 'application/json' };
      const customer = await fetch(`${API}/v1/customers`, {
        method: 'POST',
        headers: { ...auth, 'Idempotency-Key': `customer-${playerId}` },
        body: JSON.stringify({ external_id: playerId }),
      }).then((r) => r.json()); // 201 created or 200 existing, either way { id: 'cust_...' }
      const session = await fetch(`${API}/v1/customer_sessions`, {
        method: 'POST',
        headers: { ...auth, 'Idempotency-Key': crypto.randomUUID() },
        body: JSON.stringify({ grant_type: 'secret_key', customer_id: customer.id }),
      }).then((r) => r.json());
      // customer_id: the player's cust_ id. Customer event ids are already per player.
      return {
        token: session.customer_token,
        expires_at: session.expires_at,
        customer_id: customer.id,
      };
    }

    Expose it as an endpoint of your game server (for example /api/orishare-session, method POST) that only a signed-in player can call.

  6. Browser: read and act. Every customer call carries the publishable key and the token:

    // client/orishare.js
    const API = 'https://api.orishare.com';
    const PUBLISHABLE_KEY = 'opk_...'; // public config
    const SESSION_ENDPOINT = '/api/orishare-session'; // your game server, step 5
    let token = null; // memory only: never localStorage, cookies or URLs
    
    async function session() {
      if (!token) token = (await (await fetch(SESSION_ENDPOINT, { method: 'POST' })).json()).token;
      return token;
    }
    export async function customer(path, init = {}) {
      const res = await fetch(API + '/v1/customer/' + path, {
        ...init,
        credentials: 'omit',
        headers: {
          Authorization: `Bearer ${PUBLISHABLE_KEY}`,
          'X-Orishare-Customer-Token': await session(),
          'Content-Type': 'application/json',
          ...(init.method && init.method !== 'GET'
            ? { 'Idempotency-Key': crypto.randomUUID() }
            : {}),
          ...init.headers,
        },
      });
      if (res.status === 401) token = null; // expired: get a new one on the next call
      return res.json();
    }
    // HUD: customer('levels'), customer('balances'), customer('streaks'), customer('badges'),
    // customer('quests'), customer('leagues'), customer('referral_codes')

    The reads and the player actions (repair, referral code, quest claim) are in Game setup, player screens. Sessions last 15 minutes by default: renew or mint again (Session lifetime).

  7. Allow the game's origin. A browser on another origin than your server is refused until its origin is allowed. Origins given as allowed_origins at signup are already set (GET /v1/client_configuration); to add one, PATCH /v1/client_configuration with the test secret key (Hosted widget, allow your page's origin shows the body and the If-Match header). http://localhost works in the test environment.

  8. Test in the test environment with Game setup, test: send a few matches for a test player, then read the player's state.

  9. Go live with Game setup, go live.

Optional

On this page