Activity tracking

How long your staff are actually in your game, measured by your own Roblox servers and reported to Rostack on a timer. It is the number payroll and quotas are built on, so the whole design is arranged around one rule: reporting the same thing twice must never pay anybody twice.

What it measures

A single Luau script runs on your game servers. It watches everyone in the server and keeps three running totals per visit: wall-clock minutes since they joined, chat messages they sent, and how much of that time they were idle. Every five minutes, and once more as the server shuts down, it posts those totals to Rostack.

The numbers come from your own servers rather than from anything Rostack infers. Nothing here polls Roblox to guess who is online: if a server reported it, it happened, and if no server has ever reported, the Activity page says it is not connected instead of showing a convincing zero.

From those visits Rostack derives what the rest of the product reads: minutes and active minutes, chat counts, how many distinct game servers somebody joined, and how many days in a window they were seen at all.

Setup

  1. 1

    Create a workspace API key

    In your workspace, open Ranking and create an API key. One key per game is enough. It is shown once and stored only as a hash, so copy it straight into the script.

    Treat it like a password

    Anyone holding the key can post activity for your workspace. Keep it out of client scripts and out of anything you share.
  2. 2

    Turn on HTTP requests

    In Studio: Game Settings, then Security, then Allow HTTP Requests. Without it the script cannot reach Rostack, and it says so in the output window rather than failing quietly.

  3. 3

    Add the module to ServerScriptService

    Download rostack.app/rostack-activity.lua, which the Activity page in your workspace also links, and paste its contents into a new Script in ServerScriptService. It has to be a Script: a LocalScript runs on the client, and the module switches itself off if it finds itself there.

  4. 4

    Paste your key into CONFIG.API_KEY

    The placeholder at the top of the file is rsk_live_PASTE_YOUR_KEY_HERE. The script refuses to start while that is still there, so a forgotten key is one warning in the output window rather than a week of missing minutes.

  5. 5

    Publish the place

    In Studio, game.PlaceId and game.GameId are both 0 until the place has been published, and the script sends nothing without them. Publish, then join. The first report lands within five minutes.

The endpoint

POST https://rostack.app/api/v1/activity
Authorization: Bearer rsk_live_…
Content-Type: application/json

The body describes one server and the visits it is reporting:

FieldTypeDescription
placeIdnumbergame.PlaceId. Positive, so an unpublished place cannot report.
universeIdnumbergame.GameId. Positive, for the same reason.
jobIdstringgame.JobId, which identifies one running server. Letters, digits, dash and underscore only, because it goes into a document id.
sessionsarrayOne entry per player being reported, at most 200 per request. A fuller server sends more than one request.

Each entry in that array is one visit to this server:

FieldTypeDescription
robloxUserIdnumberWho the visit belongs to.
usernamestringTheir Roblox username at the time of the report, 1 to 50 characters.
startedAtnumberWhen they joined, in epoch seconds. Part of the record's identity, so it must not change during a visit.
endedAtnumber or nullWhen they left, in epoch seconds. Null while they are still there. Recorded once and never moved afterwards.
minutesnumberCumulative whole minutes in this server since startedAt, at most 1440. Never a delta.
chatsnumberCumulative chat messages sent during this visit.
afkMinutesnumberThe idle part of minutes. It may never exceed minutes, and the whole report is rejected if it does.
presentbooleanWhether they were still in the server when the report was cut. A visit with an end time cannot also be present.

Timestamps are epoch seconds, which is what os.time() returns. Formatted dates are rejected.

curl

curl -X POST https://rostack.app/api/v1/activity \
  -H "Authorization: Bearer rsk_live_xxx" \
  -H "Content-Type: application/json" \
  -d '{
    "placeId": 4922741943,
    "universeId": 1719029934,
    "jobId": "e6f1c0d2-9b64-4f0a-8f2b-1d5a7c33ab10",
    "sessions": [
      {
        "robloxUserId": 3961562630,
        "username": "xFirmastic",
        "startedAt": 1757318400,
        "endedAt": null,
        "minutes": 42,
        "chats": 17,
        "afkMinutes": 5,
        "present": true
      }
    ]
  }'

Why a retry is safe

This is the most important thing on the page. Every report carries the cumulative totals for the whole visit, never the change since the last report, and each visit is written at a document id the reporting server can reproduce from what it already knows. Sending the same report ten times writes the same document ten times.

the same visit, reported four times

document id   {robloxUserId}_{jobId}_{startedAt}
example       3961562630_e6f1c0d2-9b64-…-ab10_1757318400

report 1      minutes 42   ->  written
report 2      minutes 42   ->  unchanged, nothing written
report 3      minutes 47   ->  written, 47 replaces 42
retry of 2    minutes 42   ->  47 is kept, the larger of the two

Where a stored counter and an incoming one disagree, the larger wins. Counters within one visit only ever rise, so taking the maximum is idempotent, meaning a duplicate changes nothing, and order independent, meaning a retry delayed behind a newer report cannot walk somebody's minutes backwards. The day totals underneath obey the same rule: they are recomputed from the visits rather than added to, so replaying a week of reports produces the same numbers as playing them once.

These minutes decide Robux

An HTTP response can be lost after the write has already committed, and a game server cannot tell that from a rejection, so it retries. Nothing in this path increments anything. Change the module to send "minutes since the last report" and one lost response counts that time twice, in a payout that cannot be clawed back.

Who is recorded

The module reports everybody in the server. Rostack keeps activity only for people who have a staff record in the workspace, which means they hold one of the Roblox ranks you marked as staff on the Roles page. A retired staff record is treated the same as a stranger.

Everyone else is dropped before anything is written, and nothing about them is stored. A public game is full of people your group has no employment relationship with, and keeping their names and play time would be collecting personal data on strangers for no purpose anybody asked for.

A misconfigured role list is diagnosable from inside the game

The response counts the players that were skipped as ignored. If your staff are in the server and every report comes back with accepted: 0 and a large ignored, the ranks nominated on the Roles page are not the ranks your staff actually hold.

AFK time

The module takes a sample every 15 seconds. Somebody who has moved more than three studs since the last sample, or sent a chat message, counts as active. Roblox also tells the server when a player's own client has seen no input for a while, and the module uses that to backdate their last sign of life rather than waiting for the movement check to notice. Five minutes with no sign of life starts accumulating idle time from that point on, sample by sample, so a person who drifts in and out of idle is measured rather than written off from the first moment they stood still.

Idle time is reported separately and is never subtracted from the total. minutes is wall-clock time in the server and includes the AFK part; active minutes are simply minutes - afkMinutes, and Rostack recomputes that rather than trusting a stored copy. Both are kept because a community decides for itself whether idle time is paid, and a payroll rule reading the wrong one is a money bug. Payroll rules choose between them with a countAfk switch, which is off by default.

Reading the response

200 OK

{
  "ok": true,
  "accepted": 1,
  "unchanged": 0,
  "ignored": 12,
  "days": 1
}
FieldWhat it counts
acceptedVisits written because something had actually moved since the last report.
unchangedVisits whose stored totals already matched or led the report. A retry, or a report cut inside the same minute. Nothing was written, and nothing was lost.
ignoredPlayers in the body with no staff record in this workspace. Nothing about them was stored.
daysDay totals recomputed as a result: one per person per day the report touched.

A steady stream of unchanged is healthy. It means people are in the server and their totals have not crossed a minute boundary yet. Failures carry a stable error code with a human message: missing_key or unauthorized at 401, invalid_request at 400 with the expected shape attached, and internal at 500. The module gives up on 400, 401 and 403, because a malformed body or a revoked key fails identically five seconds later, and retries anything else after 2, 5 and 15 seconds before leaving it to the next cycle.

Honest limits

LimitWhat it means for you
A visit belongs to the UTC day it started onA session running across midnight counts entirely against the day it began. The reporting server can reproduce that attribution without asking Rostack anything, and splitting it would make the document id ambiguous.
A revoked key keeps working for up to five minutesResolved keys are cached in the process that handled the request, because authenticating every report against the database would dominate the cost of the busiest endpoint in the product. Revoking takes effect in the database at once and at this endpoint within five minutes.
All time over a large community is expensiveReads scale with the number of active person-days in the window rather than with the size of your roster, so a year across four hundred staff is a lot of documents. Prefer the narrowest range that answers the question.
A crashed server leaves visits open for a whileA force-shut server never sends an end time. A sweep closes visits that have not been reported for 15 minutes. Closing changes no counter, so the totals are the same either way.
Windows are trailing, not calendar alignedThis week means the last seven days including today, not the days since Monday. A window that reset on a boundary would let somebody idle out Sunday and start clean.

Turning the module off does not turn off the record

Activity is a workspace module and can be removed from the sidebar. Game servers keep reporting while it is off and nothing is lost. Switching off a page hides a view, never the evidence.