{"_id":"@buffalo-game/originals-protocol","_rev":"4-aa19260ef36123cefe401b069f73234d","name":"@buffalo-game/originals-protocol","dist-tags":{"latest":"0.3.3"},"versions":{"0.1.0":{"name":"@buffalo-game/originals-protocol","version":"0.1.0","license":"SEE LICENSE IN LICENSE","_id":"@buffalo-game/originals-protocol@0.1.0","maintainers":[{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"}],"dist":{"shasum":"d1b71ec7c5537850e28f3dbfbeb7eb041c2e95a9","tarball":"https://registry.npmjs.org/@buffalo-game/originals-protocol/-/originals-protocol-0.1.0.tgz","fileCount":25,"integrity":"sha512-qANQmkVFeGPITDE1rbi+mzxKqMFPz6UKCvhV1gPiu3h5QoIM5qnWCWNnDdWmWMDAGWY5nzgV0Dyu7CWknd7aWg==","signatures":[{"sig":"MEUCIAdHzy70qqSjagT3yen/X3byoDayHy3C6dPhUOCNVREEAiEA1Jn/g/cHRRjsBtnhzdQ8u/R1+COQZNw8o7fqu+tVgx4=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":134227},"main":"./dist/index.js","type":"module","_from":"file:buffalo-game-originals-protocol-0.1.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -b"},"_npmUser":{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"},"_resolved":"/private/var/folders/w0/874ycw9s0dv73b2d1476sl8m0000gn/T/08ca42448bd2f4ed081b5ee4d3614265/buffalo-game-originals-protocol-0.1.0.tgz","_integrity":"sha512-qANQmkVFeGPITDE1rbi+mzxKqMFPz6UKCvhV1gPiu3h5QoIM5qnWCWNnDdWmWMDAGWY5nzgV0Dyu7CWknd7aWg==","_npmVersion":"10.9.2","description":"Wire types, money handling and error codes shared by the Originals RGS and its game clients.","directories":{},"_nodeVersion":"22.14.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/originals-protocol_0.1.0_1787123790240_0.5735406925987436","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@buffalo-game/originals-protocol","version":"0.2.0","license":"SEE LICENSE IN LICENSE","_id":"@buffalo-game/originals-protocol@0.2.0","maintainers":[{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"}],"dist":{"shasum":"8f5dbb22f57fc0ecea8ae1a63ca762da7479687a","tarball":"https://registry.npmjs.org/@buffalo-game/originals-protocol/-/originals-protocol-0.2.0.tgz","fileCount":25,"integrity":"sha512-fkaXObrm6MdEsechp2qHafh3kyPY+t9JDKg7fPfHC7xpNhgeHQ+aGGPmNTm5H4qZ5a4IO6BSTDjVX2DNuTgKMQ==","signatures":[{"sig":"MEQCIATchTqfiDyYxjZUgguTBpT42AuYTwidmQ1dPdbqf79yAiBGQ4WrR+xiPFhOYXFv2iehxOrSN2uO+chUPCHf74K9dg==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":141738},"main":"./dist/index.js","type":"module","_from":"file:.release/protocol/buffalo-game-originals-protocol-0.2.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -b"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:11a8926e-7c2d-48f5-8a49-e11a0c780644"}},"_resolved":"/home/runner/work/gamegen-originals/gamegen-originals/.release/protocol/buffalo-game-originals-protocol-0.2.0.tgz","_integrity":"sha512-fkaXObrm6MdEsechp2qHafh3kyPY+t9JDKg7fPfHC7xpNhgeHQ+aGGPmNTm5H4qZ5a4IO6BSTDjVX2DNuTgKMQ==","_npmVersion":"12.0.2","description":"Wire types, money handling and error codes shared by the Originals RGS and its game clients.","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/originals-protocol_0.2.0_1787383033332_0.505058941812881","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@buffalo-game/originals-protocol","version":"0.3.0","license":"SEE LICENSE IN LICENSE","_id":"@buffalo-game/originals-protocol@0.3.0","maintainers":[{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"}],"dist":{"shasum":"4b050f2dc4c3a1cd62214b46cc1387024714c48a","tarball":"https://registry.npmjs.org/@buffalo-game/originals-protocol/-/originals-protocol-0.3.0.tgz","fileCount":28,"integrity":"sha512-O3nmx7cbd1A5PxeAWQMETfWweR2r32KgIltDcj2FF+EGpCqynRsCHLfYY7hXwq4Mp49uI3Nt23l5h4Ua0YTFNA==","signatures":[{"sig":"MEYCIQD1sGrhHDSYsbKS7wfguyTKV2Mti4zr8Ig446Y/h0mV6QIhAM9rp3LcOsyDe8xRHz0Zy5FAmLR79PemC665iyuFVvQP","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":254562},"main":"./dist/index.js","type":"module","_from":"file:.release/protocol/buffalo-game-originals-protocol-0.3.0.tgz","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"scripts":{"build":"tsc -b"},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:11a8926e-7c2d-48f5-8a49-e11a0c780644"}},"_resolved":"/home/runner/work/gamegen-originals/gamegen-originals/.release/protocol/buffalo-game-originals-protocol-0.3.0.tgz","_integrity":"sha512-O3nmx7cbd1A5PxeAWQMETfWweR2r32KgIltDcj2FF+EGpCqynRsCHLfYY7hXwq4Mp49uI3Nt23l5h4Ua0YTFNA==","_npmVersion":"12.0.2","description":"Wire types, money handling and error codes shared by the Originals RGS and its game clients.","directories":{},"_nodeVersion":"24.19.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"_npmOperationalInternal":{"tmp":"tmp/originals-protocol_0.3.0_1787668684332_0.1978512018078209","host":"s3://npm-registry-packages-npm-production"}},"0.3.3":{"name":"@buffalo-game/originals-protocol","version":"0.3.3","description":"Wire types, money handling and error codes shared by the Originals RGS and its game clients.","license":"SEE LICENSE IN LICENSE","type":"module","main":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"}},"publishConfig":{"access":"public"},"scripts":{"build":"tsc -b"},"_id":"@buffalo-game/originals-protocol@0.3.3","_integrity":"sha512-TFxBVS9M34MqDDSadxlPlZVLLfU21K2a3naRVgKylY34z8r5sMe6LFI7EI3Vj9XmmvvwdkJ5zqcQmCM/7dfQuw==","_resolved":"/home/runner/work/gamegen-originals/gamegen-originals/.release/protocol/buffalo-game-originals-protocol-0.3.3.tgz","_from":"file:.release/protocol/buffalo-game-originals-protocol-0.3.3.tgz","_nodeVersion":"24.19.0","_npmVersion":"12.0.2","dist":{"integrity":"sha512-TFxBVS9M34MqDDSadxlPlZVLLfU21K2a3naRVgKylY34z8r5sMe6LFI7EI3Vj9XmmvvwdkJ5zqcQmCM/7dfQuw==","shasum":"9f2e74dba18ea884d8a8ee6b2be837f55abe425d","tarball":"https://registry.npmjs.org/@buffalo-game/originals-protocol/-/originals-protocol-0.3.3.tgz","fileCount":28,"unpackedSize":289113,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCaK3FSazwhkqrESSRZmXwHLY6Bj8S2b995jqAXOYOc6gIgdbfRj0kjiEe8CfzBTJt9yftmflHqx8a2yD2t16PQ4bY="}]},"_npmUser":{"name":"GitHub Actions","email":"npm-oidc-no-reply@github.com","trustedPublisher":{"id":"github","oidcConfigId":"oidc:11a8926e-7c2d-48f5-8a49-e11a0c780644"}},"directories":{},"maintainers":[{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/originals-protocol_0.3.3_1788465147006_0.3100001003182171"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-19T07:16:29.879Z","modified":"2026-09-03T19:52:27.379Z","0.1.0":"2026-08-19T07:16:30.396Z","0.2.0":"2026-08-22T07:17:13.526Z","0.3.0":"2026-08-25T14:38:04.473Z","0.3.3":"2026-09-03T19:52:27.154Z"},"license":"SEE LICENSE IN LICENSE","description":"Wire types, money handling and error codes shared by the Originals RGS and its game clients.","maintainers":[{"name":"buffalo-ops","email":"manta@dev.gamegen.diy"}],"readme":"# @buffalo-game/originals-protocol\n\nThe wire contract between the Originals RGS and every game client: request and\nresponse types, the error-code set, and the money primitives both sides share.\n\n> **Licence.** Proprietary. Use requires a current service agreement. See\n> [LICENSE](./LICENSE). Publication on npm is for distribution to authorised\n> licensees; it is not an offer of a licence to the public.\n\nMost integrations do not depend on this package directly —\n[`@buffalo-game/originals-client`](https://www.npmjs.com/package/@buffalo-game/originals-client)\nbrings it in and re-exports the types you need. Depend on it directly only if\nyou are writing your own client, or checking a server's behaviour against the\ncontract.\n\n## Install\n\n```bash\nnpm install @buffalo-game/originals-protocol\n```\n\n⚠️ **Coming from `0.2.0`?** `0.3.0` is a breaking release in three independent\nways — amounts are JSON integers rather than decimal strings, multipliers are\nbare floats rather than 1e6-scaled strings, and `GameConfig.maxPayout` is\nremoved. **Only the multiplier change can be wrong without an error**; the other\ntwo are refused outright. [CHANGELOG.md](./CHANGELOG.md) has the field list and\na migration order.\n\n## What is in it\n\n| Module | What it holds |\n|---|---|\n| `types` | Every request and response the RGS accepts and returns |\n| `money` | Amounts as `bigint` counts of millionths, and the JSON integer they travel as |\n| `currency` | Symbol and display precision per currency code — metadata, never arithmetic |\n| `bet` | How the bet ceiling is folded from the game's, the operator's, and the wallet's limits |\n| `settlement` | The settlement contract in force: exact, versioned, recorded |\n| `validate` | The validation primitives every request check is built from |\n| `request` | Validation for each request the RGS accepts |\n| `errors` | The `ERR_*` set |\n| `playback` | Replaying a round's events from wherever the player already got to |\n| `jurisdiction` | The twelve presentation rules a licence imposes, and the inert default |\n| `rational` | Exact non-negative rationals, for figures that must not drift |\n\n## Money\n\nEvery amount is a non-negative `bigint` count of **millionths of one currency\nunit**, and travels on the wire as a **JSON integer** of those same millionths —\n`1_000_000` is 1.00, which is Stake Engine's own encoding.\n\n```ts\nimport { MONEY_SCALE, MAX_WIRE_MINOR } from '@buffalo-game/originals-protocol'\n\nMONEY_SCALE      // 1_000_000n  — one unit of currency\nMAX_WIRE_MINOR   // 9_007_199_254_740_991n — the most the wire can carry\n```\n\n⚠️ **This changed in `0.3.0`.** Through `0.2.0` the same values travelled as\ndecimal-integer strings; a client written against that encoding sends `'1000000'`\nwhere the server now requires `1000000`, and is refused with `ERR_VAL`.\n\nThe scale did not move and the arithmetic rule did not soften. Nothing in this\nsystem computes in `number`: an inbound amount is checked with\n`Number.isSafeInteger` and converted to `bigint` **in the same expression that\naccepts it**, so no amount is ever an operand of a floating-point operation. A\n`number` here is a transport encoding, not a numeric type.\n\n`parseWireAmount` answers `{ ok: true, value }` or `{ ok: false, code, message }`\n— it never throws and never guesses:\n\n```ts\nparseWireAmount(1000000)     // ✅ { ok: true, value: 1_000_000n }\nparseWireAmount('1000000')   // ❌ STRING_INPUT — that is the 0.2.0 encoding\nparseWireAmount(1.5)         // ❌ FRACTIONAL\nparseWireAmount(-1000000)    // ❌ SIGNED\n```\n\n⚠️ **The encoding is bounded, which the string form was not.** `MAX_WIRE_MINOR`\nis `Number.MAX_SAFE_INTEGER` minor units — about 9.007e9 currency units. Above\nit `JSON.parse` has already changed the value before any check can run, so both\ndirections refuse rather than round: `parseWireAmount` fails inbound (`ERR_VAL`,\nbefore a wallet is touched) and `toWireAmount` **throws** outbound (`ERR_GEN`),\nnever emitting a truncated figure. **Bets never reach the wall; balances can** —\na balance comes from the operator's wallet and nothing bounds it, so\nhigh-denomination currencies hit it for real: roughly $360,000 in VND, $563,000\nin IDR.\n\n**The decimal-integer string is not gone — it is no longer the wire.**\n`parseDeclaredAmount` still reads *declaration sources* (a version's\n`manifest.json` bet ladder, the catalog, CLI arguments), because those bytes go\ninto `logicHash` and re-encoding them would invalidate every published RTP.\nReach for `parseWireAmount` for anything arriving over HTTP and\n`parseDeclaredAmount` for anything read off disk; they are not interchangeable.\n\n**Multipliers are numbers too, but they are not scaled.** `payoutMultiplier`\nand `maxWinMultiplier` are bare JSON floats — `2` is 2×, and\n`payoutMultiplier: 1.9` sits next to `payout: 19000000` in one response. The two\nencodings differ on purpose and both are Stake's: a live `/wallet/play` answers\n`payoutMultiplier: 1.09` beside `payout: 1090000`, and the RGS schema behind\nStake's own SDK types the field `PayoutMultiplier: number`, described as\n**\"Payout Multiplier for the bet. Payout / Amount.\"**\n\n⚠️ **Through `0.2.0` these were 1e6-scaled decimal strings**, so a client carried\nforward from that release divides by 1e6 and lands a millionth of the real\nfigure. `MULTIPLIER_SCALE` is still exported — it describes the server's internal\n`Mult` — but it takes no part in reading these two fields.\n\nThe scale did not move; only the wire did. `Mult` is still an exact 1e6-scaled\n`bigint`, every multiplier is still computed as a `Rational`, and\n`toWireMultiplier` does the single division at the boundary. `payout` remains\nthe authoritative number — the multiplier is literally `Payout / Amount`, and\n`RoundView` pairs the two, both `null` while the round is still open. Never\nre-derive a payment from a multiplier.\n\n## A round says whether it is open, not how it went\n\n`RoundView.active` is a `boolean`: `true` while the round is still open, `false`\nonce it is settled.\n\n⚠️ **This field was `status: 'ACTIVE' | 'ENDED'` through `0.2.0`**, and\n`RoundStatus` no longer exists. `active` is Stake Engine's spelling, and matching\nit is what lets a stock Stake client cash out at all — `ts-client` calls\n`EndRound()` only from inside `if (data?.round?.active)`.\n\nIt is a lifecycle, not a result. A win, a loss, a cash-out and a push all report\n`active: false`; what the round *did* is `payoutMultiplier` and the game's own\nevents. That is deliberate, and it is why the field is a boolean rather than a\nunion that could grow a third member.\n\n### `RoundView.state` is what the game draws, and its shape is the game's\n\nTyped `unknown`, because it belongs to the game rather than to this package.\nThere are two shapes, and which one a game gets never varies round to round:\n\n- a game computed in real time answers **its `publicState()` projection**, an\n  object — only ever what the player is entitled to see at that moment;\n- a game whose rounds are **pre-generated** answers **an array of frames**, the\n  round as its generator wrote it. It is the same array\n  `GET /bet/replay/{game}/{version}/{mode}/{event}` serves under the same name,\n  so a shared replay link renders the round its player watched.\n\n⚠️ **`state` is not `events`.** `events` is the server's stream for the round —\n`roundStart`, the game's own events, `winInfo`, `roundEnd` — numbered from 0 by\nthe server, with the bare-float multipliers described above. A pre-generated\ngame's `state` keeps the generator's numbering and the generator's multiplier\nencoding, which is typically ×100; a frame field named `amount` is in that\nmultiplier space and is **not money**, unlike `RoundView.amount` beside it.\n\nSettle from `payout`. Animate from `state`.\n\n**`AuthenticateResponse.round` is absent when there is none — not `null`.** It\nwas `RoundView | null` through `0.2.0`; a live Stake `/wallet/authenticate`\ncarries exactly two top-level keys, `config` and `balance`, so ours now omits\nthe key too:\n\n```ts\ntype Auth = import('@buffalo-game/originals-protocol').AuthenticateResponse\nfunction isResuming(response: Auth): boolean {\n  return response.round !== undefined\n}\n```\n\n⚠️ `if (response.round)` is unchanged and still correct. What stops working is\n`response.round === null` as a positive test for \"no round in progress\" — legal\nTypeScript against an optional property, so it compiles and quietly never\nmatches. Write `!response.round`.\n\n## Twelve jurisdiction flags, and whose they are\n\n`GameConfig.jurisdiction` is **required and always sent**: eleven booleans and\none duration, describing what the player's jurisdiction permits and requires of\nthe presentation.\n\n```ts\nconfig.jurisdiction.socialCasino          // false\nconfig.jurisdiction.disabledAutoplay      // false\nconfig.jurisdiction.displayRTP            // false\nconfig.jurisdiction.minimumRoundDuration  // 0\n```\n\nThe names and types are Stake Engine's `JurisdictionFlags`, taken field for\nfield. Required rather than optional because a stock Stake client reads all\ntwelve off `data.config.jurisdiction` with no optional chaining — an absent\nobject is a `TypeError` on the first call a game makes, before a bet button is\never drawn.\n\n⚠️ **These are compliance statements, and they belong to the operator.**\n`socialCasino` says the play is not for money; `displayRTP` and\n`displaySessionTimer` are disclosures some regulators mandate;\n`minimumRoundDuration` is a speed-of-play limit several set by law. So the\nserver resolves them from the operator's record and from nowhere else, because\nthe operator holds the licence and knows the player's jurisdiction — the RGS\ndoes not, and filling them in on the operator's behalf would be the RGS making a\nregulatory claim.\n\nAn operator that has declared nothing gets `DEFAULT_JURISDICTION`, every flag\n`false` and `minimumRoundDuration: 0`. **That is a placeholder meaning \"the\noperator has told us nothing\", not a statement that no rules apply.**\n`resolveJurisdiction` folds a partial declaration onto it and is exported so an\nintegrator can build the same object the server does. Both tolerate `undefined`\nand `null` for \"not declared\" — a cleared DynamoDB attribute and an absent JSON\nobject both arrive as the latter.\n\n🔴 **As shipped, nothing declares anything.** The operator directory a deployed\nRGS builds reads an id and a secret out of the legacy operator table and returns\nthose two fields alone; the table has no jurisdiction attribute and there is no\ninterface for setting one. **So `DEFAULT_JURISDICTION` is not the fallback in\nproduction — it is the whole of production**, for every operator, until that\nconfiguration path is built. `OperatorRecord.betLimits` is in exactly the same\nstate for exactly the same reason.\n\nThe wire contract is finished and the resolution logic is finished; **the\nconfiguration path is not**. Nothing here changes shape when it lands — the same\ntwelve fields simply start carrying values somebody chose — so a client written\nagainst this section today keeps working, and a client that only handles\nall-`false` starts silently ignoring a real jurisdiction on that day.\n\n```ts\nimport { DEFAULT_JURISDICTION, resolveJurisdiction } from '@buffalo-game/originals-protocol'\nconst declared = resolveJurisdiction({ minimumRoundDuration: 3 })\ndeclared.minimumRoundDuration      // 3 — the operator's value\ndeclared.displayRTP                // false — undeclared, so still inert\nDEFAULT_JURISDICTION.socialCasino  // false — the placeholder, not a clearance\n```\n\n**There is no `maxPayout` field**, and there was one through `0.2.0`. An\noperator's per-round payout cap reaches the client as a *bet* limit: the server\ndivides it by `maxWinMultiplier` and the quotient is already folded into\n`effectiveMaxBet`. Reading it was reading the input to a figure you already had.\n\n### Currency is display, and payouts are the same figure in every currency\n\n⚠️ **This changed in `0.3.0`.** A currency's `decimals` used to decide what an\namount *meant*: payouts were quantized to it, downward, per currency, so a ¥\nplayer and a $ player were paid different figures for the same round. They are\nnot any more. Money is **six decimal places deep for every currency alike**, and\n`settleRational` settles at that depth for everyone.\n\n⚠️ **And this changed again in `0.3.2`.** A payout that is not a whole number of\nmillionths is now **floored** there rather than refused — at most one millionth\nof a currency unit per round, the same bound whatever the currency, which is why\nthe paragraph above still holds. Use `settleRationalParts` if you need the\nresidue; it comes back beside the payout.\n\nThat is Stake Engine's own model, in its own words: *\"Monetary values in the\nStake Engine are integers with six decimal places of precision. Currency impacts\nonly the display layer - it does not affect gameplay logic.\"*\n\nThree consequences for an integrator:\n\n- **`balance.decimals` is gone from the wire.** A `Balance` is `{ amount,\n  currency }`, which is what Stake's `/wallet/balance` answers. Take the symbol\n  and the number of places to render from `requireCurrency(balance.currency)`.\n- **A payout can be finer than the smallest coin.** ¥0.0045 is a real payout and\n  is credited as `4500`. Render it at the currency's precision if you like — but\n  do not treat the rendered figure as the amount, and do not scale by it.\n- **`formatMoney` rounds for display only.** Pass `{ decimals: MONEY_DECIMALS }`\n  when you need the exact figure.\n\n```ts\nimport { formatMoney, requireCurrency, MONEY_DECIMALS } from '@buffalo-game/originals-protocol'\n\nconst jpy = requireCurrency('JPY')\nformatMoney(4500n, jpy) // '¥0'\nformatMoney(4500n, jpy, { decimals: MONEY_DECIMALS }) // '¥0.004500'\n```\n\nThe currency table now carries **only codes Stake Engine also supports**, with\nStake's own symbols and display precision. Codes that were ours alone were\nremoved in `0.3.0`; a `/launch` naming one is refused with `ERR_VAL` and an\n`UNKNOWN_CURRENCY` issue on `player.currency`.\n\nRounding, where it happens at all, is display rounding and is marked as such.\nIt is never applied to money, and it is not left to the language.\n\n## Requests are a closed envelope, with room for the fields Stake sends\n\nAn unknown top-level field is **refused**, not ignored: a client that sends\n`metaa` instead of `meta` would otherwise place a real bet under default\nparameters and never hear about it. Nested game payloads — `meta`, `action`,\nlaunch `config` — stay open; only the envelope is closed. Responses are the\nother way round and stay additive, so a client may ignore fields it does not\nknow.\n\nTwo fields exist inside that envelope purely so that a body written to Stake\nEngine's shape is a legal request here:\n\n```ts\nconst authenticate: import('@buffalo-game/originals-protocol').AuthenticateRequest = {\n  sessionID: 'the id the launch URL arrived with',\n  language: 'en',                              // optional; accepted, never read\n}\n\nconst play: import('@buffalo-game/originals-protocol').PlayRequest = {\n  sessionID: authenticate.sessionID,\n  mode: 'BASE',\n  amount: 1000000,\n  currency: 'USD',                             // optional; checked, never used to select\n}\n```\n\n- **`language`** is accepted and deliberately unused. Every message this service\n  produces is an English literal, so there is nothing for it to select, and it is\n  not new information either — the operator declared it at `/v1/launch` and the\n  launch URL handed it to the client as `?lang=`. It is taken because refusing it\n  refused every standard Stake client on its first call.\n- **`currency`** is a cross-check, not a choice. The session's currency was fixed\n  at launch and confirmed against the operator's wallet; sending a *different*\n  one is refused with `ERR_VAL` rather than silently betting the session's. Send\n  the currency you were given in `balance`, or omit the field.\n\n**`EndRoundRequest.roundId` is optional** for the same reason: Stake's\n`req_end_round` is `{ sessionID }` and nothing else. Name the round when you know\nit — a named round is bound into the idempotency record before the wallet is\ntouched, so a retry provably settles the same round — and omit it to get Stake's\nmodel, \"end the round I am in\". Only one round can be open per player and game,\nso the resolved round is never ambiguous; what is lost is the ability to tell\n\"you already ended it\" from \"you never had one\", since both answer\n`ERR_ROUND_STATE`.\n\nA third field moved rather than appeared: **the game's own payload on `/play` is\nnow called `meta`, not `params`.** Stake's `req_play` and `req_action` both carry\na `meta`, described as \"values that are not determined by the RGS but by the Game\nand the Game Provider… sent as is and is not validated by the RGS\" — the same\nslot, word for word. Renaming rather than accepting both is what avoids ever\nhaving to answer \"which one wins when a client sends both\".\n\nThe *type* is unchanged and stays ours: Stake declares `Meta` as\n`Record<string, never>`, an object with no legal properties, which no real game\nparameter could be assigned to. Here it is `Readonly<Record<string, unknown>>`,\nas `params` was.\n\nA request still on the `0.2.0` name is refused, loudly, rather than played at\ndefault parameters — by the compiler if you upgraded the types, and by the server\neither way:\n\n```ts\ntype Play = import('@buffalo-game/originals-protocol').PlayRequest\nconst fresh: Play = { sessionID: '…', mode: 'BASE', amount: 1000000, meta: { mines: 3 } }\nconst stale: Play = { sessionID: '…', mode: 'BASE', amount: 1000000, params: { mines: 3 } } // ❌ UNKNOWN_FIELD\n```\n\nNothing else Stake's schema lists is accepted; `metaa`, `paramz` and every other\nnear miss is still an `UNKNOWN_FIELD`.\n\n## Errors\n\n```ts\nimport { httpStatusFor } from '@buffalo-game/originals-protocol'\n```\n\nEleven codes. Eight match Stake Engine's set:\n\n`ERR_VAL` · `ERR_IPB` · `ERR_IS` · `ERR_ATE` · `ERR_GLE` · `ERR_LOC` ·\n`ERR_GEN` · `ERR_MAINTENANCE`\n\nThree are ours, for round and idempotency state:\n\n`ERR_ROUND_STATE` · `ERR_IDEMPOTENCY` · `ERR_IN_PROGRESS`\n\n⚠️ `ERR_IDEMPOTENCY` and `ERR_IN_PROGRESS` are both 409 and call for **opposite**\nresponses — never resend, versus resend unchanged. See the client README.\n","readmeFilename":"README.md"}