Playmos Anti-Cheat.
Status: SDK implementation; hosted Anti-Cheat integration pending. This is an optional verification feature alongside Playmos commerce. It has browser/client helpers and server-only verification/finality helpers. It does not change fees, contracts, payment verification, settlement or refunds. Existing hosted services do not automatically gain Anti-Cheat by installing this code.
What it provides#
Server-issued runs bound to an authenticated player, admitted entry, game, contest, release, verifier version, frozen rules and course seed. Ordered server-timed checkpoints retain a bounded input log. The backend replays approved deterministic scoring code and requests signed proposal/prepare/commit evidence from a permissioned validator roster. A score is eligible only after the backend verifies that certificate and the exact durable result binding.
A valid simulation does not establish human play. Bots can generate legal inputs in real time. humanVerified is always false. Server-side anomaly heuristics and an optional advisory model reviewer are implemented; appeals and player restrictions are not.
Install and run locally#
This feature is in the 0.3.28 source and built package. Verify npm view @playmos/[email protected] version before installing the release. The earlier 0.3.27 tag did not publish because the shared Go packaging gate rejected its JavaScript tag; 0.3.28 preserves that history and corrects the gate.
npm --prefix sdk run buildnode examples/anticheat/demo.mjsnode --test examples/anticheat/store.test.mjsnpm --prefix sdk test -- --run test/anticheat.test.ts
The SQLite reference requires Node with node:sqlite and DatabaseSync.isTransaction (Node 22.16+). The core SDK and PostgreSQL driver interface do not import SQLite. Final SDK regression, synthetic demo and durable-store checks also passed on supported Node 24.19.0. Node 26.8.1 was used during initial development; the declared package support remains Node >=18 <25, and oldest-supported Node 18 was not exercised. The demo generates synthetic keys/admission, uses four file-backed stores in one process, omits the fourth voting replica and verifies score 6 with three votes. This is local plumbing evidence, not an independently deployed BFT cluster, confirmed purchase, hosted game, production payout or power-loss test.
Connect a browser game#
import { Playmos, createAntiCheatHttpTransport } from '@playmos/sdk';const playmos = new Playmos({apiKey: 'pk_test_your_studio_key',anticheat: {transport: createAntiCheatHttpTransport({baseUrl: 'https://your-game.example/anticheat/',credentials: 'same-origin',}),},});
The URL is your authenticated game backend, not an existing Playmos hosted endpoint. For a cross-origin deployment explicitly configure credentials, CORS, CSRF and your session-token transport. headers accepts a callback returning player-session headers; never send studio sk_* keys or validator secrets. HTTPS is required except for localhost. Redirects are refused, requests bypass caches, and requests time out after 15 seconds. There is no automatic write retry: preserve the request and retry unchanged.
Alternatively implement AntiCheatTransport using your own authenticated RPC. Without a configured transport the namespace throws an explicit configuration error. This feature does not use the SDK's commerce API-key HTTP client or mock an authoritative result.
Client API#
| Method | Input | Result |
|---|---|---|
playmos.anticheat.begin | gameId, contestId, entryId, persisted idempotencyKey | AntiCheatRun |
playmos.anticheat.checkpoint | runId, ticket, sequence, previousReceipt, elapsedMs, events | sequence, receipt, cumulative evidence hash |
playmos.anticheat.submit | runId, ticket, previousReceipt, elapsedMs, remaining events | pending or final verdict |
playmos.anticheat.get | runId | pending or final verdict |
AntiCheatRun includes id, game/contest/release IDs, seed, issue/expiry time, checkpoint interval and a server-authenticated ticket. A run ID is stable for the same studio/player/begin request. The durable entry constraint prevents a new idempotency key or another account from reusing the same admitted entry.
events is game-specific JSON. It rejects undefined, nonfinite numbers, sparse arrays, getters, class instances, symbol-bearing objects, excessive nesting, oversized evidence and excessive node counts. Each checkpoint appends events; final submission appends only the remaining tail, not a second copy of the full run. The server retains full final evidence before requesting votes.
Recording and retries#
- Save the begin request/idempotency key before sending; reuse the same request after uncertainty.
- Save the returned run/ticket. Record inputs against a monotonic gameplay clock.
- Send checkpoints inside the declared interval. Start sequence at 1 and previousReceipt at null. Later requests use the previous server receipt.
elapsedMsmust increase and cannot outrun the server clock plus the approved tolerance. - Save each exact checkpoint request before transmission. Lost responses can be recovered by retrying unchanged; conflicting sequences fail. Save the returned receipt before sending the next batch.
- Save the final request, including tail events and prior receipt. Submit before run expiry with the latest checkpoint intact. Never substitute evidence or duration after submission.
- Read
getafter uncertainty. For a pending submission retry the identicalsubmitafter the verification lease expires (30 seconds).getonly reads status; it does not start a worker. An identical already-admitted submission may recover after run expiry. A new late submission cannot.
The caller owns persistence of requests and gameplay buffers. The SDK does not transparently rerecord lost inputs or restart paid attempts.
Server API and authentication#
Import from @playmos/sdk/server:
createAntiCheatBackend: session lifecycle, replay, finality verification, durable verdicts.createAntiCheatHttpHandler: a Fetch-standard authenticated Request/Response handler.createAntiCheatValidator: server-only Ed25519 proposal/prepare/commit signer.verifyAntiCheatCertificate,antiCheatRosterHash,antiCheatHash.createPostgresAntiCheatStores,ANTI_CHEAT_POSTGRES_SCHEMA,createSqliteAntiCheatStores.createMemoryAntiCheatStores: explicit development/test use only.
createAntiCheatBackend requires studioId, signing key ring/active key ID, durable store, authorize, replay, and finality. Optional development marks every verdict as development; optional now supports a deterministic test clock. HMAC keys require at least 32 bytes and should be cryptographically random. Keep old key IDs until outstanding sessions/receipts no longer need them. Keys, database backups and validator signing state require operator custody.
Its operations are begin(playerId, input), checkpoint(playerId, input), submit(playerId, input), get(playerId, runId) and getFinalized(playerId, runId, expected). Derive playerId from your trusted authenticated session for EVERY call, never from client JSON. Studio and player mismatches fail closed.
Required host adapters#
| Adapter | Obligation |
|---|---|
authorize(playerId, beginInput) | Authenticate/admit a real player and entry; prove entry confirmation and ownership; enforce contest admission and attempt limits; resolve approved frozen release/version/rules/seed/limits. Never copy untrusted requested terms into authorization. |
replay(context, signal) | Independently recompute score and state hash from stored evidence and frozen configuration. Return {score, stateHash} or null for invalid gameplay; throw on infrastructure failures. Score must be a nonnegative safe integer and stateHash a SHA-256 hex digest. |
finality.collect(candidate, lookup, signal) | Send the exact candidate to independent approved validators, obtain signed primary proposal, prepared quorum and commit quorum, and return the certificate. Every signer independently validates admission, release, rules, seed, full evidence, duration, score and state. |
| Durable storage | Commit uniqueness/CAS/vote-lock writes before returning, across all workers. Do not expire entry-use records while reuse remains possible. |
AntiCheatDefinition freezes gameId, contestId, entryId, playerId, releaseId, verifierVersion, rules, seed, maxDurationMs, graceMs, checkpointIntervalMs, timingToleranceMs and maxEvidenceBytes. Replay workers receive only whitelisted scoring configuration/evidence, without playerId, entryId, run tickets or extra authorization fields. Collectors receive the candidate and a studio/run lookup only; validators fetch trusted admission/evidence independently. Keep secrets and personal data out of public rules/seed. Native telemetry accepts bft_finalized as normal and invalid_replay as invalid_result; rejected runs never report clear. Use a shared trusted course seed where contest fairness requires it. Policies are server-supplied: duration <=24h, grace <=60s, evidence <=1 MiB, checkpoint interval >=100ms, and timing tolerance <=checkpoint interval. Evidence is additionally limited to 100,000 nodes, depth 32, and 10,000 checkpoints. These ceilings are not recommended gameplay settings; choose tighter values per game.
Approved studio verifiers need real process/worker isolation, CPU/memory limits and immutable code hashes. The SDK's 10-second asynchronous deadline cannot interrupt blocking JavaScript. Cancellation is passed to replay/finality adapters; honor it for I/O and terminate isolated jobs separately. Late workers are fenced by submission lease identity before writing verdicts.
HTTP routes#
createAntiCheatHttpHandler({backend, authenticate, prefix, maxRequestBytes}) returns a Fetch-standard handler. Default prefix is /anticheat; body limit is 64 KiB and may be explicitly raised to <=1 MiB. Mount it in your game backend:
| Method | Route | Operation |
|---|---|---|
| POST | /anticheat/begin | begin JSON |
| POST | /anticheat/checkpoint | checkpoint JSON |
| POST | /anticheat/submit | submit JSON |
| GET | /anticheat/runs/:runId | read own status |
authenticate(request) must enforce session authentication, origin/CSRF policy and per-player/network/game rate limits and return the trusted player ID or null. The helper itself cannot infer your session or rate-limit system. JSON is required for writes and the byte limit applies to streamed bodies even without Content-Length. Responses are no-store, and errors omit secrets and stored details.
Unauthenticated requests return 401; inaccessible runs 404; conflicting retries/entries 409; oversized body 413; wrong content type 415; wrong method 405; invalid/refused requests 400. Infrastructure errors can also appear as a sanitized refusal; persist inputs and diagnose server logs. Host servers must enforce request-read deadlines, connection limits and observability. submit returns pending if asynchronous replay/finality fails, quorum is missing or the verification deadline expires.
Storage and crash safety#
AntiCheatRunStore defines create, get, and compareAndSwap. create must enforce BOTH unique run ID and unique (studioId, entryId) atomically. CAS changes revision exactly once and prevents stale workers overwriting a replacement lease. AntiCheatVoteStore.claim(slot, digest) must permanently reserve a slot before a signature is produced; identical retries succeed, different digests fail. Signer locks are shared across proposal, prepare and commit phases and bind the normalized public signing key plus studio/run. Renaming a validator or changing its roster/network cannot unlock a run.
PostgreSQL: migrate ANTI_CHEAT_POSTGRES_SCHEMA, then wrap a dedicated autocommit writer pool as {autocommit: true, query: (sql, values) => pool.query(sql, values)}. Never pass a transaction-bound Client or asynchronous read replica. Each resolved write MUST be durably committed before signing. The generic driver contract cannot enforce a dishonest adapter's guarantees. PostgreSQL 17.10 integration checks passed locally for conflicting concurrent vote claims, entry uniqueness, stale-write CAS and persistence across pool reconnects. CI runs the same tests against a PostgreSQL 17 service. This does not prove an independently deployed cluster, replica failover or power-loss recovery.
SQLite: createSqliteAntiCheatStores(db) requires a real file and a driver exposing isTransaction. It uses FULL synchronous commits and busy timeout; operations refuse an enclosing transaction to prevent sign → rollback → sign conflicting value. Separate connections use uniqueness/CAS. Do not copy a live validator database among machines or restore an old vote database while retaining the current signing key. Local restart/rollback tests are not power-loss or distributed database proof.
Memory: explicitly enable development on backend AND validators. State loss permits repeat signing and invalidates BFT safety. Development verdicts are not eligible as production evidence.
Consensus model and player pools#
The implemented protocol is a permissioned fixed-view Byzantine certification protocol, not a complete PBFT network. It has a pinned roster/network, primary (lexicographically first validator ID), signed proposal, prepared certificate and commit certificate. It has no view changes, automatic primary replacement, dynamic membership, state transfer or networking implementation.
Use exactly 3f+1 unique approved identities/public keys, with f>=1. Both prepare and commit require 2f+1 distinct valid signatures. Four independent replicas permit at most one faulty replica; seven permit at most two. Votes bind protocol domain, roster/network, view zero and the complete candidate. Honest replicas independently verify evidence and durably sign one candidate per studio/run. Two quorums overlap in at least f+1 signers, including an honest signer under the fault bound, preventing conflicting certificates.
This protects finality only under the stated membership, independent-validation and durable-signing assumptions. It does not make a wrong shared simulation correct, prevent censorship by the session coordinator, prove entry truth without independent admission checks, or protect against more than f compromised replicas. Four keys on one machine are not four independent fault domains. A faulty primary or conflicting proposals can stop progress. Do not lower the threshold, reset vote locks or issue a new run identity to bypass a stalled result.
Same-game player pools: the current implementation does not grant player devices authoritative votes. Players could independently replay others' runs and report discrepancies (advisory shared security), or enter a permissioned BFT roster alongside the API. Neither player feature is implemented yet. Merely joining the same game pool, paying entry or owning a wallet does not establish Sybil resistance. Authoritative player voting requires immutable authenticated membership, duplicate-identity controls, independent evidence access, protected private keys, durable device signing locks and a credible bound on compromised/abandoned devices. If the API is one of four replicas, at most one of all four may be faulty and three must sign. The server-only signer is not a browser/mobile validator client.
For full failover and membership management, integrate a mature BFT runtime with independently verified finality and tested recovery/view-change behavior. Do not market this fixed-view implementation as full PBFT.
Result and settlement admission#
pending: no final certificate/result yet; never rank or pay. Missing quorum/infrastructure errors stay pending.
verified: approved replay plus a valid certificate. Includes server score, SHA-256 state hash, evidence hash, binding hash, verification timestamp, signed receipt, environment and humanVerified:false.
rejected: replay returned null for invalid gameplay; no eligible score. Invalid envelopes/timing are refused before replay rather than automatically creating a rejected verdict.
review: reserved transport type; no automated risk review or appeal workflow currently emits it.
On your backend call:
const result = await backend.getFinalized(playerId, runId, {gameId: savedContest.gameId,contestId: savedContest.id,entryId: savedEntry.id,releaseId: savedContest.releaseId,});if (result && result.environment === 'backend') {// Host must atomically persist ranking provenance; never trust client scores.}
Use independently stored trusted expectations. All four fields are required. The gate reloads the durable record and checks issue binding, receipt authenticity, final evidence digest and both certificate phases. Store provenance and admit the run at most once in a transaction in your ranking system. verifyReceipt only checks HMAC authenticity; it is not a score/settlement gate. No browser verdict is authoritative.
The existing Playmos leaderboard/settlement service is not wired to this module yet. Enforce the same trusted gate there before ranking or paying, preserve entry/round identity and existing contract authority, and prove the integration on its real target before launch. These helpers do not settle money or change contract timeout/refund semantics.
Anomaly monitoring and recorded patterns#
The server entry exports normalizeAntiCheatTelemetry, detectAntiCheatAnomalies, createAntiCheatAnomalyMonitor, createOpenAiAntiCheatReviewer, and createSqliteAntiCheatMonitoringStore. The monitor is an opt-in backend adapter, not a hosted always-on service. Call observe(trustedObservation, trustedHistory) from your backend job/ingestion flow. The host supplies trustworthy run metrics and scoped historical records; this module does not scrape players or automatically instrument every game.
| Signal | Recorded evidence | Meaning |
|---|---|---|
invalid_result | Score/state mismatch, invalid gameplay inputs, or independently established maximum exceeded | Invalid run evidence, not a blanket account accusation |
tampering | Reused nonce, bad ticket, conflicting submission, stale build | Investigate the specific run; outdated clients and retries need context |
timing_regularity | At least 30 action timestamps; gap mean and variation | Suspicion only: accessibility tools, fixed-frame sampling and skilled play can look regular |
replay_reuse | Same nontrivial input digest/course/contest across different player pseudonyms | Possible copied runs or coordinated automation; matching empty logs are excluded |
rapid_attempts | At least 11 observed attempts in one contest/minute | Rate/automation investigation; compare game policy and legitimate retries |
score_outlier | Median/MAD baseline from at least 30 distinct verified peer players on the same course | Review signal, never proof against an exceptional player |
perfect_streak | At least 30 actions, all recorded as perfect by the trusted game adapter | Review alongside other evidence |
infrastructure | Pending verification or unavailable quorum/provider | Operations issue; no accusation, restriction or model call |
Telemetry uses studio-scoped opaque player references, run/game/release/contest/course IDs, server-observed time/duration, action timestamps, hashes, explicit outcome/reason enums, score/max and perfect/total counts. Extra free-text or identity fields are discarded. Reject malformed IDs/times/numbers. History is bounded to 2,000 records and a 24-hour window, and comparisons require the same studio/game/release/contest/course where applicable. Store gameplay telemetry only through your approved backend retention policy. It is not a general keylogger or hardware-fingerprinting system.
Lightweight model reviewer#
Default model: gpt-6-luna, the current documented efficient model for focused high-volume work, with reasoning.effort: none. Override explicitly through createOpenAiAntiCheatReviewer({apiKey, model}); there is no silent stronger-model fallback. The default is configurable because model availability can change. Older nano models are currently marked deprecated in the model catalog.
The OpenAI Responses request has no tools, store:false, strict JSON-schema output, <=16 KiB numeric/enum signal summary and <=600 output tokens. Actual input arrays, player/studio/game/contest identifiers, wallets, emails, IP addresses and credentials are not sent; model-facing references are hashed signal IDs. Schema validity is not proof that a conclusion is correct.
The model returns observe, human_review or insufficient_evidence, qualitative confidence, cited EXISTING signal IDs and a short rationale. Unknown references, punishment dispositions, malformed/incomplete/refused responses and provider failure become unavailable. Render rationale as plain text. enforcement is always none; confidence is not a calibrated probability. No model output can settle a contest, ban a player or overwrite verified scores.
createAntiCheatAnomalyMonitor filters locally, then durably reserves suspicious runs before any API request. Default cap: 100 model calls per studio/UTC day (explicitly configurable 1–10,000). One automatic reservation per studio/game/run survives retries, restarts and evolving historical context. Provider failures still consume the reservation to prevent an uncertain response from being automatically rebilled. Calls time out after 15 seconds. Results are saved in a first-write-only SQLite journal; custom distributed implementations must provide the same atomic reserve/dedup/quota semantics. No reserve is made for a normal run or an infrastructure-only case. Changed history for an already reviewed run returns its original assessment; deliberate re-review is a separate operator policy, not an automatic extra bill.
import { DatabaseSync } from 'node:sqlite';import {createAntiCheatAnomalyMonitor,createOpenAiAntiCheatReviewer,createSqliteAntiCheatMonitoringStore,} from '@playmos/sdk/server';const db = new DatabaseSync('./anticheat-monitor.sqlite');const monitor = createAntiCheatAnomalyMonitor({store: createSqliteAntiCheatMonitoringStore(db),reviewer: createOpenAiAntiCheatReviewer({ apiKey: process.env.OPENAI_API_KEY! }),maxCallsPerStudioDay: 100,});// Feed this from your authenticated durable backend job queue, not browser JSON.const assessment = await monitor.observe(trustedRunMetrics, trustedScopedHistory);// Store/render the advisory outcome; it cannot enforce account or money actions.
examples/anticheat/monitor.mjs defaults to an explicitly labeled local fixture. Add --live to make ONE small real OpenAI request on synthetic telemetry; configure the server key through an ignored env file. The script verifies the duplicate is cached. Provider/API access remains separate from package installation. An expiring development key is not a production credential; provision durable managed server credentials before deploying continuous monitoring.
Internal/publisher views and reactions#
Corresponding dashboards are NOT implemented in this SDK release. Proposed internal view: operational health, cross-game incident queue, replay/certificate evidence, model advisory rationale, audited decisions, appeals and expiring ranked-play restriction controls. Proposed publisher view: its own games/incidents, verdicts and investigation status; cross-studio identity/evidence must not be exposed. Authenticate and enforce tenant/role scope on backend reads/actions, not merely in UI filters.
The proposed response ladder separates infrastructure (pending/retry, no penalty), invalid evidence (reject the run), suspicious automation (review evidence) and confirmed repeated exploits (audited temporary ranked-play restriction with reason, expiry and appeal). “Jail” means an explicit competition restriction, not confiscation of funds. A restriction must not silently change contract settlement/refund rules. Publishers should not be able to impose platform-wide bans, and model recommendations never impose restrictions. The dashboard, restriction and appeal APIs still require implementation and product policy approval.
References: GPT-6 Luna, Structured Outputs.
Production readiness#
Implemented locally: client namespace, HTTP transport/handler, frozen sessions, bounded checkpoints, replay adapter, fixed-view signature/certificate verification, durable store contracts/PostgreSQL adapter/SQLite reference, result-admission gate, crash/retry fences, anomaly heuristics, optional bounded OpenAI review and adversarial tests.
Remaining: Stack/game migration and real event recording; confirmed entry integration; authenticated rate-limited deployed routes; actual ranking/settlement enforcement; independent validator hosts and authenticated transport; release packaging/publication; live PostgreSQL and partition/crash/power-loss testing; key custody/backup procedures; hosted metrics/monitoring and review/appeal operations; external protocol/security audit. Full PBFT failover and player-pool shared security need separate implementation and validation. SDK publication and docs deployment are verified separately from hosted Anti-Cheat deployment.
References: PBFT overview, CometBFT light-client verification, Raft paper.