ArcaneClient::init runs it once at launch, for the title named in ARCANE_GAME_ID; you do not add a second ownership step, and you pass nothing.
Init asks the Arcane desktop app first, on loopback (127.0.0.1:39284), and opens Arcane via the arcane-powered:// deep link if the app is not running. A launch mints a fresh ticket — that is what the launch is for. The local cache answers exactly one case: the desktop app is running and tells the SDK it cannot reach the cloud.
Game id
The game id Arcane Powered hands your process inARCANE_GAME_ID is the id of your title in the Arcane portal. It keys the local flag and ticket files, names the title in every desktop route, and must match the ticket’s gid claim.
It is read and validated before any I/O: unset or empty is missing_game_id; ASCII letters, digits, _, -, ., at most 256 bytes, and anything else is invalid_game_id. During local development you set the variable yourself with the value from the portal.
It is not a secret. The id identifies a title; it proves nothing on its own. What proves ownership is the signed ticket below — an ES256 JWT bound to an account and a device — which nobody can mint from the id.
Default init policy
ArcaneClient::init():
- Reads the game id from
ARCANE_GAME_IDand validates it - Probes the desktop health endpoint, opening
arcane-powered://sdk/ownership?game_id=…when Arcane is not running - Calls the desktop’s ownership refresh, which mints a ticket for this account and this device
- Runs the offline check against what the desktop just wrote
- If the desktop reported DRM is off for this title and no ticket appeared, resolves to
DrmDisabledrather than failing
offline — it is running, it simply cannot reach the cloud. Then the flow becomes: read flags/{game_id}.json, and if DRM is on, run the offline check against the ticket the last launch left on disk. It is verified in full, exactly as a fresh one would be.
Anything else the desktop says stops the launch. not_owned and not_authenticated are answers, not gaps, and a cached ticket must not paper over them; arcane_unavailable means the desktop app never spoke at all, which is not a state a game may start in.
This is deliberately not cache-first. A cached ticket that still parses would otherwise outlive what it attests: an account key rotated since it was minted leaves the ticket bound to a fingerprint the machine can no longer present, and
device_mismatch does not ask for a refresh. The player would be locked out of a title they own, with no way back that does not involve deleting a file by hand.user_id, ticket expiry — is kept on the client, alongside the game_id init read from the environment. See Client.
Offline check (what init runs when DRM is on)
- Resolves the ticket for the signed-in account (see below)
- Rejects clock rollback vs
last_seen_wall_time - If
drm_enabledon the ticket file is false →DrmDisabled - Ensures the ticket string is non-empty
- Verifies the JWT (ES256, issuer
arcane-drm, audiencearcane-game-sdk) - Requires the
devclaim to name a fingerprint this machine can present
device_hash field is not checked. It is unsigned, so it proves nothing the dev claim does not.
Which account’s ticket
ARCANE_USER_ID — set by the Arcane desktop app on the game process — names the account it launched the game for. session.json, written on sign-in and sign-out, is the offline source of truth when the variable is absent.
The no-fallback rule matters on shared machines. A ticket belonging to a different account is correctly signed and correctly bound to the device, so nothing else would reject it — only the account check does.
Naming an account is not a way past the check: the variable selects a file, and the ticket in that file is verified in full.
Desktop refresh errors
When init must refresh online, the desktop (and cloud) can return distinct failures:
An unrecognised desktop error code becomes
ticket_invalid with the original string preserved in the error’s context.desktop_error, so a newer launcher never produces a silent failure. See Errors for the full table.
Data layout
{app_data} is the OS application data directory (the SDK resolves this internally).
Ticket claims
Device fingerprint
machine_id is a UUID written once under the DRM root (mode 0600 on Unix). device_hash is the first 16 bytes of SHA-256(machine_id), hex-encoded, and is readable from the client as device_hash(). Tickets are bound to that hash so they cannot be copied to another machine.
JWKS
Verification keys come fromjwks.json. The JWT kid selects the key when present. If JWKS is missing or the kid is unknown, the SDK returns ticket_invalid naming the resolved path — refresh via the desktop app.