Orishare
Recipes

Roblox

One prompt: XP, levels, coins, streaks, badges, quests, leaderboards, leagues and referrals in a Roblox experience, from server scripts with HttpService.

Roblox clients cannot call outside services; only server Scripts can, with HttpService. So everything goes through the server: it reports what players do, reads their progress and hands it to the client with a RemoteEvent or RemoteFunction. The secret key never leaves the server.

The prompt

Paste this into your coding agent from the root of the Rojo (or Argon) project, or ask it to write the scripts for you to paste into Studio.

Add a reward system to this Roblox experience 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/roblox.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, write the server module and scripts the recipe describes, test the exact HTTP calls
against the test environment, then give me the claim link, where to put the secret in Studio, and a short
list of what you built. The secret key goes only in Roblox Secrets, never in a script.

What runs where

PieceHoldsDoes
Server ModuleScript OrishareThe secret key, from HttpService:GetSecret("orishare_key")Every call to Orishare
Server ScriptCalls the module on join, match end, level clear; answers the client's remote calls
Client LocalScriptNothing secretShows the HUD from what the server sends; asks for a repair or a referral code through a RemoteFunction

The player's id in Orishare is roblox-<UserId>: stable, yours, and the same on every server.

Steps

  1. Sign up and store the credentials with Start here for AI agents. Keep the test secret key and agent token in your own notes or .env outside the place file.

  2. Apply the game starter in the test environment: Game setup.

  3. Put the key in Roblox Secrets. Creator Dashboard, your experience, Secrets: name orishare_key, value the secret key, domain api.orishare.com. In Studio, Game Settings, Security: turn on Allow HTTP Requests. (Local Studio testing reads secrets from Game Settings, Security, Secrets in the same format.)

  4. The server module (ServerScriptService/Orishare):

    local HttpService = game:GetService("HttpService")
    local API = "https://api.orishare.com"
    local Orishare = {}
    
    local function request(method, path, body, idempotencyKey)
      local key = HttpService:GetSecret("orishare_key")
      local headers = {
        ["Authorization"] = key:AddPrefix("Bearer "),
        ["Content-Type"] = "application/json",
      }
      if idempotencyKey then headers["Idempotency-Key"] = idempotencyKey end
      local ok, res = pcall(HttpService.RequestAsync, HttpService, {
        Url = API .. path,
        Method = method,
        Headers = headers,
        Body = body and HttpService:JSONEncode(body) or nil,
      })
      if not ok then return nil, res end
      local decoded = res.Body ~= "" and HttpService:JSONDecode(res.Body) or nil
      if not res.Success then return nil, decoded end
      return decoded
    end
    
    function Orishare.customerId(player) return "roblox-" .. player.UserId end
    
    -- One event per fact. Reuse the same id on a retry: Orishare answers "duplicate".
    -- JSONEncode turns an empty table into [] (an array), which the API refuses with 400
    -- "expected record, received array": leave properties out when there are none.
    function Orishare.track(player, name, properties, id)
      local body = {
        id = id or HttpService:GenerateGUID(false),
        name = name,
        customer_id = Orishare.customerId(player),
        mode = "sync",
      }
      if properties and next(properties) ~= nil then body.properties = properties end
      return request("POST", "/v1/events", body)
    end
    
    -- The internal cust_ id (created on first use), cached per server.
    local custIds = {}
    function Orishare.customer(player)
      if custIds[player.UserId] then return custIds[player.UserId] end
      local c = request("POST", "/v1/customers", { external_id = Orishare.customerId(player) }, "customer-" .. player.UserId)
      if c then custIds[player.UserId] = c.id end
      return custIds[player.UserId]
    end
    
    -- Player reads (level, streak, badges, quests, leaderboard, league) are customer routes: the
    -- server mints a session for the player and calls them with the PUBLISHABLE key and that token.
    -- The token stays on the server. Sessions last 15 minutes; mint a new one when it expires.
    local PUBLISHABLE_KEY = "opk_..." -- not a secret; use the live environment's key in the published game
    local sessions = {}
    local function session(player)
      local s = sessions[player.UserId]
      if s and s.expires > os.time() + 30 then return s.token end
      local r = request("POST", "/v1/customer_sessions",
        { grant_type = "secret_key", customer_id = Orishare.customer(player) }, HttpService:GenerateGUID(false))
      if not r then return nil end
      sessions[player.UserId] = { token = r.customer_token, expires = os.time() + 14 * 60 }
      return r.customer_token
    end
    
    -- path: "balances", "levels", "streaks", "badges", "quests", "leaderboard_positions", "leagues"
    function Orishare.read(player, path)
      local token = session(player)
      if not token then return nil end
      local ok, res = pcall(HttpService.RequestAsync, HttpService, {
        Url = API .. "/v1/customer/" .. path,
        Method = "GET",
        Headers = { ["Authorization"] = "Bearer " .. PUBLISHABLE_KEY, ["X-Orishare-Customer-Token"] = token },
      })
      if not ok then return nil end
      if res.StatusCode == 401 then sessions[player.UserId] = nil end -- expired: mint again next call
      if not res.Success then return nil end
      return HttpService:JSONDecode(res.Body)
    end
    
    return Orishare

    Secret:AddPrefix builds the header without the key ever being a Lua string. Requests are limited per server (500 a minute): send one event per player action that matters, not per frame.

  5. The server script (ServerScriptService/Rewards):

    local Players = game:GetService("Players")
    local Orishare = require(script.Parent.Orishare)
    
    Players.PlayerAdded:Connect(function(player)
      -- once per UTC day per player is enough for the daily streak
      local t = os.date("!*t")
      local day = string.format("%04d-%02d-%02d", t.year, t.month, t.day)
      Orishare.track(player, "session.started", {}, "session-" .. player.UserId .. "-" .. day)
    end)
    
    -- Call from your round logic: one id per player per match.
    local function onMatchEnded(matchId, player, score, won)
      Orishare.track(player, "match.completed", { score = score, won = won }, "match-" .. matchId .. "-" .. player.UserId)
    end

    Send level.completed with { level = n } when a player clears a level. Then read the player's progress for the HUD with Orishare.read(player, "levels") (and "balances", "streaks", ...) and send the fields to the client with a RemoteEvent. The fields are in Game setup, server reads.

  6. Player actions from the server. The client asks through a RemoteFunction; the server acts for that player only:

    • Repair a broken streak with coins, when the streaks read shows repair.available: POST /v1/streaks/{id}/repairs ({id} is the daily_play program id, from GET /v1/programs?limit=100) with { customer_id = Orishare.customer(player), charge = true } and an Idempotency-Key. This is the server route, for the secret key the server holds; do not use POST /v1/customer/streaks/repairs from Roblox (it needs a player session and answers 401 invalid_customer_session to a secret key).
    • A referral code to share: POST /v1/referral_codes with { program_id = "<invite_friends program id>", customer_id = Orishare.customer(player) }. A new player types a friend's code: the server sends it to POST /v1/customer/referral_claims { code = ... } with that player's session (publishable key and token, like Orishare.read).
    • When the new player has really played, the server sends referral.qualified (Game setup, referrals). It pays automatically after a one-day cooling-off; only a referral over the recipe's limits waits for a review with the agent token (the game's secret key cannot approve it). Never trust a client value for anything but the choice to act: the server decides who the player is (player.UserId), never an id the client sent.
  7. Test the exact calls of the module from a terminal against the test environment (the module is a thin wrapper over them): Game setup, test. Then press Play in Studio with HTTP requests allowed.

  8. Go live with Game setup, go live: put the live secret key in Roblox Secrets for the published experience.

Notes

  • HttpService requests only work from the server; a LocalScript call fails.
  • Do not show a coin balance the client computed; show what the server read.
  • A display name other players see (PATCH /v1/customer/social_profile, leaderboard pseudonyms) must go through Roblox text filtering (TextService:FilterStringAsync) before you send it.
  • A mode: "sync" event can answer 202 with fallback: true under load: it is recorded and evaluated shortly, so read the HUD again a moment later instead of trusting the answer's effects.
  • Roblox players have no push device for Orishare: push messages to them are suppressed with no_device. Show reminders in game from the streaks read (at_risk_at). Email works without a device: if the player gives you an address and agrees, your server records both with POST /v1/customers/{id}/notification_contact (secret key, cust_ id) { "email": "[email protected]", "email_consent": "opted_in", "locale": "en" }, and the message types get an email route ({ "channel": "email", "path": "builtin" } in routes: send the full list with PATCH /v1/notification_message_types/{id} and If-Match: "<revision>" in a test environment, or as the change set command notification_message_type.update with key and expected_version in a live environment). That is enough: the default email_consent_mode is opt_in, and the recorded opted_in is that consent. Check that collecting an address fits Roblox's own rules for your game before you ask for one. Real email goes out only after a person claims the project; see Notifications.
  • Shop, items, energy, chests and the battle pass: Game economy.

On this page