npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@flayerlabs/gamemode-gate

v0.9.1

Published

Server-authoritative rooms, economy, admission and registry for Flaunch Game Modes

Readme

@flayerlabs/gamemode-gate

Run authoritative Game Mode rooms, spending allowance, admission and game discovery. You can run SDK rules or accept point awards from an existing authoritative game server.

Flaunch runs this package as the platform gate, one process per chain for every game (startPlatformGate, see the platform gate guide). A game developer does not need to deploy it. A game server that posts awards to the platform gate may still import helpers from it, such as createScoreRateMonitor and verifyPlayerJoinTicket. Everything below is for running a gate yourself.

Requirements

You need:

  • Node.js 20 or later
  • PostgreSQL
  • @flayerlabs/gamemode-spec at the matching SDK version

Install the package and direct database dependency:

pnpm add @flayerlabs/gamemode-gate @flayerlabs/gamemode-spec pg

Start a local gate without a chain

createDemoGate starts the real gate flow with a stand-in wallet and no chain access. It expects PostgreSQL at postgres://postgres:[email protected]:55439/gamemode unless you set DATABASE_URL.

import { createDemoGate } from '@flayerlabs/gamemode-gate'
import { rules, type Config } from './game/rules.js'

const config: Config = {
  rounds: 3,
  ceiling: 10,
  points: 500,
  roundMs: 10_000,
}

const demo = await createDemoGate({
  game: rules,
  config,
  gateOrigin: 'http://127.0.0.1:4000',
})

await demo.app.listen({ host: '127.0.0.1', port: 4000 })

This path uses the real room, database, socket and signed spend authorisations. It records purchases instead of submitting them to a chain. Do not use createDemoGate in production.

Build a production gate

Run migrate(pool) before the gate accepts traffic.

If you already ran the pre-release schema, run migrate(pool) again. An empty setup upgrades in place. Delete any pre-release test rounds first because their game ID and point limit are unknown.

A createGate setup needs:

  • Sessions configured with the page domain and the gate's exact public origin
  • Claims, Ledger and PayloadSigner for spending authorisations
  • pointsPerDollar, usdPerEth (the ETH pairing's price) and the allowed game origins

Add the chain services used by your deployment:

  • Discovery verifies each launch before the gate adopts it. It resolves the coin's own PositionManager and adopts the coin when that manager is wired to your calculator (checked once per manager, from the chain), so one gate serves one calculator and every manager wired to it; the configured manager is the primary one, not the only one
  • Settlement updates recorded spending from the chain
  • RegistryPrices prices any pairing the manager's PairedTokenRegistry approves, from the calculator it was registered with; pass its priceFor and serve announceSpendToken so a new approval needs no gate change (startGate does both)

Since 0.7.0 the gate signs a cumulative spend ceiling rather than a per-purchase amount, so nothing is held: Settlement reads walletSpentWei two blocks behind the head and records it, never backwards, and a declined buy leaves the player's allowance where it was. See the production gate guide's cumulative spend ceilings section for the wire shape and the deploy order.

Run one gate process for each game, or one platform gate process per chain. The current room writer does not support multiple replicas.

Set TURNSTILE_SITE_KEY and TURNSTILE_SECRET to require Cloudflare Turnstile at sign-in. The gate announces the site key so the trusted parent can render the widget. Set TRUSTED_PROXY_HOPS so admission sees the player's address, not your platform's router. The Turnstile policy checks session creation. Game action and claim policy remain the gate operator's responsibility.

Set PLAYER_NOTE_SIGNERS to require a player note from the platform at sign-in. A note is the platform's signed word that this wallet is one account's game wallet, bound to this gate and this sign-in code, carrying a per-gate account pseudonym and an allocation share. On a game-server gate, caps (maxPoints on a consumed ticket) and boots then follow the account, and boot cooldowns escalate by themselves with an account's recent boots (BOOT_ESCALATION). The note format and the JSON session evidence that carries it beside a Turnstile token live in @flayerlabs/gamemode-spec/embed (playerNoteTypedData, encodePlayerNote, encodeSessionEvidence), so the platform signs exactly what the gate checks.

Read the production gate guide before deploying a gate.

Connect an existing game server

Use createGameServerGate() when your server already owns gameplay and scoring:

import {
  createGameServerGate,
  verifyPlayerJoinTicket,
} from '@flayerlabs/gamemode-gate'

const awardToken = process.env.AWARD_TOKEN
if (!awardToken || awardToken.length < 32) {
  throw new Error('AWARD_TOKEN must contain at least 32 characters')
}

const app = createGameServerGate({
  pool,
  sessions,
  claims,
  discovery,
  settlement,
  gameId: 'my-game',
  maxPointsPerPlayer: 10_000,
  awardToken,
  pointsPerDollar: 500,
  usdPerEth: 3_000,
  allowedOrigins: ['https://game.example'],
})

The launch page reads GET /config before it lets anyone launch a coin through your game, and createGameServerGate() serves that route only when you pass announce — without it the form reports your gate as not answering. Add it to the options:

const app = createGameServerGate({
  // ...the options above...
  announce: () => ({
    chainId: 84532,
    contracts: {
      positionManager: '0x4E7cB1e6800a7B297B38BddcecAF9Ca5b6616FDC',
      // The legacy stack's version 1 calculator. The paired stack's v2 calculator (0.7.0) is
      // 0x8a36826253882c8b505D6D78a71B780Ac9c98eA9 on Base Sepolia.
      spendGatedCalculator: '0x8cbbE6b4cFA5Ccf68399Dbf1429d91A21097ebA5',
    },
    signer: signerAddress,
    settler: signerAddress,
    walletCap: '25000000000000000',
    roundDurationMs: 90_000,
    minLobbyLeadMs: 60_000,
    gateEndsAtGraceS: 10,
    flaunchVariant: 'v1_2',
    requiresEoa: false,
    accepting: true,
    publicLaunchesOpen: true,
    privateLaunches: false,
  }),
})

The addresses are per chain (the pair above is Base Sepolia); announce a signer you actually hold — launches write it into their pools as the trusted signer. Verify with curl https://<gate>/config.

The served response adds gateVersion, the version of @flayerlabs/gamemode-gate answering. The route sets it; an announcement cannot override it. GET /stats carries the same field, and the package exports GATE_VERSION for logs. The Flaunch dashboard reads it to confirm a gate runs a current SDK.

Generate a random 256-bit award token, for example with openssl rand -hex 32. Keep it on the gate and authoritative game server. Never send it to the browser or use a memorable value.

Use a stable, distinct gameId for each game. It scopes durable rounds and awards when games share a PostgreSQL database.

The browser calls joinTicket() after it joins the economy and sends the opaque result as its first game-server socket message. Verify it before accepting gameplay:

const playerJoin = verifyPlayerJoinTicket(message.ticket, {
  awardToken,
  gameId: 'my-game',
  roundId,
  audience: 'https://gate.example',
})

if (!playerJoin || usedTicketIds.has(playerJoin.jti)) {
  socket.close(4401, 'invalid join ticket')
  return
}
usedTicketIds.add(playerJoin.jti)

The default audience is the gate origin. This works when a reverse proxy serves the game WebSocket from a path on that origin. For a separate origin, pass the same reviewed gameServerOrigins list to createGameServerGate() and the platform registration, then use that exact origin as the ticket audience. A ticket is short lived; also reject replays and supersede an earlier connection for the same playerJoin.player.

Your server adds points with POST /internal/rounds/:id/awards. The gate checks the token, event ID, wallet, round window and per-player limit before it changes the ledger. It also makes identical retries safe.

GET /internal/rounds/:id/players/:player reports what one wallet has earned and how much more it may earn, so your server can refuse a seat before the player takes one. That read is advisory; the award route still enforces the ceiling.

POST /internal/heartbeat, sent every 30 seconds by startGameServerHeartbeat, tells the gate your server is up between rounds; the platform reads it back from GET /games/:gameId/health.

Every gate also serves two public reads the Flaunch coin page renders: GET /coins/:coin/rankings, the round's standings once it is over (and the running order while it is live), and a roster on GET /rounds/:id naming who holds a seat. Nothing to configure — see read the standings.

Use the external game-server steps for the request format, responses and deployment checks. The runnable external-server example shows the complete browser, WebSocket and award flow. Do not call the ledger or signer directly.

Boot it from the environment

startGameServerGate({ maxPointsPerPlayer }) does for a game-server gate what startGate does for a rules gate: it reads the environment in DEPLOY.md, checks the chain, migrates Postgres and listens. It adds AWARD_TOKEN, GAME_SERVER_ORIGINS and GAME_ID.

Catch scoring that no player could manage

createScoreRateMonitor judges what each player earns in fixed windows (10 seconds by default) against what players earned in the same window of recent rounds. Points are divided by your room's enemy scale, compared on a log scale against a robust median and deviation, and a player is judged on any of: a window over the supply the room could have offered, one extreme spike, two high windows in three, a sustained excess (CUSUM), or a running total far ahead of everyone for several windows. One great window is never enough.

const monitor = createScoreRateMonitor({
  baseline: () => latestBaseline,            // ScoreBaseline.fromJSON(await GET /internal/score-baseline)
  enemyScale: (players) => 1 + 0.5 * (players - 1),
  supplyCeiling: (window, players) => maxPointsTheRoomCouldSpawn(window, players),
})
const round = monitor.round(roundStartsAt)
round.award(player, points, Date.now())       // on every kill
for (const verdict of round.close(Date.now(), playersInRoom)) {
  // POST /internal/rounds/:id/players/:player/boot, close their socket, drop what they had not banked
}
// at the end: POST /internal/rounds/:id/score-windows with round.samples(roundId)

Run it in shadow mode first: log the verdicts, replay them against real rounds, then enforce.

The score is not the only thing a server can judge. If it measures how each player plays (reaction times, aim speed, accuracy), it can keep those measurements on the gate too, so its baseline survives a redeploy: POST /internal/rounds/:id/input-features with { gameVersion, samples: [{ player, feature, value, flagged? }] } at the end of a round, and GET /internal/input-baseline?gameVersion= for the newest values per feature, leaving out players flagged in their round or booted from it. Bump gameVersion when a gameplay change moves what normal play looks like; the new version starts a baseline of its own.

Run the central game registry

createGameRegistry provides:

  • public GET /games/:chainId/:coin lookups for trusted Flaunch pages
  • authenticated internal routes for the Flaunch launch backend to pin or disable a registration

The registry is a central Flaunch service. Do not run it inside a creator gate. Its writerToken must contain at least 32 characters and stay on the launch backend.

Read the current request and response behaviour in the game registry source.

For custom gate assembly, pass readiness: createGateReadiness({ origin, chainId, positionManager, signer, pool, client }) using the gameplay dependencies. startGate wires this automatically. See readiness setup.

Serve play analytics

Every gate serves GET /stats/plays for the Flaunch developer dashboard. It returns durable per-game play counts that survive a room ending: { trackingSince, sessions, nextCursor }, where each session carries id, coin and startedAt (Unix milliseconds).

The gate records one accepted session per wallet and round on every join path — the SDK-managed join, the external-server join and the external-server ticket. Reconnects and repeated joins keep the first timestamp. Session IDs are stable hashes; wallet addresses are never returned. Rounds without a coin (practice and mock rooms) are excluded.

The route returns at most 250 rows ordered by start time and ID. Pass nextCursor back as ?cursor= to continue; null ends the pass. A malformed cursor returns 400. Consumers should deduplicate by id and validate coin against their own launch pins. trackingSince is the time the migration first ran on that gate: earlier plays are not counted.

See what changed

This package ships CHANGELOG.md and MIGRATING.md, shared by all four Game Mode packages. Read them on GitHub: changelog and migration guide.