<?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: Motzumoto</title>
    <description>The latest articles on DEV Community by Motzumoto (@motzumoto).</description>
    <link>https://dev.to/motzumoto</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%2F4153258%2Ff8700edd-96ae-434e-b19d-fad11c24183e.jpg</url>
      <title>DEV Community: Motzumoto</title>
      <link>https://dev.to/motzumoto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/motzumoto"/>
    <language>en</language>
    <item>
      <title>my dashboard had 24 raid settings and the bot ignored 11 of them</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:59:21 +0000</pubDate>
      <link>https://dev.to/motzumoto/my-dashboard-had-24-raid-settings-and-the-bot-ignored-11-of-them-576o</link>
      <guid>https://dev.to/motzumoto/my-dashboard-had-24-raid-settings-and-the-bot-ignored-11-of-them-576o</guid>
      <description>&lt;p&gt;i build Akiko's dashboard and the bot in two separate repos. they deploy separately and share no code. turns out that's a good way to end up with settings that save, show up on the dashboard, and do nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  how it happens
&lt;/h2&gt;

&lt;p&gt;the dashboard builds each module's form straight from the database table. every column becomes a field, minus a list of ones i've hidden by hand. the bot then reads whichever columns it feels like.&lt;/p&gt;

&lt;p&gt;if the bot stops reading one, nothing complains. the dashboard keeps rendering it, the save goes through, the row updates. the bot just never looks.&lt;/p&gt;

&lt;p&gt;nothing ties "the dashboard offers this" to "the bot reads this" except me remembering.&lt;/p&gt;

&lt;h2&gt;
  
  
  what i found
&lt;/h2&gt;

&lt;p&gt;it started as one automod bug and kept going.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the account age and avatar checks had a punishment box that was free text. anything other than the literal word &lt;code&gt;ban&lt;/code&gt; quietly became a kick.&lt;/li&gt;
&lt;li&gt;the message filters offered "escalate repeat offenders" and the filter code never mentioned either column.&lt;/li&gt;
&lt;li&gt;the link filter and mention spam had infraction columns the dashboard labelled and the code hardcoded over.&lt;/li&gt;
&lt;li&gt;raid heat had 24 settings. 11 of them had no reader at all. a few more only changed the colour of the graph.&lt;/li&gt;
&lt;li&gt;auto-roles, which i wrote about last time, turned out to be the entire feature.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;two of the hints were worse than missing. one said panic mode auto-locks during coordinated raids. it posts an embed.&lt;/p&gt;

&lt;h2&gt;
  
  
  the check
&lt;/h2&gt;

&lt;p&gt;before writing a description for any setting, i run this per field:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-rl&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;column_name&amp;gt;"&lt;/span&gt; src/features src/core | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nb"&gt;test&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;zero hits means it's decoration. writing a hint for a dead field is the real mistake, because now the bug has documentation.&lt;/p&gt;

&lt;h2&gt;
  
  
  fixing it was the scary part
&lt;/h2&gt;

&lt;p&gt;wiring up a dead setting is a behaviour change. the raid heat enforcement fields had been sitting on defaults nobody chose, so switching them on would have started handing out timeouts in servers that never asked for them.&lt;/p&gt;

&lt;p&gt;i implemented all of it and put enforcement behind a new column that defaults to off.&lt;/p&gt;

&lt;p&gt;if you've got a dashboard and a bot in different repos, go grep your columns. i'd bet you've got a few too :3&lt;/p&gt;

&lt;p&gt;Akiko's free to add: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>stripe told me my webhook was broken. i found the email 17 days later.</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:56:14 +0000</pubDate>
      <link>https://dev.to/motzumoto/stripe-told-me-my-webhook-was-broken-i-found-the-email-17-days-later-4kaj</link>
      <guid>https://dev.to/motzumoto/stripe-told-me-my-webhook-was-broken-i-found-the-email-17-days-later-4kaj</guid>
      <description>&lt;p&gt;this one's a bit embarrassing. Akiko has a premium tier that runs through Stripe, and for about ten weeks every webhook Stripe sent her dashboard failed. i had no idea.&lt;/p&gt;

&lt;p&gt;i only found out because of an email from Stripe that had been sitting unread for seventeen days.&lt;/p&gt;

&lt;h2&gt;
  
  
  what happened
&lt;/h2&gt;

&lt;p&gt;i retired the old Express backend on the dashboard and moved everything into Next.js. the Stripe keys lived in the root &lt;code&gt;.env&lt;/code&gt;. Next runs from the &lt;code&gt;web/&lt;/code&gt; folder under pm2, so it reads &lt;code&gt;web/.env&lt;/code&gt; instead, and that file had no Stripe variables at all.&lt;/p&gt;

&lt;p&gt;so every delivery hit the webhook route, found no secret, and threw. the catch block turned any error with "Stripe" in the message into a 400. a missing config looked exactly like a bad signature, to Stripe and to me poking at it with curl.&lt;/p&gt;

&lt;p&gt;roughly: last event stored June 3. Express retired June 11. first failed delivery July 22. Stripe disabled the endpoint on July 31 after 10 attempts over 9 days. i found it August 16.&lt;/p&gt;

&lt;p&gt;there was a second bug in the same file. six variables were written &lt;code&gt;NAME = value&lt;/code&gt; with spaces around the equals, which dotenv reads as a variable called &lt;code&gt;"NAME "&lt;/code&gt; with a trailing space. even with the right file loaded they'd have come back undefined.&lt;/p&gt;

&lt;h2&gt;
  
  
  then it got worse
&lt;/h2&gt;

&lt;p&gt;while i was checking what the dead webhook had broken, i looked at how premium ends. it ends one way. a &lt;code&gt;customer.subscription.deleted&lt;/code&gt; event flips &lt;code&gt;active&lt;/code&gt; to false, and that's the only code in the repo that does. no sweep, nothing else.&lt;/p&gt;

&lt;p&gt;so with the webhook dead, nobody's premium could end. it would just run forever.&lt;/p&gt;

&lt;p&gt;the data to catch this was right there. &lt;code&gt;expires_at&lt;/code&gt; was written in four places and read in none. there was even an index built for a query nobody had written.&lt;/p&gt;

&lt;p&gt;the two date columns also disagreed. &lt;code&gt;expires_at&lt;/code&gt; and &lt;code&gt;current_period_end&lt;/code&gt; are supposed to mirror each other, but old rows never got mirrored. two lapsed subscriptions had a past &lt;code&gt;expires_at&lt;/code&gt; and a NULL &lt;code&gt;current_period_end&lt;/code&gt;. the AI limits code checked &lt;code&gt;current_period_end&lt;/code&gt;, saw NULL, took that to mean a lifetime grant, and gave premium quotas to a subscription that ended in May.&lt;/p&gt;

&lt;h2&gt;
  
  
  the fix
&lt;/h2&gt;

&lt;p&gt;both gates now deny if either date is in the past. NULL in both still means lifetime, because comps and staff grants are made outside Stripe with no expiry and i didn't want to cut those off. an unparseable date fails open, since a malformed value is my bug and i'd rather not lock out someone who's paying.&lt;/p&gt;

&lt;p&gt;i also added a daily sweep that clears &lt;code&gt;active&lt;/code&gt; on anything whose own dates have passed. i ran it against a clone of the real table first. it revoked the two lapsed rows, left the lifetime grants alone, and the second run touched nothing.&lt;/p&gt;

&lt;p&gt;if you give people access because of an event, also give them an end date the gate actually reads. otherwise one missed webhook turns into free forever.&lt;/p&gt;

&lt;p&gt;go read your Stripe emails (i didn't :3)&lt;/p&gt;

&lt;p&gt;Akiko's free to add if you want to watch what i break next: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>stripe</category>
      <category>webdev</category>
      <category>debugging</category>
    </item>
    <item>
      <title>Your Discord bot's member cache is lying to you. Mine never timed out a single member.</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:49:58 +0000</pubDate>
      <link>https://dev.to/motzumoto/your-discord-bots-member-cache-is-lying-to-you-mine-never-timed-out-a-single-member-13i7</link>
      <guid>https://dev.to/motzumoto/your-discord-bots-member-cache-is-lying-to-you-mine-never-timed-out-a-single-member-13i7</guid>
      <description>&lt;p&gt;I set &lt;code&gt;largeThreshold: 50&lt;/code&gt; in my Discord bot's client config. The comment next to it said it saved memory with no functional loss. That was wrong, and it took a log search to find out how wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the setting really does
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;largeThreshold&lt;/code&gt; is the &lt;code&gt;large_threshold&lt;/code&gt; field in Discord's gateway IDENTIFY payload. It is the member count above which the gateway stops sending offline members when a guild loads. Above it, a guild counts as "large" and the initial guild payload only carries members who are online.&lt;/p&gt;

&lt;p&gt;Nothing fills the gap later. My only full-roster fetch ran from the guild-join event, and that event only fires for brand new guilds. Guilds the bot was already in at startup never got it. So after every restart, the member cache held the online members plus whoever the bot had seen since.&lt;/p&gt;

&lt;p&gt;That means this line is not doing what it looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;member&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;guild&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;members&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="p"&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;It does not iterate every member. It iterates the members who happen to be online or recently active.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I found it
&lt;/h2&gt;

&lt;p&gt;I have a verification timeout sweep. If a member does not finish verification in time, the bot acts on them. I searched 200MB of production logs for the line it writes when it actions someone: &lt;code&gt;verification timeout action applied&lt;/code&gt;. It appeared zero times. It had never once actioned anyone.&lt;/p&gt;

&lt;p&gt;Two of the four servers using the gate were above the threshold, at 532 and 54 members. The 54 member server was only affected because I had set the threshold to 50. At Discord's default of 250 it would have worked.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Do not treat the cache as a roster. There are two honest options:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep a durable record of who needs checking. I added a &lt;code&gt;pending_verification&lt;/code&gt; table and made it the source of candidates.&lt;/li&gt;
&lt;li&gt;Call the full member fetch explicitly before you iterate, and report a "not cached" count, so that "nobody matched" is distinguishable from "most of the server was never looked at".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The threshold itself is a real memory optimisation and I kept it. The mistake was letting several features read the cache as if it were complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trap inside the trap
&lt;/h2&gt;

&lt;p&gt;The sweep's happy path returns early and quietly when it finds no candidates. So the absence of log output proved nothing for weeks. A check that stays silent when it finds nothing cannot tell you that it is blind.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Before you walk a guild's members, ask whether you need the offline ones. If yes, the cache is the wrong source.&lt;/li&gt;
&lt;li&gt;Make sweeps report what they examined, not only what they did.&lt;/li&gt;
&lt;li&gt;Comments that claim "no functional loss" deserve a test.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I build Akiko, a Discord bot with AI chat that remembers you, plus economy, music, moderation and a web dashboard. Free to add: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>node</category>
      <category>debugging</category>
      <category>typescript</category>
    </item>
    <item>
      <title>Discord gives your bot exactly 100 slash commands. I hit 100 and found out from a failing test.</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:45:08 +0000</pubDate>
      <link>https://dev.to/motzumoto/discord-gives-your-bot-exactly-100-slash-commands-i-hit-100-and-found-out-from-a-failing-test-702</link>
      <guid>https://dev.to/motzumoto/discord-gives-your-bot-exactly-100-slash-commands-i-hit-100-and-found-out-from-a-failing-test-702</guid>
      <description>&lt;p&gt;Discord limits an app to 100 global slash commands. Subcommands and subcommand groups do not count toward it. My bot, Akiko, was sitting at exactly 100 when this happened. i've folded a few more commands into groups since, so she's at 95 now.&lt;/p&gt;

&lt;p&gt;I learned where the wall is the hard way. I added a &lt;code&gt;/settings&lt;/code&gt; command, the total became 101, and a test failed with &lt;code&gt;expected 101 to be less than or equal to 100&lt;/code&gt;. In production the same mistake shows up as Discord error code 30032 when you deploy your commands.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most useful thing in the repo is a boring test
&lt;/h2&gt;

&lt;p&gt;There is a test that builds the real list of commands that would be deployed globally and asserts the count is at most 100. Without it you find out at deploy time, on the live bot. The test's own docstring also lists the escape routes in order, and says to never loosen the bound.&lt;/p&gt;

&lt;h2&gt;
  
  
  The escape routes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Fold the command into an existing one as a subcommand group. &lt;code&gt;/report_setup&lt;/code&gt; became &lt;code&gt;/report setup&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Make it prefix-only, so it only works with the text prefix.&lt;/li&gt;
&lt;li&gt;Make it developer-only. Those deploy to a separate control guild instead of globally, and that scope had plenty of room. Global is the binding constraint.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The scope rules are simple: global means visible, not prefix-only and not developer-only.&lt;/p&gt;

&lt;h2&gt;
  
  
  The permission trap
&lt;/h2&gt;

&lt;p&gt;Discord ignores &lt;code&gt;default_permissions&lt;/code&gt; on subcommands. Permissions are set on the top-level command. So when I folded &lt;code&gt;/report_setup&lt;/code&gt; (admin only) into &lt;code&gt;/report&lt;/code&gt; (used by ordinary members to file reports), I could not require Manage Server on the setup subcommand alone. Putting the stricter permission on the whole command would have locked ordinary members out of &lt;code&gt;/report user&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The check for that one subcommand has to live in the command body.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would do differently
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Count your commands in a test, not in your head.&lt;/li&gt;
&lt;li&gt;Design command names as groups from day one. Every new top-level name spends a scarce slot.&lt;/li&gt;
&lt;li&gt;Never assume a permission set on a subcommand is enforced. Check it in code.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I build Akiko, a Discord bot with AI chat that remembers you, plus economy, music, moderation and a web dashboard. Free to add: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>typescript</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>My Discord bot's auto-role setting said ON in 127 servers. It worked in 2.</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:44:34 +0000</pubDate>
      <link>https://dev.to/motzumoto/my-discord-bots-auto-role-setting-said-on-in-127-servers-it-worked-in-2-3g31</link>
      <guid>https://dev.to/motzumoto/my-discord-bots-auto-role-setting-said-on-in-127-servers-it-worked-in-2-3g31</guid>
      <description>&lt;p&gt;The dashboard showed the toggle on. The status command said enabled. No member had ever received a role.&lt;/p&gt;

&lt;p&gt;I found this while working on something else. A dashboard to-do claimed that auto-roles and my verification gate "both act on join and conflict", and asked me to add a warning about the clash. Before building the warning I went to check how the two interact.&lt;/p&gt;

&lt;p&gt;They don't interact, because auto-roles had no code path on join at all. The member-join handler never touched them. The only place that granted auto-roles was the handler that runs when a member passes a verification gate. So the feature worked exactly where a gate was enabled and nowhere else: 2 guilds out of the 127 that had auto-roles switched on with a role selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nothing noticed
&lt;/h2&gt;

&lt;p&gt;Everything around the feature agreed with itself. The settings page read the row, the status command read the row, and the row said enabled. Nothing checked the outcome: whether a member actually ended up holding the role. A feature can be fully configured and never run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix, and one decision worth stealing
&lt;/h2&gt;

&lt;p&gt;Adding a grant on join is easy. The interesting part is what happens when a gate is on. A verification gate exists so a member holds no roles until they pass it. Granting on join would hand out exactly the roles the gate is there to withhold.&lt;/p&gt;

&lt;p&gt;So the join path checks the gate's own enabled flag and steps aside when the gate is on. The gate keeps ownership. The check reads that local flag instead of watching what the gate did, because the gate call is fire-and-forget and its outcome is not visible from the join handler. If the two ever disagree, the member gets nothing. That is deliberate: a missing role is a support message, a role handed to someone the gate meant to stop is an incident.&lt;/p&gt;

&lt;p&gt;The same planning function feeds both the join path and a backfill for existing members, so the prechecks cannot drift apart between the two.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I took from it
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Verify the premise of a bug report before building the fix. The spec described a conflict that did not exist. The real bug was a missing branch.&lt;/li&gt;
&lt;li&gt;"Enabled" in a settings page proves a row exists, not that the feature runs. Test the outcome.&lt;/li&gt;
&lt;li&gt;When two paths can grant the same thing, decide which one fails safe before you write either.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I build Akiko, a Discord bot with AI chat that remembers you, plus economy, music, moderation and a web dashboard. Free to add: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>typescript</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Giving a Discord bot long-term memory, and the two ways it went wrong</title>
      <dc:creator>Motzumoto</dc:creator>
      <pubDate>Wed, 30 Sep 2026 23:30:39 +0000</pubDate>
      <link>https://dev.to/motzumoto/giving-a-discord-bot-long-term-memory-and-the-two-ways-it-went-wrong-mkg</link>
      <guid>https://dev.to/motzumoto/giving-a-discord-bot-long-term-memory-and-the-two-ways-it-went-wrong-mkg</guid>
      <description>&lt;p&gt;I run a Discord bot called Akiko. The feature I care about most is that she remembers people across servers and DMs. Building it taught me more about what not to store than what to store. Two bugs stand out.&lt;/p&gt;

&lt;h2&gt;
  
  
  The memory was 97% junk
&lt;/h2&gt;

&lt;p&gt;My first extractor pulled "facts" out of chat with a loose pronoun pattern. It looked fine in testing. In production, when I finally read the table, almost all of it was noise: fragments that matched the pattern but described nothing about the person.&lt;/p&gt;

&lt;p&gt;The fix had three parts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Clean the existing rows down to the ones worth keeping (about 100 survived).&lt;/li&gt;
&lt;li&gt;Make the writer classify a candidate before it saves it, instead of saving everything that matched.&lt;/li&gt;
&lt;li&gt;Treat "not yet judged" as its own state (NULL) and retry the judgment on an hourly job, so a temporary failure never turns into a permanent bad row.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The lesson: a memory system that only ever adds will bury itself. The write path needs a filter as strict as the read path.&lt;/p&gt;

&lt;h2&gt;
  
  
  She remembered what people had not said
&lt;/h2&gt;

&lt;p&gt;The extractor also started storing absence as a fact, things like "User has not shared their birthday". That is worse than noise, because recall then surfaces it and the bot sounds like it is keeping a file on what you withheld.&lt;/p&gt;

&lt;p&gt;I first tried marking these rows as low priority, but recall never filtered on that flag, so it changed nothing. The real fix was an extractor rule that refuses negative statements, plus a recall-side filter as a second line of defense.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it something users can control
&lt;/h2&gt;

&lt;p&gt;Memory is only comfortable if the user can see and change it. Akiko has commands to list, add, delete and export what she has stored. There is also an import that takes a memory export from another AI and condenses it into clean facts, so a new user does not start from zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell anyone building this
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Read your memory table in production early. It will not look like your tests.&lt;/li&gt;
&lt;li&gt;Filter on the way in, not only on the way out.&lt;/li&gt;
&lt;li&gt;Never store the absence of information.&lt;/li&gt;
&lt;li&gt;Give users list, delete and export from day one.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Akiko is free to add: &lt;a href="https://i.hep.gg/akiko" rel="noopener noreferrer"&gt;https://i.hep.gg/akiko&lt;/a&gt;&lt;br&gt;
Dashboard: &lt;a href="https://akiko.motzumoto.com" rel="noopener noreferrer"&gt;https://akiko.motzumoto.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>discord</category>
      <category>typescript</category>
      <category>postgres</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
