<?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: Yash Gurav</title>
    <description>The latest articles on DEV Community by Yash Gurav (@yash_gurav_31d3dc3cc193d1).</description>
    <link>https://dev.to/yash_gurav_31d3dc3cc193d1</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%2F2440629%2F70aa3838-7a98-4a6f-afd3-c8c1836b307a.png</url>
      <title>DEV Community: Yash Gurav</title>
      <link>https://dev.to/yash_gurav_31d3dc3cc193d1</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yash_gurav_31d3dc3cc193d1"/>
    <language>en</language>
    <item>
      <title>I gave NPCs a graph database. Now the whole village knows I stole an apple.</title>
      <dc:creator>Yash Gurav</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:41:38 +0000</pubDate>
      <link>https://dev.to/yash_gurav_31d3dc3cc193d1/i-gave-npcs-a-graph-database-now-the-whole-village-knows-i-stole-an-apple-18p0</link>
      <guid>https://dev.to/yash_gurav_31d3dc3cc193d1/i-gave-npcs-a-graph-database-now-the-whole-village-knows-i-stole-an-apple-18p0</guid>
      <description>&lt;p&gt;description: Hearsay is a memory-and-gossip engine for game characters built on FalkorDB. Villagers remember what they see, spread rumours along trust, exaggerate them, and can explain exactly who told whom.&lt;br&gt;
cover_image: &lt;/p&gt;

&lt;p&gt;I stole an apple in a video game. Only the merchant saw it.&lt;/p&gt;

&lt;p&gt;By evening, the innkeeper had heard I &lt;em&gt;robbed his stall&lt;/em&gt;. By night, the town guard had heard I &lt;em&gt;robbed the market&lt;/em&gt;. Next morning, the guard, who never saw a thing, blocked the town gate.&lt;/p&gt;

&lt;p&gt;So I asked her &lt;strong&gt;why&lt;/strong&gt;. She told me: &lt;em&gt;"I heard it from Bertha; Bertha heard it from Old Tomas; Old Tomas saw it first-hand."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;That's &lt;strong&gt;Hearsay&lt;/strong&gt;, the project I built for &lt;strong&gt;Graph Hacks: Context for AI Agents&lt;/strong&gt; (WeMakeDevs × FalkorDB). It's a small 3D game, but the real thing is the engine underneath: game characters whose memories, beliefs and relationships live in a graph.&lt;/p&gt;

&lt;p&gt;▶ &lt;strong&gt;Play it:&lt;/strong&gt; &lt;a href="https://hearsay-six.vercel.app" rel="noopener noreferrer"&gt;https://hearsay-six.vercel.app&lt;/a&gt;&lt;br&gt;
📦 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/Yashgurav002/hearsay" rel="noopener noreferrer"&gt;https://github.com/Yashgurav002/hearsay&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/CvcvtnGFUsM" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem: NPCs are amnesiacs
&lt;/h2&gt;

&lt;p&gt;Game characters forget everything. The shopkeeper greets you warmly five seconds after you robbed them. And when games do remember, it's usually a single number, a "reputation" or "honour" meter, that every character somehow knows instantly. Nobody heard it from anyone, nobody misremembers it, and nobody can tell you where it came from.&lt;/p&gt;

&lt;p&gt;Real reputation doesn't work like that. It travels &lt;strong&gt;person to person&lt;/strong&gt;, only between people who &lt;strong&gt;trust&lt;/strong&gt; each other, it &lt;strong&gt;changes&lt;/strong&gt; in the retelling, and it takes &lt;strong&gt;time&lt;/strong&gt; to reach the next town.&lt;/p&gt;

&lt;p&gt;Underneath, this is an agent-memory problem, not a game problem. Agents that share a world need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;memory that persists;&lt;/li&gt;
&lt;li&gt;knowledge that moves between agents only through realistic channels;&lt;/li&gt;
&lt;li&gt;a record of &lt;strong&gt;where each belief came from&lt;/strong&gt;;&lt;/li&gt;
&lt;li&gt;decisions you can trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: memories, beliefs and trust as a graph
&lt;/h2&gt;

&lt;p&gt;Here's the whole model for one rumour chain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(Tomas)-[:WITNESSED]-&amp;gt;(Event: theft at the market)
(Tomas) -[:HOLDS {confidence: 1.00}]-&amp;gt; (Rumour sev 1: "took an apple")      ─┐
(Pip)   -[:HOLDS {confidence: 0.45}]-&amp;gt; (Rumour sev 2: "stole a basket")      │ each one
(Bertha)-[:HOLDS {confidence: 0.75}]-&amp;gt; (Rumour sev 3: "robbed the stall")    │ -[:ORIGIN]-&amp;gt;
(Ilsa)  -[:HOLDS {confidence: 0.51}]-&amp;gt; (Rumour sev 4: "robbed the market")   │ the real Event
                                                                            ─┘
 Pip's version   -[:HEARD_FROM]-&amp;gt; Tomas's version
 Bertha's version -[:HEARD_FROM]-&amp;gt; Pip's version  AND  -[:HEARD_FROM]-&amp;gt; Tomas's version
 Ilsa's version  -[:HEARD_FROM]-&amp;gt; Bertha's version

 (Ilsa)-[:TRUSTS {weight: 0.75}]-&amp;gt;(Bertha)-[:TRUSTS {weight: 0.8}]-&amp;gt;(Tomas)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;Event&lt;/strong&gt; is what &lt;em&gt;actually&lt;/em&gt; happened.&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;Rumour&lt;/strong&gt; is one villager's &lt;em&gt;version&lt;/em&gt; of it. Every retelling creates a &lt;strong&gt;new&lt;/strong&gt; Rumour node for the listener, with its own wording, severity and confidence.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;HOLDS&lt;/code&gt; carries &lt;strong&gt;how sure&lt;/strong&gt; they are. &lt;code&gt;HEARD_FROM&lt;/code&gt; is the &lt;strong&gt;provenance chain&lt;/strong&gt;. &lt;code&gt;ORIGIN&lt;/code&gt; always points back to the truth, however distorted the story got.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;TRUSTS&lt;/code&gt; is directed: "Ilsa trusts Bertha" means Bertha can tell Ilsa things.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each player gets their &lt;strong&gt;own graph&lt;/strong&gt; (&lt;code&gt;hearsay_&amp;lt;playerId&amp;gt;&lt;/code&gt;), created with &lt;code&gt;GRAPH.COPY&lt;/code&gt; from a read-only template village. That gives per-player memory isolation from the database itself, not from &lt;code&gt;WHERE player = ...&lt;/code&gt; filters I might forget.&lt;/p&gt;
&lt;h2&gt;
  
  
  Three queries that do the real work
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Who can gossip right now?
&lt;/h3&gt;

&lt;p&gt;Every in-game hour, villagers move to their scheduled places. Then one pattern match finds every possible conversation:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;a:&lt;/span&gt;&lt;span class="n"&gt;Villager&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:AT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;pl:&lt;/span&gt;&lt;span class="n"&gt;Place&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:AT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;b:&lt;/span&gt;&lt;span class="n"&gt;Villager&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;t:&lt;/span&gt;&lt;span class="n"&gt;TRUSTS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;a.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;teller&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;listener&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t.weight&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;trust&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pl.id&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;place&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Read it as: &lt;em&gt;A and B are in the same place, and B trusts A, so A can tell B things.&lt;/em&gt; That one line is why gossip in Hearsay feels social. The guard never hears anything from the kid, because she doesn't trust the kid.&lt;/p&gt;

&lt;p&gt;When A tells B a story, B gets their &lt;strong&gt;own version&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HOLDS&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;confidence:&lt;/span&gt; &lt;span class="n"&gt;$conf&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;since:&lt;/span&gt; &lt;span class="n"&gt;$tick&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;last_reinforced:&lt;/span&gt; &lt;span class="n"&gt;$tick&lt;/span&gt;&lt;span class="ss"&gt;}]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;
       &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;Rumour&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$id&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;text:&lt;/span&gt; &lt;span class="n"&gt;$text&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;severity:&lt;/span&gt; &lt;span class="n"&gt;$severity&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;hops:&lt;/span&gt; &lt;span class="n"&gt;$hops&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HEARD_FROM&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;tick:&lt;/span&gt; &lt;span class="n"&gt;$tick&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="py"&gt;place:&lt;/span&gt; &lt;span class="n"&gt;$place&lt;/span&gt;&lt;span class="ss"&gt;}]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ORIGIN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;The maths is kept simple on purpose:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Confidence drops&lt;/strong&gt; every hop: &lt;code&gt;teller's confidence × trust × 0.9&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Severity can climb&lt;/strong&gt; one step on a fixed ladder ("took an apple" → "stole a basket" → "robbed the stall" → "robbed the market" → "is part of a gang of thieves") with the teller's &lt;em&gt;exaggeration&lt;/em&gt; chance. Pip, the village kid, exaggerates 80% of the time.&lt;/li&gt;
&lt;li&gt;If B &lt;strong&gt;already&lt;/strong&gt; knows the story from someone else, no new node is made. B gets a second &lt;code&gt;HEARD_FROM&lt;/code&gt; arrow and &lt;strong&gt;corroboration&lt;/strong&gt; raises their confidence: &lt;code&gt;1 − (1 − old) × (1 − new)&lt;/code&gt;. Two people telling you the same thing makes you more sure.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  2. "Why do you think that?"
&lt;/h3&gt;

&lt;p&gt;This is the query I'm proudest of, because it's the one that would be painful anywhere but a graph:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;MATCH&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;v:&lt;/span&gt;&lt;span class="n"&gt;Villager&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$villager&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="py"&gt;h:&lt;/span&gt;&lt;span class="n"&gt;HOLDS&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;r:&lt;/span&gt;&lt;span class="n"&gt;Rumour&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ABOUT&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;:Player&lt;/span&gt; &lt;span class="ss"&gt;{&lt;/span&gt;&lt;span class="py"&gt;id:&lt;/span&gt; &lt;span class="n"&gt;$player&lt;/span&gt;&lt;span class="ss"&gt;})&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;h.confidence&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;MATCH&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:HEARD_FROM&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="o"&gt;..&lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;o:&lt;/span&gt;&lt;span class="n"&gt;Rumour&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="nc"&gt;:ORIGIN&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="py"&gt;e:&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;o.hops&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;WITH&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="nf"&gt;length&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;ASC&lt;/span&gt; &lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="ss"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;IN&lt;/span&gt; &lt;span class="nf"&gt;nodes&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="py"&gt;n:&lt;/span&gt;&lt;span class="n"&gt;Rumour&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="n"&gt;n.id&lt;/span&gt;&lt;span class="ss"&gt;]&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;rumour_ids&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e.base_text&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;what_happened&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Take the villager's strongest belief and follow &lt;code&gt;HEARD_FROM&lt;/code&gt; arrows, zero to six of them, back to someone who saw it first-hand (&lt;code&gt;hops = 0&lt;/code&gt;). The &lt;code&gt;*0..6&lt;/code&gt; covers "I saw it myself" for free. In SQL this is a recursive self-join; here it's one pattern, and it runs in about &lt;strong&gt;2 ms&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The game turns the result into a comic strip, so you can watch the story get worse hop by hop:&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%2F0709m4w56du0zp1lvnga.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%2F0709m4w56du0zp1lvnga.png" alt="The Why? strip: Tomas saw " width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3&gt;
  
  
  3. Whose word spreads furthest?
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight cypher"&gt;&lt;code&gt;&lt;span class="k"&gt;CALL&lt;/span&gt; &lt;span class="n"&gt;algo.pageRank&lt;/span&gt;&lt;span class="ss"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Villager'&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'TRUSTS'&lt;/span&gt;&lt;span class="ss"&gt;)&lt;/span&gt; &lt;span class="k"&gt;YIELD&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt;
&lt;span class="k"&gt;RETURN&lt;/span&gt; &lt;span class="n"&gt;node.name&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;villager&lt;/span&gt;&lt;span class="ss"&gt;,&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;score&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;TRUSTS&lt;/code&gt; points from a listener to the person they believe, so PageRank flows toward the &lt;strong&gt;most-believed&lt;/strong&gt; villagers. Bertha the innkeeper comes out on top. Add &lt;code&gt;algo.WCC&lt;/code&gt; over strong trust for cliques ("the inn crowd", "the chapel pair"), &lt;code&gt;algo.betweenness&lt;/code&gt; for brokers, and &lt;code&gt;shortestPath&lt;/code&gt; for the fastest route to get a story to someone, and you have a &lt;strong&gt;spymaster&lt;/strong&gt; that helps the player plan:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;"Captain Ilsa trusts Bertha most (0.75). Bertha will be at the inn from 10:00: do a good deed where she can see it, or whisper it to Pip, who reaches her in 1 hop."&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Graph algorithms as tools for an agent, and the player's way out of the mess.&lt;/p&gt;
&lt;h2&gt;
  
  
  The design choices that made it believable
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A distortion ladder, not free-form AI exaggeration.&lt;/strong&gt; The engine decides the severity (1–5). The language model only rephrases it in the teller's voice ("Oh my stars, the traveller just snatched Tomas's stuff!"). The distortion is real, deterministic and testable, because it's in the graph.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The echo-chamber guard.&lt;/strong&gt; A story is never told back to the person it came from. Otherwise two friends would keep "confirming" each other's rumour until they were 100% sure. In Cypher that's a variable-length pattern predicate: &lt;code&gt;NOT (r)-[:HEARD_FROM*1..6]-&amp;gt;(mine)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The sceptic.&lt;/strong&gt; Mira the blacksmith ignores any rumour with fewer than two different tellers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Forgetting.&lt;/strong&gt; Every in-game midnight, beliefs nobody reinforced in 24 hours fade (&lt;code&gt;confidence × 0.97&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Procedural memory.&lt;/strong&gt; Behaviour lives in &lt;code&gt;Rule&lt;/code&gt; nodes, like "guard: block the gate below opinion −0.3" or "guard: treat a gift as a bribe if she already distrusts you". The code evaluates the rules; the model never decides anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Two coordination channels.&lt;/strong&gt; Gossip is informal. When the guard gets really angry, she posts a &lt;strong&gt;wanted notice&lt;/strong&gt; at the gate, and anyone who walks past reads it. That's shared state between agents.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Why the AI doesn't hold the memory
&lt;/h2&gt;

&lt;p&gt;It's tempting to throw everything into an LLM's context and call it memory. But a chat history:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;grows until it doesn't fit;&lt;/li&gt;
&lt;li&gt;vanishes between sessions;&lt;/li&gt;
&lt;li&gt;can't be shared &lt;em&gt;selectively&lt;/em&gt; between characters;&lt;/li&gt;
&lt;li&gt;can't tell you where a belief came from.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The graph does all four.&lt;/p&gt;

&lt;p&gt;So the villager agent loop is &lt;strong&gt;perceive&lt;/strong&gt; (graph queries) → &lt;strong&gt;decide&lt;/strong&gt; (code, from &lt;code&gt;Rule&lt;/code&gt; nodes) → &lt;strong&gt;speak&lt;/strong&gt; (AI, with only the facts the graph returned) → &lt;strong&gt;remember&lt;/strong&gt; (graph write). I tested it over 50 scripted conversations with Gemma 3 4B running locally: &lt;strong&gt;0 / 50&lt;/strong&gt; replies mentioned a villager outside the speaker's graph context. If the model is off or slow, template lines built from the same facts take over, and the game never breaks.&lt;/p&gt;
&lt;h2&gt;
  
  
  The numbers
&lt;/h2&gt;

&lt;p&gt;Measured on a laptop with FalkorDB in Docker:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;What&lt;/th&gt;
&lt;th&gt;Result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Belief + opinion reads, &lt;strong&gt;500 player worlds alive in one FalkorDB&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;~1 ms p50, 2.3 ms p95&lt;/strong&gt;, flat from 10 to 500 worlds&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A full hour of village gossip (~20 queries)&lt;/td&gt;
&lt;td&gt;~15 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Why?" end to end&lt;/td&gt;
&lt;td&gt;7.7 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Theft reaching the guard, over 50 seeds&lt;/td&gt;
&lt;td&gt;50/50 times, median 9 hours, arriving at mean severity 2.5&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  What broke (and how I fixed it)
&lt;/h2&gt;

&lt;p&gt;The honest part. These are the things that cost me real time:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Duplicated rows from an &lt;code&gt;OPTIONAL MATCH&lt;/code&gt;.&lt;/strong&gt; My first echo-chamber guard was &lt;code&gt;OPTIONAL MATCH echo = (r)-[:HEARD_FROM*1..6]-&amp;gt;(mine)&lt;/code&gt; when &lt;code&gt;mine&lt;/code&gt; could be null. FalkorDB expanded it to &lt;em&gt;every&lt;/em&gt; path and duplicated the rows, while &lt;code&gt;echo&lt;/code&gt; still read as null. The guard told Ilsa the same rumour twice in one hour. Moving the check into a &lt;code&gt;WHERE&lt;/code&gt; pattern predicate fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;GRAPH.COPY&lt;/code&gt; under load.&lt;/strong&gt; The load test died with &lt;code&gt;could not fork&lt;/code&gt;: copies fork the server, and two at once fail. I serialised copies behind a lock with a short retry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ORDER BY length(path)&lt;/code&gt; after &lt;code&gt;RETURN&lt;/code&gt;&lt;/strong&gt; errored because &lt;code&gt;path&lt;/code&gt; wasn't projected. Ordering in a &lt;code&gt;WITH&lt;/code&gt; first fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A thinking model in a game loop.&lt;/strong&gt; Gemma 4 on AI Studio always emits a &lt;code&gt;&amp;lt;thought&amp;gt;&lt;/code&gt; block before answering, sometimes runs out of tokens mid-thought, and took 14–34 s per call. I strip the thought block and retry server errors. Retellings are written instantly with template text and reworded in the background, so ticks never wait on the model. The hosted demo uses templates; local play uses Gemma 3 4B on Ollama (~1–4 s).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Promise that crashed React.&lt;/strong&gt; Chrome's &lt;code&gt;scrollIntoView()&lt;/code&gt; now returns a Promise. An arrow-body &lt;code&gt;useEffect&lt;/code&gt; returned it, React tried to call it as a cleanup function, and the whole page unmounted. Effects get block bodies now.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel guessed wrong.&lt;/strong&gt; It saw &lt;code&gt;fastapi&lt;/code&gt; in &lt;code&gt;requirements.txt&lt;/code&gt;, picked the FastAPI preset, and sent every request, even &lt;code&gt;/&lt;/code&gt;, to Python. Pinning &lt;code&gt;"framework": "vite"&lt;/code&gt; made the frontend static and kept Python for &lt;code&gt;/api&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;
  
  
  Beyond one village
&lt;/h2&gt;

&lt;p&gt;The village is a demo. The reusable part is a small contract: &lt;strong&gt;events go in&lt;/strong&gt; ("theft happened here, these characters are here"), and &lt;strong&gt;beliefs, opinions and explanations come out&lt;/strong&gt; ("the guard believes you robbed the market, at 0.51 confidence, because Bertha told her").&lt;/p&gt;

&lt;p&gt;Picture that in an open world:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A shop clerk sees a robbery and tells a cousin in a gang, and the gang prices you differently.&lt;/li&gt;
&lt;li&gt;A bard carries "slew a griffin bare-handed" to the next town.&lt;/li&gt;
&lt;li&gt;A sceptical alderman won't believe it until two people say so.&lt;/li&gt;
&lt;li&gt;Tracing a rumour to its source &lt;em&gt;becomes&lt;/em&gt; the detective mechanic.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A GTA-sized world would need gossip "level of detail" (full simulation only near the player), region graphs and batched ticks. But the model and the queries stay the same, and at ~1 ms per lookup, per-character graph reasoning is affordable.&lt;/p&gt;
&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;▶ &lt;strong&gt;Play:&lt;/strong&gt; &lt;a href="https://hearsay-six.vercel.app" rel="noopener noreferrer"&gt;https://hearsay-six.vercel.app&lt;/a&gt;. Steal an apple, skip to the evening, then ask the guard "Why?". The 🔭 Spyglass shows the live graph, PageRank and every Cypher query as it runs.&lt;/li&gt;
&lt;li&gt;📦 &lt;strong&gt;Code, graph model and all 45 Cypher queries:&lt;/strong&gt; &lt;a href="https://github.com/Yashgurav002/hearsay" rel="noopener noreferrer"&gt;https://github.com/Yashgurav002/hearsay&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;🛠️ &lt;strong&gt;Stack:&lt;/strong&gt; FalkorDB, FastAPI, React + Three.js, Gemma.&lt;/li&gt;
&lt;/ul&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/Yashgurav002" rel="noopener noreferrer"&gt;
        Yashgurav002
      &lt;/a&gt; / &lt;a href="https://github.com/Yashgurav002/hearsay" rel="noopener noreferrer"&gt;
        hearsay
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      NPCs that remember what you did, and gossip about it. A 3D game + NPC memory engine on a FalkorDB graph (Graph Hacks 2026).
    &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;Hearsay: &lt;em&gt;Everyone's talking about you.&lt;/em&gt;
&lt;/h1&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;A memory engine that lets game characters remember what you did, gossip about it, and explain exactly who told whom, built on a &lt;a href="https://www.falkordb.com/" rel="nofollow noopener noreferrer"&gt;FalkorDB&lt;/a&gt; graph.&lt;/strong&gt; It's playable here as a small 3D village. The same engine is a baseline for NPC memory in any open-world or story game.&lt;/p&gt;

&lt;div class="markdown-heading"&gt;
&lt;h3 class="heading-element"&gt;▶ Play it live: &lt;strong&gt;&lt;a href="https://hearsay-six.vercel.app" rel="nofollow noopener noreferrer"&gt;https://hearsay-six.vercel.app&lt;/a&gt;&lt;/strong&gt; · Repo: &lt;a href="https://github.com/Yashgurav002/hearsay" rel="noopener noreferrer"&gt;https://github.com/Yashgurav002/hearsay&lt;/a&gt;
&lt;/h3&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Hackathon:&lt;/strong&gt; Graph Hacks: Context for AI Agents (WeMakeDevs × FalkorDB)
&lt;strong&gt;Tracks:&lt;/strong&gt; Track 02 — &lt;strong&gt;Agent Memory and Coordination&lt;/strong&gt; (primary) · Track 01 — &lt;strong&gt;Agents That Act on Connected Data&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Docs:&lt;/strong&gt; &lt;a href="https://github.com/Yashgurav002/hearsay/docs/HOW_TO_PLAY.md" rel="noopener noreferrer"&gt;How to play&lt;/a&gt; · &lt;a href="https://github.com/Yashgurav002/hearsay/docs/READING_THE_GRAPH.md" rel="noopener noreferrer"&gt;Reading the graph (every chart in the UI explained)&lt;/a&gt; · &lt;a href="https://github.com/Yashgurav002/hearsay/docs/ARCHITECTURE.md" rel="noopener noreferrer"&gt;Architecture&lt;/a&gt; · &lt;a href="https://github.com/Yashgurav002/hearsay/docs/results.md" rel="noopener noreferrer"&gt;Measured results&lt;/a&gt; · &lt;a href="https://github.com/Yashgurav002/hearsay/DECISIONS.md" rel="noopener noreferrer"&gt;Decisions&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a rel="noopener noreferrer" href="https://github.com/Yashgurav002/hearsay/docs/screenshots/3d-inn.png"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FYashgurav002%2Fhearsay%2FHEAD%2Fdocs%2Fscreenshots%2F3d-inn.png" alt="Third-person 3D: the inn crowd at dusk, mood rings at their feet"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Why this is a baseline for NPC memory in any game&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;The village is a demo. The reusable part is the engine underneath: &lt;strong&gt;a graph that stores what every character saw, what they've heard, from whom, and how sure&lt;/strong&gt;…&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/Yashgurav002/hearsay" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;em&gt;Built for Graph Hacks: Context for AI Agents (WeMakeDevs × FalkorDB). AI disclosure: I built this with an AI coding assistant (Claude Code) for much of the code and this write-up, and reviewed it myself; in-game voices use Gemma. All villagers, places and events are fictional.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>ai</category>
      <category>database</category>
      <category>python</category>
    </item>
    <item>
      <title>My dad's PF clients wait weeks for their own money over one wrong</title>
      <dc:creator>Yash Gurav</dc:creator>
      <pubDate>Sat, 17 Oct 2026 11:28:43 +0000</pubDate>
      <link>https://dev.to/yash_gurav_31d3dc3cc193d1/my-dads-pf-clients-wait-weeks-for-their-own-money-over-one-wrong-m83</link>
      <guid>https://dev.to/yash_gurav_31d3dc3cc193d1/my-dads-pf-clients-wait-weeks-for-their-own-money-over-one-wrong-m83</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week2-2026-10-12"&gt;Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;My dad is a Provident Fund (PF) consultant in Vasai, near Mumbai. He handles PF for dozens of small companies: monthly filings, and the claims workers file when they leave a job and want their savings back.&lt;/p&gt;

&lt;p&gt;The part that hurts is rejections. A worker's father's name is "Kannen Krishnadas Bhat" in the EPFO records and "Kannen Bhat" at the bank. Nobody notices. The claim goes in, and three weeks later EPFO rejects it with a remark like:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;F/H NAME NOT MATCHING WITH BANK A/C. PL SUBMIT CORRECTED CHQ&lt;/code&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The worker waits even longer for their own money. My dad gets the calls: &lt;em&gt;"PF kab aayega?"&lt;/em&gt; ("When will my PF come?"). This isn't rare. EPFO's 2024-25 annual report puts it at roughly &lt;strong&gt;1 in 5 claims rejected&lt;/strong&gt; (&lt;a href="https://www.businesstoday.in/personal-finance/news/story/epfos-instant-pf-withdrawal-promise-has-a-catch-one-in-five-claims-still-gets-rejected-541466-2026-07-07" rel="noopener noreferrer"&gt;Business Today&lt;/a&gt;), and for final settlements it was about 1 in 3 in 2022-23 (&lt;a href="https://the-ken.com/story/your-provident-fund-claim-is-rejected-but-its-not-your-fault/" rel="noopener noreferrer"&gt;The Ken&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;PF Desk&lt;/strong&gt; is a back office for PF consultants. It does the experienced colleague's job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Pre-check before filing.&lt;/strong&gt; It compares names, father's name, date of birth, bank, KYC, Aadhaar link and exit date across records, and says exactly what to fix and which rule says so.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection risk and settlement date.&lt;/strong&gt; TabPFN, trained on past claims, says "High, 59%" and "likely by 29 October".&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rejection Decoder.&lt;/strong&gt; Paste EPFO's cryptic remark and get the real reason, the fix steps, and a message for the worker in Hindi, Marathi and English. This is a model I fine-tuned with Tinker.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WhatsApp drafts&lt;/strong&gt; to the worker in their language, ready to copy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A private help link for workers.&lt;/strong&gt; "Mera PF kab aayega?" gets answered from their own claim's facts. Anything off-script, like a loan question, goes to the consultant as a task.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The monthly cycle per client company&lt;/strong&gt;, as durable Temporal workflows: ask for the salary sheet, validate contributions, chase, remind before the due date.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A Rule Book&lt;/strong&gt; where every rule cites its source, editable by the consultant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Who pays:&lt;/strong&gt; independent PF consultants like my dad. They charge small employers about &lt;strong&gt;₹1,000–2,400 a month per client&lt;/strong&gt; for PF compliance (listings on &lt;a href="https://m.indiamart.com/impcat/epf-compliance.html" rel="noopener noreferrer"&gt;IndiaMART&lt;/a&gt;). At &lt;strong&gt;₹499–999 a month&lt;/strong&gt;, PF Desk costs less than one client brings in. It pays for itself if it saves one rejected claim's worth of re-filing and phone calls.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Live demo (synthetic data only):&lt;/strong&gt; &lt;a href="https://pfdesk.buildwithyash.tech" rel="noopener noreferrer"&gt;https://pfdesk.buildwithyash.tech&lt;/a&gt; · passcode &lt;code&gt;demo&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/XmHqfiWaHPk" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;&lt;em&gt;The free hosting sleeps when idle, so the first visit can take about a minute, and AI answers on the free Gemma API take 15–40 seconds. On the laptop, which is how a consultant actually runs it, they take a few seconds. The video is recorded locally.&lt;/em&gt;&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/Yashgurav002" rel="noopener noreferrer"&gt;
        Yashgurav002
      &lt;/a&gt; / &lt;a href="https://github.com/Yashgurav002/pf-desk" rel="noopener noreferrer"&gt;
        pf-desk
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      
    &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;PF Desk&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Private back office for Provident Fund consultants: pre-checks claims before EPFO rejects them, decodes rejection remarks, estimates settlement dates, and answers employees in Hindi / Marathi / English. Open models only (Gemma via Ollama); real data stays on the consultant's machine.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;PF Desk helps consultants prepare and check. It does not file with EPFO, and it is not legal or financial advice. Rules change: always verify with the latest EPFO circulars.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Live demo (synthetic data): &lt;a href="https://pfdesk.buildwithyash.tech" rel="nofollow noopener noreferrer"&gt;https://pfdesk.buildwithyash.tech&lt;/a&gt; · Spec: &lt;a href="https://github.com/Yashgurav002/pf-desk/PFDESK_PRD.md" rel="noopener noreferrer"&gt;&lt;code&gt;PFDESK_PRD.md&lt;/code&gt;&lt;/a&gt; · Progress: &lt;a href="https://github.com/Yashgurav002/pf-desk/TASKS.md" rel="noopener noreferrer"&gt;&lt;code&gt;TASKS.md&lt;/code&gt;&lt;/a&gt; · License: MIT&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Run locally&lt;/h2&gt;
&lt;/div&gt;
&lt;p&gt;Needs Node 24+, pnpm, and &lt;a href="https://ollama.com" rel="nofollow noopener noreferrer"&gt;Ollama&lt;/a&gt; with &lt;code&gt;gemma3:4b&lt;/code&gt; and &lt;code&gt;nomic-embed-text&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;ollama pull gemma3:4b &lt;span class="pl-k"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ollama pull nomic-embed-text
pnpm install
pnpm --filter tabpfn setup  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Python venv for TabPFN (CPU torch); optional — API falls back to baselines&lt;/span&gt;
cp .env.example .env      &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; DEMO_MODE=true loads synthetic data&lt;/span&gt;
pnpm dev                  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; web http://localhost:3000 (passcode: demo), API :8787, TabPFN :8790&lt;/span&gt;
pnpm seed                 &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; wipe&lt;/span&gt;&lt;/pre&gt;…
&lt;/div&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Yashgurav002/pf-desk" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;MIT licensed. TypeScript (Hono API + Next.js), Python for the ML pieces, Postgres + pgvector.&lt;/p&gt;

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

&lt;p&gt;The rule I set on day one: &lt;strong&gt;an AI never decides anything a rule can decide, and never sees an identifier.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Deterministic checks first.&lt;/strong&gt; Pre-check is plain code driven by Rule Book parameters. Name matching understands Indian name variants: initials ("R. K. Patil"), transliteration (Sunita/Suneeta), and bank-style CAPS. On 800 planted errors it catches all 800, with 0 false alarms on 1,000 clean claims. That's synthetic, so it only proves the rules fire as designed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. TabPFN for risk and settlement time.&lt;/strong&gt; The open TabPFN v2 runs locally on CPU as a small service. Pre-check calls it through two functions, &lt;code&gt;predictRisk&lt;/code&gt; and &lt;code&gt;predictEta&lt;/code&gt;, with de-identified columns only: form type, service months, match flags, office. No names, no IDs. On a time-ordered split it beat every baseline at ranking risky claims:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;ROC-AUC&lt;/th&gt;
&lt;th&gt;Brier&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Rejection rate per form&lt;/td&gt;
&lt;td&gt;0.549&lt;/td&gt;
&lt;td&gt;0.240&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gradient boosting&lt;/td&gt;
&lt;td&gt;0.751&lt;/td&gt;
&lt;td&gt;0.202&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Logistic regression&lt;/td&gt;
&lt;td&gt;0.817&lt;/td&gt;
&lt;td&gt;0.166&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.834&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.163&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Settlement-time error dropped from 3.67 to 2.92 days. The history is synthetic for now; every claim PF Desk sees settled or rejected becomes a new training row automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tinker for the Rejection Decoder.&lt;/strong&gt; EPFO remarks are terse and inconsistent. I fine-tuned &lt;strong&gt;Qwen3-8B with a LoRA on Tinker&lt;/strong&gt; to turn a remark into strict JSON: &lt;code&gt;{category, fields_involved, fix_step_ids}&lt;/code&gt;. Gemma then writes the human messages from that JSON.&lt;/p&gt;

&lt;p&gt;The interesting part was the evaluation. My first test set was synthetic, and the model scored 94%, which proved nothing. So I collected &lt;strong&gt;148 real remarks&lt;/strong&gt; that members had posted publicly, and kept them &lt;strong&gt;out of training&lt;/strong&gt;. On those, &lt;strong&gt;43% fit none of my 15 categories&lt;/strong&gt;. They were pension (EPS) membership problems, contribution gaps, pending transfers and TDS. I added five categories, wrote new training templates in my own words, and retrained twice. Total training cost on Tinker was about &lt;strong&gt;$1.65&lt;/strong&gt;.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;On 148 real remarks&lt;/th&gt;
&lt;th&gt;Accuracy&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Gemma 3 4B, few-shot&lt;/td&gt;
&lt;td&gt;53%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qwen3-8B, no fine-tune&lt;/td&gt;
&lt;td&gt;58%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Qwen3-8B + PF Desk LoRA&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;72%&lt;/strong&gt; (100% valid JSON)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Same LoRA on my laptop (Q4, llama-server)&lt;/td&gt;
&lt;td&gt;72%, about 6 s per remark&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Honesty note: the last two rounds were guided by this set's error categories (never its text), so treat 72% as optimistic. The real test is my dad's own remarks; there's a labelling tool built in for that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Gemma does the language.&lt;/strong&gt; Explanations, WhatsApp drafts and helpline answers come from Gemma: &lt;code&gt;gemma3:4b&lt;/code&gt; on the laptop through Ollama, and Gemma 4 on Google AI Studio for the public demo. It's &lt;strong&gt;context-first&lt;/strong&gt;: code gathers the facts, ETA and matching rules, and Gemma only writes. A rejected claim never gets a model-written date; it gets a fixed "your consultant will call" reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Privacy layer.&lt;/strong&gt; Every prompt passes through a tokenizer that swaps names, UAN, Aadhaar, PAN, bank account and phone for &lt;code&gt;[NAME_1]&lt;/code&gt;, &lt;code&gt;[UAN_1]&lt;/code&gt;, and so on. It's tested against 200 red-team strings with 0 leaks. While recording the demo, I caught a real bug. Gemma echoed the &lt;code&gt;[UAN_1]&lt;/code&gt; example from its own instructions, and my un-tokenizer helpfully swapped in the worker's real UAN. Now only placeholders that were actually sent can be restored, and identifiers always come back masked (&lt;code&gt;XXXXXXXX6259&lt;/code&gt;). A test covers exactly that case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;6. Temporal for the monthly cycle.&lt;/strong&gt; Each client's month is a workflow: request the sheet, wait for it, chase, validate, remind on the due date, mark paid. I killed the worker at random moments 20 times. The first run scored 17/20, and the failures exposed a real bug: a salary sheet arriving after the chase window was never validated. After fixing it: &lt;strong&gt;20/20 resumed, 0 duplicate tasks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;7. Rules, researched.&lt;/strong&gt; I couldn't sit my dad down this week, so I checked every Rule Book entry against official sources instead. That's how I found that the &lt;strong&gt;EPF Scheme, 2026&lt;/strong&gt; took effect on 1 July: a full PF withdrawal now needs &lt;strong&gt;12 months&lt;/strong&gt; out of work, not 2. Also, the wage ceiling rose to &lt;strong&gt;₹25,000&lt;/strong&gt; in September. Pre-check now knows this and suggests the 75% partial withdrawal instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Hono + Next.js on Node 24; PGlite (embedded Postgres + pgvector) on the laptop and &lt;strong&gt;Tiger Data&lt;/strong&gt; Postgres for the demo; hybrid keyword + vector Rule Book search, which finds the right rule in the top 5 for 29 of 30 test questions; deployed on &lt;strong&gt;Render&lt;/strong&gt;, at a &lt;strong&gt;.tech&lt;/strong&gt; domain.&lt;/p&gt;

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

&lt;p&gt;Look at what a PF consultant handles: &lt;strong&gt;Aadhaar numbers, bank accounts, salaries, family names&lt;/strong&gt; for hundreds of workers. That's exactly the data that should never be pasted into a closed AI service.&lt;/p&gt;

&lt;p&gt;Open models make the only acceptable version of this product possible:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;It runs on the consultant's own laptop.&lt;/strong&gt; Gemma through Ollama, TabPFN on CPU, and my fine-tuned decoder as a 185 MB LoRA on llama-server. Real client data never leaves the machine. The public demo uses only synthetic people.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A small open model beats a general one at the narrow job.&lt;/strong&gt; EPFO remark-speak ("F/H", "DOE", "PL SUBMIT CORRECTED CHQ") is its own dialect. A $1.65 fine-tune of an 8B model beat Gemma few-shot by 19 points on real remarks, and runs offline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The cost per claim is basically zero.&lt;/strong&gt; That's what makes ₹499 a month viable for a one-person business. No per-token bill scales with my dad's client list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No lock-in.&lt;/strong&gt; If a model gets better or cheaper, swap it. The rules, data and fine-tune are his.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My First User
&lt;/h2&gt;

&lt;p&gt;My dad, the PF consultant this was built for. I showed him PF Desk, and he said yes: he's approved it, and he'll use it on his clients' claims. That's the whole point. This started as "build my dad's practice a back office", not as a hackathon idea.&lt;/p&gt;

&lt;p&gt;For real work it runs in &lt;strong&gt;local mode on his laptop&lt;/strong&gt;, so his clients' Aadhaar, bank and salary data never touches the cloud. The demo site only has synthetic people. His first job for it: label his own past rejection remarks in the built-in labelling tool, so the next decoder is trained on the remarks his regional office actually writes.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma:&lt;/strong&gt; all explanations, WhatsApp drafts and helpline answers; local through Ollama, Gemma 4 on AI Studio for the demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN:&lt;/strong&gt; rejection risk (classification) and settlement ETA (regression) inside every Pre-check and the worker helpline, beating logistic regression and gradient boosting on a time-ordered split.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tinker:&lt;/strong&gt; LoRA fine-tune of Qwen3-8B for EPFO remark decoding. 72% vs 58% base on held-out real remarks, about $1.65 to train, runs offline on a laptop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Render:&lt;/strong&gt; hosts the public demo (web app + API) at pfdesk.buildwithyash.tech.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal:&lt;/strong&gt; monthly compliance cycle and claim follow-up workflows, with 20/20 resumes after forced worker kills.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Tiger Data:&lt;/strong&gt; Postgres + pgvector for the Rule Book's hybrid keyword + vector search on the live demo.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of a .tech Domain:&lt;/strong&gt; &lt;a href="https://pfdesk.buildwithyash.tech" rel="noopener noreferrer"&gt;https://pfdesk.buildwithyash.tech&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;em&gt;PF Desk helps consultants prepare and check. It doesn't file with EPFO, and it isn't legal or financial advice. Rules change, so always verify with the latest EPFO circulars.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;I built this with an AI coding agent (Claude Code) as my pair programmer. The idea, the product decisions and the problem are my family's. All demo data is synthetic.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Sitspot — my places, calling me back outside</title>
      <dc:creator>Yash Gurav</dc:creator>
      <pubDate>Sat, 10 Oct 2026 13:20:21 +0000</pubDate>
      <link>https://dev.to/yash_gurav_31d3dc3cc193d1/sitspot-my-places-calling-me-back-outside-2pmh</link>
      <guid>https://dev.to/yash_gurav_31d3dc3cc193d1/sitspot-my-places-calling-me-back-outside-2pmh</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/challenges/hacktoberfest-week1-2026-10-05"&gt;Hacktoberfest Open-Source AI Challenge Week 1: Touch Grass&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I live in Vasai, near Mumbai. Within 20 minutes of my desk there's a park, a lake edge and three beaches. Last month I went to none of them. Not because I didn't want to, but because in an Indian city in October &lt;strong&gt;timing is everything&lt;/strong&gt;: afternoons feel like 38°C, air quality swings by the hour, and the coast is only nice near low tide and before dark. Nobody checks five apps to find the good 40 minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sitspot&lt;/strong&gt; watches a few real places &lt;em&gt;you&lt;/em&gt; choose and calls you back outside only when one of them opens a genuinely good window.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You add 3–5 spots once (a park, a beach, your terrace) — that's most of the screen time you'll ever spend.&lt;/li&gt;
&lt;li&gt;Every hour it pulls weather, air quality, tide timing, sun position and recent eBird sightings for each spot, and forecasts how good birding will be with &lt;strong&gt;TabPFN&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;A plain, tested &lt;strong&gt;decision function&lt;/strong&gt; (code, not a model) scores every spot × hour — and five &lt;strong&gt;hard safety rules&lt;/strong&gt; can veto anything: no coast after sunset − 30 min, none near high tide, nothing at ≥ 38°C feels-like or US AQI ≥ 200, quiet hours are absolute.&lt;/li&gt;
&lt;li&gt;When a window clears the bar, &lt;strong&gt;Gemma&lt;/strong&gt; writes a short, grounded invitation ("…Leave by 6:40, it stays good until 8:00. The air is poor (US AQI 150), so keep it short and easy. Want to go?"), and your phone buzzes.&lt;/li&gt;
&lt;li&gt;Tap it and &lt;strong&gt;Sitspot talks to you&lt;/strong&gt; (ElevenLabs voice, with Gemma as the brain). Ask "how far is it?", say "yes, let's go".&lt;/li&gt;
&lt;li&gt;At the spot, tap &lt;strong&gt;I'm here&lt;/strong&gt; and put the phone in your pocket: &lt;strong&gt;BirdNET runs on the phone&lt;/strong&gt; and names what it hears, always with an honest confidence word. &lt;strong&gt;Audio never leaves the phone.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;In the evening it writes a short field note, and every night it learns from what you accepted.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The screen is the shortest part of it: ~3 minutes of setup, then a buzz, a tap, and you're out.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Live app: &lt;a href="https://sitspot.vercel.app" rel="noopener noreferrer"&gt;https://sitspot.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Video (phone screen recording): &lt;a href="https://www.youtube.com/shorts/BJddW_OH_ZY" rel="noopener noreferrer"&gt;https://www.youtube.com/shorts/BJddW_OH_ZY&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Honest note on the video.&lt;/strong&gt; Vasai's air was &lt;em&gt;hazardous&lt;/em&gt; for my whole build week — US AQI 200–299 most evenings, 35–39°C feels-like. Sitspot did exactly what it should and &lt;strong&gt;refused to invite me anywhere&lt;/strong&gt;; even its "send a TEST invitation" button refuses when no window passes the safety rules. So the recording uses a &lt;strong&gt;TEST invitation&lt;/strong&gt; (the real pipeline — Gemma script, push, voice, visit, field note — with only the score threshold skipped, never safety) for a clean-air test spot, recorded at home. You can see my real Vasai spots on the home screen marked &lt;em&gt;AQI 229 · very unhealthy&lt;/em&gt;. In pocket mode it heard a &lt;strong&gt;House Crow&lt;/strong&gt; (they're everywhere here) and a &lt;strong&gt;Barn Owl — "possibly"&lt;/strong&gt; at 2:50 pm indoors, which is almost certainly a false detection. That "possibly" is the point: the app is designed to say how sure it is and never to invent a bird.&lt;/p&gt;

&lt;p&gt;The forecast says the air improves this weekend; the first real dawn invitation is queued for my park.&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/Yashgurav002" rel="noopener noreferrer"&gt;
        Yashgurav002
      &lt;/a&gt; / &lt;a href="https://github.com/Yashgurav002/sitspot" rel="noopener noreferrer"&gt;
        sitspot
      &lt;/a&gt;
    &lt;/h2&gt;
    &lt;h3&gt;
      Your places, calling you. Watches a few real places near you with open data and calls you when one opens a genuinely good window. Open models: Gemma, BirdNET (on-device), TabPFN. Hacktoberfest 2026 Touch Grass.
    &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;Sitspot&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Your places, calling you.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You add the 3–5 real places you could actually go: a creek, a fort, a beach, a terrace. Sitspot watches them every hour using open data (weather, air quality, tide, sun, recent eBird sightings). When one of them opens a genuinely good window, your phone &lt;strong&gt;buzzes&lt;/strong&gt;, and tapping it opens a voice conversation with Sitspot in the browser (ElevenLabs voice, Gemma brain). The call gives you the place, the reasons with real numbers, and when to leave. If you go, you tap "I'm here", put the phone in your pocket, and walk. In the evening it writes a short field note from what actually happened, and it learns from what you accepted and declined.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://sitspot.vercel.app" rel="nofollow noopener noreferrer"&gt;https://sitspot.vercel.app&lt;/a&gt; · &lt;strong&gt;Write-up:&lt;/strong&gt; &lt;a href="https://github.com/Yashgurav002/sitspot/docs/devto-post.md" rel="noopener noreferrer"&gt;docs/devto-post.md&lt;/a&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Phone calls: the ElevenLabs + Twilio path is implemented (&lt;code&gt;apps/api/src/delivery/call.ts&lt;/code&gt;), but new Twilio &lt;strong&gt;trial&lt;/strong&gt; accounts block streaming call audio to an AI agent…&lt;/p&gt;
&lt;/blockquote&gt;&lt;/div&gt;
  &lt;/div&gt;
  &lt;div class="gh-btn-container"&gt;&lt;a class="gh-btn" href="https://github.com/Yashgurav002/sitspot" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;A pnpm monorepo: Next.js 16 PWA (&lt;code&gt;apps/web&lt;/code&gt;), Hono API (&lt;code&gt;apps/api&lt;/code&gt;), Temporal workflows (&lt;code&gt;workflows/&lt;/code&gt;), and small packages for the policy, agent, LLM client, data pullers and DB. ~300 tests across TypeScript and Python; CI runs typecheck + tests on every push.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Open models, each doing the one thing it's best at:&lt;/strong&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Job&lt;/th&gt;
&lt;th&gt;Open model&lt;/th&gt;
&lt;th&gt;Where it runs&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Invitation scripts + evening notes&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemma 4 31B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Google AI Studio (free tier)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Live voice replies&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Gemma 3 1B&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;My laptop, via Ollama (p50 time-to-first-token 379 ms)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bird ID&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;BirdNET&lt;/strong&gt; (TF.js build)&lt;/td&gt;
&lt;td&gt;Inside the phone's browser, in a Web Worker (~120–340 ms per 3-second window)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;"Will birding be good here at 7 am?"&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;TabPFN&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Python job, hourly, on my laptop&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Note search embeddings&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;nomic-embed-text&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Ollama&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Context-first, not tool-calling.&lt;/strong&gt; The server gathers every fact (weather, tide timing, sightings, your preferences) into a small context block; the model only &lt;em&gt;writes&lt;/em&gt;. Then validators check every output: every number must appear in the facts, no bird may be named that wasn't detected or reported, coastal scripts must say "stay on firm ground", nothing may encourage the coast after dark, confidence words must match the detection score. A failure gets one retry with the problems fed back, then a deterministic template. On my 40-turn adversarial eval (Gemma 3 1B), the raw model named a bird that wasn't there in 5% of turns; &lt;strong&gt;0% reached the user&lt;/strong&gt;. Unsafe advice reaching the user went from 12.5% → 2.5% after I enforced the coast-after-dark rule inside the agent itself — the eval caught that before any real person did.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Temporal makes it durable.&lt;/strong&gt; One workflow per day evaluates hourly; each invitation is its own workflow (sleep until send time → re-check conditions → write script → deliver → wait for answer → wait for arrival → wait for the visit to end → write the note). I killed the worker 20 times at every waiting point: &lt;strong&gt;20/20 resumed, 0 duplicate notifications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TabPFN, honestly.&lt;/strong&gt; I trained on &lt;strong&gt;19,588 real eBird checklists&lt;/strong&gt; (2022–2026, Palghar/Thane/Mumbai) joined with Open-Meteo history. On the held-out 2026 season TabPFN &lt;strong&gt;tied&lt;/strong&gt; the simple hotspot × hour baseline on ROC-AUC (0.649 vs 0.650) — it did &lt;em&gt;not&lt;/em&gt; hit my target of +0.05. But it had the &lt;strong&gt;best precision at the invite threshold (0.627 vs 0.549)&lt;/strong&gt; while flagging far fewer hours, which is exactly what an app that should rarely interrupt you needs. The main limit: eBird's rate limits pushed me to the daily feed, so 96% of rows lack checklist duration and the label is noisy. Results file: &lt;code&gt;evaluation/tabpfn_results.md&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memory.&lt;/strong&gt; Say "the creek is too far on weekdays" and that becomes a rule — weekday creek windows are held back, and the reason quotes your own words. Every preference must store the user's actual sentence. Overnight it nudges your threshold and learns which hours you say yes to.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hosting for ₹0.&lt;/strong&gt; The web app is on Vercel; the API, Temporal worker and Ollama run on my laptop behind a free ngrok domain, with Vercel proxying &lt;code&gt;/api&lt;/code&gt; so it's one origin. Postgres is PGlite (embedded, with pgvector) — swap in any Postgres via &lt;code&gt;DATABASE_URL&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What didn't work (and what I did instead).&lt;/strong&gt; I wanted a real phone call. ElevenLabs + Twilio is wired and tested up to Twilio — but &lt;strong&gt;new Twilio trial accounts block streaming call audio to an AI agent&lt;/strong&gt;, so the "call" happens in the browser instead: the push opens the call page and Sitspot speaks through ElevenLabs. The Twilio-direct code is in the repo and works on a paid account.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy you can verify.&lt;/strong&gt; Bird ID runs on the phone. Your microphone audio never leaves your pocket; only "House Crow, 0.71, 07:31" is sent. A closed audio API would mean streaming your walk to someone's server.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It works where the signal doesn't.&lt;/strong&gt; Mangroves and beaches have bad coverage. The model runs in the browser; detections queue in IndexedDB and sync later.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Swappable brains.&lt;/strong&gt; Every LLM call goes through one OpenAI-compatible client. I moved scripts from local Gemma 3 4B to Gemma 4 31B on AI Studio, and kept voice on local Gemma 3 1B for latency, by changing environment variables. When Gemma 4 turned out to "think" before answering (and that can't be switched off), I stripped it in one place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open data in, honest numbers out.&lt;/strong&gt; eBird checklists from thousands of birders, Open-Meteo forecasts and history, OpenStreetMap tiles, and an open tabular model I could actually evaluate — including publishing that it tied the baseline.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It costs nothing to run.&lt;/strong&gt; Free tiers, a laptop, and open weights. An always-on agent that checks five places every hour would be a real monthly bill on closed APIs.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  My Agent Session
&lt;/h2&gt;

&lt;p&gt;Built with Claude Code as my coding agent: it wrote the PRD, split it into 20 tasks, ran sub-agents in parallel per package, and every task had to pass its tests before commit. &lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Gemma&lt;/strong&gt; — Gemma 4 31B writes every invitation and note; Gemma 3 1B runs locally as the voice agent's brain; both behind validators.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of TabPFN&lt;/strong&gt; — hourly "good birding" forecast trained on 19.6k real eBird checklists, benchmarked against three baselines with the honest result above.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of ElevenLabs&lt;/strong&gt; — the Sitspot voice: an ElevenLabs agent whose LLM is my own open-model endpoint (Gemma), so the voice is grounded in the same facts and safety rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Best Use of Temporal&lt;/strong&gt; — durable invitation workflows with signals, re-checks and retries; 20/20 resumes after forced worker kills, zero duplicates.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;em&gt;Credits: TabPFN by Prior Labs; BirdNET (Kahl et al., 2021, CC BY-NC-SA 4.0); eBird / Cornell Lab of Ornithology; Open-Meteo (CC BY 4.0); © OpenStreetMap contributors.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>hf26challenge</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
