<?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: Mohammed Arshad Ansari</title>
    <description>The latest articles on DEV Community by Mohammed Arshad Ansari (@mohammed_arshadansari_f2).</description>
    <link>https://dev.to/mohammed_arshadansari_f2</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%2F3609964%2Fd0a642d8-0372-4976-99f8-0575aa92a93c.png</url>
      <title>DEV Community: Mohammed Arshad Ansari</title>
      <link>https://dev.to/mohammed_arshadansari_f2</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mohammed_arshadansari_f2"/>
    <language>en</language>
    <item>
      <title>I systematised sales like an engineer: 9 stages, one path, and a free 3-minute diagnostic</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 03 Aug 2026 05:15:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/i-systematised-sales-like-an-engineer-9-stages-one-path-and-a-free-3-minute-diagnostic-2e1o</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/i-systematised-sales-like-an-engineer-9-stages-one-path-and-a-free-3-minute-diagnostic-2e1o</guid>
      <description>&lt;p&gt;I learned sales the way an engineer would — by refusing to believe it can't be systematised.&lt;/p&gt;

&lt;p&gt;The knowledge existed: Godin on permission, Dunford on positioning, Hormozi on offers, Miller on message, Brunson on funnels, Voss on negotiation. Each held one piece. Nobody held the sequence.&lt;/p&gt;

&lt;p&gt;So I arranged it: nine stages, one path — the order a buyer actually travels.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Mindset → Market → Position → Offer → Message → Attention → Journey → Conversation → Scale.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The engineering insight that made it click: a sales system is a pipeline, and a pipeline is only as strong as its weakest stage. Most advice fails for a boring reason — it's the right fix for the wrong stage. Upstream problems masquerade as downstream ones: weak positioning looks like a lead problem, a missing journey looks like a closing problem. So people buy ads to fix positioning and sales training to fix funnels, and nothing moves.&lt;/p&gt;

&lt;p&gt;The debugging rule applies to revenue too: diagnose before you patch.&lt;/p&gt;

&lt;p&gt;I built a free 3-minute test that does the diagnosis — 18 questions, no email, no login. It names the exact stage costing you customers and gives you one concrete action for it today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;→ &lt;a href="https://www.strangertosold.com/quiz?utm_source=devto&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;Take the Sales Bottleneck Finder&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;(Based on my book, From Stranger to Sold.)&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>The most dangerous API response is HTTP 200 with an empty body</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Sun, 02 Aug 2026 19:02:07 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/the-most-dangerous-api-response-is-http-200-with-an-empty-body-3j8g</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/the-most-dangerous-api-response-is-http-200-with-an-empty-body-3j8g</guid>
      <description>&lt;p&gt;Every pipeline eventually inherits a source that moves. Ours did: &lt;code&gt;dataservices.imf.org&lt;/code&gt;&lt;br&gt;
stopped resolving in DNS entirely — not a 500, not a timeout, the hostname itself was gone.&lt;br&gt;
The IMF had migrated its data platform and the old host was on its way out. No villain in&lt;br&gt;
this story; following a publisher to its new home is the consumer's job.&lt;/p&gt;

&lt;p&gt;The interesting part is the failure mode the migration exposed, which has nothing to do with&lt;br&gt;
the IMF and everything to do with how most of us write ingestion code.&lt;/p&gt;
&lt;h2&gt;
  
  
  The symptom
&lt;/h2&gt;

&lt;p&gt;Monthly CPI for a set of non-OECD countries went quietly stale, frozen at December 2025, while&lt;br&gt;
every other source kept ticking. Nothing screamed. The daily job ran green. Downstream scoring&lt;br&gt;
kept computing on last-known values, because CPI resolves through a fallback chain — if the&lt;br&gt;
freshest monthly print is unavailable, the model reaches for the next-best source rather than&lt;br&gt;
failing outright.&lt;/p&gt;

&lt;p&gt;That fallback is a feature: one dead source should not take scoring down for a whole tier of&lt;br&gt;
countries. It also has a cost. &lt;strong&gt;Graceful degradation and silent staleness are the same&lt;br&gt;
mechanism viewed from two angles.&lt;/strong&gt; A freshness dashboard is what keeps the second angle&lt;br&gt;
visible; without one, the design that saves you also hides the problem from you.&lt;/p&gt;
&lt;h2&gt;
  
  
  The migration itself
&lt;/h2&gt;

&lt;p&gt;The new home is &lt;code&gt;api.imf.org&lt;/code&gt;, on SDMX 2.1, and it still speaks the same StructureSpecific XML&lt;br&gt;
dialect — so the parser barely changed. All the pain was in the &lt;em&gt;keys&lt;/em&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;CPI now lives on the &lt;code&gt;IMF.STA,CPI&lt;/code&gt; dataflow, and the all-items series is keyed with COICOP
&lt;code&gt;_T&lt;/code&gt; (the SDMX "total" convention), not the &lt;code&gt;CP00&lt;/code&gt; the old platform used.&lt;/li&gt;
&lt;li&gt;Balance-of-payments moved to &lt;code&gt;IMF.STA,BOP&lt;/code&gt;, where the legacy indicator codes do &lt;strong&gt;not&lt;/strong&gt; port
one-to-one. No find-and-replace; each series had to be re-derived against the new structure
definition and re-tested.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tedious rather than clever — and easy to get subtly wrong, because of what a wrong key does.&lt;/p&gt;
&lt;h2&gt;
  
  
  The gotcha worth stealing
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A bad key returns HTTP 200 with an empty &lt;code&gt;DataSet&lt;/code&gt;.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Not a 404. Not a 400. A cheerful &lt;code&gt;200 OK&lt;/code&gt;, a well-formed SDMX document, zero observations&lt;br&gt;
inside it. If your ingestion trusts the status line:&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;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&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="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="c1"&gt;# &amp;amp;lt;- the bug
&lt;/span&gt;    &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&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;store&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                &lt;span class="c1"&gt;# stores nothing, reports success
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;…then a typo in a dimension code, or an un-migrated series key, sails through as success and&lt;br&gt;
writes nothing. Job green. Data frozen. You find out weeks later from a freshness alert, if&lt;br&gt;
you have one, or from a customer, if you do not.&lt;/p&gt;

&lt;p&gt;Our fix is a &lt;strong&gt;canary&lt;/strong&gt;: the request always carries a segment we know must return data — United&lt;br&gt;
States all-items CPI — and a parse that yields zero series therefore cannot mean "no data".&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;series&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parse_sdmx21_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;xml_bytes&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="n"&gt;series&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;IMFIFSUnavailableError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;IMF.STA,CPI returned an empty DataSet incl. the USA canary — &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;key schema change or platform fault&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The USA rows are then filtered &lt;em&gt;out&lt;/em&gt; before storage — its index base differs from our other&lt;br&gt;
US source and double-sourcing would corrupt the derived series. The canary lives in the&lt;br&gt;
request, not in the warehouse.&lt;/p&gt;

&lt;p&gt;Three lines, and it is the difference between finding a structural break in one run versus one&lt;br&gt;
month. Having built it for CPI, it generalises to every SDMX-style source that can return a&lt;br&gt;
well-formed empty response: pick a segment that must exist, assert it came back populated, let&lt;br&gt;
a broken key contract throw. Note what it is &lt;em&gt;not&lt;/em&gt; for: a genuine upstream outage is a&lt;br&gt;
different event, and that one deliberately does &lt;strong&gt;not&lt;/strong&gt; raise — it materialises with an&lt;br&gt;
&lt;code&gt;imf_ifs_outage&lt;/code&gt; flag, because failing hard there would skip every downstream scoring asset in&lt;br&gt;
the same run.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same bug one layer up: a fetch that succeeds is not a document
&lt;/h2&gt;

&lt;p&gt;The identical mistake, in a different shape, was sitting in our central-bank statement&lt;br&gt;
scrapers. They fetched a listing page, followed a link, extracted text, and handed it to an&lt;br&gt;
LLM for a hawkish/dovish read. Every step returned 200. Every step "worked".&lt;/p&gt;

&lt;p&gt;What was actually being scored, once we read the stored text back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a bank's login form, because its statement listing redirects to &lt;code&gt;/login&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;a press-release index whose top headline was Treasury-bill auction results&lt;/li&gt;
&lt;li&gt;a SharePoint "you may be trying to access this site from a secured browser" notice&lt;/li&gt;
&lt;li&gt;115 characters of "this page depends on JavaScript"&lt;/li&gt;
&lt;li&gt;a rates &lt;em&gt;statistics&lt;/em&gt; nav link, because the link pattern &lt;code&gt;interest.*rate&lt;/code&gt; matched it and it
sorted first in the DOM&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The parallel to the empty &lt;code&gt;DataSet&lt;/code&gt; is exact: nothing failed, so nothing raised, so a&lt;br&gt;
plausible-looking value went downstream. The fixes were the same shape as the canary —&lt;br&gt;
assert the &lt;em&gt;content&lt;/em&gt;, not the transport:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;A length floor and a bot/JS-notice check, so a page that cannot be a statement is skipped
rather than scored.&lt;/li&gt;
&lt;li&gt;Feeds and APIs instead of HTML scraping where the bank publishes one (RSS/Atom, or the
PDF-minutes API where the web page is a JS shell).&lt;/li&gt;
&lt;li&gt;A source is only enabled once its extracted text has been read back and confirmed to be a
monetary policy decision — and a source that cannot pass is switched off &lt;em&gt;in config with
the reason recorded&lt;/em&gt;, not left nominally "covered". Ten of seventeen configured banks are
on; seven are off behind a login wall, a client-rendered listing, or commercial bot
management we decline to work around.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Two reusable lessons
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Assert data presence, not status codes.&lt;/strong&gt; &lt;code&gt;200 OK&lt;/code&gt; means the HTTP conversation
succeeded. It says nothing about whether the payload contains what you asked for.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Connection outages and silent-empty responses are different failure modes needing
different handling.&lt;/strong&gt; A DNS failure throws — retry with backoff and you will know. A
200-plus-empty never throws, so &lt;code&gt;try/except&lt;/code&gt; + retry leaves it completely uncovered. It
needs an affirmative presence check. Handle both, separately, on purpose.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The payoff, incidentally, was not just damage control: following the source to its new home&lt;br&gt;
took BOP coverage from roughly 70 to 148 countries and made reserves available quarterly. The&lt;br&gt;
endpoint you are reluctantly forced onto is often the one the publisher is actually investing&lt;br&gt;
in.&lt;/p&gt;

&lt;p&gt;Full write-up, with the migration detail:&lt;/p&gt;

&lt;p&gt;What the pipeline feeds — a daily 0–100 credibility score for 169 countries on 100% free&lt;br&gt;
public data, with per-indicator provenance:&lt;/p&gt;




</description>
      <category>api</category>
      <category>backend</category>
      <category>monitoring</category>
    </item>
    <item>
      <title>I open-sourced AEGIS: a self-hosted, flow-first personal AI orchestration platform</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 13 Jul 2026 15:26:15 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/i-open-sourced-aegis-a-self-hosted-flow-first-personal-ai-orchestration-platform-4c74</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/i-open-sourced-aegis-a-self-hosted-flow-first-personal-ai-orchestration-platform-4c74</guid>
      <description>&lt;p&gt;For the past year I've run most of my day on a system I built for exactly one user: me. Last week I open-sourced it. It's called AEGIS, it's MIT-licensed, and this is the honest tour.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bet
&lt;/h2&gt;

&lt;p&gt;Every week there's a new agent framework that promises to do everything. AEGIS is a smaller, stranger bet: that software can learn the shape of one person's life well enough to interrupt &lt;em&gt;less&lt;/em&gt;. It watches the boring things — tasks, email, money, a&lt;br&gt;
  knowledge base, homelab alerts — and only reaches for me when a decision is genuinely mine to make. It's not a chatbot I log into; it's a fleet of scheduled and event-driven workflows that mostly run without me.&lt;/p&gt;

&lt;p&gt;## The shape&lt;/p&gt;

&lt;p&gt;Four named agents, each a permission boundary with a personality: Sebas (GTD), Raphael (research), Maou (money), Pandora's Actor (infrastructure). The spine is FastAPI + Postgres (with pgvector) + Temporal, on a small Docker Swarm at home. Models&lt;br&gt;
  resolve through a LiteLLM proxy — local-first, reaching for Claude or GPT only when a job needs the horsepower.&lt;/p&gt;

&lt;p&gt;A few design decisions did most of the work.&lt;/p&gt;

&lt;p&gt;## One primitive for every interruption&lt;/p&gt;

&lt;p&gt;The decision I'm proudest of is a table. Every time the system needs a human, it's the same shape: a row in a Postgres &lt;code&gt;interactions&lt;/code&gt; table, a card in my chat app, and a Temporal workflow that durably waits — for days if it has to — until I tap a&lt;br&gt;
  button.&lt;/p&gt;

&lt;p&gt;Approvals, choices, drafts to review, plain acknowledgements — one mechanism, five card kinds, one callback format. No per-feature approval tables; adding a new "ask the human" moment costs nothing. And because interrupting me is now a formal act with a&lt;br&gt;
  paper trail, flows get written to do more work before they ask. That one decision turned AEGIS from a notification machine into a queue of interruptions that have to earn their way in.&lt;/p&gt;

&lt;p&gt;## Durability instead of cron-and-hope&lt;/p&gt;

&lt;p&gt;A card a workflow waits on for three days is miserable to build with cron and a queue — you hand-roll a state machine and reconcile it after every deploy. Temporal's durable execution is exactly this: the workflow awaits a signal, and the wait survives&lt;br&gt;
  restarts, redeploys, and the occasional node reboot. Schedules reconcile from DB config, so changing a flow's cadence needs no redeploy.&lt;/p&gt;

&lt;p&gt;## Behavior is data, not code&lt;/p&gt;

&lt;p&gt;The change that made AEGIS forkable was deleting every line that said &lt;code&gt;if agent == "sebas"&lt;/code&gt;. Capabilities, tool grants, and routing now live in database metadata, edited from an admin panel. The code asks "who owns GTD?" and gets an answer; it never&lt;br&gt;
  names names. Rename the agents, re-scope them, or add your own — no Python.&lt;/p&gt;

&lt;p&gt;## Local-LLM-first, for real&lt;/p&gt;

&lt;p&gt;Everything routes through a LiteLLM proxy exposing three tiers — fast / balanced / smart. Each agent is assigned a &lt;em&gt;tier&lt;/em&gt;, never a model name, so swapping models is proxy config and the app code never changes. One reasoning-model gotcha is handled&lt;br&gt;
  explicitly: reasoning models bill hidden reasoning tokens against &lt;code&gt;max_tokens&lt;/code&gt; before any visible output, so a tight cap returns &lt;code&gt;finish_reason=length&lt;/code&gt; with empty content — the client detects that and raises a typed truncation error instead of handing&lt;br&gt;
  an empty string to &lt;code&gt;json.loads&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;## What it is not&lt;/p&gt;

&lt;p&gt;Not a SaaS — no hosted version, and it does nothing until you point it at your own accounts and models. Not another framework to build on; it's a complete, opinionated application you fork and configure for your own life. If that sounds like more setup&lt;br&gt;
  than you want, reading the code is a perfectly good outcome.&lt;/p&gt;

&lt;p&gt;## Take it apart&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/hikmahtech/aegis" rel="noopener noreferrer"&gt;https://github.com/hikmahtech/aegis&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;The longer tour + design essays: &lt;a href="https://hikmahtechnologies.com/aegis" rel="noopener noreferrer"&gt;https://hikmahtechnologies.com/aegis&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I'd genuinely like to know what breaks — and what you'd reach for on the "smart" tier these days. That's the slot I still escalate most often.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>selfhosted</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
