<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Saleh Nouri</title>
    <description>The latest articles on DEV Community by Saleh Nouri (@codiologism).</description>
    <link>https://dev.to/codiologism</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4245656%2Fa8a7af69-e6f6-4e7c-b80d-34fdb6e46283.jpg</url>
      <title>DEV Community: Saleh Nouri</title>
      <link>https://dev.to/codiologism</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/codiologism"/>
    <language>en</language>
    <item>
      <title>How we made battles deterministic and server-verified in a Telegram mini-game</title>
      <dc:creator>Saleh Nouri</dc:creator>
      <pubDate>Sun, 15 Nov 2026 09:52:25 +0000</pubDate>
      <link>https://dev.to/codiologism/how-we-made-battles-deterministic-and-server-verified-in-a-telegram-mini-game-26l2</link>
      <guid>https://dev.to/codiologism/how-we-made-battles-deterministic-and-server-verified-in-a-telegram-mini-game-26l2</guid>
      <description>&lt;p&gt;We are building Seraph Legion, an angel-collector RPG that runs inside Telegram as a Mini App. This is a writeup of the most interesting engineering decision in the project: &lt;strong&gt;how battles work&lt;/strong&gt;, and what a farming exploit taught us about trusting clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint
&lt;/h2&gt;

&lt;p&gt;A Telegram Mini App is just a webview. Anything your client computes, a motivated player can fake - the JS is right there, the API is a plain HTTPS endpoint. So the design contract from day one:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The request body selects &lt;em&gt;what&lt;/em&gt; the player wants to do. The server decides &lt;em&gt;how much&lt;/em&gt; they get.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Deterministic battles
&lt;/h2&gt;

&lt;p&gt;The entire battle simulator lives in a pure TypeScript core ("game-core") shared between client and server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Seeded RNG.&lt;/strong&gt; Every fight derives its randomness from a seed generated at battle/start. No Math.random(). The seed, the team composition, and the data tables fully determine the outcome.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pure functions.&lt;/strong&gt; Given the same inputs, the sim produces the same turn-by-turn log - on the client, on the server, in tests, everywhere.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replay verification.&lt;/strong&gt; When the client calls battle/finish, it reports what it thinks happened. The server re-runs the sim with the same seed and inputs and compares. Report matches the replay? Rewards are computed server-side from the data tables and written to the ledger. Mismatch? Rejected.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The client still runs the full sim for the visual battle experience - animations, turn order, damage numbers. It just does not get to decide the result.&lt;/p&gt;

&lt;h2&gt;
  
  
  The exploit that hardened everything
&lt;/h2&gt;

&lt;p&gt;We had one endpoint that broke the contract: a migration importer for players coming from a prototype build. It accepted declared progress and clamped it generously (gold under a big cap, gems under a smaller one).&lt;/p&gt;

&lt;p&gt;A farm of ~23 accounts, all from one IP, hit it within a half-hour window and minted themselves millions in currency.&lt;/p&gt;

&lt;p&gt;The fix went deeper than closing the endpoint:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The import route is now &lt;strong&gt;sealed by default&lt;/strong&gt; - admin flips a switch for supervised stragglers&lt;/li&gt;
&lt;li&gt;Registration IP is captured at auth; shared-IP cohorts get flagged&lt;/li&gt;
&lt;li&gt;An anomaly scanner watches for plush balances vs account age and ledger drift (stored points vs the ledger sum)&lt;/li&gt;
&lt;li&gt;Everything suspicious lands in a review queue - flag, never silently allow&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every other write path was already server-verified - summons, quests, raids, ads, sweeps. The farm found the one door we had left unlatched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic sims are cheap anti-cheat.&lt;/strong&gt; If your game logic can run identically on both sides, replay verification costs almost nothing and defeats result forgery completely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The body is a wish, not a fact.&lt;/strong&gt; Keep every currency/progress column out of reach of request values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assume a farm, not a cheater.&lt;/strong&gt; The expensive attacks are scripted multi-account, not one player hex-editing.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The game is live at &lt;a href="https://seraphlegion.com" rel="noopener noreferrer"&gt;seraphlegion.com&lt;/a&gt; - happy to answer questions about the sim or the TON jetton work.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>security</category>
    </item>
    <item>
      <title>Showdev: Seraph Legion - a gothic angel-collector RPG that runs inside Telegram</title>
      <dc:creator>Saleh Nouri</dc:creator>
      <pubDate>Sun, 15 Nov 2026 09:42:32 +0000</pubDate>
      <link>https://dev.to/codiologism/showdev-seraph-legion-a-gothic-angel-collector-rpg-that-runs-inside-telegram-4gfl</link>
      <guid>https://dev.to/codiologism/showdev-seraph-legion-a-gothic-angel-collector-rpg-that-runs-inside-telegram-4gfl</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnty1bqynv24lq6v7g8kk.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fnty1bqynv24lq6v7g8kk.jpg" alt=" " width="440" height="900"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Hey dev.to - long-time lurker, first post. I want to show you the game we have been building at Codiologism: &lt;strong&gt;Seraph Legion&lt;/strong&gt;, a gothic angel-collector RPG that runs entirely inside Telegram as a Mini App - and also in the plain browser, no install needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it is
&lt;/h2&gt;

&lt;p&gt;You summon angels across warring choirs, build a five-angel legion, and fight through a campaign plus a daily roguelite gauntlet called the Pilgrim's Path (12 rites, escalating boons). Progression is idle-friendly - your legion keeps working between sessions - and everything feeds points that will convert into an airdrop of our upcoming SRPH token on TON.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Client:&lt;/strong&gt; React 18 + Vite + zustand, one codebase serving both the Telegram Mini App and the browser build&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Game core:&lt;/strong&gt; pure TypeScript - data tables, seeded RNG, and a deterministic battle simulator&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Server:&lt;/strong&gt; Fastify + PostgreSQL. Telegram initData is the only auth; sessions are bearer tokens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain:&lt;/strong&gt; a TEP-74 jetton written in Tact, tested in &lt;a class="mentioned-user" href="https://dev.to/ton"&gt;@ton&lt;/a&gt;/sandbox&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The interesting part: trustless-feeling battles
&lt;/h2&gt;

&lt;p&gt;Battles are simulated deterministically from a seed. When a fight ends, the client sends the outcome and the server &lt;em&gt;replays the same sim&lt;/em&gt; to verify it before writing rewards. The request body can say anything - the server decides what actually happened. Rewards, summons, and progress are computed server-side from the data tables; the client only selects what to do.&lt;/p&gt;

&lt;p&gt;We learned this the hards way: a farming ring hit an early migration endpoint and minted themselves balances, so now the API contract treats every client as hostile. Happy to write that story up if people are curious.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Play (Telegram): &lt;a href="https://t.me/SeraphLegionBot/play" rel="noopener noreferrer"&gt;https://t.me/SeraphLegionBot/play&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Play (browser): &lt;a href="https://app.seraphlegion.com" rel="noopener noreferrer"&gt;https://app.seraphlegion.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://seraphlegion.com" rel="noopener noreferrer"&gt;https://seraphlegion.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Also on itch: &lt;a href="https://seraphlegion.itch.io/seraph-legion" rel="noopener noreferrer"&gt;https://seraphlegion.itch.io/seraph-legion&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Feedback, bug reports, gacha horror stories - all welcome. We are a tiny team and we read everything.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>gamedev</category>
      <category>webdev</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
