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

@buffalo-game/originals-protocol

v0.3.3

Published

Wire types, money handling and error codes shared by the Originals RGS and its game clients.

Readme

@buffalo-game/originals-protocol

The wire contract between the Originals RGS and every game client: request and response types, the error-code set, and the money primitives both sides share.

Licence. Proprietary. Use requires a current service agreement. See LICENSE. Publication on npm is for distribution to authorised licensees; it is not an offer of a licence to the public.

Most integrations do not depend on this package directly — @buffalo-game/originals-client brings it in and re-exports the types you need. Depend on it directly only if you are writing your own client, or checking a server's behaviour against the contract.

Install

npm install @buffalo-game/originals-protocol

⚠️ Coming from 0.2.0? 0.3.0 is a breaking release in three independent ways — amounts are JSON integers rather than decimal strings, multipliers are bare floats rather than 1e6-scaled strings, and GameConfig.maxPayout is removed. Only the multiplier change can be wrong without an error; the other two are refused outright. CHANGELOG.md has the field list and a migration order.

What is in it

| Module | What it holds | |---|---| | types | Every request and response the RGS accepts and returns | | money | Amounts as bigint counts of millionths, and the JSON integer they travel as | | currency | Symbol and display precision per currency code — metadata, never arithmetic | | bet | How the bet ceiling is folded from the game's, the operator's, and the wallet's limits | | settlement | The settlement contract in force: exact, versioned, recorded | | validate | The validation primitives every request check is built from | | request | Validation for each request the RGS accepts | | errors | The ERR_* set | | playback | Replaying a round's events from wherever the player already got to | | jurisdiction | The twelve presentation rules a licence imposes, and the inert default | | rational | Exact non-negative rationals, for figures that must not drift |

Money

Every amount is a non-negative bigint count of millionths of one currency unit, and travels on the wire as a JSON integer of those same millionths — 1_000_000 is 1.00, which is Stake Engine's own encoding.

import { MONEY_SCALE, MAX_WIRE_MINOR } from '@buffalo-game/originals-protocol'

MONEY_SCALE      // 1_000_000n  — one unit of currency
MAX_WIRE_MINOR   // 9_007_199_254_740_991n — the most the wire can carry

⚠️ This changed in 0.3.0. Through 0.2.0 the same values travelled as decimal-integer strings; a client written against that encoding sends '1000000' where the server now requires 1000000, and is refused with ERR_VAL.

The scale did not move and the arithmetic rule did not soften. Nothing in this system computes in number: an inbound amount is checked with Number.isSafeInteger and converted to bigint in the same expression that accepts it, so no amount is ever an operand of a floating-point operation. A number here is a transport encoding, not a numeric type.

parseWireAmount answers { ok: true, value } or { ok: false, code, message } — it never throws and never guesses:

parseWireAmount(1000000)     // ✅ { ok: true, value: 1_000_000n }
parseWireAmount('1000000')   // ❌ STRING_INPUT — that is the 0.2.0 encoding
parseWireAmount(1.5)         // ❌ FRACTIONAL
parseWireAmount(-1000000)    // ❌ SIGNED

⚠️ The encoding is bounded, which the string form was not. MAX_WIRE_MINOR is Number.MAX_SAFE_INTEGER minor units — about 9.007e9 currency units. Above it JSON.parse has already changed the value before any check can run, so both directions refuse rather than round: parseWireAmount fails inbound (ERR_VAL, before a wallet is touched) and toWireAmount throws outbound (ERR_GEN), never emitting a truncated figure. Bets never reach the wall; balances can — a balance comes from the operator's wallet and nothing bounds it, so high-denomination currencies hit it for real: roughly $360,000 in VND, $563,000 in IDR.

The decimal-integer string is not gone — it is no longer the wire. parseDeclaredAmount still reads declaration sources (a version's manifest.json bet ladder, the catalog, CLI arguments), because those bytes go into logicHash and re-encoding them would invalidate every published RTP. Reach for parseWireAmount for anything arriving over HTTP and parseDeclaredAmount for anything read off disk; they are not interchangeable.

Multipliers are numbers too, but they are not scaled. payoutMultiplier and maxWinMultiplier are bare JSON floats — 2 is 2×, and payoutMultiplier: 1.9 sits next to payout: 19000000 in one response. The two encodings differ on purpose and both are Stake's: a live /wallet/play answers payoutMultiplier: 1.09 beside payout: 1090000, and the RGS schema behind Stake's own SDK types the field PayoutMultiplier: number, described as "Payout Multiplier for the bet. Payout / Amount."

⚠️ Through 0.2.0 these were 1e6-scaled decimal strings, so a client carried forward from that release divides by 1e6 and lands a millionth of the real figure. MULTIPLIER_SCALE is still exported — it describes the server's internal Mult — but it takes no part in reading these two fields.

The scale did not move; only the wire did. Mult is still an exact 1e6-scaled bigint, every multiplier is still computed as a Rational, and toWireMultiplier does the single division at the boundary. payout remains the authoritative number — the multiplier is literally Payout / Amount, and RoundView pairs the two, both null while the round is still open. Never re-derive a payment from a multiplier.

A round says whether it is open, not how it went

RoundView.active is a boolean: true while the round is still open, false once it is settled.

⚠️ This field was status: 'ACTIVE' | 'ENDED' through 0.2.0, and RoundStatus no longer exists. active is Stake Engine's spelling, and matching it is what lets a stock Stake client cash out at all — ts-client calls EndRound() only from inside if (data?.round?.active).

It is a lifecycle, not a result. A win, a loss, a cash-out and a push all report active: false; what the round did is payoutMultiplier and the game's own events. That is deliberate, and it is why the field is a boolean rather than a union that could grow a third member.

RoundView.state is what the game draws, and its shape is the game's

Typed unknown, because it belongs to the game rather than to this package. There are two shapes, and which one a game gets never varies round to round:

  • a game computed in real time answers its publicState() projection, an object — only ever what the player is entitled to see at that moment;
  • a game whose rounds are pre-generated answers an array of frames, the round as its generator wrote it. It is the same array GET /bet/replay/{game}/{version}/{mode}/{event} serves under the same name, so a shared replay link renders the round its player watched.

⚠️ state is not events. events is the server's stream for the round — roundStart, the game's own events, winInfo, roundEnd — numbered from 0 by the server, with the bare-float multipliers described above. A pre-generated game's state keeps the generator's numbering and the generator's multiplier encoding, which is typically ×100; a frame field named amount is in that multiplier space and is not money, unlike RoundView.amount beside it.

Settle from payout. Animate from state.

AuthenticateResponse.round is absent when there is none — not null. It was RoundView | null through 0.2.0; a live Stake /wallet/authenticate carries exactly two top-level keys, config and balance, so ours now omits the key too:

type Auth = import('@buffalo-game/originals-protocol').AuthenticateResponse
function isResuming(response: Auth): boolean {
  return response.round !== undefined
}

⚠️ if (response.round) is unchanged and still correct. What stops working is response.round === null as a positive test for "no round in progress" — legal TypeScript against an optional property, so it compiles and quietly never matches. Write !response.round.

Twelve jurisdiction flags, and whose they are

GameConfig.jurisdiction is required and always sent: eleven booleans and one duration, describing what the player's jurisdiction permits and requires of the presentation.

config.jurisdiction.socialCasino          // false
config.jurisdiction.disabledAutoplay      // false
config.jurisdiction.displayRTP            // false
config.jurisdiction.minimumRoundDuration  // 0

The names and types are Stake Engine's JurisdictionFlags, taken field for field. Required rather than optional because a stock Stake client reads all twelve off data.config.jurisdiction with no optional chaining — an absent object is a TypeError on the first call a game makes, before a bet button is ever drawn.

⚠️ These are compliance statements, and they belong to the operator. socialCasino says the play is not for money; displayRTP and displaySessionTimer are disclosures some regulators mandate; minimumRoundDuration is a speed-of-play limit several set by law. So the server resolves them from the operator's record and from nowhere else, because the operator holds the licence and knows the player's jurisdiction — the RGS does not, and filling them in on the operator's behalf would be the RGS making a regulatory claim.

An operator that has declared nothing gets DEFAULT_JURISDICTION, every flag false and minimumRoundDuration: 0. That is a placeholder meaning "the operator has told us nothing", not a statement that no rules apply. resolveJurisdiction folds a partial declaration onto it and is exported so an integrator can build the same object the server does. Both tolerate undefined and null for "not declared" — a cleared DynamoDB attribute and an absent JSON object both arrive as the latter.

🔴 As shipped, nothing declares anything. The operator directory a deployed RGS builds reads an id and a secret out of the legacy operator table and returns those two fields alone; the table has no jurisdiction attribute and there is no interface for setting one. So DEFAULT_JURISDICTION is not the fallback in production — it is the whole of production, for every operator, until that configuration path is built. OperatorRecord.betLimits is in exactly the same state for exactly the same reason.

The wire contract is finished and the resolution logic is finished; the configuration path is not. Nothing here changes shape when it lands — the same twelve fields simply start carrying values somebody chose — so a client written against this section today keeps working, and a client that only handles all-false starts silently ignoring a real jurisdiction on that day.

import { DEFAULT_JURISDICTION, resolveJurisdiction } from '@buffalo-game/originals-protocol'
const declared = resolveJurisdiction({ minimumRoundDuration: 3 })
declared.minimumRoundDuration      // 3 — the operator's value
declared.displayRTP                // false — undeclared, so still inert
DEFAULT_JURISDICTION.socialCasino  // false — the placeholder, not a clearance

There is no maxPayout field, and there was one through 0.2.0. An operator's per-round payout cap reaches the client as a bet limit: the server divides it by maxWinMultiplier and the quotient is already folded into effectiveMaxBet. Reading it was reading the input to a figure you already had.

Currency is display, and payouts are the same figure in every currency

⚠️ This changed in 0.3.0. A currency's decimals used to decide what an amount meant: payouts were quantized to it, downward, per currency, so a ¥ player and a $ player were paid different figures for the same round. They are not any more. Money is six decimal places deep for every currency alike, and settleRational settles at that depth for everyone.

⚠️ And this changed again in 0.3.2. A payout that is not a whole number of millionths is now floored there rather than refused — at most one millionth of a currency unit per round, the same bound whatever the currency, which is why the paragraph above still holds. Use settleRationalParts if you need the residue; it comes back beside the payout.

That is Stake Engine's own model, in its own words: "Monetary values in the Stake Engine are integers with six decimal places of precision. Currency impacts only the display layer - it does not affect gameplay logic."

Three consequences for an integrator:

  • balance.decimals is gone from the wire. A Balance is { amount, currency }, which is what Stake's /wallet/balance answers. Take the symbol and the number of places to render from requireCurrency(balance.currency).
  • A payout can be finer than the smallest coin. ¥0.0045 is a real payout and is credited as 4500. Render it at the currency's precision if you like — but do not treat the rendered figure as the amount, and do not scale by it.
  • formatMoney rounds for display only. Pass { decimals: MONEY_DECIMALS } when you need the exact figure.
import { formatMoney, requireCurrency, MONEY_DECIMALS } from '@buffalo-game/originals-protocol'

const jpy = requireCurrency('JPY')
formatMoney(4500n, jpy) // '¥0'
formatMoney(4500n, jpy, { decimals: MONEY_DECIMALS }) // '¥0.004500'

The currency table now carries only codes Stake Engine also supports, with Stake's own symbols and display precision. Codes that were ours alone were removed in 0.3.0; a /launch naming one is refused with ERR_VAL and an UNKNOWN_CURRENCY issue on player.currency.

Rounding, where it happens at all, is display rounding and is marked as such. It is never applied to money, and it is not left to the language.

Requests are a closed envelope, with room for the fields Stake sends

An unknown top-level field is refused, not ignored: a client that sends metaa instead of meta would otherwise place a real bet under default parameters and never hear about it. Nested game payloads — meta, action, launch config — stay open; only the envelope is closed. Responses are the other way round and stay additive, so a client may ignore fields it does not know.

Two fields exist inside that envelope purely so that a body written to Stake Engine's shape is a legal request here:

const authenticate: import('@buffalo-game/originals-protocol').AuthenticateRequest = {
  sessionID: 'the id the launch URL arrived with',
  language: 'en',                              // optional; accepted, never read
}

const play: import('@buffalo-game/originals-protocol').PlayRequest = {
  sessionID: authenticate.sessionID,
  mode: 'BASE',
  amount: 1000000,
  currency: 'USD',                             // optional; checked, never used to select
}
  • language is accepted and deliberately unused. Every message this service produces is an English literal, so there is nothing for it to select, and it is not new information either — the operator declared it at /v1/launch and the launch URL handed it to the client as ?lang=. It is taken because refusing it refused every standard Stake client on its first call.
  • currency is a cross-check, not a choice. The session's currency was fixed at launch and confirmed against the operator's wallet; sending a different one is refused with ERR_VAL rather than silently betting the session's. Send the currency you were given in balance, or omit the field.

EndRoundRequest.roundId is optional for the same reason: Stake's req_end_round is { sessionID } and nothing else. Name the round when you know it — a named round is bound into the idempotency record before the wallet is touched, so a retry provably settles the same round — and omit it to get Stake's model, "end the round I am in". Only one round can be open per player and game, so the resolved round is never ambiguous; what is lost is the ability to tell "you already ended it" from "you never had one", since both answer ERR_ROUND_STATE.

A third field moved rather than appeared: the game's own payload on /play is now called meta, not params. Stake's req_play and req_action both carry a meta, described as "values that are not determined by the RGS but by the Game and the Game Provider… sent as is and is not validated by the RGS" — the same slot, word for word. Renaming rather than accepting both is what avoids ever having to answer "which one wins when a client sends both".

The type is unchanged and stays ours: Stake declares Meta as Record<string, never>, an object with no legal properties, which no real game parameter could be assigned to. Here it is Readonly<Record<string, unknown>>, as params was.

A request still on the 0.2.0 name is refused, loudly, rather than played at default parameters — by the compiler if you upgraded the types, and by the server either way:

type Play = import('@buffalo-game/originals-protocol').PlayRequest
const fresh: Play = { sessionID: '…', mode: 'BASE', amount: 1000000, meta: { mines: 3 } }
const stale: Play = { sessionID: '…', mode: 'BASE', amount: 1000000, params: { mines: 3 } } // ❌ UNKNOWN_FIELD

Nothing else Stake's schema lists is accepted; metaa, paramz and every other near miss is still an UNKNOWN_FIELD.

Errors

import { httpStatusFor } from '@buffalo-game/originals-protocol'

Eleven codes. Eight match Stake Engine's set:

ERR_VAL · ERR_IPB · ERR_IS · ERR_ATE · ERR_GLE · ERR_LOC · ERR_GEN · ERR_MAINTENANCE

Three are ours, for round and idempotency state:

ERR_ROUND_STATE · ERR_IDEMPOTENCY · ERR_IN_PROGRESS

⚠️ ERR_IDEMPOTENCY and ERR_IN_PROGRESS are both 409 and call for opposite responses — never resend, versus resend unchanged. See the client README.