<?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>The most dangerous API response is HTTP 200 with an empty body</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Tue, 15 Sep 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/the-most-dangerous-api-response-is-http-200-with-an-empty-body-40c2</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/the-most-dangerous-api-response-is-http-200-with-an-empty-body-40c2</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". Fifteen of twenty configured banks are
on; five are off behind commercial bot management, a client-rendered shell, hard 403s on
the decision pages, or a listing API that refuses anonymous reads — protections 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>python</category>
      <category>dataengineering</category>
      <category>api</category>
      <category>programming</category>
    </item>
    <item>
      <title>DuckDB in production: what it's actually good at (and what it isn't)</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Mon, 14 Sep 2026 14:00:01 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/duckdb-in-production-what-its-actually-good-at-and-what-it-isnt-9c</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/duckdb-in-production-what-its-actually-good-at-and-what-it-isnt-9c</guid>
      <description>&lt;p&gt;DuckDB is having a moment, and like every tool having a moment, the hype runs ahead of the nuance. "Just use DuckDB" is now a reflex answer to questions it doesn't actually fit. So let me give you the honest version, from someone who ships it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DuckDB is
&lt;/h2&gt;

&lt;p&gt;DuckDB is an in-process analytical (OLAP) database — think "SQLite for analytics." It runs a columnar, vectorized query engine inside your application process. No server, no network hop, no cluster. You point it at Parquet, CSV, or its own format and run real SQL — window functions, joins, the works — at speeds that embarrass a round-trip to a cloud warehouse for the same-size data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Is DuckDB production-ready? Do people actually run it?
&lt;/h2&gt;

&lt;p&gt;Yes — with a shape attached, and the shape is the entire answer.&lt;/p&gt;

&lt;p&gt;"Production-ready" doesn't mean "safe to drop in anywhere." It means the engine is stable, the file format is stable, and the failure modes are known and boring. DuckDB clears that bar. It's past 1.0, it's MIT-licensed, and there's a foundation and a company behind it rather than one maintainer with a day job. In the systems I run it has never lost data or quietly returned a wrong answer. The bugs have all been in my SQL.&lt;/p&gt;

&lt;p&gt;What people are usually asking is the less polite version: &lt;em&gt;do serious teams run this, or am I the guinea pig?&lt;/em&gt; They run it — mostly in a shape you can't see from outside. DuckDB rarely appears on an architecture diagram as "our warehouse." It appears inside a batch job, a service, a CLI, a notebook that runs in CI. It's an engine, not a platform, so it hides inside things, which is why real usage is far more common than the blog posts about it.&lt;/p&gt;

&lt;p&gt;The caveat worth internalising: production-readiness here is a property of &lt;strong&gt;the system you build around it&lt;/strong&gt;, not of the database. A warehouse hands you concurrency, access control and a serving layer in the box. DuckDB hands you a query engine and expects you to bring the rest. Bring it, and it's production-ready. Expect it included, and you'll conclude the tool is immature when what's missing is your service layer.&lt;/p&gt;




&lt;p&gt;This is the first part. The full post — including the rest of the working details — is on my site: &lt;a href="https://hikmahtechnologies.com/blog/duckdb-in-production-what-its-good-at/?utm_source=devto&amp;amp;utm_medium=syndication&amp;amp;utm_campaign=blog" rel="noopener noreferrer"&gt;DuckDB in production: what it's actually good at (and what it isn't)&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>database</category>
      <category>sql</category>
    </item>
    <item>
      <title>We gave our macro dataset an MCP server (and made every number show its source)</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Fri, 11 Sep 2026 10:30:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/we-gave-our-macro-dataset-an-mcp-server-and-made-every-number-show-its-source-5cmd</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/we-gave-our-macro-dataset-an-mcp-server-and-made-every-number-show-its-source-5cmd</guid>
      <description>&lt;p&gt;"Trust the data" is easy to say and hard to check. Two things we shipped to make it checkable&lt;br&gt;
— by people, and by the agents they increasingly send to read the web for them.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Every number prints its source
&lt;/h2&gt;

&lt;p&gt;We publish a daily 0–100 sovereign credibility score for 169 countries, composed of seven&lt;br&gt;
dimensions, built entirely on free public data (World Bank, IMF, BIS, FRED, OECD, ILO, GDELT,&lt;br&gt;
ECB). Each dimension on a country page now expands to show &lt;strong&gt;the source that produced the&lt;br&gt;
reading, the as-of date of the underlying data, and the coverage tier behind it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Turkey's page, right now:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Credibility gap   40.0   IMF / national CPI          as of 2026-06-01   gold
Behind the curve  78.7   BIS policy rates + CPI      as of 2026-08-14   gold
Communication     54.5   Central-bank statements     as of 2026-08-26   gold
Geopolitical      44.5   GDELT                       as of 2026-08-26   gold
Growth            37.3   World Bank WDI / IMF WEO    as of 2026-08-26   gold
Liquidity         43.1   IMF BOP / BIS / FRED        as of 2026-08-26   gold
Governance        37.5   World Bank WGI              as of 2026-08-26   gold
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Note the first row. The credibility gap is dated almost three months back, because that is&lt;br&gt;
genuinely the last monthly CPI print we hold for Turkey. Printing the as-of date is the whole&lt;br&gt;
point: a number you cannot date is a number you cannot check. No client-side JS is involved —&lt;br&gt;
it is a plain expandable block, so it works everywhere and loads instantly.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A public MCP server
&lt;/h2&gt;

&lt;p&gt;The scores are now readable by agents directly, over the Model Context Protocol, at&lt;br&gt;
&lt;code&gt;https://www.quantamentry.com/api/mcp&lt;/code&gt; — JSON-RPC 2.0 over HTTP POST, read-only. &lt;code&gt;GET&lt;/code&gt; the&lt;br&gt;
same URL for a plain descriptor; &lt;code&gt;POST&lt;/code&gt; to use it. Point an MCP-capable client at it and it&lt;br&gt;
queries the data without scraping a single HTML page.&lt;/p&gt;

&lt;p&gt;Three tools are free with no key — &lt;code&gt;list_countries&lt;/code&gt; (rank, region, credibility band),&lt;br&gt;
&lt;code&gt;get_country&lt;/code&gt; (one country's composite score, band, regional rank, 7-day move and one-line&lt;br&gt;
read) and &lt;code&gt;get_latest_briefing&lt;/code&gt; — the same surface as the public pages, at 30 requests/min&lt;br&gt;
per IP. An API key, sent as &lt;code&gt;Authorization: Bearer qm_…&lt;/code&gt;, swaps in the full set at 60&lt;br&gt;
requests/min per key:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tool&lt;/th&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_country_score&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current composite + all seven dimensions, with per-dimension provenance and recent deltas&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;list_rankings&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;countries ranked by composite, optionally filtered by region&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_score_history&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;90-day daily composite history for one country&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_coverage&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;per-source freshness and per-tier country counts&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;get_methodology&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;current methodology version and what the score is (and isn't)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;compare_countries&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;2–6 countries side by side across all seven dimensions&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Implementation notes for anyone standing one up next to an existing web app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It is a &lt;strong&gt;route in the same Next.js app&lt;/strong&gt;, not a separate service. One POST handler doing
&lt;code&gt;initialize&lt;/code&gt; / &lt;code&gt;tools/list&lt;/code&gt; / &lt;code&gt;tools/call&lt;/code&gt;, tool schemas in one module, backed by the same
API the pages read. Every tool serves only what the same caller could already open on the
site — the free tools mirror the public pages, the keyed tools mirror a subscription — which
is what makes the tiering defensible rather than a risk. An invalid or lapsed key degrades
to the free tier instead of erroring.&lt;/li&gt;
&lt;li&gt;Read-only by construction. No tool writes, so there is no consent model to design, no
destructive-action confirmation, and the rate limits are the entire abuse surface.&lt;/li&gt;
&lt;li&gt;Version the &lt;em&gt;methodology&lt;/em&gt;, not just the API. Agents cache; &lt;code&gt;get_methodology&lt;/code&gt; returning a
version string is how a consumer detects that the formula moved under them. Ours is on v4
and the site carries a dated changelog.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. The rest of the machine-readable surface
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;/llms.txt&lt;/code&gt; — the whole site summarised for models that ground on it.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/pricing.md&lt;/code&gt; — the pricing page as plain Markdown.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/api/product-catalog&lt;/code&gt; — the same tiers as JSON.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last two are generated from one source of truth, so the JSON an agent reads cannot drift&lt;br&gt;
from what a human sees on the page. That is a small thing that turns out to matter: a&lt;br&gt;
machine-readable surface that disagrees with the human one is worse than not having it.&lt;/p&gt;

&lt;p&gt;None of this is a roadmap item — the endpoints are live. Try it:&lt;br&gt;
 (POST) or start from&lt;/p&gt;

&lt;p&gt;Full write-up:&lt;/p&gt;

&lt;p&gt;How each dimension is built and sourced:&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>api</category>
      <category>showdev</category>
    </item>
    <item>
      <title>Your website gets traffic but no sales. The leak is after the click.</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Fri, 04 Sep 2026 19:50:40 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/your-website-gets-traffic-but-no-sales-the-leak-is-after-the-click-23b9</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/your-website-gets-traffic-but-no-sales-the-leak-is-after-the-click-23b9</guid>
      <description>&lt;p&gt;Traffic with no sales feels like a traffic problem, so the instinct is to get more of it. But a hundred visitors who all leave without buying behave exactly like a thousand visitors who all leave without buying. Volume does not fix a leak — it runs more water past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, be honest about the word "traffic"
&lt;/h2&gt;

&lt;p&gt;If most of your visitors are peers, friends, and people who sell what you sell — which is what a launch post usually produces — you do not have traffic, you have applause. Applause does not convert. If genuine potential buyers are landing and still leaving, the leak is on the page, in one of three places. They fail in a fixed order; check them in that order.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 1 — The message
&lt;/h2&gt;

&lt;p&gt;Run the five-second test. Show your homepage to a stranger for five seconds, close it, and ask: What do they sell? Who is it for? What would I get? If they hesitate on any of the three, visitors are hesitating too. The most common failure is not ugliness — it is cleverness. "Unleash your potential" tells a buyer nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 2 — The offer
&lt;/h2&gt;

&lt;p&gt;If strangers understand the page but do not buy, look at what you are asking them to buy. An offer that reads like every competitor forces comparison on the only dimension left: price. And on price, someone is always cheaper. You do not fix this with a discount — a discount on an uncompelling offer is a cheaper uncompelling offer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check 3 — The next step
&lt;/h2&gt;

&lt;p&gt;The quiet leak almost nobody checks: a visitor is interested but not ready to buy today — which describes most visitors. What do you offer that person? If the answer is "the buy button or nothing", you convert only the tiny fraction who arrived pre-sold and silently lose everyone your message actually persuaded. Interest that has nowhere to go expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  The false fixes
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Redesign the site:&lt;/strong&gt; a confusing page in a prettier coat is still confusing. &lt;strong&gt;Run ads to it:&lt;/strong&gt; paying to send more strangers past the same leak converts budget into silence, faster. &lt;strong&gt;Add more sections:&lt;/strong&gt; length is not persuasion.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published as a &lt;a href="https://www.strangertosold.com/learn/traffic-but-no-sales?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;field note&lt;/a&gt; at strangertosold.com. The free &lt;a href="https://www.strangertosold.com/quiz?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;3-minute Sales Bottleneck Finder&lt;/a&gt; — 18 questions, no email — names the stage of your sales system where strangers are getting lost.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Why is no one signing up for your SaaS? Four leaks, in order</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Wed, 12 Aug 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/why-is-no-one-signing-up-for-your-saas-four-leaks-in-order-4c6d</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/why-is-no-one-signing-up-for-your-saas-four-leaks-in-order-4c6d</guid>
      <description>&lt;p&gt;You shipped. The product works. The Show HN post got a few upvotes — and the signups chart is a flat line.&lt;/p&gt;

&lt;p&gt;The reflex is to conclude the product is not good enough yet and build more of it. That reflex is almost always wrong, and expensive, because building is the one activity that feels like progress while changing nothing. No-signups has four usual causes, and they must be checked &lt;strong&gt;in order&lt;/strong&gt; — a fix downstream is invisible while an upstream leak is open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak 1 — Nobody with the problem is arriving
&lt;/h2&gt;

&lt;p&gt;How many people who plausibly &lt;em&gt;have the problem your product solves&lt;/em&gt; saw your page last week? Not peers, not launch-day tourists — potential users. For most SaaS with zero signups the honest answer is "a handful", and no page converts a handful of anybody. If that is you, stop optimising the page and go where the problem is discussed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak 2 — It is built for "anyone"
&lt;/h2&gt;

&lt;p&gt;"A project management tool for teams" selects nobody. When a product is for everyone, no visitor feels it is for &lt;em&gt;them&lt;/em&gt;. A product for "developers" loses to a product for "freelance mobile developers who bill hourly" every time the second one shows up — even if yours is technically better. Narrowing is not shrinking your market; it is choosing the smallest market you can actually win.&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak 3 — No answer to "why you specifically?"
&lt;/h2&gt;

&lt;p&gt;A visitor who has the problem and feels selected still asks one silent question: &lt;em&gt;why this one and not the incumbent I have already heard of?&lt;/em&gt; If your page describes you as a member of a category ("a modern CRM"), the visitor fills in the answer themselves — usually "no reason".&lt;/p&gt;

&lt;h2&gt;
  
  
  Leak 4 — The page talks about the product, not the visitor
&lt;/h2&gt;

&lt;p&gt;Features are evidence, not argument. The visitor is asking "what does my Tuesday look like after I sign up?" and the page is answering "we use vector search". First sentence: their problem. Second: the outcome. Features prove it lower down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-minute test
&lt;/h2&gt;

&lt;p&gt;Write one sentence: &lt;em&gt;"[Product] is for [the narrowest group you would bet on] who [specific situation], so they can [outcome they would pay for]."&lt;/em&gt; If you cannot fill it in without "anyone", "teams", or "businesses" — that is your leak, and no amount of traffic fixes it before the sentence does.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published as a &lt;a href="https://www.strangertosold.com/learn/saas-no-signups?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;field note&lt;/a&gt; at strangertosold.com. The free &lt;a href="https://www.strangertosold.com/quiz?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;3-minute Sales Bottleneck Finder&lt;/a&gt; — 18 questions, no email — names which of the nine stages of your sales system is actually blocking you.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>marketing</category>
      <category>saas</category>
      <category>startup</category>
    </item>
    <item>
      <title>"I can build, but nobody buys": the technical founder-2019s sales problem</title>
      <dc:creator>Mohammed Arshad Ansari</dc:creator>
      <pubDate>Sat, 08 Aug 2026 08:00:00 +0000</pubDate>
      <link>https://dev.to/mohammed_arshadansari_f2/i-can-build-but-nobody-buys-the-technical-founderu2019s-sales-problem-542d</link>
      <guid>https://dev.to/mohammed_arshadansari_f2/i-can-build-but-nobody-buys-the-technical-founderu2019s-sales-problem-542d</guid>
      <description>&lt;p&gt;&lt;strong&gt;"I can build, but nobody buys."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That sentence is the defining problem of the AI era for technical people. Building used to be the moat: if you could ship, you were rare. Now anyone can ship a working product in a weekend — which means the bottleneck has moved, for everyone, to the same place: strangers becoming customers. And most of us never trained for that at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  It is not a personality flaw
&lt;/h2&gt;

&lt;p&gt;Technical people tell themselves a specific story: &lt;em&gt;I am not a salesperson. Not pushy enough, smooth enough, extroverted enough.&lt;/em&gt; The story survives because it is flattering — it frames the gap as character rather than skill, and you cannot be blamed for your character. But watch what it protects: as long as selling is a personality you were not born with, you are excused from learning it, and free to do the thing that feels productive instead. Which is build more.&lt;/p&gt;

&lt;p&gt;The reframe that changes everything: the pushy salesperson is not the skilled version of selling — it is the &lt;em&gt;failed&lt;/em&gt; version. Pushing is what people do when they have skipped every earlier step and are trying to force a stranger through a wall. Skilled selling is closer to what you already do when you debug: observe, hypothesise, test, fix the actual fault.&lt;/p&gt;

&lt;h2&gt;
  
  
  "One more feature" is procrastination with a clean conscience
&lt;/h2&gt;

&lt;p&gt;When the product is not selling, building feels like the responsible response. It has visible output, it is within your control, and nobody rejects you while you do it. That last property is the tell. Adding a feature is the only response to "nobody buys" that involves zero contact with the people not buying — which is why it is the most popular and the least effective. The information you are missing is not in your codebase. It is in their heads, and there is no API for that.&lt;/p&gt;

&lt;h2&gt;
  
  
  What selling actually is
&lt;/h2&gt;

&lt;p&gt;A stranger becomes a customer along a path with fixed stages: they have to want something (market), see why you specifically (position), find the trade obviously worth it (offer), understand you instantly (message), encounter you at all (attention), have a route from curious to committed (journey), and be listened to rather than pitched at (conversation). None of those requires charisma. Every one can be diagnosed.&lt;/p&gt;

&lt;p&gt;The trap: the stages fail silently and point the blame downstream. Weak positioning presents as "a marketing problem". A missing journey presents as "bad luck with conversions". So builders fix the visible symptom while the actual fault sits untouched two stages earlier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do this today
&lt;/h2&gt;

&lt;p&gt;Write down, as a falsifiable statement, why you believe people are not buying — the way you would write a bug hypothesis. If you cannot finish the sentence with something you have tested against real potential buyers, that is the diagnosis: you are debugging without logs. Your next unit of work is not code — it is five conversations.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Originally published as a &lt;a href="https://www.strangertosold.com/learn/built-it-nobody-buys?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;field note&lt;/a&gt; at strangertosold.com. If you want logs instead of theory: the free &lt;a href="https://www.strangertosold.com/quiz?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=fieldnotes" rel="noopener noreferrer"&gt;3-minute Sales Bottleneck Finder&lt;/a&gt; — 18 questions, no email — names the weakest of the nine stages in your sales system.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>software</category>
      <category>startup</category>
    </item>
    <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>
