uloggdDevelopers
Developer portaluloggd
Get startedResourcesArcade
Developer portaluloggdArcadeHostingThe frameSDKIdentityVerifying in other languagesData eventsTestingReview policyPrivacy dutiesEngines

Identity

Know who is playing, from a five-minute token signed by uloggd and verified by your server, offline.

A game's client can claim anything about who is playing. Your server needs proof, and gets it as an identity token: a JWT signed by uloggd for one player, one app and one nonce your server issued, valid for five minutes. Your server verifies it offline, against uloggd's public keys. There is no uloggd API to call and no secret to keep.

The flow

  1. Your server issues a nonce, ties it to the session that will receive the token, and remembers it.
  2. The game calls authorize({ scopes: ["identify"] }) once, then getIdentityToken({ nonce }).
  3. The game sends the token to your server.
  4. Your server verifies it, spends the nonce, and opens its own session for sub.

Verify again at every session start, and keep your sessions to 24 hours at most. A player who revokes your game's access stops getting tokens at once, but the sessions your server already opened are yours to end: the grant_revoked data event tells you when.

The token

header  { "alg": "ES256", "typ": "uloggd-arcade-id+jwt", "kid": "arc-2026-10" }
payload {
  "iss": "https://uloggd.com",
  "aud": "YOUR_APP_ID",
  "sub": "4f1c2b7e-...",   the player's id in your game
  "nickname": "Ada",
  "scope": "identify",
  "iid": "0b7e9a31-...",   the play, as instance_id in the launch URL
  "mode": "live",          or "test"
  "nonce": "...",          the one your server issued
  "iat": 1791860000,
  "exp": 1791860300,       iat + 300
  "jti": "9d2e5f60-..."
}
  • sub is a uuid unique to this player in this game; another game sees a different one for the same person. It stays the same across plays, and changes only when the player chooses Forget this game.
  • nickname is the one the player chose for your game. It can change, and the nickname_changed data event tells you.
  • mode is test when you play your own release in test mode, or your development URL. Such a token has the same iss and aud as any other, and no claim of its own. Reviewers play in live, with a fresh sub and nickname for each review.
  • Never in it: the email, uloggd's own ids, the birth date, the age, the IP address.

Verify it in Node

import {
  verifyIdentityToken,
  ArcadeTokenError,
} from "@uloggd/arcade-sdk/server";

app.post("/session", express.json(), async (req, res) => {
  const nonce = takeNonce(req); // the nonce issued to this session; usable once
  try {
    const player = await verifyIdentityToken(req.body.token, {
      appId: process.env.ULOGGD_APP_ID,
      nonce,
    });
    // { subject, nickname, scopes, instanceId, mode, id, issuedAt, expiresAt }
    if (!(await rememberOnce(player.id))) return res.status(409).end();
    res.json({ session: await openSession(player.subject, player.nickname) });
  } catch (error) {
    if (error instanceof ArcadeTokenError)
      return res.status(401).json({ error: error.code });
    throw error;
  }
});

On a CommonJS server, require("@uloggd/arcade-sdk/server") gives the same functions.

Option
appIdYour app id. Required.
nonceThe nonce your server issued. Required.
allowTestModefalse refuses tokens from test mode (mode: "test"). They are accepted by default.
allowMockAccept the mock's tokens (iss: "mock"). Local development only.
clockSkewSecondsThe clock skew to allow; 60 by default.
fetchA fetch of your own, for the key set.
jwksUrl, issuerOnly https://uloggd.com, or a loopback origin in tests. Leave them out.

A refusal is an ArcadeTokenError with a code: malformed, unsupported_header, unknown_key, bad_signature, wrong_type, wrong_issuer, wrong_audience, expired, not_yet_valid, too_old, nonce_mismatch, mock_refused, test_refused or jwks_unavailable.

The verifier keeps the keys for 5 minutes at most, whatever the response headers say, and refetches them on an unknown kid at most once a minute. Without current keys, a failed fetch makes it wait 10 seconds before asking again, refusing with jwks_unavailable meanwhile.

Verify it in any language

Any JOSE library that lets you pin the algorithm will do, as long as every rule below is checked:

  1. Shape. At most 8192 characters, in three base64url segments separated by dots. Decode the first two as JSON objects.
  2. Header. Exactly alg, typ and kid, and nothing else. alg is ES256: pin it in your code, never let the token choose. typ is uloggd-arcade-id+jwt, and kid is a string. Refuse jku, jwk, x5u, x5c, crit and any other member.
  3. Key. Find kid in the key set at https://uloggd.com/api/v1/arcade/jwks. An unknown kid may trigger one refetch, at most once a minute.
  4. The set. Accept only a 200 answer, following no redirect, of at most 64 KiB, that is JSON with a keys array. Read only its first 20 entries, and of those only the ones with kty EC, crv P-256, x and y of 43 base64url characters, alg absent or ES256, and use absent or sig. For each kid the first valid entry counts. After a failed fetch, wait 10 seconds before fetching again.
  5. Signature. ECDSA over P-256 with SHA-256, over the bytes of the first two segments as received, joined by a dot. The signature is the raw 64 bytes of r then s; refuse a DER-encoded one, and convert if your library wants DER.
  6. iss is exactly https://uloggd.com.
  7. aud is exactly your app id.
  8. Time. iat and exp are integers, and exp comes after iat. Allowing 60 seconds of clock skew: exp has not passed, iat is not in the future, and iat is at most 300 seconds old.
  9. Members. sub, jti, nickname and iid are strings.
  10. nonce is the one your server issued to this session, not used before. Mark it used.
  11. scope includes identify.
  12. mode is live or test. Decide where your own test plays may sign in, and refuse test everywhere else.
  13. Replay. Remember each jti and each nonce for longer than a token lives, and refuse a repeat. Deduplicate by jti and nonce, never by the token's text: ECDSA signatures are malleable, so the same claims can arrive with a different signature.

Verifying in other languages publishes a complete verifier in Python, which checks every one of these rules and is verified against the Node SDK, with the rules for data event tokens, the refusal codes and what to check when you use a JWT library. Verifiers in Go and C# are written in uloggd's repository (packages/arcade-sdk/examples, not part of the npm package) but not yet verified; the page publishes each one once it passes.

The key set

GET https://uloggd.com/api/v1/arcade/jwks answers a public JSON Web Key Set, with Cache-Control: public, max-age=300, stale-while-revalidate=60. It lists the key that signs today and any key that is about to sign or on its way out.

{
  "keys": [
    {
      "kty": "EC",
      "crv": "P-256",
      "x": "...",
      "y": "...",
      "kid": "arc-2026-10",
      "alg": "ES256",
      "use": "sig"
    }
  ]
}
  • Keep it cached for 10 minutes at most. The longer a verifier holds the keys, the longer a withdrawn key keeps working for it.
  • Fetch it from https://uloggd.com only. Never from a *.squareweb.app address: those names belong to uloggd's hosting provider, and a name released there can be claimed by another customer serving keys of its own.
  • Pinning the keys below in your verifier, instead of fetching them, is supported, as long as you follow the rotation calendar (see Pinning the keys).

Rotation

Keys are named arc-YYYY-MM and rotate every 6 months:

  1. The new key joins the set, for verifying only, and is listed on this page at the same moment, with the date it starts signing. Pinned verifiers need those 48 hours.
  2. 48 hours later it starts signing.
  3. 24 hours after that, the old key leaves the set.

A verifier that refetches the set at least every 10 minutes never notices. A pinned verifier must add the new key within those 48 hours.

If a key is ever compromised, it leaves the set at once, and every Arcade developer gets an email.

Current public keys

kidSigning sinceLeaves the set
arc-2026-102026-10-14no date yet

The next rotation is planned for April 2027, with the key arc-2027-04. The exact dates are posted here at least 7 days ahead.

The key that signs today, as the key set publishes it:

{
  "kty": "EC",
  "crv": "P-256",
  "x": "43itCWOfjgyC_mP-7YI8Vg3wmHMC2bkPKlod1D1qYcA",
  "y": "jey41Kgw1_imPbgSuhlhTHutWiZeY9jnoHf45jKd_UA",
  "kid": "arc-2026-10",
  "alg": "ES256",
  "use": "sig"
}

The whole set, with any key about to sign or on its way out, is always at GET https://uloggd.com/api/v1/arcade/jwks.

Pinning the keys

When the set is fetched, any answer that is not JSON, such as a challenge page or an error, makes the SDK refuse with jwks_unavailable. A verifier that would rather not depend on that fetch can pin the keys above: pass verifyIdentityToken a fetch option that returns the published set as a Response with status 200 and cache-control: max-age=300.

import { verifyIdentityToken } from "@uloggd/arcade-sdk/server";

// The set as this page publishes it. Add the new key as soon as it is
// listed under "Current public keys".
const PINNED_JWKS = JSON.stringify({
  keys: [
    {
      kty: "EC",
      crv: "P-256",
      x: "43itCWOfjgyC_mP-7YI8Vg3wmHMC2bkPKlod1D1qYcA",
      y: "jey41Kgw1_imPbgSuhlhTHutWiZeY9jnoHf45jKd_UA",
      kid: "arc-2026-10",
      alg: "ES256",
      use: "sig",
    },
  ],
});

const pinned = async () =>
  new Response(PINNED_JWKS, {
    status: 200,
    headers: { "cache-control": "max-age=300" },
  });

const player = await verifyIdentityToken(token, {
  appId: process.env.ULOGGD_APP_ID,
  nonce,
  fetch: pinned,
});

verifyEventToken takes the same fetch option. Follow the rotation: add the new key within the 48 hours between its listing here and its first signature, and drop the old one once it has left the set.

SDK

@uloggd/arcade-sdk: the client, every command, event, error and close code, and the mock.

Verifying in other languages

Complete verifiers for identity and data event tokens, with the same rules and refusal codes as the Node SDK.

On this page

The flowThe tokenVerify it in NodeVerify it in any languageThe key setRotationCurrent public keysPinning the keys