<?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: Ruita Wachira</title>
    <description>The latest articles on DEV Community by Ruita Wachira (@ruita_wachira).</description>
    <link>https://dev.to/ruita_wachira</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%2F940555%2F4845cb43-c408-4d6a-b80f-48745ff2b091.jpeg</url>
      <title>DEV Community: Ruita Wachira</title>
      <link>https://dev.to/ruita_wachira</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ruita_wachira"/>
    <language>en</language>
    <item>
      <title>Karibu Give; USSD Micro-Philanthropy for the Next Billion Givers</title>
      <dc:creator>Ruita Wachira</dc:creator>
      <pubDate>Sun, 06 Sep 2026 21:31:25 +0000</pubDate>
      <link>https://dev.to/ruita_wachira/karibu-give-ussd-micro-philanthropy-for-the-next-billion-givers-46a3</link>
      <guid>https://dev.to/ruita_wachira/karibu-give-ussd-micro-philanthropy-for-the-next-billion-givers-46a3</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/weekend-2026-09-03"&gt;Weekend Challenge: Generosity Edition&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Karibu Give&lt;/strong&gt; (Swahili for &lt;em&gt;Welcome, Give&lt;/em&gt;) is a &lt;strong&gt;USSD micro-donation platform that works on a kabambe phone(feature phone) with no data, no app, no account&lt;/strong&gt; , just a phone number and a mobile-money PIN. Built for the International Day of Charity.&lt;/p&gt;

&lt;p&gt;Two things stop generosity from scaling in Kenya and across Africa:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;You need a smartphone to give.&lt;/strong&gt; Most donation platforms are web-forms that assume Chrome, data bundles, and card rails. 40% of Kenyan adults still use feature phones.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You need trust to give again.&lt;/strong&gt; Donations disappear into a black box. Donors never see where 50 KES actually went.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Karibu Give attacks both:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Dial &lt;code&gt;*384*6120#&lt;/code&gt; → 1. Donate → Pick a cause → Enter 50 → Confirm → M-Pesa STK prompt in 2 seconds.&lt;/strong&gt; No internet, no app store, no signup. Session state is managed server-side via &lt;code&gt;sessionId&lt;/code&gt; (Africa's Talking USSD callback at &lt;code&gt;POST /ussd&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real M-Pesa money movement&lt;/strong&gt; — STK Push is triggered via my dedicated &lt;strong&gt;M-Pesa Service &lt;code&gt;https://mpesa-service-3s2d.onrender.com/stkpush&lt;/code&gt;&lt;/strong&gt;, which wraps Daraja API (&lt;code&gt;POST {phone:"2547...", amount}&lt;/code&gt; → &lt;code&gt;CheckoutRequestID:"ws_CO_..."&lt;/code&gt;). &lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;SQLite = source of truth, Snowflake = audit trail&lt;/strong&gt; — every &lt;code&gt;pending → completed/failed&lt;/code&gt; transition via &lt;code&gt;POST /payment-callback&lt;/code&gt; (&lt;code&gt;Body.stkCallback.CheckoutRequestID&lt;/code&gt; Daraja shape + AT shape) is synced to &lt;code&gt;DONATIONS_ANALYTICS&lt;/code&gt; in Snowflake with &lt;code&gt;phone_hash = SHA256(phone).slice(0,16)&lt;/code&gt; , never raw PII. &lt;code&gt;POST /admin/sync-snowflake&lt;/code&gt; batch-retries unsynced rows.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Only 3 causes can be active at a time&lt;/strong&gt; — admin (&lt;code&gt;/admin&lt;/code&gt; behind &lt;code&gt;ADMIN_USER/PASSWORD&lt;/code&gt; Basic Auth) creates charities (&lt;code&gt;name, emoji, target_amount, description&lt;/code&gt;), toggles active, edits, deletes (blocked if donations exist). USSD and landing page render &lt;strong&gt;only active causes&lt;/strong&gt;, so the choice stays focused. The limit is enforced in SQLite (&lt;code&gt;countActive()&amp;lt;3&lt;/code&gt;) and in the UI (&lt;code&gt;Activate&lt;/code&gt; disables at 3/3).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Two &lt;em&gt;separate&lt;/em&gt; AI cards — not one bolted-on feature:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;✨ Google AI Impact Summary (Gemini 1.5 Flash via &lt;code&gt;@google/generative-ai&lt;/code&gt;)&lt;/strong&gt; — plain-language donor transparency paragraph: &lt;em&gt;"Together 6 gifts totaling KES 845… School Books leads… avg 141 KES… Asante!"&lt;/em&gt; Built from last 20 completed donations + per-cause progress. Purple card, purple &lt;code&gt;Generate summary&lt;/code&gt; button.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;❄️ Snowflake AI Analytics (Cortex &lt;code&gt;COMPLETE('llama3-8b', ?)&lt;/code&gt;)&lt;/strong&gt; — trend analytics over live aggregates from &lt;code&gt;DONATIONS_ANALYTICS&lt;/code&gt; (totals by cause, daily trend, largest gift, average). Prompt is bound via &lt;code&gt;binds: ['llama3-8b', prompt]&lt;/code&gt; per Node.js driver docs, no string concat. Blue card, blue &lt;code&gt;Refresh analytics&lt;/code&gt; button — on-demand, not on every page load.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Mock-everything gracefully&lt;/strong&gt; — no &lt;code&gt;AT_API_KEY&lt;/code&gt;? MOCK checkout. No &lt;code&gt;GOOGLE_AI_API_KEY&lt;/code&gt;? Heuristic summary. No &lt;code&gt;SNOWFLAKE_*&lt;/code&gt;? SQLite aggregates with &lt;code&gt;(Generated locally — Snowflake Cortex mock mode)&lt;/code&gt;. &lt;code&gt;npm install &amp;amp;&amp;amp; npm start&lt;/code&gt; demos end-to-end offline.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;🔗 &lt;strong&gt;Live app:&lt;/strong&gt; &lt;code&gt;https://karibugive.onrender.com&lt;/code&gt; &lt;em&gt;(or &lt;code&gt;http://localhost:3000&lt;/code&gt; locally)&lt;/em&gt;&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%2Fvnfrga2rwk3xbqbr8epq.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%2Fvnfrga2rwk3xbqbr8epq.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;M-Pesa Service:&lt;/strong&gt; &lt;code&gt;https://mpesa-service-3s2d.onrender.com&lt;/code&gt; → &lt;code&gt;POST /stkpush&lt;/code&gt;&lt;br&gt;
&lt;/p&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/Charlo-tech" rel="noopener noreferrer"&gt;
        Charlo-tech
      &lt;/a&gt; / &lt;a href="https://github.com/Charlo-tech/karibugive" rel="noopener noreferrer"&gt;
        karibugive
      &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;Karibu Give ❤ — USSD Micro-Donation Platform&lt;/h1&gt;
&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;DEV.to "generosity" weekend challenge&lt;/strong&gt; — anyone with a basic phone (no smartphone, no data) can donate small amounts via mobile money to causes, using Africa's Talking USSD + M-Pesa.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Quick start&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;cp .env.example .env   &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; fill AT_API_KEY, GOOGLE_AI_API_KEY, Snowflake etc.&lt;/span&gt;
npm install
npm start              &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; http://localhost:3000&lt;/span&gt;&lt;/pre&gt;

&lt;/div&gt;
&lt;p&gt;Admin: &lt;code&gt;http://localhost:3000/admin&lt;/code&gt; (default &lt;code&gt;admin / karibu123&lt;/code&gt; — set &lt;code&gt;ADMIN_USER/PASSWORD&lt;/code&gt; in &lt;code&gt;.env&lt;/code&gt;).&lt;/p&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;Endpoints&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Landing page — total raised, causes + progress bars, dial code&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /ussd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AT USSD callback — session state machine&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /payment-callback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;AT payment webhook — marks &lt;code&gt;pending→completed/failed&lt;/code&gt;, syncs to Snowflake&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /payment-callback/simulate&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dev helper to simulate a callback&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;GET /admin&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Dashboard (HTTP Basic Auth)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /admin/generate-impact&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Google Gemini donor transparency summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /admin/generate-cortex&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Snowflake Cortex analytics summary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;POST /admin/sync-snowflake&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Batch-sync unsynced rows&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;div class="markdown-heading"&gt;
&lt;h2 class="heading-element"&gt;USSD flow&lt;/h2&gt;

&lt;/div&gt;
&lt;div class="snippet-clipboard-content notranslate position-relative overflow-auto"&gt;
&lt;pre class="notranslate"&gt;&lt;code&gt;*384*6120# →
  1. Donate  2. Check total raised  0. Exit
  → pick cause (1..3)&lt;/code&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/Charlo-tech/karibugive" rel="noopener noreferrer"&gt;View on GitHub&lt;/a&gt;&lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;&lt;strong&gt;60-second tour:&lt;/strong&gt;&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%2Fqbs97vbh4z774vh5ct2s.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%2Fqbs97vbh4z774vh5ct2s.PNG" alt=" " width="611" height="544"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Visit &lt;code&gt;/&lt;/code&gt;&lt;/strong&gt; — see live &lt;code&gt;KES 845 raised&lt;/code&gt;, 3 active cause cards with progress bars, and the &lt;strong&gt;Dial &lt;code&gt;*384*6120#&lt;/code&gt;&lt;/strong&gt; instruction. No donate button — intentional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulate USSD&lt;/strong&gt; (no AT SIM needed):
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/ussd &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"sessionId=TEST123&amp;amp;phoneNumber=%2B254711082000&amp;amp;text="&lt;/span&gt;
   &lt;span class="c"&gt;# CON Welcome … 1. Donate  2. Check total raised&lt;/span&gt;
   &lt;span class="c"&gt;# text=1      → CON Choose a cause: 1. Clean Water …&lt;/span&gt;
   &lt;span class="c"&gt;# text=1*1    → CON You chose: Clean Water / Enter amount&lt;/span&gt;
   &lt;span class="c"&gt;# text=1*1*50 → CON Donate KES 50 to Clean Water? 1. Confirm&lt;/span&gt;
   &lt;span class="c"&gt;# text=1*1*50*1 → END Thank you! Check your phone…  (STK push fired)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Or browser: Open the ussd simulator at &lt;/p&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
      &lt;div class="c-embed__body flex items-center justify-between"&gt;
        &lt;a href="https://developers.africastalking.com/simulator" rel="noopener noreferrer" class="c-link fw-bold flex items-center"&gt;
          &lt;span class="mr-2"&gt;developers.africastalking.com&lt;/span&gt;
          

        &lt;/a&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;and enter code *384*6120# to try out the sandbox ussd flow.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Go to &lt;code&gt;/admin&lt;/code&gt; (admin / karibu123) → Manage Charity Causes&lt;/strong&gt; — see &lt;code&gt;3 of 3 active&lt;/code&gt;, try &lt;code&gt;Activate&lt;/code&gt; on a 4th → &lt;code&gt;302 Cannot activate — already 3 active…&lt;/code&gt;. Add &lt;code&gt;Girls Education 🎓 / 900,000&lt;/code&gt; as inactive, deactivate one, then activate it — landing + USSD now show the new cause instantly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trigger payment:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/payment-callback &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"checkoutRequestId":"ws_CO_...","status":"Success"}'&lt;/span&gt;
   &lt;span class="c"&gt;# or Daraja shape&lt;/span&gt;
   curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3000/mpesa-callback &lt;span class="se"&gt;\&lt;/span&gt;
     &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{"Body":{"stkCallback":{"CheckoutRequestID":"ws_CO_...","ResultCode":0}}}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;→ donation flips to &lt;code&gt;completed&lt;/code&gt;, Snowflake sync fires, total on &lt;code&gt;/&lt;/code&gt; increments.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Click ✨ Generate summary&lt;/strong&gt; (Gemini) and &lt;strong&gt;❄️ Refresh analytics&lt;/strong&gt; (Cortex) — two independent cards, two providers, two buttons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Try deleting a cause with donations&lt;/strong&gt; → &lt;code&gt;Cannot delete — 2 donation(s) reference… Deactivate instead.&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;strong&gt;The flow:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   [ Feature phone — no data ]
              │
              ▼  *384*6120#  (Africa's Talking USSD)
   ┌──────────────────────┐
   │   POST /ussd         │ ──► sessionId state machine
   │  (active causes ≤3)  │ ──► pending donation row
   └──────────────────────┘
              │
              ▼  1*1*50*1 Confirm
   ┌──────────────────────┐
   │  M-Pesa Service      │ ──► POST https://mpesa-service-3s2d.onrender.com/stkpush
   │  POST {phone,amount} │ ──► ws_CO_...  (Daraja)
   │  AT PAYMENT fallback │ ──► STK Push prompt on phone
   └──────────────────────┘
              │
              ▼  M-Pesa PIN
   ┌──────────────────────┐
   │ POST /payment-callback│ ──► pending → completed/failed
   │  (AT + Daraja shapes) │ ──► Snowflake sync (hashed phone)
   └──────────────────────┘
              │
      ┌───────┴────────┐
      ▼                ▼
  Gemini 1.5 Flash   Cortex COMPLETE('llama3-8b')
  Impact Summary      Trend Summary
  (donor-facing)      (operator-facing)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;code&gt;https://github.com/Charlo-tech/karibugive&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="s"&gt;server.js&lt;/span&gt;                         &lt;span class="c1"&gt;# Express + EJS +dotenv, mounts ussd/payment/admin/public&lt;/span&gt;
&lt;span class="s"&gt;src/db/&lt;/span&gt;
  &lt;span class="s"&gt;schema.sql&lt;/span&gt;                      &lt;span class="c1"&gt;# donations + causes (is_active) + indexes&lt;/span&gt;
  &lt;span class="s"&gt;db.js&lt;/span&gt;                           &lt;span class="c1"&gt;# better-sqlite3 with node:sqlite fallback, WAL, seed 3 causes&lt;/span&gt;
  &lt;span class="s"&gt;causes.js&lt;/span&gt;                       &lt;span class="c1"&gt;# DB-backed CRUD with MAX_ACTIVE=3 enforcement&lt;/span&gt;
&lt;span class="s"&gt;src/services/&lt;/span&gt;
  &lt;span class="s"&gt;donations.js&lt;/span&gt;                    &lt;span class="c1"&gt;# create/get/update, getRaisedByCause({activeOnly})&lt;/span&gt;
  &lt;span class="s"&gt;atClient.js&lt;/span&gt;                     &lt;span class="c1"&gt;# africastalking SDK wrapper with MOCK&lt;/span&gt;
  &lt;span class="s"&gt;mpesa.js&lt;/span&gt;                        &lt;span class="c1"&gt;# fetch → POST https://mpesa-service-3s2d.onrender.com/stkpush {phone,amount}&lt;/span&gt;
  &lt;span class="s"&gt;googleAI.js&lt;/span&gt;                     &lt;span class="c1"&gt;# Gemini generateImpactSummary() with heuristic mock&lt;/span&gt;
  &lt;span class="s"&gt;snowflake.js&lt;/span&gt;                    &lt;span class="c1"&gt;# snowflake-sdk: createConnection→connect→execute with binds, Cortex&lt;/span&gt;
&lt;span class="s"&gt;src/routes/&lt;/span&gt;
  &lt;span class="s"&gt;ussd.js&lt;/span&gt;                         &lt;span class="c1"&gt;# POST+GET /ussd, Text handling, amount validation 10-70000, STK trigger&lt;/span&gt;
  &lt;span class="s"&gt;payment.js&lt;/span&gt;                      &lt;span class="c1"&gt;# POST /payment-callback + /mpesa-callback + /daraja-callback (AT + Daraja shapes)&lt;/span&gt;
  &lt;span class="s"&gt;admin.js&lt;/span&gt;                        &lt;span class="c1"&gt;# Basic Auth, cause CRUD, generate-impact/cortex, sync-snowflake&lt;/span&gt;
  &lt;span class="s"&gt;public.js&lt;/span&gt;                       &lt;span class="c1"&gt;# GET / (active causes only), GET /health&lt;/span&gt;
&lt;span class="s"&gt;src/views/&lt;/span&gt;
  &lt;span class="s"&gt;index.ejs + admin.ejs + partials/header,footer&lt;/span&gt;
  &lt;span class="s"&gt;public/css/style.css&lt;/span&gt;            &lt;span class="c1"&gt;# no framework, minimal&lt;/span&gt;
&lt;span class="s"&gt;.env.example&lt;/span&gt;                      &lt;span class="c1"&gt;# AT_USERNAME, MPESA_SERVICE_URL, GOOGLE_AI_API_KEY, SNOWFLAKE_*, ADMIN_*&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Node 24, Express 4, EJS, &lt;code&gt;better-sqlite3@13&lt;/code&gt;, &lt;code&gt;snowflake-sdk@1.13&lt;/code&gt;, &lt;code&gt;africastalking@0.7&lt;/code&gt;, &lt;code&gt;@google/generative-ai@0.21&lt;/code&gt;. Clone + &lt;code&gt;npm install&lt;/code&gt; + &lt;code&gt;npm start&lt;/code&gt;.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  M-Pesa as the real money rail
&lt;/h3&gt;

&lt;p&gt;M-pesa is a huge financial enabler in the Kenyan scene and I wanted to give donors a payment experience they are familiar with and one that is seamless to use.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// POST https://mpesa-service-3s2d.onrender.com/stkpush&lt;/span&gt;
&lt;span class="c1"&gt;// {phone: "254712345678", amount: 1} → 200&lt;/span&gt;
&lt;span class="c1"&gt;// {MerchantRequestID, CheckoutRequestID:"ws_CO_...", ResponseCode:"0"}&lt;/span&gt;
&lt;span class="c1"&gt;// {phoneNumber:...} → 400 {error:"Phone and amount are required"}&lt;/span&gt;
&lt;span class="c1"&gt;// {phone:"+254..."} → 500 Invalid PhoneNumber&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So &lt;code&gt;mpesa.js&lt;/code&gt; normalizes &lt;code&gt;+254711→254711&lt;/code&gt;, &lt;code&gt;0711→254711&lt;/code&gt;, and posts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;MPESA_SERVICE_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/stkpush`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;User-Agent&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KaribuGive/1.0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;phone&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;normalized&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;amount&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;Success returns &lt;code&gt;ws_CO_…&lt;/code&gt; which becomes &lt;code&gt;checkout_request_id&lt;/code&gt;. Failure returns &lt;code&gt;MOCK-MPESA-…&lt;/code&gt; so USSD never blocks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Daraja callbacks are not AT callbacks
&lt;/h3&gt;

&lt;p&gt;AT sends &lt;code&gt;{checkoutRequestId, status}&lt;/code&gt;. Daraja sends &lt;code&gt;{Body:{stkCallback:{CheckoutRequestID, ResultCode, CallbackMetadata:{Item:[...]}}}}&lt;/code&gt;. &lt;code&gt;payment.js&lt;/code&gt; unwraps both:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Body&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;stkCallback&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stkCallback&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;ResultCode&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;checkoutRequestId&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CheckoutRequestID&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ResultCode&lt;/span&gt;&lt;span class="o"&gt;===&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Success&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Failed&lt;/span&gt;&lt;span class="dl"&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;Mounted at three aliases so the M-Pesa service can be pointed at any of them: &lt;code&gt;/payment-callback&lt;/code&gt;, &lt;code&gt;/mpesa-callback&lt;/code&gt;, &lt;code&gt;/daraja-callback&lt;/code&gt;. All mark &lt;code&gt;status=completed/failed&lt;/code&gt;, set &lt;code&gt;completed_at&lt;/code&gt;, then &lt;code&gt;syncDonationToSnowflake()&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snowflake Node.js driver — by the docs
&lt;/h3&gt;

&lt;p&gt;Implemented strictly per &lt;a href="https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver:" rel="noopener noreferrer"&gt;https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver:&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;snowflake&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createConnection&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;warehouse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;database&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;schema&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;role&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;application&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;KaribuGive&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="cm"&gt;/* isValidAsync() → ensureTable() */&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="nx"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;sqlText&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;binds&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;:(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;stmt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;executeSql()&lt;/code&gt; wraps that pattern. Inserts use &lt;code&gt;binds: [id, phone_hash, cause_id, …]&lt;/code&gt; — no string concat. Cortex uses &lt;code&gt;binds: ['llama3-8b', prompt]&lt;/code&gt; → &lt;code&gt;SELECT SNOWFLAKE.CORTEX.COMPLETE(?,?)&lt;/code&gt; . If creds missing, &lt;code&gt;mockMode=true&lt;/code&gt;, SQLite aggregates produce the same summary with &lt;code&gt;(Generated locally…)&lt;/code&gt; — same code path in tests, no deploy secrets needed. &lt;code&gt;destroy()&lt;/code&gt; on &lt;code&gt;SIGINT&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini vs Cortex — why two AIs?
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Gemini: donor-facing plain language. Prompt built from &lt;code&gt;getRaisedByCause({activeOnly:false})&lt;/code&gt; + last 20 donations, &lt;code&gt;temperature&lt;/code&gt; default, &lt;code&gt;gemini-1.5-flash&lt;/code&gt;. No key → heuristic &lt;code&gt;Together, N gifts totaling KES X…&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Cortex: operator-facing trends. Aggregates &lt;code&gt;GROUP BY cause_id&lt;/code&gt;, &lt;code&gt;TO_DATE(completed_at)&lt;/code&gt;, &lt;code&gt;ORDER BY amount DESC LIMIT 1&lt;/code&gt; → JSON → prompt → &lt;code&gt;CORTEX.COMPLETE('llama3-8b', ?)&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both are &lt;code&gt;POST …/generate-*&lt;/code&gt; on demand, not on page load, to control cost/latency.&lt;/p&gt;

&lt;h3&gt;
  
  
  The 3-active constraint
&lt;/h3&gt;

&lt;p&gt;I made it admin-configurable but &lt;strong&gt;capped at 3&lt;/strong&gt; to keep the USSD menu usable on a 160-char screen. DB has &lt;code&gt;is_active INTEGER&lt;/code&gt;, &lt;code&gt;countActive()&lt;/code&gt; guards &lt;code&gt;createCause&lt;/code&gt; and &lt;code&gt;setActive&lt;/code&gt;. UI disables &lt;code&gt;Activate&lt;/code&gt;/&lt;code&gt;is_active&lt;/code&gt; at &lt;code&gt;3/3&lt;/code&gt; and shows &lt;code&gt;Full (3/3)&lt;/code&gt;. Landing + USSD query &lt;code&gt;WHERE is_active=1 ORDER BY rowid&lt;/code&gt; — creation order, not alphabetical, so menu numbers stay stable. Delete blocked if &lt;code&gt;COUNT(*) FROM donations WHERE cause_id=? &amp;gt;0&lt;/code&gt; — deactivate instead.&lt;/p&gt;

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

&lt;h3&gt;
  
  
  Best Use of Google AI
&lt;/h3&gt;

&lt;p&gt;Gemini &lt;strong&gt;is the trust layer donors see&lt;/strong&gt;. The Impact Summary isn't a chatbot — it's a transparency report generated from live &lt;code&gt;completed&lt;/code&gt; donations, shown on the admin dashboard next to Snowflake's card but visually distinct (purple vs blue). Remove the key and it degrades to a deterministic heuristic, so the challenge can be judged without my API key.&lt;/p&gt;

&lt;h3&gt;
  
  
  Best Use of Snowflake
&lt;/h3&gt;

&lt;p&gt;Snowflake is the &lt;strong&gt;audit layer operators see&lt;/strong&gt;. &lt;code&gt;DONATIONS_ANALYTICS&lt;/code&gt; mirrors completed donations with hashed PII, synced synchronously on &lt;code&gt;payment-callback&lt;/code&gt; + batch &lt;code&gt;POST /admin/sync-snowflake&lt;/code&gt;. Cortex &lt;code&gt;COMPLETE('llama3-8b', ?)&lt;/code&gt; runs &lt;em&gt;over that table's aggregates&lt;/em&gt; — not over SQLite — and is executed via the official Node.js driver with &lt;code&gt;binds&lt;/code&gt;. Without Snowflake creds the same query runs against SQLite so the demo never 500s.&lt;/p&gt;

&lt;h3&gt;
  
  
  💜 Generosity, Why Karibu Give.
&lt;/h3&gt;

&lt;p&gt;Generosity scales when you &lt;strong&gt;remove the need for generosity to be tech-savvy&lt;/strong&gt;. Karibu Give doesn't ask a mama mboga in Turkana to install an app — it asks her to dial a code she already knows. Causes are curated (3 active max) so small gifts feel focused, progress is live, and AI makes the impact visible in plain language. That's generosity by design, not by donation form.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Karibu means welcome. Give small, give often.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stack:&lt;/strong&gt; Node.js + Express + EJS, Africa's Talking USSD, M-Pesa Daraja via &lt;code&gt;mpesa-service-3s2d.onrender.com&lt;/code&gt;, SQLite (&lt;code&gt;better-sqlite3&lt;/code&gt; + &lt;code&gt;node:sqlite&lt;/code&gt; fallback), Snowflake (&lt;code&gt;snowflake-sdk&lt;/code&gt;), Gemini (&lt;code&gt;@google/generative-ai&lt;/code&gt;), &lt;code&gt;dotenv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Run:&lt;/strong&gt; &lt;code&gt;npm install &amp;amp;&amp;amp; cp .env.example .env &amp;amp;&amp;amp; npm start&lt;/code&gt; → &lt;code&gt;http://localhost:3000&lt;/code&gt; | &lt;code&gt;POST /ussd&lt;/code&gt; | &lt;code&gt;GET /admin&lt;/code&gt; (admin/karibu123)&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>gemini</category>
      <category>snowflake</category>
      <category>weekendchallenge</category>
    </item>
    <item>
      <title>I Built an AI CRM That Runs Itself Using MeDo</title>
      <dc:creator>Ruita Wachira</dc:creator>
      <pubDate>Wed, 20 May 2026 10:42:26 +0000</pubDate>
      <link>https://dev.to/ruita_wachira/i-built-an-ai-crm-that-runs-itself-using-medo-344l</link>
      <guid>https://dev.to/ruita_wachira/i-built-an-ai-crm-that-runs-itself-using-medo-344l</guid>
      <description>&lt;p&gt;Most CRMs feel like spreadsheets with extra suffering.&lt;/p&gt;

&lt;p&gt;You meet a client, then spend the next 5 minutes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creating a lead&lt;/li&gt;
&lt;li&gt;updating stages&lt;/li&gt;
&lt;li&gt;adding reminders&lt;/li&gt;
&lt;li&gt;writing notes&lt;/li&gt;
&lt;li&gt;setting follow-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wanted to explore a different approach:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What if the CRM handled all of that automatically?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;So I built &lt;strong&gt;Ghost Mode CRM&lt;/strong&gt; using &lt;a href="https://medo.dev?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;MeDo&lt;/a&gt; — an AI-powered CRM where you simply type what happened, and the system organizes everything for you.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“Met John in Westlands, needs cement in 2 weeks”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The system instantly:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;creates a lead&lt;/li&gt;
&lt;li&gt;extracts the location&lt;/li&gt;
&lt;li&gt;schedules a follow-up&lt;/li&gt;
&lt;li&gt;updates the pipeline&lt;/li&gt;
&lt;li&gt;calculates a lead score&lt;/li&gt;
&lt;li&gt;generates AI follow-up suggestions&lt;/li&gt;
&lt;li&gt;places the lead on an interactive Nairobi map&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;Most sales workflows are naturally conversational.&lt;/p&gt;

&lt;p&gt;But traditional CRMs force users into rigid forms and repetitive admin work.&lt;/p&gt;

&lt;p&gt;I wanted to see what happens when the CRM becomes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;conversational&lt;/li&gt;
&lt;li&gt;AI-native&lt;/li&gt;
&lt;li&gt;proactive&lt;/li&gt;
&lt;li&gt;lightweight&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Instead of manually configuring everything, the user simply communicates naturally and the system structures the workflow automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  Built with MeDo
&lt;/h2&gt;

&lt;p&gt;I used &lt;a href="https://medo.dev?utm_source=chatgpt.com" rel="noopener noreferrer"&gt;MeDo&lt;/a&gt; to rapidly prototype and build the application experience.&lt;/p&gt;

&lt;p&gt;The UI was inspired by:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Attio → structured CRM design&lt;/li&gt;
&lt;li&gt;Notion → conversational interaction&lt;/li&gt;
&lt;li&gt;Pipedrive → visual pipelines&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AI-powered lead creation&lt;/li&gt;
&lt;li&gt;Kanban pipeline management&lt;/li&gt;
&lt;li&gt;Lead scoring&lt;/li&gt;
&lt;li&gt;Intelligent reminder rules&lt;/li&gt;
&lt;li&gt;Bulk email workflows&lt;/li&gt;
&lt;li&gt;Interactive lead map visualization&lt;/li&gt;
&lt;li&gt;Dark mode support&lt;/li&gt;
&lt;li&gt;CSV export&lt;/li&gt;
&lt;li&gt;Activity timelines&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  One of the Hardest Challenges
&lt;/h2&gt;

&lt;p&gt;Natural language automation sounds simple until one sentence affects multiple systems.&lt;/p&gt;

&lt;p&gt;Example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;“Call James next week about paint delivery”
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single sentence impacts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;lead updates&lt;/li&gt;
&lt;li&gt;reminder creation&lt;/li&gt;
&lt;li&gt;scoring&lt;/li&gt;
&lt;li&gt;pipeline stages&lt;/li&gt;
&lt;li&gt;task scheduling&lt;/li&gt;
&lt;li&gt;timeline parsing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Designing the logic flow around conversational input became one of the most interesting parts of the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  Unexpected Favorite Feature
&lt;/h2&gt;

&lt;p&gt;The Nairobi lead map became one of my favorite parts of the build.&lt;/p&gt;

&lt;p&gt;Visualizing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;hot leads&lt;/li&gt;
&lt;li&gt;geographic clusters&lt;/li&gt;
&lt;li&gt;sales zones&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;…turned CRM data into something strategic instead of administrative.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Out
&lt;/h2&gt;

&lt;p&gt;Live Project: &lt;a href="https://app-bfop339bbf29.appmedo.com/" rel="noopener noreferrer"&gt;[Ghost-CRM]&lt;/a&gt;&lt;br&gt;
Try Out MeDo: &lt;a href="https://medo.dev" rel="noopener noreferrer"&gt;[medo.dev]&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Final Thought&lt;/p&gt;

&lt;p&gt;AI tools shouldn’t just generate content.&lt;/p&gt;

&lt;p&gt;They should remove friction from real workflows.&lt;/p&gt;

&lt;p&gt;Ghost Mode CRM was an experiment in making business software feel less like software — and more like a smart assistant quietly working in the background.&lt;/p&gt;

</description>
      <category>builtwithmedo</category>
      <category>ai</category>
      <category>productivity</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
