Docs · Quickstart

Integrate Bindseal in one sitting.

One happy path, copy-paste, end to end: get a license, construct the client, activate, gate a feature on a server-signed answer, and heartbeat.

Requirements

  • Node.js 20 or newer (the SDK uses the global fetch), or a Node-based runtime such as Electron for your game or launcher.
  • The @bindseal/sdk package — plain JavaScript ES modules with zero npm dependencies and no native bindings.
  • A Bindseal product and license on the license server at https://api.bindseal.com.

JavaScript is the only SDK available today.

1. What you need from the server

Three things:

  • Product id — the id of your game on the license server, e.g. my-game. It is compiled into your build.
  • Trust anchor — the signing public key and its kid (key id). The anchor is how the client verifies that an activation response really came from the license server. The client never trusts a plain “ok” boolean; it verifies the signature against this key.
  • A license key — a lic_… string, the key a player enters (or your store integration supplies).
The anchor is the public key, not the kid

The kid is only a short id (a hash of the key) used to pick the right key. Embed the full Ed25519 public key in your build at build time. It can verify a signature but can’t forge one, so nothing secret ships in your game.

2. Construct the client

The SDK’s BindsealClient composes the whole license lifecycle behind one small API over a single post(path, body) transport. The quickest way to stand one up is createClient, which builds the trust store from a plain array of anchors and supplies a fetch-based transport for a baseUrl:

license.js
import crypto from "node:crypto";
import { createClient } from "@bindseal/sdk";

// 1. Turn the published anchor (raw Ed25519 public key, hex) into a KeyObject.
const SIG_ALG_ED25519 = 0x01;
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
const publicKey = crypto.createPublicKey({
  key: Buffer.concat([ED25519_SPKI_PREFIX, Buffer.from(PUBKEY_HEX, "hex")]),
  format: "der",
  type: "spki",
});

// 2. One call: trust store + default HTTPS transport.
const client = createClient({
  baseUrl: "https://api.bindseal.com",
  product: "my-game",
  trustAnchors: [{ kidHex: KID, alg: SIG_ALG_ED25519, publicKey }],
  fingerprint,          // this machine's fingerprint-v1 object (see below)
  buildId: 1,           // only needed if your product uses version gating
});

Or construct it directly

If you need to inject a clock, a nonce factory, or a persistent clockStore, build the client yourself. The constructor needs post, product, trustStore and fingerprint:

license.js
import { BindsealClient } from "@bindseal/sdk";

// Trust store: kidHex -> { alg, publicKey }
const trustStore = new Map([[KID, { alg: SIG_ALG_ED25519, publicKey }]]);

// A fetch-based transport. Return parsed JSON for ANY HTTP status (declines and
// update_required stay visible to the SDK); return null on a network failure
// (so the SDK can fail OPEN on an outage).
const post = async (path, body) => {
  let res;
  try {
    res = await fetch("https://api.bindseal.com" + path, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify(body),
    });
  } catch { return null; }
  try { return await res.json(); } catch { return null; }
};

const client = new BindsealClient({
  post,
  product: "my-game",
  trustStore,
  fingerprint,
  buildId: 1,
});
Why the transport contract matters

A transport that throws on a 4xx makes a server decline look like a network outage, so the lease would fail open when it should surface the refusal. Return the parsed body for every status, and null only when the request never completed.

The machine fingerprint

Machine-bound licenses need this machine’s fingerprint-v1 object, sent on activation. It is a set of independent, keyed (HMAC) signal digests plus quality metadata — raw hardware identifiers never leave the player’s machine.

fingerprint shape
const fingerprint = {
  v: 1,
  env: "bare_metal",
  os: "windows",
  collected_at: "2026-10-18T10:15:30Z",
  signals: { /* keyed digests, e.g. "os.machine_id", "fw.system_uuid", … */ },
  quality: { /* … */ },
};

3. Activate

license.js
const status = await client.activate(LICENSE_KEY); // "valid" | "grace" | "expired"

activate() sends the license key, verifies the signed activation response against your embedded anchor, caches the machine id and the lease window, and returns the offline status.

  • If the signature doesn’t verify — a forged or man-in-the-middled response — it throws. There is no boolean to patch.
  • A refusal (revoked, suspended, update_required, …) is surfaced verbatim as a thrown BindsealClientError, so your UI can tell the player exactly what happened. update_required is an update prompt, not a lockout.

4. Gate a feature

Gate on the signed server response, never on a local flag. There are two shapes.

Lease gate — available on every license

status() is derived offline from the signed lease window:

game.js
if (client.status() === "valid") {
  unlockFullGame();   // the lease is server-signed; skip activation and status() is "inactive"
}

A lease gate is still a check that runs on the player’s machine, so a determined cracker can patch around it. It’s the right default for most of a game; for value that must not exist in a cracked copy at all, use a hollow, fused or sealed feature.

Hollow feature — the server computes the value

For a feature whose result must not exist in the client, the server runs the operation and returns a signed capability; the client only consumes its result:

game.js
const result = await client.feature({ op: "report.render", scope: ["feature.pro"], input });

If the server declines (unlicensed, revoked, rate-limited, wrong build), there is no result to use — the feature is inert, not bypassed. Hollow operations must be configured on the server for your product; until one is, feature() returns nothing, and you gate on the signed lease instead.

Fused and sealed features (fusedFeature(), sealedSection()) fold a server-supplied operand or key into local computation, so the wrong token yields the wrong output rather than an error to patch out. See the SDK reference.

5. Heartbeat and the offline lease

game.js
const status = await client.heartbeat(); // refreshes the lease; propagates revoke/suspend/expiry

The lease is a fail-open window evaluated entirely offline from the cached, signed response:

  • valid — within lease_expires. The game runs; no server needed.
  • grace — past lease_expires but within grace_until. Still runs, on borrowed time, so a transient outage never hard-locks a paying player.
  • expired — past grace_until. The gated content stops.

heartbeat() on a network failure returns the cached status rather than throwing — the game keeps running on its lease. A server decline (revoked, suspended) does throw, so a real revocation propagates on the next contact.

The window is guarded against a player winding their clock back; a heartbeat with a corrected clock recovers. Use clockRollbackSuspected() to explain an "expired" status caused by a turned-back clock.

For a game that must run offline for long stretches, fetch a signed offline grant once while online with client.fetchOfflineGrant(…), persist it, and reload it at startup with client.loadStoredOfflineGrant(…). See Offline grant.

6. Tamper reporting

The client reports tamper evidence; the server adjudicates. The client never bricks itself on its own evidence:

game.js
const { action } = await client.reportTamper({ evidence, report });
// action is the server's verdict (e.g. "observe"); a failed report never locks the client.

See reportTamper() and the POST /v1/adjudicate endpoint.

Honest boundary

These mechanisms make a cracked client yield nothing instead of a bypass for server-backed features, and raise the cost of re-cracking each release. They are not a claim that your game can’t be cracked — given enough time, client-side checks can be patched. Decide per feature which tier is worth it.