<?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: Alex Miano</title>
    <description>The latest articles on DEV Community by Alex Miano (@mianohh).</description>
    <link>https://dev.to/mianohh</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%2F4163161%2F8a465e1d-0354-459c-be29-3f241f140c6d.png</url>
      <title>DEV Community: Alex Miano</title>
      <link>https://dev.to/mianohh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mianohh"/>
    <language>en</language>
    <item>
      <title>I Gave a Chatbot Memory It Can Prove : Here's the Architecture</title>
      <dc:creator>Alex Miano</dc:creator>
      <pubDate>Mon, 05 Oct 2026 08:14:23 +0000</pubDate>
      <link>https://dev.to/mianohh/i-gave-a-chatbot-memory-it-can-prove-heres-the-architecture-345g</link>
      <guid>https://dev.to/mianohh/i-gave-a-chatbot-memory-it-can-prove-heres-the-architecture-345g</guid>
      <description>&lt;p&gt;&lt;strong&gt;Mnemo AI&lt;/strong&gt; is a chatbot that remembers you across sessions — and every memory is encrypted, stored on &lt;strong&gt;Walrus mainnet&lt;/strong&gt;, and provable. Postgres is only a mirror; the source of truth lives on-chain, and a daily reconciler proves it. Built with Next.js 16 + Gemini Flash + Sui/Walrus. Live at &lt;a href="https://mnemoai.xyz" rel="noopener noreferrer"&gt;mnemoai.xyz&lt;/a&gt;, source at &lt;a href="https://github.com/mianohh/mnemo" rel="noopener noreferrer"&gt;github.com/mianohh/mnemo&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: every chatbot starts from zero
&lt;/h2&gt;

&lt;p&gt;Language models are stateless. They see the current conversation and nothing else. You tell a bot "never ship on Fridays," come back the next day, ask "can we ship this Friday?" and it answers: &lt;em&gt;"Sure, what time works?"&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Chat history files and bigger context windows are the usual patch. I wanted something better: memory that survives sessions and devices, is readable only by its owner, and whose existence you can &lt;strong&gt;verify&lt;/strong&gt; — not trust.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop around every reply
&lt;/h2&gt;

&lt;p&gt;Every chat turn runs a fixed pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart TD
    U["User message"] --&amp;gt; R["Recall in namespace&amp;lt;br/&amp;gt;relevance floor 0.2"]
    R --&amp;gt;|"hits + per-request nonce"| G["Gemini streams the answer"]
    G --&amp;gt; CHIP["UI chip: [N] memories applied&amp;lt;br/&amp;gt;expand to see fact + score"]
    G --&amp;gt; E["Extract pass&amp;lt;br/&amp;gt;keep at most 3 durable facts"]
    E --&amp;gt; L["Relayer: embed, SEAL-encrypt,&amp;lt;br/&amp;gt;upload to Walrus, index"]
    L --&amp;gt; W[("Walrus mainnet&amp;lt;br/&amp;gt;source of truth")]
    L --&amp;gt; M[("Postgres mirror")]
    M -.-&amp;gt;|"daily parity check"| W&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Three details do the heavy lifting:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Receipts.&lt;/strong&gt; Every contextual reply carries a &lt;code&gt;[N] memories applied&lt;/code&gt; chip. Expand it and you see each stored fact and its cosine relevance score — the model's memory, auditable by anyone reading the screen.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Memories can't become instructions.&lt;/strong&gt; Recalled facts are injected behind a nonce boundary (&lt;code&gt;BEGIN_UNTRUSTED_WALRUS_MEMORY_…&lt;/code&gt;) in their own message, with a fixed untrusted-data policy in the system prompt. No memory byte can ever hold system priority  so a poisoned blob can't prompt-inject the bot.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Write, encrypt, then forget locally.&lt;/strong&gt; The extract pass keeps at most three durable facts per turn (Zod-validated), and the relayer encrypts them with SEAL before they ever hit storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Your address &lt;em&gt;is&lt;/em&gt; the namespace
&lt;/h2&gt;

&lt;p&gt;Multi-tenancy isn't a &lt;code&gt;user_id&lt;/code&gt; column. it's cryptographic. Both sign-in paths resolve to a verified Sui address:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Wallet:&lt;/strong&gt; personal-message signature challenge.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Email:&lt;/strong&gt; Google OIDC → zkLogin address derived server-side from the verified JWT (no proving service).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The namespace is then deterministically &lt;code&gt;mnemo-user-{address}&lt;/code&gt;. Clients cannot forge, switch, or inspect someone else's namespace  the server derives it from the signer, every request.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mirror is not the truth
&lt;/h2&gt;

&lt;p&gt;The design rule: &lt;em&gt;PostgreSQL serves exclusively as an ephemeral read-through mirror, while Walrus Memory is the decentralized source of truth.&lt;/em&gt; So the dashboard doesn't display its own row count — it cross-checks it against the relayer's on-chain metric:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mirrorCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;146&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The dashboard renders those two numbers side by side (&lt;code&gt;146 ↔ 146&lt;/code&gt;) and only claims parity when they're equal. Two independent sources agreeing is the proof the memories really exist on-chain, not just in one database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving what's on-chain (the part nobody else has)
&lt;/h2&gt;

&lt;p&gt;Walrus Memory's SDK exposes no "what is the status of blob X?" endpoint, so I built a reconciler against Sui's GraphQL API:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Owned &lt;code&gt;Blob&lt;/code&gt; objects are the truth.&lt;/strong&gt; The relayer transfers every blob it writes to the account owner, so the owner's object set on Sui is what actually exists.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exact epoch math.&lt;/strong&gt; The current Walrus epoch is read from the shared system object's &lt;code&gt;future_accounting&lt;/code&gt; ring buffer — not guessed from timestamps. Every blob has a mandatory end epoch, and &lt;em&gt;a lapsed blob cannot be renewed or recovered&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cron that fails loudly.&lt;/strong&gt; A daily GitHub Actions job reports &lt;code&gt;chainBlobs&lt;/code&gt;, per-epoch expiry buckets, and the object IDs needed to renew — and fails the run within 3 epochs of any risk.
&lt;/li&gt;
&lt;/ul&gt;

&lt;pre data-lang="mermaid"&gt;&lt;code&gt;flowchart LR
    GQL["Sui GraphQL API"] --&amp;gt; REC["Reconciler"]
    SYS["System object&amp;lt;br/&amp;gt;future_accounting ring"] --&amp;gt; REC
    REC --&amp;gt; SET["Owned Blob set&amp;lt;br/&amp;gt;start + end epochs"]
    SET --&amp;gt; CRON["Daily cron&amp;lt;br/&amp;gt;fails within 3 epochs of risk"]&lt;/code&gt;&lt;/pre&gt;



&lt;p&gt;Honest wart, because engineering posts should have them: 21 mirror rows still point at blob IDs the chain doesn't recognize. The account-level report is chain-driven and correct; the mismatch is tracked as a known issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it actually remember?
&lt;/h2&gt;

&lt;p&gt;The chat UI has a &lt;strong&gt;Memory ON/OFF toggle&lt;/strong&gt;, which makes the before/after a one-button demo. Memory off: I told Mnemo &lt;em&gt;"Never ship on Fridays — that's our release-freeze window,"&lt;/em&gt; then asked in a later session &lt;em&gt;"Can we ship this Friday?"&lt;/em&gt; → &lt;em&gt;"Sure, what time works?"&lt;/em&gt; Memory on, same question: &lt;em&gt;"You've said Friday is your release-freeze window — want Thursday or Monday?"&lt;/em&gt; — with the stored constraint and its score sitting right there in the chip. One bot, two very different Fridays.&lt;/p&gt;

&lt;p&gt;More of the build story — what broke along the way, the prompt-injection boundary, and the honest version of the A/B — is in &lt;a href="https://miano369.medium.com/how-i-gave-a-chatbot-permanent-memory-and-it-runs-on-walrus-mainnet-24dcf78f705c" rel="noopener noreferrer"&gt;the long-form article&lt;/a&gt;.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/mianohh/mnemo
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cp&lt;/span&gt; .env.example .env
npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node ≥ 24, any Postgres, a Gemini key — and it remembers you tomorrow. Or talk to it first at &lt;a href="https://mnemoai.xyz" rel="noopener noreferrer"&gt;mnemoai.xyz&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>architecture</category>
      <category>walrus</category>
    </item>
  </channel>
</rss>
