Writing a client
Windows · macOS · Android · home
A complete updater, in the order the calls should be made. The shape below is what a host keeping a Roblox client up to date actually needs; adapt the install step to your target.
The loop
- Ask what should be installed.
- Compare against what is installed. Stop if equal.
- Download, following redirects.
- Verify the sha256.
- Install according to
kind. - Record the version you installed.
Compare version, not displayVersion in step 2.
The dotted string is not unique, so two different desktop builds can carry the same one and an
updater keyed on it will skip a real update.
bash
#!/usr/bin/env bash
set -euo pipefail
API="https://rbxoffsets.com"
PLATFORM="${1:-windows}" # windows | macos | android
STATE="/var/lib/roblox-tracker/$PLATFORM-version"
mkdir -p "$(dirname "$STATE")"
# 1. what should be installed
meta="$(curl -fsS "$API/api/v1/$PLATFORM/files/latest")"
version="$(printf '%s' "$meta" | jq -r .version)"
kind="$(printf '%s' "$meta" | jq -r .kind)"
name="$(printf '%s' "$meta" | jq -r .fileName)"
want_sha="$(printf '%s' "$meta" | jq -r .sha256)"
# 2. already there? (compare the identity, never displayVersion)
if [ -f "$STATE" ] && [ "$(cat "$STATE")" = "$version" ]; then
echo "up to date: $version"; exit 0
fi
# 3. download - -L is required, the endpoint redirects to Cloudflare
out="/tmp/$name"
curl -fL --retry 3 --retry-delay 5 -o "$out" "$API/download/$PLATFORM/$version"
# 4. verify
if [ -n "$want_sha" ]; then
got="$(sha256sum "$out" | cut -d' ' -f1)"
[ "$got" = "$want_sha" ] || { echo "sha256 mismatch, refusing"; rm -f "$out"; exit 1; }
else
echo "warning: this build is unpinned (no published sha256)"
fi
# 5. install
case "$kind" in
apk) adb install -r "$out" ;;
xapk) tmp="$(mktemp -d)"; unzip -q "$out" -d "$tmp"
adb install-multiple "$tmp"/*.apk ;;
zip) unzip -oq "$out" -d /opt/roblox ;;
esac
# 6. remember
printf '%s' "$version" > "$STATE"
rm -f "$out"
echo "installed $version"
python
import hashlib, requests
API = "https://rbxoffsets.com"
PLATFORM = "windows"
meta = requests.get(f"{API}/api/v1/{PLATFORM}/files/latest", timeout=15).json()
with requests.get(f"{API}/download/{PLATFORM}/{meta['version']}", stream=True,
allow_redirects=True, timeout=(15, 600)) as r:
r.raise_for_status()
digest = hashlib.sha256()
with open(meta["fileName"], "wb") as f:
for chunk in r.iter_content(1 << 20):
f.write(chunk)
digest.update(chunk)
if meta["sha256"] and digest.hexdigest() != meta["sha256"]:
raise SystemExit("sha256 mismatch")
Offsets for the build you run, in eight languages
The other half of most clients: ask for the offsets of the exact build installed, verified only, and stop if there are none. Each example follows the redirect and refuses to substitute.
Python (standard library only)
import urllib.request, urllib.error
API = "https://rbxoffsets.com"
version = open("installed-version.txt").read().strip()
try:
with urllib.request.urlopen(f"{API}/api/v1/windows/offsets/{version}/hpp?tier=verified") as r:
open("offsets.hpp", "wb").write(r.read()) # urllib follows the 302
print("tier:", r.headers.get("x-offsets-tier"))
except urllib.error.HTTPError as e:
raise SystemExit(f"no verified offsets for {version}: {e.code}")
Node.js / TypeScript (18+)
const API = "https://rbxoffsets.com";
const version = (await (await fetch(`${API}/api/v1/windows/version.txt`)).text()).trim();
const res = await fetch(`${API}/api/v1/windows/offsets/${version}/hpp?tier=verified`);
if (!res.ok) throw new Error(`no verified offsets for ${version}: ${res.status}`);
await (await import("node:fs/promises")).writeFile("offsets.hpp", Buffer.from(await res.arrayBuffer()));
C# (.NET 6+)
using var http = new HttpClient(); // follows redirects by default
const string Api = "https://rbxoffsets.com";
var version = (await http.GetStringAsync($"{Api}/api/v1/windows/version.txt")).Trim();
var res = await http.GetAsync($"{Api}/api/v1/windows/offsets/{version}/hpp?tier=verified");
if (!res.IsSuccessStatusCode) throw new Exception($"no verified offsets for {version}: {(int)res.StatusCode}");
await File.WriteAllBytesAsync("offsets.hpp", await res.Content.ReadAsByteArrayAsync());
C++ (Windows, WinHTTP)
// Link winhttp.lib. WinHTTP follows the 302 to storage automatically.
#include <windows.h>
#include <winhttp.h>
#include <string>
std::string Get(const std::wstring& host, const std::wstring& path, DWORD* status) {
std::string body;
HINTERNET s = WinHttpOpen(L"my-client/1.0", WINHTTP_ACCESS_TYPE_AUTOMATIC_PROXY, nullptr, nullptr, 0);
HINTERNET c = WinHttpConnect(s, host.c_str(), INTERNET_DEFAULT_HTTPS_PORT, 0);
HINTERNET r = WinHttpOpenRequest(c, L"GET", path.c_str(), nullptr, nullptr, nullptr, WINHTTP_FLAG_SECURE);
if (WinHttpSendRequest(r, nullptr, 0, nullptr, 0, 0, 0) && WinHttpReceiveResponse(r, nullptr)) {
DWORD size = sizeof(*status);
WinHttpQueryHeaders(r, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, nullptr, status, &size, nullptr);
for (DWORD n = 0; WinHttpQueryDataAvailable(r, &n) && n;) {
std::string chunk(n, '\0'); DWORD read = 0;
WinHttpReadData(r, chunk.data(), n, &read); body.append(chunk, 0, read);
}
}
WinHttpCloseHandle(r); WinHttpCloseHandle(c); WinHttpCloseHandle(s);
return body;
}
// DWORD status = 0;
// auto hpp = Get(L"your.host", L"/api/v1/windows/offsets/" + version + L"/hpp?tier=verified", &status);
// if (status != 200) { /* no verified offsets for this build: do not attach */ }
Rust (reqwest, blocking)
let api = "https://rbxoffsets.com";
let version = reqwest::blocking::get(format!("{api}/api/v1/windows/version.txt"))?.text()?.trim().to_string();
let res = reqwest::blocking::get(format!("{api}/api/v1/windows/offsets/{version}/hpp?tier=verified"))?;
if !res.status().is_success() {
anyhow::bail!("no verified offsets for {version}: {}", res.status());
}
std::fs::write("offsets.hpp", res.bytes()?)?;
Go
api := "https://rbxoffsets.com"
v, _ := http.Get(api + "/api/v1/windows/version.txt")
raw, _ := io.ReadAll(v.Body); v.Body.Close()
version := strings.TrimSpace(string(raw))
res, err := http.Get(api + "/api/v1/windows/offsets/" + version + "/hpp?tier=verified") // follows the 302
if err != nil || res.StatusCode != 200 {
log.Fatalf("no verified offsets for %s", version)
}
defer res.Body.Close()
f, _ := os.Create("offsets.hpp"); defer f.Close()
io.Copy(f, res.Body)
Lua
-- Shells out to curl, which every desktop OS ships.
local api = "https://rbxoffsets.com"
local version = io.popen("curl -fsS " .. api .. "/api/v1/windows/version.txt"):read("*l")
local ok = os.execute(("curl -fsSL -o offsets.hpp %q"):format(
api .. "/api/v1/windows/offsets/" .. version .. "/hpp?tier=verified"))
if not ok then error("no verified offsets for " .. version) end
Watching every platform at once
curl -s https://rbxoffsets.com/api/v1/platforms \
| jq -r '.platforms[] | "\(.label)\t\(.displayVersion)\t\(.releasedAt)"'
Polling politely
The tracker itself checks upstream every 60 seconds, so asking
more often than that learns nothing new. Once every 5 to 15 minutes is plenty
for an updater. The metadata endpoints are database reads and cost this server almost nothing;
/api/v1/{platform}/current may reach upstream, so prefer
/files/latest in a loop and keep /current for the moment you
actually intend to install.
Responses are sent with Cache-Control: no-store. There is no rate limit today.
Set a real User-Agent so an unexpected traffic pattern can be traced to its owner
rather than blocked.
Things that will bite you
- Treating
displayVersionas the identity. It is not unique. Key everything onversion. - Not following redirects. You get a 302 body, not a file, and it looks like a corrupt download.
- Caching the signed URL. It expires in about 60 minutes. Always start from the
/download/path. - Assuming
latestequals the newest Roblox release. It is the newest stored build that carries your ABI, which on Android can be several releases behind what Roblox is serving. When they differ,/api/v1/{platform}/currentexplains why inheldBack. - Assuming offsets belong to the live build. They belong to the newest
x86_64build, which is whatoffsetsTargetmarks. When Roblox is serving an arm-only release those are different versions, and pairing offsets with the live version number will not work. - Falling back to
latestoffsets. Offsets for a neighbouring build are not approximately right. Fail instead. - Assuming offsets are verified. Without
?tier=verifiedyou get the best set that exists, which may be the default tier. Readx-offsets-tier, or ask for verified explicitly if your client must not run otherwise. - Treating an upcoming build as released. Upcoming builds can be pulled. Install one only if you are deliberately testing ahead of production.
- Ignoring
kind. Anxapkinstalled as a plain APK fails every time. - Treating an empty
sha256as verified. Empty means unknown. Log it. - Sorting Android versions as strings.
2.734.99sorts above2.734.917lexically and is older. Compare the dotted segments as integers, or compareversionCode. Desktop identities do not sort at all — usereleasedAt.