<?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: SemTiOne</title>
    <description>The latest articles on DEV Community by SemTiOne (@semtione).</description>
    <link>https://dev.to/semtione</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%2F3743907%2F9daaecd8-aae4-4abf-a945-cc0032d4f624.jpg</url>
      <title>DEV Community: SemTiOne</title>
      <link>https://dev.to/semtione</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/semtione"/>
    <language>en</language>
    <item>
      <title>Position Evaluator — A Chess Diagnostic for Your Daily Decisions</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Mon, 13 Jul 2026 01:44:28 +0000</pubDate>
      <link>https://dev.to/semtione/position-evaluator-a-chess-diagnostic-for-your-daily-decisions-3omj</link>
      <guid>https://dev.to/semtione/position-evaluator-a-chess-diagnostic-for-your-daily-decisions-3omj</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-07-09"&gt;Weekend Challenge: Passion Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;I've been obsessed with chess for years, and I kept noticing that a handful of chess concepts describe everyday life decisions better than any generic advice ever could. "Zugzwang": being forced to move when every option makes things worse. "Fork": one situation threatening two things you care about at once. "Zwischenzug": sneaking in a small unrelated move before dealing with the real problem.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;Position Evaluator&lt;/strong&gt;, a daily diagnostic log that takes a short description of a decision or situation you're facing, and classifies it into one of seven formal chess concepts using Gemini, such as Zugzwang, Fork, Prophylaxis, Gambit, Zwischenzug, Overextension, or Tempo Loss. You get a classification, a confidence level, and a short clinical explanation of why. Then, over time, a dashboard shows which "condition" you land in most often.&lt;/p&gt;

&lt;p&gt;It's a structured classification system with a strict decision order. The interface leans into the bit too, it's styled like a hospital lab-requisition form crossed with a chess scoresheet, complete with a stamped classification badge, a case-file number, and a "clear all records" control for starting a fresh log.&lt;/p&gt;

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

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uIwytRwPZxw"&gt;
  &lt;/iframe&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/SemTiOne" rel="noopener noreferrer"&gt;
        SemTiOne
      &lt;/a&gt; / &lt;a href="https://github.com/SemTiOne/position-evaluator" rel="noopener noreferrer"&gt;
        position-evaluator
      &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;Position Evaluator&lt;/h1&gt;
&lt;/div&gt;
&lt;p&gt;Daily diagnostic log that classifies your personal decisions into formal chess concepts (Zugzwang, Fork, Zwischenzug, Prophylaxis, Gambit, Overextension, Tempo Loss) using Gemini structured output. Built for the DEV Weekend Challenge: Passion Edition.&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Setup&lt;/h2&gt;
&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;python -m venv venv
&lt;span class="pl-c1"&gt;source&lt;/span&gt; venv/bin/activate  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; Windows: venv\Scripts\activate&lt;/span&gt;
pip install -r requirements.txt

cp .env.example .env
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; edit .env: add your GEMINI_API_KEY and MySQL credentials&lt;/span&gt;

mysql -u root -p &lt;span class="pl-k"&gt;&amp;lt;&lt;/span&gt; schema.sql

python app.py
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; open http://127.0.0.1:5000&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Running tests&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="highlight highlight-source-shell notranslate position-relative overflow-auto js-code-highlight"&gt;
&lt;pre&gt;&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; fast, no API calls, safe to run anytime — this is what CI runs on every push&lt;/span&gt;
pytest tests/ -k &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;not eval_set&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;

&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; slower, calls the real Gemini API, costs quota — run after editing the&lt;/span&gt;
&lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; system prompt in gemini_client.py to check for prompt drift&lt;/span&gt;
pytest tests/ -k &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;eval_set&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;&lt;code&gt;.github/workflows/test.yml&lt;/code&gt; runs the deterministic tests on every push/PR to &lt;code&gt;main&lt;/code&gt;. The Gemini eval-set tests are deliberately excluded from CI. They call…&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/SemTiOne/position-evaluator" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


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

&lt;p&gt;Stack: Python (Flask), MySQL, Gemini API (gemini-3.1-flash-lite) via the google-genai SDK, vanilla JS/CSS frontend.&lt;/p&gt;

&lt;p&gt;Seven overlapping chess concepts can describe the same situation in slightly different ways ("procrastinating on a task to watch anime" could plausibly read as either Zwischenzug or Tempo Loss). I solved this by giving Gemini an explicit decision order in the system prompt; check for Fork first, then Zugzwang, then Prophylaxis, and so on down to Tempo Loss as the default; rather than asking it to "pick whichever fits best" from a flat list. That single change made classifications far more consistent across similar inputs.&lt;/p&gt;

&lt;p&gt;The other detail I'm most proud of is that the Pydantic schema for Gemini's structured output declares reasoning before classification. Since Gemini's controlled JSON generation fills fields in the order they're declared, this forces the model to articulate its reasoning before it's allowed to commit to a label.&lt;/p&gt;

&lt;p&gt;I also intentionally didn't build a numeric confidence score. LLMs aren't well-calibrated at self-reporting a percentage like "87% confident," so instead of a falsely precise number, the model picks a categorical low / medium / high bucket based on whether the situation contains an explicit signal for its chosen category, or is more of an inference.&lt;/p&gt;

&lt;p&gt;That same principle got tested again late in the build, when I wanted a chess-engine-style evaluation score next to each badge for extra flavor, something like FORK [-2.5]. The tempting shortcut was to just ask Gemini to invent that number too. I didn't, for the same reason as above, it would've quietly reintroduced the exact false precision I'd deliberately avoided with the confidence bucket. Instead, the score is computed deterministically in plain code, from a fixed lookup table keyed on the classification and confidence Gemini already returned.&lt;/p&gt;

&lt;p&gt;Everything else, like the entry log, the duplicate-submit handling (same-day identical entries return the existing diagnosis instead of creating a new row), and the distribution dashboard, is built around one goal: making the "diagnostic report" feel real.&lt;/p&gt;

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

&lt;p&gt;Submitting for Best Use of Google AI. The entire product only works because of Gemini's structured output support (response_schema bound to a Pydantic model).&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>StandupBot</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Sat, 11 Jul 2026 12:23:10 +0000</pubDate>
      <link>https://dev.to/semtione/standupbot-8jn</link>
      <guid>https://dev.to/semtione/standupbot-8jn</guid>
      <description>&lt;p&gt;A CLI tool that reads your recent git commits and generates a standup update — Yesterday / Today / Blockers, ready to paste into Slack, Jira, or wherever your team posts standups.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;standup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the command. It reads commits from the last 24 hours (configurable), classifies them, and generates the summary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two backends, both free
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Provider&lt;/th&gt;
&lt;th&gt;Cost&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;Ollama&lt;/td&gt;
&lt;td&gt;Free&lt;/td&gt;
&lt;td&gt;100% local&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Groq&lt;/td&gt;
&lt;td&gt;Free tier&lt;/td&gt;
&lt;td&gt;Cloud&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;No subscription. Run it with Ollama and nothing leaves your machine.&lt;/p&gt;

&lt;p&gt;If you use Groq, commit messages get scanned and redacted before they're sent — private IPs, internal hostnames, GitHub tokens, AWS keys, Slack tokens, API keys, credentialed URIs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Caching + rate limiting
&lt;/h2&gt;

&lt;p&gt;Cache key is a hash of your actual commit hashes. If nothing changed since your last standup, it serves the cached result instead of calling the LLM again. There's also a cooldown + daily cap so a script or a bad habit can't burn through a free tier's quota.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output formats
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;standup &lt;span class="nt"&gt;--template&lt;/span&gt; slack
standup &lt;span class="nt"&gt;--template&lt;/span&gt; jira
standup &lt;span class="nt"&gt;--raw&lt;/span&gt;
standup &lt;span class="nt"&gt;--copy&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five built-in templates, or define your own with a fixed set of variables ({yesterday}, {today}, {blockers}, {repos}, etc.), no arbitrary format-string injection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quality scoring
&lt;/h2&gt;

&lt;p&gt;Every generated standup gets scored 0-100. Set a minimum score and it retries with refined guidance up to twice before returning what it has.&lt;/p&gt;

&lt;h3&gt;
  
  
  Install
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="nb"&gt;.&lt;/span&gt;
standup &lt;span class="nt"&gt;--setup&lt;/span&gt;
standup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Ollama:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ollama pull llama3
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Contributing
&lt;/h2&gt;

&lt;p&gt;MIT-licensed. A few scoped "good first issue" tasks are open if you want to look at the codebase without needing full context first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/SemTiOne/standup-bot" rel="noopener noreferrer"&gt;github.com/SemTiOne/standup-bot&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>opensource</category>
      <category>python</category>
      <category>github</category>
    </item>
    <item>
      <title>Heatmap Bot v1.2.0 - Automating Ko-fi Payments with Webhooks</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Fri, 20 Mar 2026 03:02:32 +0000</pubDate>
      <link>https://dev.to/semtione/heatmap-bot-v120-automating-ko-fi-payments-with-webhooks-ibh</link>
      <guid>https://dev.to/semtione/heatmap-bot-v120-automating-ko-fi-payments-with-webhooks-ibh</guid>
      <description>&lt;p&gt;Quick update on Heatmap Bot - just shipped v1.2.0.&lt;/p&gt;

&lt;h2&gt;
  
  
  The main change: automated Ko-fi activation
&lt;/h2&gt;

&lt;p&gt;Previously the payment flow was painful:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer pays on Ko-fi&lt;/li&gt;
&lt;li&gt;I get an email notification&lt;/li&gt;
&lt;li&gt;I manually run /grant in Discord&lt;/li&gt;
&lt;li&gt;Customer waits&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Now:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer pays on Ko-fi, includes their Discord Server ID in message&lt;/li&gt;
&lt;li&gt;Ko-fi webhook fires&lt;/li&gt;
&lt;li&gt;Bot parses the Server ID from the message&lt;/li&gt;
&lt;li&gt;Paid tier activates instantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The activation code system handles edge cases where they forget their Server ID.&lt;/p&gt;

&lt;h2&gt;
  
  
  Other changes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;/announce command — lets me broadcast updates to subscribed servers&lt;/li&gt;
&lt;li&gt;Feedback reference IDs (#A3F92B1C) for support tracking
&lt;/li&gt;
&lt;li&gt;Brute force protection on /activate — max 5 attempts/hour&lt;/li&gt;
&lt;li&gt;HMAC verification on Ko-fi webhooks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full bot: &lt;a href="https://ko-fi.com/semtione" rel="noopener noreferrer"&gt;https://ko-fi.com/semtione&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Invite Link: &lt;a href="https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601408&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands" rel="noopener noreferrer"&gt;https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601408&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>security</category>
      <category>showdev</category>
      <category>sideprojects</category>
    </item>
    <item>
      <title>How I Added Paid Tiers to My Discord Bot Without Stripe or Any Payment API</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Tue, 17 Mar 2026 07:46:18 +0000</pubDate>
      <link>https://dev.to/semtione/how-i-added-paid-tiers-to-my-discord-bot-without-stripe-or-any-payment-api-4mf9</link>
      <guid>https://dev.to/semtione/how-i-added-paid-tiers-to-my-discord-bot-without-stripe-or-any-payment-api-4mf9</guid>
      <description>&lt;p&gt;Most tutorials for monetizing a Discord bot assume you'll integrate Stripe or LemonSqueezy directly into the bot.&lt;/p&gt;

&lt;p&gt;I went a different route, manual grants via Ko-fi. Here's why and how.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The problem with payment APIs in bots&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;For a new bot with low volume, integrating a full payment API means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Webhook endpoints to maintain&lt;/li&gt;
&lt;li&gt;Subscription management logic&lt;/li&gt;
&lt;li&gt;Refund handling&lt;/li&gt;
&lt;li&gt;Failed payment handling&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That's a lot of complexity for a bot that might have 10 paying customers at launch.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The Ko-fi manual grant approach&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Instead, I set up Ko-fi as the payment page and handle grants manually:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Customer buys on Ko-fi&lt;/li&gt;
&lt;li&gt;I get a notification&lt;/li&gt;
&lt;li&gt;I run /grant guild_id:123456 days:30 in my admin server&lt;/li&gt;
&lt;li&gt;Bot updates the database and unlocks paid features
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;python&lt;/span&gt;&lt;span class="nd"&gt;@app_commands.command&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;grant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;grant_paid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guild_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;is_bot_owner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interaction&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;expires_at&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;86400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;upsert_subscription&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;guild_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;guild_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="n"&gt;tier&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;paid&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;expires_at&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  &lt;strong&gt;When to upgrade to automated payments&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Once you hit ~20-30 paying customers, the manual process gets tedious. That's when it makes sense to add Ko-fi webhooks (requires Ko-fi Gold) or switch to Stripe.&lt;/p&gt;

&lt;p&gt;For now, manual is fine, and it means I talk to every single customer personally.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The result&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Clean, simple, zero ongoing maintenance cost. Works well for early stage.&lt;/p&gt;

&lt;p&gt;Invite Link: &lt;a href="https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601472&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands" rel="noopener noreferrer"&gt;https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601472&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Full bot: &lt;a href="https://ko-fi.com/semtione" rel="noopener noreferrer"&gt;https://ko-fi.com/semtione&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>discord</category>
      <category>indiehacker</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How I Built a Discord Activity Heatmap Bot with Python and discord.py</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Tue, 17 Mar 2026 07:40:35 +0000</pubDate>
      <link>https://dev.to/semtione/how-i-built-a-discord-activity-heatmap-bot-jm0</link>
      <guid>https://dev.to/semtione/how-i-built-a-discord-activity-heatmap-bot-jm0</guid>
      <description>&lt;p&gt;I've been running a Discord server for a while and always wondered: "when is my community actually online?"&lt;/p&gt;

&lt;p&gt;Discord doesn't show you this. You can see who's online right now, but there's no way to see patterns over time, no weekly view, no hourly breakdown, nothing.&lt;/p&gt;

&lt;p&gt;So I built Heatmap Bot.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;What it does&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;Heatmap Bot tracks your server's activity and turns it into a heatmap, a 7×24 grid showing activity by day and hour for the past week. It also supports:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Daily bar chart view&lt;/li&gt;
&lt;li&gt;Monthly trend chart&lt;/li&gt;
&lt;li&gt;Channel breakdown&lt;/li&gt;
&lt;li&gt;Member leaderboard&lt;/li&gt;
&lt;li&gt;Week-over-week comparison&lt;/li&gt;
&lt;li&gt;Weekly auto-reports&lt;/li&gt;
&lt;li&gt;Peak activity alerts&lt;/li&gt;
&lt;li&gt;CSV data export&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Tech stack&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Python 3.13 with discord.py 2.x&lt;/li&gt;
&lt;li&gt;SQLite with aiosqlite for async DB access&lt;/li&gt;
&lt;li&gt;matplotlib + numpy for heatmap rendering&lt;/li&gt;
&lt;li&gt;aiohttp for the webhook server&lt;/li&gt;
&lt;li&gt;Railway for hosting&lt;/li&gt;
&lt;li&gt;Ko-fi for payments (manual grant flow)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The interesting part, rendering heatmaps&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The heatmap is a 7×24 numpy array. Each cell is the count of events at that day/hour combination, converted from UTC to the server's local timezone.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;pythondef&lt;/span&gt; &lt;span class="nf"&gt;aggregate_to_heatmap_grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;grid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;tz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pytz&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;timestamp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fromtimestamp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tz&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;weekday&lt;/span&gt;&lt;span class="p"&gt;()][&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hour&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;grid&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The rendering uses matplotlib with a custom blurple→pink gradient to match Discord's aesthetic.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;The hard part - Railway + Discord rate limits&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;The trickiest issue was Discord's Cloudflare rate limiting. Too many reconnects from the same IP gets you a 1015 error, temporarily banned. The fix was switching Railway regions to get a new IP.&lt;/p&gt;

&lt;p&gt;If you're deploying a Discord bot, make sure your restart policy doesn't cause rapid reconnect loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Free vs paid tier&lt;/strong&gt;
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free:&lt;/strong&gt; 7 days history, messages only, watermark&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paid ($4.99/month):&lt;/strong&gt; 90 days, all event types, no watermark, leaderboard, CSV export&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;Add Heatmap Bot to your server: &lt;a href="https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601472&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands" rel="noopener noreferrer"&gt;https://discord.com/oauth2/authorize?client_id=1482225606761386085&amp;amp;permissions=2147601472&amp;amp;integration_type=0&amp;amp;scope=bot+applications.commands&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Ko-fi: &lt;a href="https://ko-fi.com/semtione" rel="noopener noreferrer"&gt;https://ko-fi.com/semtione&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Happy to answer questions about the implementation in the comments!&lt;/p&gt;

</description>
      <category>python</category>
      <category>discord</category>
      <category>showdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>StandupBot – Generate daily standups from git history using Ollama or Groq</title>
      <dc:creator>SemTiOne</dc:creator>
      <pubDate>Thu, 12 Mar 2026 08:11:32 +0000</pubDate>
      <link>https://dev.to/semtione/standupbot-generate-daily-standups-from-git-history-using-ollama-or-groq-2pfj</link>
      <guid>https://dev.to/semtione/standupbot-generate-daily-standups-from-git-history-using-ollama-or-groq-2pfj</guid>
      <description>&lt;p&gt;Tired of trying to remember what you did yesterday for standup? &lt;/p&gt;

&lt;p&gt;I built &lt;strong&gt;StandupBot&lt;/strong&gt;, a CLI tool that reads your git history and generates a clean standup summary using a local or free cloud LLM. No paid APIs required.&lt;/p&gt;

&lt;h2&gt;
  
  
  Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Private with Ollama (runs locally)&lt;/li&gt;
&lt;li&gt;Free cloud option via Groq&lt;/li&gt;
&lt;li&gt;One command: &lt;code&gt;standup&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Copy to clipboard: &lt;code&gt;standup --copy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Health check: &lt;code&gt;standup doctor&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;

&lt;p&gt;Install Ollama from &lt;a href="https://ollama.com" rel="noopener noreferrer"&gt;https://ollama.com&lt;/a&gt;, then:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ollama pull llama3
pip install -e .
standup --setup
standup
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;h2&gt;
  
  
  Example Output
&lt;/h2&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**Yesterday:** Fixed authentication bug and refactored token validation.
**Today:** Working on the new /users endpoint.
**Blockers:** None
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;🔗 &lt;a href="https://github.com/SemTiOne/standup-bot" rel="noopener noreferrer"&gt;https://github.com/SemTiOne/standup-bot&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome!&lt;/p&gt;

</description>
      <category>python</category>
      <category>cli</category>
      <category>opensource</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
