<?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: Arpan Sarkar</title>
    <description>The latest articles on DEV Community by Arpan Sarkar (@iambetaraybill).</description>
    <link>https://dev.to/iambetaraybill</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%2F1061718%2F69be10e7-aa01-4d23-a59b-0053f6f49461.jpeg</url>
      <title>DEV Community: Arpan Sarkar</title>
      <link>https://dev.to/iambetaraybill</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/iambetaraybill"/>
    <language>en</language>
    <item>
      <title>I built an auction for our FIFA draft night, and a local Gemma bids for the friend who can't be there</title>
      <dc:creator>Arpan Sarkar</dc:creator>
      <pubDate>Mon, 05 Oct 2026 00:32:07 +0000</pubDate>
      <link>https://dev.to/iambetaraybill/i-built-an-auction-for-our-fifa-draft-night-and-a-local-gemma-bids-for-the-friend-who-cant-be-4mn6</link>
      <guid>https://dev.to/iambetaraybill/i-built-an-auction-for-our-fifa-draft-night-and-a-local-gemma-bids-for-the-friend-who-cant-be-4mn6</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-weekend-2026-10-01"&gt;Hacktoberfest Weekend Challenge: Build for a Friend&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;Six of us play FIFA on a PS5. We were setting up a proper tournament, and the part that kept falling apart was picking squads.&lt;/p&gt;

&lt;p&gt;We'd always done it with a spreadsheet, which produces exactly the evening you'd expect: someone reads out names, someone else quietly takes all the good ones, and two people spend twenty minutes arguing about whether the list was fair before anyone touches a controller.&lt;/p&gt;

&lt;p&gt;An auction fixes that. Everyone gets the same budget, you bid for the players you want, and you live with the squad you built. Nobody can complain about a draft they set the prices in.&lt;/p&gt;

&lt;p&gt;Except an auction only works if everyone turns up. And Rohit was going to miss it.&lt;/p&gt;

&lt;p&gt;The usual fix is for someone in the room to "bid on his behalf," which means one of two things. Either you bid badly for him because you're busy bidding for yourself, or you quietly build yourself the better squad. Neither is a draft he'd have wanted.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Draft Night&lt;/strong&gt;: an auction that runs on one laptop over the living room wifi, with the TV showing the lot and everyone bidding from their phones. And anyone who can't be there gets a proxy manager — a local open-weight model that bids for them, live, against everyone in the room, inside their budget, from instructions they wrote in their own words.&lt;/p&gt;

&lt;p&gt;Rohit sent me this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want pace up front above all else. Get a keeper early so I'm not stuck with whoever is left. Never spend more than 40% of my budget on one player.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That paragraph is the whole input. No sliders, no priority rankings, no config file. He typed a few sentences into a text box and the model bid his draft.&lt;/p&gt;

&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%2Fi1io5z7bb852ehjbplf8.png" 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%2Fi1io5z7bb852ehjbplf8.png" alt="he TV screen during a live lot" width="800" height="507"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx7xaupeea10v4e5vwiz.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Frx7xaupeea10v4e5vwiz.gif" alt="TV Screen" width="600" height="1306"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfu9ek6a8k27smu5tz79.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fpfu9ek6a8k27smu5tz79.gif" alt="Phone Screen" width="800" height="391"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There's no deployed link, and that's deliberate rather than laziness. The whole thing runs on one laptop over a local network with no internet connection. Hosting it would undo the point of it.&lt;/p&gt;

&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%2Fv13skwdblle9ctk6mrfw.png" 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%2Fv13skwdblle9ctk6mrfw.png" alt="The phone view during bidding" width="460" height="880"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Your phone shows three things: what you've got left, who's on the block, and one big button. That's all anyone can handle while six people are shouting.&lt;/p&gt;

&lt;h2&gt;
  
  
  Code
&lt;/h2&gt;


&lt;div class="ltag-github-readme-tag"&gt;
  &lt;div class="readme-overview"&gt;
    &lt;h2&gt;
      &lt;img src="https://assets.dev.to/assets/github-logo-5a155e1f9a670af7944dd5e12375bc76ed542ea80224905ecaf878b9157cdefc.svg" alt="GitHub logo"&gt;
      &lt;a href="https://github.com/iambetaraybill" rel="noopener noreferrer"&gt;
        iambetaraybill
      &lt;/a&gt; / &lt;a href="https://github.com/iambetaraybill/draft-night" rel="noopener noreferrer"&gt;
        draft-night
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      An offline auction draft for a living-room FIFA tournament. Friends bid from their phones; whoever can't make it is represented by a local open-weight model.
    &lt;/h3&gt;
  &lt;/div&gt;
  &lt;div class="ltag-github-body"&gt;
    
&lt;div id="readme" class="md"&gt;&lt;div class="markdown-heading"&gt;
&lt;h1 class="heading-element"&gt;Draft Night&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;&lt;a href="https://github.com/iambetaraybill/draft-night/actions/workflows/tests.yml" rel="noopener noreferrer"&gt;&lt;img src="https://github.com/iambetaraybill/draft-night/actions/workflows/tests.yml/badge.svg" alt="tests"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;An auction draft for a living room full of friends. Everyone bids from their
phone, the TV shows the lot on the block, and whoever couldn't make it is
represented by a proxy manager: a local open-weight model that bids on their
behalf, inside their budget, from instructions they wrote in plain English.&lt;/p&gt;
&lt;p&gt;Runs on one laptop over your own wifi. No accounts, no cloud, no internet, no
API keys, nothing to pay per bid.&lt;/p&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;&lt;pre class="notranslate"&gt;&lt;code&gt;  GK
  Niko Jovic                          Managers
  rated 91 · FC Harbourside           Rohit              148
                                        1 GK · 2 DEF
      42  with Rohit                  Sam  proxy · in     92
                                        2 DEF · 1 FWD
  ████████████░░░░░░░░                Dev                 61
  6.4s left. Any bid puts 7              1 MID · 2 FWD
  seconds back on the clock
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this exists&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Six of us run a FIFA tournament on a PS5. Picking squads used to be a
spreadsheet and an argument. An auction…&lt;/p&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/iambetaraybill/draft-night" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Two dependencies, Flask and requests. No build step, no npm, no bundler. Phones need a browser and the URL.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;draftnight/
  rules.py     budgets and squad legality. no I/O, no model, no randomness
  agents.py    proxy bidders: model valuations, clamping, heuristic fallback
  room.py      state machine, clock, agent scheduling, event broadcast
  players.py   CSV loading with loose column matching
  server.py    Flask routes and the server-sent event stream
static/
  common.js    shared by both views: escaping, fetch, event stream, clock
  host.html    the TV
  play.html    phones
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How I Built It
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemma running locally through Ollama&lt;/strong&gt;, with structured JSON output. No API keys, no account, no network.&lt;/p&gt;

&lt;p&gt;The design decision the whole project rests on is this one:&lt;/p&gt;

&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%2F0osvg4agdwm7tjney7a3.png" 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%2F0osvg4agdwm7tjney7a3.png" alt="One model call per lot, not per bid" width="800" height="427"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A bidding war has a dozen exchanges inside fifteen seconds. On a CPU-only laptop a small model needs one to three seconds for even a short JSON answer. Calling the model on every exchange would either stall the auction dead or force me down to a model too small to actually read a paragraph of strategy.&lt;/p&gt;

&lt;p&gt;So the model answers one question per lot — &lt;em&gt;what is this player worth to this manager, and why?&lt;/em&gt; — and deterministic code does the bidding up to that ceiling, with a randomised human-feeling pause between raises so the agents don't snipe instantly and make the room feel rigged.&lt;/p&gt;

&lt;p&gt;Ollama's structured output takes a JSON schema and constrains generation to it, which is what makes a small model reliable enough to put in a live loop at all:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;PLAN_SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;object&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;properties&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;want&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;string&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;want&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;reason&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The model never touches money.&lt;/strong&gt; It suggests a ceiling; &lt;code&gt;rules.py&lt;/code&gt; decides what's legal. Budgets, bid validity, roster slots, increments and the clock are all plain Python with no model anywhere near them.&lt;/p&gt;

&lt;p&gt;That isn't a stylistic preference, it's the thing that makes an unpredictable model safe to hand a wallet. A model that replies &lt;code&gt;max_bid: 99999&lt;/code&gt; produces a legal bid of exactly that manager's cap and a logged clamp:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;asked&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max_bid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;clamped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asked&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;
&lt;span class="n"&gt;plan&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Plan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;want&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;want&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;asked&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;rules&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_bid&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;max_bid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asked&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cap&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;   &lt;span class="c1"&gt;# the model does not get the last word
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a test for exactly that, because "the model probably won't do that" is not a budget control.&lt;/p&gt;

&lt;p&gt;The other rule worth naming is the reserve. Before any bid is accepted, the server checks you can still fill every remaining slot at the minimum price:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;max_bid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Manager&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slots_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;reserve&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;slots&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_bid&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;budget_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;reserve&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without it, somebody spends 95% of their budget on one galactico and fields six empty shirts. The test I care most about spends the maximum allowed on &lt;em&gt;every single lot&lt;/em&gt; and checks the squad still completes legally.&lt;/p&gt;

&lt;h3&gt;
  
  
  Three bugs worth admitting to
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;The agents sat out the entire first auction.&lt;/strong&gt; Twenty-four lots, proxy manager bought nothing. The valuations were fine — the model wanted players. My test harness was closing each lot the instant it opened, faster than the agents' deliberately human reaction delay. The bug wasn't in the agent; it was that my timings were hardcoded constants with no way to run fast without also running &lt;em&gt;unfairly&lt;/em&gt; fast. Timings are configurable now, and the agents get a guaranteed reaction window once their plans land.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A test passed for the wrong reason, which is worse than failing.&lt;/strong&gt; I had an assertion that an absurd overbid gets refused. It checked for HTTP 400 and nothing else. In CI it was getting 400 — but for "nothing is up for auction right now," because the host token didn't match and the auction had never started. A test that goes green while the feature is broken is actively harmful. It now asserts the refusal actually mentions the budget limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The TV rebuild ate the host's typing.&lt;/strong&gt; The host screen re-renders once a second to keep the clock honest. If someone joined from their phone while you were halfway through typing a friend's strategy into the textarea, your text vanished. Now only the changed regions get patched, and in-progress input is held across rebuilds.&lt;/p&gt;

&lt;h3&gt;
  
  
  Tests
&lt;/h3&gt;

&lt;p&gt;36 unit tests, plus an end-to-end run that drives an entire auction over HTTP and asserts every squad ends full and formation-legal, nobody goes negative, and the proxy manager actually wins players. Both run in GitHub Actions across Python 3.10 to 3.13 on every push.&lt;/p&gt;

&lt;p&gt;CI also checks the committed sample pool still matches its generator, and that the CSV loader survives a deliberately messy two-thousand-row export.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Does Open Innovation Matter?
&lt;/h2&gt;

&lt;p&gt;Three reasons, none of which are about saving money.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;It has to work in a flat with bad wifi.&lt;/strong&gt; The whole thing runs on one laptop. Phones reach it over the local network. If the router loses its uplink mid-draft, nobody notices. A draft night that dies because an API is having an incident isn't a draft night.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A cloud round trip would break the bidding.&lt;/strong&gt; Valuations have to be ready before the hammer falls. That means inference on the same machine as the auction clock, which means a model I can actually hold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;My friends' instructions are theirs.&lt;/strong&gt; "Don't let Rohit get a keeper" is a daft thing to ship to someone else's server, and there's no reason to. Nothing leaves the room.&lt;/p&gt;

&lt;p&gt;There's a fourth thing I didn't expect, and it's the one that convinced me. The project ships with &lt;code&gt;--no-llm&lt;/code&gt;, a heuristic fallback that bids on ratings alone so the app still works with no model installed. It was meant as a graceful degradation path.&lt;/p&gt;

&lt;p&gt;It turned into the control group.&lt;/p&gt;

&lt;p&gt;The heuristic can read a rating. It cannot read &lt;em&gt;"get a keeper early so I'm not stuck with whoever's left."&lt;/em&gt; It cannot read &lt;em&gt;"don't let Rohit get a striker."&lt;/em&gt; Running both over the same pool, with the same budgets, is the clearest possible demonstration of what the open model is actually contributing — and I could only build that comparison because swapping the model out is a one-line change and running it a hundred times costs nothing.&lt;/p&gt;

&lt;p&gt;Closed APIs give you a better model. Open weights gave me a model I could put &lt;em&gt;inside&lt;/em&gt; the thing, on the critical path, in a living room with the internet off, and then run the whole evening again with it removed to see what it was worth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prize Categories
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Gemma&lt;/strong&gt; — Gemma runs locally through Ollama as the proxy bidders' judgement layer, with schema-constrained JSON output.&lt;/p&gt;




</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
    </item>
    <item>
      <title>Permission Based Authorization in Angular (PBAC)</title>
      <dc:creator>Arpan Sarkar</dc:creator>
      <pubDate>Wed, 12 Feb 2025 18:36:11 +0000</pubDate>
      <link>https://dev.to/iambetaraybill/permission-based-authorization-in-angular-pbac-42c7</link>
      <guid>https://dev.to/iambetaraybill/permission-based-authorization-in-angular-pbac-42c7</guid>
      <description>&lt;p&gt;&lt;strong&gt;&lt;em&gt;Authorization&lt;/em&gt;&lt;/strong&gt; is a critical part of any &lt;em&gt;Angular&lt;/em&gt; application, ensuring that users can only access the parts of the application they are permitted to.&lt;br&gt;
In most applications, users have assigned roles.&lt;br&gt;
We can enforce access control in &lt;em&gt;Angular&lt;/em&gt; routes by using route guards.&lt;/p&gt;

&lt;p&gt;In many applications, role based access control (RBAC) alone is not enough. Users may need fine grained permissions beyond just roles. For example:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Admin&lt;/em&gt;&lt;/strong&gt; - Can manage users and edit settings.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;Editor&lt;/em&gt;&lt;/strong&gt; - Can edit articles but not manage users.&lt;br&gt;
&lt;strong&gt;&lt;em&gt;Viewer&lt;/em&gt;&lt;/strong&gt; - Can only view content.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;In this article, we’ll see how in &lt;em&gt;Angular&lt;/em&gt; to include &lt;/p&gt;

&lt;blockquote&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Permission Based access control (PBAC)&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  Step 1: Setting Up the &lt;em&gt;Angular&lt;/em&gt; Application
&lt;/h2&gt;

&lt;p&gt;Start by creating a new &lt;em&gt;Angular&lt;/em&gt; app if you don’t have one&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ng new angular-pbac --routing
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 2: Create an Auth Service for Dynamic Permissions
&lt;/h2&gt;

&lt;p&gt;This service will store user roles with associated permissions, retrieve them dynamically from an API (mocked for now) and provide authentication authorization methods.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;auth.service.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from '@angular/core';
import { Router } from '@angular/router';

@Injectable({
  providedIn: 'root',
})
export class AuthService {
  private user: { role: string; permissions: string[] } | null = null;

  private rolePermissions: Record&amp;lt;string, string[]&amp;gt; = {
    admin: ['manage-users', 'edit-settings', 'view-dashboard'],
    editor: ['edit-articles', 'view-dashboard'],
    viewer: ['view-dashboard'],
  };

  constructor(private router: Router) {}

  login(role: string): void {
    const permissions = this.rolePermissions[role] || [];
    this.user = { role, permissions };
    localStorage.setItem('userPermissions', JSON.stringify(permissions));
    this.router.navigate(['/dashboard']);
  }

  logout(): void {
    this.user = null;
    localStorage.removeItem('userPermissions');
    this.router.navigate(['/login']);
  }

  getUserPermissions(): string[] {
    return JSON.parse(localStorage.getItem('userPermissions') || '[]');
  }

  hasPermission(permission: string): boolean {
    return this.getUserPermissions().includes(permission);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;For now using mock roles and permissions which normally is retrieved from a backend - &lt;code&gt;rolePermissions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Here we are doing simple &lt;code&gt;login&lt;/code&gt; method without setting user role or checking if authenticated or not. Just to keep it simple we are avoiding that&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Step 3: Create a Permission Based Route Guard
&lt;/h2&gt;

&lt;p&gt;In &lt;em&gt;Angular&lt;/em&gt;, Route guards help protect routes based on authentication and roles.&lt;br&gt;
Now, we need a route guard to check for both roles and permissions before allowing access.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;permission.guard.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, Router } from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({
  providedIn: 'root',
})
export class PermissionGuard implements CanActivate {
  constructor(private authService: AuthService, private router: Router) {}

  canActivate(route: ActivatedRouteSnapshot): boolean {
    const requiredPermission = route.data['permission'];

    if (!this.authService.hasPermission(requiredPermission)) {
      this.router.navigate(['/unauthorized']);
      return false;
    }

    return true;
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 4: Define Routes with Permission Based Access
&lt;/h2&gt;

&lt;p&gt;Now, we will apply our new permission-based guard to specific routes.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app.config.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { provideRouter, Route } from '@angular/router';
import { LoginComponent } from './login/login.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { UnauthorizedComponent } from './unauthorized/unauthorized.component';
import { PermissionGuard } from './permission.guard';


const routes: Routes = [
  { path: 'login', component: LoginComponent },
  { path: 'dashboard', component: DashboardComponent },

  // Permission based routes 
  { path: 'edit-settings', component: DashboardComponent, canActivate: [PermissionGuard], data: { permission: 'edit-settings' } },
  { path: 'manage-users', component: DashboardComponent, canActivate: [PermissionGuard], data: { permission: 'manage-users' } },

  { path: 'unauthorized', component: UnauthorizedComponent },
  { path: '', redirectTo: '/login', pathMatch: 'full' },
];

export const appConfig = {
  providers: [provideRouter(routes)],
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 5: Setup Login Component to Handle Dynamic Roles
&lt;/h2&gt;

&lt;p&gt;Now, users can log in as different roles dynamically, and permissions will be applied accordingly.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;login.component.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component } from '@angular/core';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html'
})
export class LoginComponent {
  constructor(private authService: AuthService) {}

  loginAs(role: string) {
    this.authService.login(role);
  }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;login.component.html&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Login&amp;lt;/h2&amp;gt;
&amp;lt;button (click)="loginAs('admin')"&amp;gt;Login as Admin&amp;lt;/button&amp;gt;
&amp;lt;button (click)="loginAs('editor')"&amp;gt;Login as Editor&amp;lt;/button&amp;gt;
&amp;lt;button (click)="loginAs('viewer')"&amp;gt;Login as Viewer&amp;lt;/button&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 6: Display logged in user Permissions in Dashboard
&lt;/h2&gt;

&lt;p&gt;Now, let’s display permissions dynamically on the dashboard.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;dashboard.component.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component } from '@angular/core';
import { AuthService } from '../auth.service';

@Component({
  selector: 'app-dashboard',
  templateUrl: './dashboard.component.html',
})
export class DashboardComponent {
  permissions: string[];

  constructor(private authService: AuthService) {
    this.permissions = this.authService.getUserPermissions();
  }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;dashboard.component.html&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;h2&amp;gt;Dashboard&amp;lt;/h2&amp;gt;
&amp;lt;p&amp;gt;Hey&amp;lt;/p&amp;gt;
&amp;lt;p&amp;gt;Your Permissions:&amp;lt;/p&amp;gt;
&amp;lt;ul&amp;gt;
  &amp;lt;li *ngFor="let permission of permissions"&amp;gt;{{ permission }}&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Step 7: Handling Unauthorized Access
&lt;/h2&gt;

&lt;p&gt;If a user lacks permissions, they will be redirected to an Unauthorized Page.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;unauthorized.component.ts&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { Component } from '@angular/core';

@Component({
  selector: 'app-unauthorized',
  template: '&amp;lt;h2&amp;gt;Unauthorized Access&amp;lt;/h2&amp;gt;',
})
export class UnauthorizedComponent {}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Conclusion: Building a Fortress of Access Control
&lt;/h2&gt;

&lt;p&gt;In this guide, We seen &lt;em&gt;Angular&lt;/em&gt; authorization system by:&lt;br&gt;
✅ Implementing dynamic permissions.&lt;br&gt;
✅ Protecting routes based user permissions.&lt;br&gt;
✅ Automatically handling unauthorized access with redirection.&lt;/p&gt;

&lt;p&gt;Authorization in &lt;em&gt;Angular&lt;/em&gt; is not just about opening and closing doors - it is about building a fortress with layers of security. Roles and Permissions define the broad gates users can pass through, while permissions act as the keys unlocking specific chambers within. By implementing &lt;strong&gt;&lt;em&gt;permission based access control (PBAC)&lt;/em&gt;&lt;/strong&gt;, we create a system that is both scalable and adaptive, which ensures users only access they need.&lt;/p&gt;

&lt;p&gt;As your application grows, this structured approach will become your architectural blueprint, guiding security while maintaining flexibility.&lt;/p&gt;

&lt;p&gt;Did you learn something new today? Let me know in the comments below! 👇&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
