API Reference

Everything the browser SDK, the server SDK, and the HTTP API expose.

Browser SDK

<script src="https://strafe.fun/js/strafe.js"></script>, defines the Strafe global. From npm: import { StrafeClient } from '@strafe-fun/sdk/browser'.

Setup

new Strafe({ appId })Creates the client: mounts the login widget and starts measuring sessions.
new Strafe({ appId, apiUrl })Points the client at a different host. Leave unset unless you know you need it. Login only accepts tokens from this origin.
new Strafe({ appId, consent })'auto' (default) shows the guest-analytics bar; 'manual' hands consent to your own UI; 'off' disables the region gate.
new Strafe({ appId, progression })Your milestones in order, e.g. ['game_start', 'tutorial_done', 'level_1']. Optional, it tells the progression report which sequence to measure.
new Strafe({ appId, errors })Crash reporting, on by default. false turns it off; { release, maxPerMinute, maxTotal, captureUnhandled, captureResources, beforeSend } tunes it.
new Strafe({ appId, performance })Frame rate and stalls while the game is played, on by default. Measures nothing about the player, only how their machine coped, and rides on the heartbeat already being sent. false turns it off.

Errors

strafe.captureException(err, context?)Report an error you caught yourself. context is a small object of scalars, stored with it.
strafe.captureMessage(text, context?)Report a problem that isn't an exception.
strafe.flushErrors()Promise<void>. Sends what's queued now instead of waiting for the batch.

Player

strafe.getUser(){ id, name, picture } for the signed-in player, or null.
strafe.getToken()The player's JWT, or null. Send it to your server to verify.
strafe.onChange(cb)Fires immediately with { user, token }, then on every login and logout.

Ruby

strafe.getRubies()Promise<{ balance, promos }>. The signed-in player's Ruby balance, and the promotions your game is running that they can still earn. balance is null when nobody is signed in.
strafe.spendRubies(amount, { item, key })Promise<{ ok, balance, error }>. Charges the player; the Rubies land in your game's treasury. The same key never charges twice. Requires a token issued for your app.
strafe.onRubyChange(cb)Called with { balance, rewards } now and on every change. rewards is any promotion strafe.fun has just paid this player, the moment to show '+1 Ruby'.

There is no way to give Rubies from the browser. See Ruby.

Progression

strafe.progress('level_1')Records that this player reached a milestone. Only their first reach counts toward conversion; calls are batched and flushed again on page close.

See Progression for naming steps and reading the funnel.

Diagnostics

strafe.getAppId()The app id this client reports to.
strafe.getSessionId()The session being measured, or null when this visit isn't tracked (consent declined, or Do-Not-Track).
strafe.getDeviceId()The persistent guest device id, or null when guest tracking isn't permitted.
strafe.destroy()Flushes the session, stops the heartbeats and removes the widget. For an SPA unmounting the client, a normal page doesn't need it.

Consent

strafe.optIn()Allow anonymous analytics for this visitor and start tracking.
strafe.optOut()Decline or withdraw. Stops guest tracking and drops the device id.
strafe.getConsent()'granted' | 'denied' | 'unknown'. Do-Not-Track and Global Privacy Control report 'denied'.

See Analytics for what each mode does where.

Server SDK

npm install @strafe-fun/sdk · const { Strafe } = require('@strafe-fun/sdk/server'). Needs your App Secret, so it belongs on a server you control.

Setup

new Strafe({ appId, appSecret })Connects over WebSocket and starts receiving player events. Reconnects with backoff; a bad secret closes with code 4003.
new Strafe({ appId, appSecret, errors })Crash reporting, on by default. Watches uncaught errors without changing how your process behaves; { captureUnhandled: false } leaves them alone.
strafe.close()Stops reconnecting, releases the process listeners, flushes queued reports.

Errors

strafe.captureException(err, context?)Report an error you caught yourself.
strafe.captureMessage(text, context?)Report a problem that isn't an exception.
strafe.flushErrors()Promise<void>. Sends what's queued now.

Events

strafe.on('playerJoin', cb)A player signed in. The Player object arrives with their saved data already loaded.

Player data

strafe.getPlayerData(playerId)Promise<object>. Everything saved for that player.
strafe.savePlayerData(playerId, data)Promise<void>. Replaces all data.
strafe.updatePlayerData(playerId, data)Promise<void>. Merges the given fields.

Ruby

strafe.getRubyTreasury()Promise<{ balance, history } | null>. Your game's own Rubies, and where they went.
strafe.getPlayerRubies(playerId)Promise<number | null>. What one of your players holds, across all of strafe.fun.
strafe.grantRubies(playerId, amount, { key, reason })Promise<{ ok, treasury, error }>. Pays a player out of your treasury. The key makes a retry safe; a treasury that cannot cover it returns ok: false rather than going negative.

Coin payments

strafe.createCheckout(userId, { item, priceCents, coins, reference })Promise<{ ok, url, order, replay, error }>. Opens one purchase and returns the URL to send the player to. The same reference returns the first checkout rather than charging twice.
strafe.getCoinOrder(orderId)Promise<order | null>. One purchase, whatever state it is in. What to call when a player returns from checkout, instead of trusting their word.
strafe.listCoinOrders({ since })Promise<order[]>. Every paid purchase after that instant, oldest first. Resolves [] on failure, so a cursor never advances past something it did not see.

Progression

strafe.progress(playerId, step)Promise<void>. Records a milestone your server vouches for. step is a name or an array of them.
strafe.getProgressionReport({ days, steps, population })Promise<report | null>. The funnel as JSON: players per step, drop-off, and time to reach.

Auth

strafe.verifyToken(token)Verifies a player token and returns its claims, or null if invalid or expired.

HTTP API

Base URL https://strafe.fun. Server calls authenticate with X-App-Id + X-App-Secret; browser calls with X-App-Id and, where a player is involved, Authorization: Bearer <player token>.

Auth

POST /api/auth/verify-tokenApp secret. Body { token }. Returns { valid, userId, name, email, discordId, solanaWallet } or 401.

Player data

GET /api/player/data?playerId=App secret. Returns { data }. A browser may call it for itself with a bearer token and no playerId.
PUT /api/player/dataApp secret. Body { playerId, data }. Replaces all data.
PATCH /api/player/dataApp secret. Body { playerId, data }. Merges fields.

Sessions and presence (sent for you by the browser SDK)

POST /api/player/sessionOpens a session. Body { sessionId, deviceId? }; bearer token optional; a signed-in player's account attaches to the session.
POST /api/player/session/heartbeatBody { sessionId, durationSec, deviceId? }. Duration is cumulative and applied with max, so repeats are harmless.
POST /api/player/joinBearer. Records the login and returns the player's permission status.
POST /api/player/presenceBody { anonId }. Feeds the live player count; DELETE to leave.
POST /api/player/heartbeatBearer. Keeps a signed-in player in the live count.

Checking your integration

POST /api/sdk/verifyPublic. Body { appId, sessionId?, deviceId? }. Optional bearer. Reports only on ids you supply.
GET /api/me/creations/[gameId]/integrationYour portal session. The setup checklist for one of your games, over all its players.
curl -X POST https://strafe.fun/api/sdk/verify \
  -H 'Content-Type: application/json' \
  -d '{"appId":"app_...","sessionId":"<from strafe.getSessionId()>"}'

{
  "app":     { "id": "app_...", "exists": true, "active": true },
  "session": { "sessionId": "...", "date": "2026-07-27", "startedAt": "...",
               "durationSec": 63, "identity": "account" },
  "device":  { "sessions": 4, "firstDate": "2026-07-20", "lastAt": "..." },
  "errors":  { "session": 1, "lastAt": "..." },
  "auth":    { "valid": true, "userId": "...", "name": "..." }
}
/api/sdk/verify answers only for identifiers you already hold, a session id and a device id are random values minted by your own browser. It never reports another player's session or any total for the app; those need the owner endpoint above.

Errors

POST /api/sdk/errorsBody { events[], sessionId?, deviceId? }. App ID alone files them as browser errors; App ID + App Secret as server ones. Up to 20 events a request.
GET /api/sdk/errorsApp secret. ?status=open|resolved|ignored|all, ?runtime=browser|server, ?days=, ?limit=, ?fingerprint=, ?format=markdown.
GET /api/me/creations/[gameId]/errorsYour portal session. The same list plus a summary; ?format=markdown returns the agent brief.
PATCH /api/me/creations/[gameId]/errors/[fingerprint]Your portal session. Body { status: 'open' | 'resolved' | 'ignored' }.
curl -H "X-App-Id: $STRAFE_APP_ID" -H "X-App-Secret: $STRAFE_APP_SECRET" \
  "https://strafe.fun/api/sdk/errors?status=open"

{
  "retentionDays": 30,
  "errors": [
    { "fingerprint": "a1b2c3d4e5f60718", "runtime": "browser", "type": "TypeError",
      "message": "Cannot read properties of undefined (reading 'hp')",
      "culprit": "updateHud (/js/main.js:412)", "status": "open", "count": 128,
      "firstSeen": "...", "lastSeen": "...", "regressedAt": null, "release": "1.4.2",
      "impact": { "players": 37, "sessions": 41, "events": 88 } }
  ]
}

count covers all history; impact covers the last retentionDays, since it is counted from the individual occurrences, which expire. Add &format=markdown to any of these for the brief described in Errors.

Analytics

GET /api/me/creations/[gameId]/analyticsYour portal session. ?days=7|14|30|90 (default 14), ?population=all|loggedin.
{
  "windowDays": 14,
  "range":    { "start": "2026-07-14", "end": "2026-07-27" },
  "overview": { "players": 812, "newPlayers": 240, "sessions": 3120, "avgSessionDuration": 412 },
  "previous": { ... },                       // the equally long window before it
  "lifetime": { "players": 9021, "sessions": 41233, "avgSessionDuration": 388 },
  "retention": { "d1": 34, "d7": 18, "d14": 12, "d28": 9 },   // -1 = not enough history
  "daily": [
    { "date": "2026-07-14", "dau": 61, "newPlayers": 12, "sessions": 143,
      "avgDuration": 407, "d1": { "cohort": 12, "returned": 4, "percent": 33 } },
    { "date": "2026-07-27", "dau": 58, "newPlayers": 9, "sessions": 121,
      "avgDuration": 431, "d1": null }      // null = the cohort's day 1 isn't over yet
  ]
}

daily is dense: every day in the window is present, zeros included. See Analytics for what each figure counts.

Ruby

GET /api/player/rubyBearer + X-App-Id. { balance, promos } for the signed-in player.
POST /api/player/ruby/spendBearer + X-App-Id. Body { amount, key, item? }. The token must have been issued for this app. 409 when the player cannot cover it.
GET /api/sdk/rubyApp secret. Your treasury and its history; ?playerId= adds that player's balance.
POST /api/sdk/rubyApp secret. Body { playerId, amount, key, reason? }. Pays a player out of your treasury.
GET /api/promosPublic. Every Ruby promotion running on strafe.fun, what the counter in the navbar lists.

Coin payments

POST /api/sdk/checkoutsApp secret. Body { userId, item, priceCents, coins?, reference?, accept? }. Returns { checkout, url, replay }. priceCents is 25 to 50000; reference is unique per game and makes a retry one purchase; accept lists the currencies this purchase may be paid in and defaults to USDC only.
GET /api/sdk/coin-ordersApp secret. ?orderId= is one purchase in any state; ?since=<ISO date> is every paid one after that instant, oldest first, with a cursor to walk forward, plus ?after=<order id> so two paid in the same millisecond cannot straddle a page boundary. Without either, from the oldest.

Progression

POST /api/player/progressSent for you by the browser SDK. Body { steps: [{ step, index?, elapsedMs?, count? }], deviceId? }; bearer optional. With an app secret instead, body { userId, steps } records a milestone your server vouches for.
GET /api/reports/progressionApp secret. The funnel as JSON, readable by a script or an agent, with no browser session. ?days=1..365 (default 30), ?steps=a,b,c, ?population=all|loggedin.
GET /api/me/creations/[gameId]/progressionYour portal session. The same report for one of your games, what the Dashboard tab draws.
curl -H "X-App-Id: $STRAFE_APP_ID" -H "X-App-Secret: $STRAFE_APP_SECRET" \
  "https://strafe.fun/api/reports/progression?days=30"

{
  "range":     { "days": 30, "start": "2026-06-28", "end": "2026-07-27" },
  "stepOrder": "declared",              // or "requested" / "inferred"
  "funnel": [
    { "step": "game_start",    "position": 1, "players": 412, "conversionFromEntry": 100,
      "conversionFromPrevious": null, "droppedFromPrevious": null, "dropOffRate": null,
      "medianSecondsFromEntry": 0, "medianSecondsIntoSession": 4, "reachesPerPlayer": 1.1 },
    { "step": "tutorial_done", "position": 2, "players": 328, "conversionFromEntry": 80,
      "conversionFromPrevious": 80, "droppedFromPrevious": 84, "dropOffRate": 20,
      "medianSecondsFromEntry": 190, "medianSecondsIntoSession": 194, "reachesPerPlayer": 1 }
  ],
  "summary": { "entered": 412, "completed": 37, "completionRate": 9,
               "biggestDrop": { "from": "tutorial_done", "to": "level_1_clear",
                                "fromPlayers": 328, "lostPlayers": 180, "dropOffRate": 55,
                                "medianSecondsFromEntry": 190 },
               "missingEntryStep": 0, "headline": "Players drop most between ..." },
  "unmappedSteps": [], "notes": ["..."], "definitions": { "funnel[].players": "..." }
}
Every progression report carries its own definitions and notes, so it can be pasted into a script, or an agent, without these docs alongside it. The notes say which caveats apply to those numbers. See Progression.

Types

Player (server SDK)

{
  id: string
  name: string
  data: Record<string, unknown>
  highScore: number
}

UserInfo (browser SDK)

{
  id: string
  name: string
  picture?: string
}