<?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: DatanestDigital</title>
    <description>The latest articles on DEV Community by DatanestDigital (@datanestdigital).</description>
    <link>https://dev.to/datanestdigital</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%2F3835307%2Fdb509a94-7063-4ef5-90da-159d2182eef6.png</url>
      <title>DEV Community: DatanestDigital</title>
      <link>https://dev.to/datanestdigital</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/datanestdigital"/>
    <language>en</language>
    <item>
      <title>Data Contracts: Taming Schema Drift Before It Breaks Prod</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:42:06 +0000</pubDate>
      <link>https://dev.to/datanestdigital/data-contracts-taming-schema-drift-before-it-breaks-prod-fg8</link>
      <guid>https://dev.to/datanestdigital/data-contracts-taming-schema-drift-before-it-breaks-prod-fg8</guid>
      <description>&lt;p&gt;The most expensive bugs in data engineering are the ones that don't error. A column gets renamed, a type gets widened, a null starts showing up where the business said "never null" — and for weeks, nothing crashes. The dashboard just quietly drifts. Then one Tuesday, the report is missing a month, and suddenly a "small schema change" from April is your incident.&lt;/p&gt;

&lt;p&gt;That's schema drift, and it's the difference between pipelines that feel stable and ones that feel haunted.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a data contract actually is
&lt;/h2&gt;

&lt;p&gt;A data contract is the API contract idea applied to data. When you change a REST endpoint you version it, document it, and give consumers a migration path. Data rarely gets that courtesy — producers change tables and consumers find out in prod.&lt;/p&gt;

&lt;p&gt;A contract makes the producer's promise explicit. The useful ones carry four things:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;What it answers&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Schema&lt;/td&gt;
&lt;td&gt;What fields exist, their types, nullability, allowed values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quality rules&lt;/td&gt;
&lt;td&gt;Completeness, uniqueness, range checks, accepted values&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Freshness / SLA&lt;/td&gt;
&lt;td&gt;How often data updates and by when&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ownership&lt;/td&gt;
&lt;td&gt;Who produces, who consumes, who gets paged&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The point isn't the document. It's that a change to the data now has a reviewable surface instead of being an invisible commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Schema evolution vs breaking changes
&lt;/h2&gt;

&lt;p&gt;The core skill is telling safe changes from breaking ones. A pragmatic, semver-flavored split:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Example&lt;/th&gt;
&lt;th&gt;Verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Additive&lt;/td&gt;
&lt;td&gt;New nullable column&lt;/td&gt;
&lt;td&gt;Usually safe (minor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Additive&lt;/td&gt;
&lt;td&gt;New optional field in JSON&lt;/td&gt;
&lt;td&gt;Usually safe (minor)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Widening&lt;/td&gt;
&lt;td&gt;INT → BIGINT&lt;/td&gt;
&lt;td&gt;Safe if consumers handle it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Rename or drop a column&lt;/td&gt;
&lt;td&gt;Breaking (major)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Type change, tighter nullability&lt;/td&gt;
&lt;td&gt;Breaking (major)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Breaking&lt;/td&gt;
&lt;td&gt;Reorder columns for positional reads&lt;/td&gt;
&lt;td&gt;Breaking (major)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The subtle one: Spark, Delta Lake, and most modern engines read by name, not position. Renaming a column doesn't fail — the old name just resolves to nothing, so consumers get NULLs instead of errors. A broken contract that fails loudly in CI is a gift; the same break discovered by a dashboard is a fire.&lt;/p&gt;

&lt;h2&gt;
  
  
  Versioning contracts
&lt;/h2&gt;

&lt;p&gt;Put a version on the contract and treat it like an API version. Bump the minor for additive changes, the major for breaking ones. Consumers pin to a major version, and a major bump becomes a coordinated, visible event — not a surprise in the morning batch.&lt;/p&gt;

&lt;p&gt;Versioning buys you the most important thing: a conversation. "We're moving to v3 of &lt;code&gt;orders&lt;/code&gt;" beats "why is my report empty?"&lt;/p&gt;

&lt;h2&gt;
  
  
  Enforce it in CI
&lt;/h2&gt;

&lt;p&gt;A contract nobody checks is a wish. Enforcement belongs in the pipeline's CI, before anything lands in prod. The lightweight shape:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Keep the contract next to the producer code&lt;/li&gt;
&lt;li&gt;On each PR, diff the proposed schema against the current contract&lt;/li&gt;
&lt;li&gt;Fail the build on breaking changes unless the major version was bumped&lt;/li&gt;
&lt;li&gt;Run a sample of real data through the quality rules&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A minimal check in Python:&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;assert_compatible&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fail on breaking changes unless the major version bumps.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;major&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;major&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;old_names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&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;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;old_names&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nullable&lt;/span&gt;&lt;span class="sh"&gt;"&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;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Breaking change: non-nullable &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; added &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;without a major version bump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&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;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fields&lt;/span&gt;&lt;span class="sh"&gt;"&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;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Breaking change: field &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;name&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; removed &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;without a major version bump&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;contract compatible&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;Crude, but the shape is right: small, scriptable, and impossible to skip once it's a CI step.&lt;/p&gt;

&lt;h2&gt;
  
  
  Producers and consumers
&lt;/h2&gt;

&lt;p&gt;Contracts also fix a social problem. Most teams have no roster of who consumes a table — "I wonder who uses this" is a real production skill. Put the consumer list in the contract, and a schema change becomes a notification instead of an archaeology project. Producers own the contract; consumers get pinged on change; the contract is where the two sides negotiate instead of discovering each other's needs in prod.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling landscape (honestly)
&lt;/h2&gt;

&lt;p&gt;You don't need a vendor to start. The honest landscape:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Quality testing&lt;/strong&gt;: dbt tests, Great Expectations, Soda — mature, good at row-level checks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Schema/serialization&lt;/strong&gt;: JSON Schema, Pydantic, Avro — good for validating shapes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-native&lt;/strong&gt;: Delta Live Tables expectations, Unity Catalog — good if you're on Databricks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract platforms&lt;/strong&gt;: data-contract-spec and managed tools — powerful once you have many teams; overkill at the start&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with a YAML contract and a CI script. Buy the platform when coordination hurts more than the price.&lt;/p&gt;

&lt;h2&gt;
  
  
  A lightweight contract you can adopt today
&lt;/h2&gt;

&lt;p&gt;Something you can write before lunch:&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="na"&gt;dataset&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;prod.sales.orders&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;1.2.0&lt;/span&gt;
&lt;span class="na"&gt;owner&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;billing-team&lt;/span&gt;
&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;fields&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;order_id&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;total&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;decimal&lt;/span&gt;
      &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;note&lt;/span&gt;
      &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;string&lt;/span&gt;
      &lt;span class="na"&gt;nullable&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="na"&gt;quality&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;total &amp;gt;= &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;order_id is unique&lt;/span&gt;
&lt;span class="na"&gt;freshness&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;sla_hours&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
&lt;span class="na"&gt;consumers&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;analytics&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;finance-recon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Check it into the producer repo, wire the diff check into CI, and you've got drift insurance for the cost of a YAML file.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go deeper
&lt;/h2&gt;

&lt;p&gt;If you're on Databricks or Spark, there's ready-made tooling for exactly this. The &lt;a href="https://datanest-stores.com/data-engineering/" rel="noopener noreferrer"&gt;DataStack Pro store&lt;/a&gt; carries a Schema Evolution Toolkit that detects, validates, migrates, and analyzes schema changes across Delta Lake tables automatically, plus a Data Quality Framework with pluggable completeness, accuracy, consistency, and timeliness checks, a Data Pipeline Testing Kit, and PySpark utilities covering schema evolution and lineage. For the heavier end — a full contract implementation with YAML spec, CLI generator, schema validation, SLA monitoring, and breaking-change detection — the &lt;a href="https://datanest-stores.com/datanest-platform-pro/" rel="noopener noreferrer"&gt;Data Contract Framework&lt;/a&gt; covers it.&lt;/p&gt;

&lt;p&gt;The Data Engineering Bundle puts all 17 DataStack tools in one purchase for $199 instead of $523 separately.&lt;/p&gt;

&lt;p&gt;Schema drift is a process problem, not a code problem. A contract, a version, and a CI check turn "who broke the data?" into "here's the change, here's who it affects." That's a much better way to spend a Tuesday.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://datanest-stores.com/data-engineering/" rel="noopener noreferrer"&gt;Browse the DataStack Pro catalog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>dataquality</category>
      <category>sql</category>
      <category>devops</category>
    </item>
    <item>
      <title>PySpark Performance Tuning: A Checklist You Can Actually Follow</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:42:05 +0000</pubDate>
      <link>https://dev.to/datanestdigital/pyspark-performance-tuning-a-checklist-you-can-actually-follow-102</link>
      <guid>https://dev.to/datanestdigital/pyspark-performance-tuning-a-checklist-you-can-actually-follow-102</guid>
      <description>&lt;p&gt;Most slow Spark jobs aren't slow because Spark is slow. The data landed badly, the shuffle is enormous, or the physical plan is doing something expensive nobody asked for. The good news: the usual-suspect list is short, and you can work through it in order.&lt;/p&gt;

&lt;p&gt;This is a checklist, not a theory lecture. Every section says what to look at, how to look at it, and when it's worth your time. No fake benchmark tables here — performance claims are deliberate ranges, because your data, cluster, and budget differ. Treat everything as a hypothesis and measure on your own workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  0. Read the plan before you touch a knob
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;df.explain("cost")&lt;/code&gt; shows the physical plan: which joins Spark picked, where the &lt;code&gt;Exchange&lt;/code&gt; nodes (shuffles) are, and what Catalyst decided to do with your query. The Spark UI's SQL tab shows the same thing per stage, with timings and row counts.&lt;/p&gt;

&lt;p&gt;If you don't know what Spark is actually doing, every tuning decision is a guess. A &lt;code&gt;SortMergeJoin&lt;/code&gt; where you expected a broadcast, or a shuffle between two trivial filters, is the kind of thing the plan shows in ten seconds and profiling takes an hour to find.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Partitioning and skew
&lt;/h2&gt;

&lt;p&gt;Two separate problems hide under "partitioning":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Too few or too many shuffle partitions.&lt;/strong&gt; The default is 200 (&lt;code&gt;spark.sql.shuffle.partitions&lt;/code&gt;), which is wrong for almost every job — too many for a small cluster, too few for a big one. A common starting point is 2-4 partitions per executor core, but that's a heuristic, not a law. Measure stage times, then adjust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Skew.&lt;/strong&gt; One task runs for 40 minutes while siblings finish in two — obvious in the UI stage view. Skew usually comes from a hot key (a few users, customers, or device IDs owning most rows). Classic fixes: salt the join key with a random suffix, or let AQE handle it on a recent Spark (section 5).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also check read-side partitioning: &lt;code&gt;spark.sql.files.maxPartitionBytes&lt;/code&gt; defaults to 128 MB, but thousands of tiny source files mean thousands of tiny tasks regardless. Fix the source before tuning the knob.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Broadcast joins
&lt;/h2&gt;

&lt;p&gt;If one side of a join is small enough, Spark can copy it to every executor and avoid shuffling the big side entirely. The default threshold is 10 MB (&lt;code&gt;spark.sql.autoBroadcastJoinThreshold&lt;/code&gt;); the plan shows &lt;code&gt;BroadcastHashJoin&lt;/code&gt; when it applies.&lt;/p&gt;

&lt;p&gt;You can hint manually: &lt;code&gt;big_df.join(small_df, "key").hint("broadcast")&lt;/code&gt;. In practice this is the cheapest win on dimension-style joins — often an order of magnitude faster than the sort-merge equivalent, depending on your data. It can backfire: raise the threshold too high and the driver ships a huge table to every executor. Keep the broadcast side genuinely small.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Avoid the shuffle before you optimize it
&lt;/h2&gt;

&lt;p&gt;A shuffle is not just network traffic — it's serialization, disk writes, and a stage boundary that stops pipelining. Cheapest fix: don't create it.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Filter before join.&lt;/strong&gt; Fewer rows through the shuffle.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggregate before join.&lt;/strong&gt; Fewer rows, same answer, if the aggregate is valid at that point.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;coalesce(n)&lt;/code&gt; vs &lt;code&gt;repartition(n)&lt;/code&gt;.&lt;/strong&gt; Coalesce reduces partitions without a full shuffle (only safe when reducing); repartition always shuffles. Use coalesce on write, repartition only when you genuinely need to rebalance.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;reduceByKey&lt;/code&gt; over &lt;code&gt;groupByKey&lt;/code&gt;.&lt;/strong&gt; Reduce combines on the map side before anything crosses the network.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bucketing.&lt;/strong&gt; For tables joined repeatedly on the same key, pre-bucketing can make joins shuffle-free. Plan it early — it's a schema decision.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Cache with intent, not habit
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;df.cache()&lt;/code&gt; is lazy — nothing is cached until the first action forces materialization. It pays off only when the same DataFrame is reused across multiple actions or iterations (training loops, iterative algorithms). If you read it once and move on, caching just adds a serialization round-trip.&lt;/p&gt;

&lt;p&gt;When you do cache, use the default &lt;code&gt;MEMORY_AND_DISK&lt;/code&gt; and &lt;code&gt;unpersist()&lt;/code&gt; when done. Caching "everything, just in case" evicts what you actually need later — Spark's cache is a shared pool competing with shuffle buffers and execution memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Let AQE do the boring stuff
&lt;/h2&gt;

&lt;p&gt;Adaptive Query Execution has been on by default since Spark 3.2 (on 3.0/3.1 you enable it manually via &lt;code&gt;spark.sql.adaptive.enabled&lt;/code&gt;). At runtime AQE can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;coalesce post-shuffle partitions down to something sane,&lt;/li&gt;
&lt;li&gt;detect skew and split the hot partitions,&lt;/li&gt;
&lt;li&gt;convert a sort-merge join into a broadcast join when stats say the table is actually small.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;AQE removes a surprising amount of hand-tuning — especially the "guess the shuffle partition count" game from section 1. Not magic, but the closest thing Spark has to a self-tuning default. Check &lt;code&gt;spark.sql.adaptive.coalescePartitions.enabled&lt;/code&gt; and &lt;code&gt;spark.sql.adaptive.skewJoin.enabled&lt;/code&gt; before building your own workarounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Diagnose spills before blaming the cluster
&lt;/h2&gt;

&lt;p&gt;Spill means a task ran out of execution memory and wrote intermediate data to disk. The UI shows it as shuffle spill or storage memory spill. Spill is a symptom, not a disease: too much data per task, too little memory, or a fat shuffle.&lt;/p&gt;

&lt;p&gt;Fix in this order: shrink the shuffle (sections 2-3), then tune executor memory and &lt;code&gt;spark.memory.fraction&lt;/code&gt;, and only then add nodes. Throwing machines at a spill caused by a fat shuffle pays for a symptom.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. The small-files problem
&lt;/h2&gt;

&lt;p&gt;Write a batch job with 200 shuffle partitions and you get 200 output files. A streaming job writing per micro-batch adds a new batch of tiny files every few minutes. Over weeks, a table becomes thousands of files that slow every read, file listing, and catalog operation. It compounds — fix it early.&lt;/p&gt;

&lt;p&gt;Practical fixes: &lt;code&gt;coalesce()&lt;/code&gt; on write for batch jobs, and for Delta Lake, &lt;code&gt;OPTIMIZE&lt;/code&gt; with &lt;code&gt;ZORDER BY&lt;/code&gt; on the columns you filter by. A reasonable target is files in the 64-256 MB range — but like every number in this post, verify against your own workload.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Keep Python out of the hot path
&lt;/h2&gt;

&lt;p&gt;Python UDFs are the classic "one function that makes everything crawl" culprit: each row serializes through PySpark's bridge and breaks Catalyst's whole-stage code generation. Replacing a per-row UDF with built-in SQL expressions or a vectorized pandas UDF routinely moves the needle more than any cluster knob.&lt;/p&gt;

&lt;p&gt;Prefer &lt;code&gt;selectExpr&lt;/code&gt;, &lt;code&gt;when/otherwise&lt;/code&gt;, and built-ins. For genuinely custom logic, use vectorized UDFs so serialization is amortized over batches, not paid per row.&lt;/p&gt;

&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;p&gt;Run top to bottom; each step is cheaper than the next:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Read the physical plan. Know your shuffles.&lt;/li&gt;
&lt;li&gt;Fix partitioning and skew (2-4 partitions per core as a starting point; salt or AQE for hot keys).&lt;/li&gt;
&lt;li&gt;Broadcast the small side of joins.&lt;/li&gt;
&lt;li&gt;Eliminate shuffles: filter, aggregate, coalesce, bucket.&lt;/li&gt;
&lt;li&gt;Cache only reused DataFrames; unpersist when done.&lt;/li&gt;
&lt;li&gt;Confirm AQE is on; let it tune partition counts and skew.&lt;/li&gt;
&lt;li&gt;Diagnose spills — shrink the shuffle before buying nodes.&lt;/li&gt;
&lt;li&gt;Kill per-row Python UDFs; use vectorized or built-ins.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Where to go deeper
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://datanest-stores.com/data-engineering/" rel="noopener noreferrer"&gt;Data Engineering store&lt;/a&gt; at Datanest carries the tools behind this checklist: the &lt;strong&gt;Spark Performance Masterclass&lt;/strong&gt; (25+ optimization patterns for Databricks), plus the &lt;strong&gt;Spark ETL Framework&lt;/strong&gt;, &lt;strong&gt;PySpark Utils Library&lt;/strong&gt;, and &lt;strong&gt;Delta Lake Patterns&lt;/strong&gt; for the pipeline around it. If you want the whole set, the &lt;strong&gt;Data Engineering Bundle&lt;/strong&gt; includes all 17 tools — $523 bought separately, yours for $199 (save $324, 62%).&lt;/p&gt;

&lt;p&gt;&lt;a href="https://datanest-stores.com/data-engineering/" rel="noopener noreferrer"&gt;Browse the Data Engineering store&lt;/a&gt;&lt;/p&gt;

</description>
      <category>pyspark</category>
      <category>dataengineering</category>
      <category>bigdata</category>
      <category>performance</category>
    </item>
    <item>
      <title>Apache Airflow DAG Design Patterns That Survive Production</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:36:26 +0000</pubDate>
      <link>https://dev.to/datanestdigital/apache-airflow-dag-design-patterns-that-survive-production-20ga</link>
      <guid>https://dev.to/datanestdigital/apache-airflow-dag-design-patterns-that-survive-production-20ga</guid>
      <description>&lt;p&gt;Every Airflow DAG works in a tutorial. Production is where they fall apart — and usually not because the code is wrong, but because the &lt;em&gt;design&lt;/em&gt; assumed the world would never retry, never flake, and never run 365 missed schedules at once.&lt;/p&gt;

&lt;p&gt;This is a practical set of patterns for DAGs that survive contact with production: idempotency, atomic tasks, dynamic task mapping versus dynamic DAGs, backfills, sensors (and their deadlocks), secrets, retries, trigger rules, and testing. No hype, no "10x your pipelines" — just the stuff that stops the 2 a.m. page.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Idempotency is the whole game
&lt;/h2&gt;

&lt;p&gt;If a task is not safe to re-run, it is not production-safe. Period. Airflow will re-run tasks — after retries, after manual clears, after a worker dies mid-task. Your DAG must produce the same result no matter how many times it executes.&lt;/p&gt;

&lt;p&gt;Concretely: prefer &lt;code&gt;MERGE&lt;/code&gt; / &lt;code&gt;INSERT OVERWRITE&lt;/code&gt; over &lt;code&gt;INSERT&lt;/code&gt;, and partition targets by execution date so a re-run replaces exactly the same slice:&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="c1"&gt;# Good: idempotent partition overwrite
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;overwrite&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replaceWhere&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;date = &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ds&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; \
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold.daily_metrics&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Bad: append creates duplicates on re-run
&lt;/span&gt;&lt;span class="n"&gt;df&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;delta&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;mode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;append&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;saveAsTable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;gold.daily_metrics&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;h2&gt;
  
  
  2. Atomic tasks, small tasks
&lt;/h2&gt;

&lt;p&gt;Each task should do one thing, and its state should be either "not started" or "fully complete" — never partially complete. Write to a staging location and rename on success, use transactions where possible, and write checkpoint files so a retry resumes safely instead of restarting blind.&lt;/p&gt;

&lt;p&gt;Then split the monolith. Separate extract / transform / quality / load tasks give you granular retry (only re-run what failed), clear observability (which step is slow?), and parallel execution. The mega-task that does everything is the anti-pattern that turns a one-line failure into a whole-pipeline replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Nothing at top level
&lt;/h2&gt;

&lt;p&gt;The scheduler parses every DAG file roughly every 30 seconds. Any code outside the DAG context manager or task definitions runs on &lt;em&gt;every parse&lt;/em&gt;. A &lt;code&gt;db.execute("SELECT COUNT(*) FROM orders")&lt;/code&gt; at module level means one query every 30 seconds forever, and a slow file sensor at import time stalls the whole scheduler.&lt;/p&gt;

&lt;p&gt;Put it in a task:&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="c1"&gt;# BAD — this query runs every 30 seconds during scheduling
&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# GOOD — query runs only when the task executes
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;db&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COUNT(*) FROM orders&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scalar&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  4. Backfills are explicit, not accidental
&lt;/h2&gt;

&lt;p&gt;Set &lt;code&gt;catchup=False&lt;/code&gt; by default. Forgetting this is the #1 cause of "why did 365 DAG runs just fire at once?" when you deploy a DAG with a &lt;code&gt;start_date&lt;/code&gt; in the past.&lt;/p&gt;

&lt;p&gt;When you &lt;em&gt;do&lt;/em&gt; want history, make backfill a deliberate, parameterized DAG — date-range chunking, parallelism control, progress tracking — instead of relying on catch-up behavior you didn't ask for.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. XCom is metadata, not a data bus
&lt;/h2&gt;

&lt;p&gt;XCom values live in the metadata database, and most setups have a practical limit around 48 KB per value. Pushing DataFrames or full query results through XCom bloats the DB and slows the UI.&lt;/p&gt;

&lt;p&gt;Pass small metadata — file paths, row counts, status strings. For large data, write to object storage (S3, GCS, ADLS) and push only the path. If your team routinely passes large payloads between tasks, look at a custom XCom backend that offloads storage to object storage instead of the DB.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Dynamic task mapping, not parse-time generation
&lt;/h2&gt;

&lt;p&gt;Dynamic task mapping (Airflow 2.3+) replaces the old pattern of generating tasks at parse time with runtime-determined parallelism. Map over a list with &lt;code&gt;expand()&lt;/code&gt;, pin static args with &lt;code&gt;partial()&lt;/code&gt;, and pass dicts with &lt;code&gt;expand_kwargs()&lt;/code&gt;:&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;{{ ds }}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;production&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;expand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;tables&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Cap it: &lt;code&gt;max_map_length&lt;/code&gt; in &lt;code&gt;airflow.cfg&lt;/code&gt; (default 1024), and monitor mapped task counts — each mapped instance consumes a worker slot, and unbounded &lt;code&gt;.expand()&lt;/code&gt; can exhaust the pool.&lt;/p&gt;

&lt;p&gt;If you still need DAG factories driven by YAML config, keep them small — under 100 DAGs, because each adds scheduler overhead — and use closures to bind loop variables, or every task gets the &lt;em&gt;last&lt;/em&gt; value due to late binding.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Sensors: &lt;code&gt;reschedule&lt;/code&gt;, or you'll deadlock
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;ExternalTaskSensor&lt;/code&gt; is how you wait for another DAG's task. Two rules keep you out of the worst failure modes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Prefer &lt;code&gt;mode="reschedule"&lt;/code&gt;&lt;/strong&gt; for long waits. &lt;code&gt;mode="poke"&lt;/code&gt; holds a worker slot the entire time — pool exhaustion waiting to happen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Map your cross-DAG dependencies before writing sensors.&lt;/strong&gt; If DAG A waits for DAG B and DAG B waits for DAG A, both hang forever. That's a deadlock you will not notice until nothing runs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Also watch schedule mismatch: if DAG A is &lt;code&gt;@hourly&lt;/code&gt; and DAG B is &lt;code&gt;@daily&lt;/code&gt;, the sensor's &lt;code&gt;execution_delta&lt;/code&gt; must account for the difference, or it will wait for a run that doesn't exist.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Secrets: connections and variables, not env vars
&lt;/h2&gt;

&lt;p&gt;Use Airflow Connections rather than raw env vars — encrypted at rest, auditable, UI-editable. Keep one connection per environment (&lt;code&gt;aws_prod&lt;/code&gt;, &lt;code&gt;aws_staging&lt;/code&gt;, &lt;code&gt;aws_dev&lt;/code&gt;), and for anything sensitive, store them in a secrets backend like Vault or AWS Secrets Manager rather than the metadata DB where admins can see them.&lt;/p&gt;

&lt;p&gt;For variables: &lt;code&gt;Variable.get("environment", default_var="dev")&lt;/code&gt; won't crash when missing, and batch them into one JSON variable instead of scattering &lt;code&gt;Variable.get()&lt;/code&gt; calls at module level — every one of those is a DB query during DAG parsing.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Retries and trigger rules
&lt;/h2&gt;

&lt;p&gt;Set retries per task — &lt;code&gt;@task(retries=3, retry_delay=timedelta(minutes=5))&lt;/code&gt; — and let tasks fail loudly instead of swallowing exceptions. Catching everything "just in case" hides the failure from Airflow's retry and alerting machinery, which is the entire point of the platform.&lt;/p&gt;

&lt;p&gt;Trigger rules are the less obvious lever. The default &lt;code&gt;all_success&lt;/code&gt; is right for strict pipelines, but fan-in joins often want &lt;code&gt;all_done&lt;/code&gt; (run regardless of upstream success/failure, e.g. for cleanup) or &lt;code&gt;none_failed_min_one_success&lt;/code&gt; for a branch that tolerates partial failure. A terminal task with &lt;code&gt;trigger_rule="none_failed_min_one_success"&lt;/code&gt; is how you get accurate DAG-level status instead of a forever-&lt;code&gt;failed&lt;/code&gt; run because one optional branch broke.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Test DAGs at four levels
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Import test&lt;/strong&gt; — load with &lt;code&gt;DagBag&lt;/code&gt; and assert zero &lt;code&gt;import_errors&lt;/code&gt;. Catches syntax errors before deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structure validation&lt;/strong&gt; — every DAG has an owner, tags, description, and &lt;code&gt;catchup=False&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Callable unit tests&lt;/strong&gt; — test your Python functions independently of Airflow.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Integration with &lt;code&gt;dag.test()&lt;/code&gt;&lt;/strong&gt; (Airflow 2.5+) — run an entire DAG in a single process with a fixed &lt;code&gt;execution_date&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This four-level ladder is cheap to maintain and turns "deploy and pray" into "deploy and know."&lt;/p&gt;




&lt;p&gt;The patterns above are the same ones that ship in the &lt;a href="https://datanest-stores.com/data-engineering/#product-airflow-dag-templates" rel="noopener noreferrer"&gt;Airflow DAG Templates&lt;/a&gt; pack from &lt;strong&gt;DataStack Pro&lt;/strong&gt;: 14 production-shaped DAGs covering ETL, data quality, ML pipelines, warehouse loading, CDC streaming, database replication, SLA monitoring, and dynamic task mapping — plus custom operators (Spark submit, data quality, Databricks notebook, Delta sensor), an extended S3 sensor, and a 450+ line best-practices guide covering TaskFlow, dynamic mapping, SLAs, and testing. Pair it with the &lt;a href="https://datanest-stores.com/data-engineering/#product-data-pipeline-testing" rel="noopener noreferrer"&gt;Data Pipeline Testing Kit&lt;/a&gt; if you need PySpark unit and integration test scaffolding.&lt;/p&gt;

&lt;p&gt;The full &lt;a href="https://datanest-stores.com/data-engineering/" rel="noopener noreferrer"&gt;DataStack Pro collection&lt;/a&gt; covers the rest of the modern data stack: Spark ETL frameworks, Delta Lake patterns, data quality engines, CDC replication, schema evolution, and more.&lt;/p&gt;

</description>
      <category>airflow</category>
      <category>dataengineering</category>
      <category>python</category>
      <category>devops</category>
    </item>
    <item>
      <title>Machine Payments for AI Agents: x402 + USDC on Base</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 26 Sep 2026 12:19:52 +0000</pubDate>
      <link>https://dev.to/datanestdigital/machine-payments-for-ai-agents-x402-usdc-on-base-2fi8</link>
      <guid>https://dev.to/datanestdigital/machine-payments-for-ai-agents-x402-usdc-on-base-2fi8</guid>
      <description>&lt;p&gt;We've wired up the digital storefront so an AI agent can run the whole purchase loop itself: discover a product, receive a payment challenge, pay in USDC on Base mainnet, and get the product delivered — no Stripe session, no facilitator, no human approving the transfer. It's the x402 protocol implemented as a self-custody machine-payment path beside the existing agent commerce layer, and it runs on a Cloudflare Worker that is read-only against the chain.&lt;/p&gt;

&lt;p&gt;This post walks the actual implementation: the four-step challenge → pay → verify → deliver flow, then the engineering decisions that make on-chain payments safe without a trusted third party — per-order derived recipient addresses, strict exact-amount verification, a second-RPC cross-check, and fail-closed invariants.&lt;/p&gt;

&lt;h2&gt;
  
  
  The flow: challenge → pay → verify → deliver
&lt;/h2&gt;

&lt;p&gt;x402 is an HTTP-native payment protocol: a merchant answers a request with &lt;code&gt;402 Payment Required&lt;/code&gt; and a machine-readable challenge, the agent pays, the merchant verifies on-chain, and the agent gets its content. Datanest's implementation lives at &lt;code&gt;GET /api/x402/products/{id}&lt;/code&gt; on the agent-MCP Worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Challenge.&lt;/strong&gt; The agent requests a product with no proof. The Worker mints a 128-bit secret order id, derives a per-order recipient address, computes the exact price, and durably writes the order — if that write fails, it returns &lt;code&gt;503&lt;/code&gt; rather than handing out a challenge whose record isn't durable. The response is a &lt;code&gt;402&lt;/code&gt; with a base64 &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; header describing the accepted payment (scheme &lt;code&gt;exact-transfer&lt;/code&gt;, network &lt;code&gt;eip155:8453&lt;/code&gt;, the USDC asset, the exact amount, and the derived &lt;code&gt;payTo&lt;/code&gt; address), plus the secret order id in an &lt;code&gt;X402-Order-Id&lt;/code&gt; header. The order expires after &lt;code&gt;X402_ORDER_TTL_SECONDS&lt;/code&gt; (default 900 seconds).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Pay.&lt;/strong&gt; The agent transfers exactly the price in USDC to that order's &lt;code&gt;payTo&lt;/code&gt; address. Because the recipient address is unique per order, the exact amount is the only constraint — no nonce, no overcharge.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Verify.&lt;/strong&gt; The agent retries the same URL, now carrying &lt;code&gt;PAYMENT-SIGNATURE: &amp;lt;tx-hash&amp;gt;&lt;/code&gt; and &lt;code&gt;X402-Order-Id&lt;/code&gt;. The Worker fetches the receipt, requires a successful transaction (&lt;code&gt;status === 0x1&lt;/code&gt;) at valid block heights with at least &lt;code&gt;X402_MIN_CONFIRMATIONS&lt;/code&gt; confirmations (default 5), and decodes a strict 3-topic + 1-word ERC-20 &lt;code&gt;Transfer&lt;/code&gt; event to the order's address for exactly the price — exact &lt;code&gt;===&lt;/code&gt;, never &lt;code&gt;&amp;gt;=&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Deliver.&lt;/strong&gt; On success the order is marked paid (with the payer's &lt;code&gt;from&lt;/code&gt; address recorded) and the product zip streams from R2 with a &lt;code&gt;PAYMENT-RESPONSE&lt;/code&gt; header. Re-downloading a paid order is idempotent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the tricky parts matter
&lt;/h2&gt;

&lt;p&gt;The unglamorous part of machine payments is replay protection. A tx hash is public, so it is never trusted alone: delivery requires both the secret order id (only ever handed to the payer, never written on-chain) and a transfer to that order's own derived address.&lt;/p&gt;

&lt;p&gt;The derived address is a non-hardened child of the merchant master &lt;em&gt;public&lt;/em&gt; key:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;tweak     = keccak256("x402-order-v1" ‖ orderId ‖ productId ‖ network) mod n
child_pub = master_pub + tweak·G
address   = last 20 bytes of keccak256(uncompressed child_pub)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the tweak is seeded by the 128-bit order id, a paid transaction for one order can never satisfy another — address collision is on the order of 2¹²⁸. This replaced an earlier amount-binding scheme (&lt;code&gt;price + 4-digit nonce&lt;/code&gt;) that had only 10⁴ entropy and was forceable via a few thousand free challenges. The merchant sweeps a child offline with &lt;code&gt;child_sk = (master_sk + tweak) mod n&lt;/code&gt;, so funds sent to any derived address remain recoverable — without the Worker ever holding a signing key. It holds only the master public key and address; the private key lives offline and is used solely to sweep the master and derived addresses.&lt;/p&gt;

&lt;p&gt;Verification also distrusts a single RPC provider. If &lt;code&gt;X402_RPC_URL_VERIFY&lt;/code&gt; is configured, the Worker re-fetches the receipt from a second independent provider and requires agreement on status, block, and the decoded transfer — disagreement fails closed. The fail-closed list also covers a missing or invalid &lt;code&gt;X402_MASTER_PUB&lt;/code&gt; (x402 disables itself rather than issuing weakly-bound challenges), an invalid cross-check URL, and any order-write failure. The default verification path additionally retries its primary RPC twice and fails over across a list of public endpoints.&lt;/p&gt;

&lt;p&gt;Two smaller details worth stealing: optional payer binding — if the agent declared an &lt;code&gt;X402-Payer&lt;/code&gt; header at challenge time, the verified transfer's &lt;code&gt;from&lt;/code&gt; must match it or delivery is rejected with a &lt;code&gt;payer_mismatch&lt;/code&gt;; and no full order ids ever reach the logs, only a prefix.&lt;/p&gt;

&lt;p&gt;Current state, honestly: the x402 routes are live on Base mainnet and the challenge path is reliable. Verification is code-correct but depends on RPC reachability — production-grade reads want an API-keyed RPC endpoint and the second-provider cross-check wired in as operator config. Until then the code fails closed instead of issuing weakly-bound challenges.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this unlocks
&lt;/h2&gt;

&lt;p&gt;The interesting bit is less "crypto checkout" and more "autonomous buyer." The same Worker already exposes an MCP server at &lt;code&gt;https://datanest-stores.com/mcp&lt;/code&gt; with tools to search the catalogue, inspect products, compare them, and prepare Stripe checkout links. x402 adds the self-custody leg: an agent with a funded wallet can complete the whole loop — browse, choose, pay, download — with no card rails and no human in the loop.&lt;/p&gt;

&lt;p&gt;The full public catalogue is available at &lt;code&gt;https://datanest-stores.com/catalog.json&lt;/code&gt;, and the MCP server (plus its REST API under &lt;code&gt;/api/agent/v1&lt;/code&gt;) lives at &lt;code&gt;https://datanest-stores.com/mcp&lt;/code&gt;. If you're building agents that buy things, that's a live storefront to point them at.&lt;/p&gt;

</description>
      <category>payments</category>
      <category>ai</category>
      <category>blockchain</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I Built a Multi-Chain Token Safety Scanner for $0/Month on Cloudflare Workers</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 22 Aug 2026 20:40:15 +0000</pubDate>
      <link>https://dev.to/datanestdigital/i-built-a-multi-chain-token-safety-scanner-for-0month-on-cloudflare-workers-3kjc</link>
      <guid>https://dev.to/datanestdigital/i-built-a-multi-chain-token-safety-scanner-for-0month-on-cloudflare-workers-3kjc</guid>
      <description>&lt;p&gt;TL;DR: &lt;a href="https://rugradar.datanest-stores.com" rel="noopener noreferrer"&gt;RugRadar&lt;/a&gt; scans tokens across Ethereum, BSC, Polygon, Arbitrum and Base for honeypots, mint authority, owner concentration and liquidity traps. It runs entirely on Cloudflare's free tier with Workers KV, no SQL database, no servers, zero monthly cost. Here is the architecture and the parts that surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;A single Worker serves static assets (&lt;code&gt;public/&lt;/code&gt;) and a JSON API (&lt;code&gt;/api/*&lt;/code&gt;). Risk telemetry comes from GoPlus's free token-security endpoint; scoring is local, pure, and unit-tested so every finding maps to a concrete contract behavior. KV stores licenses, invoices, scan cache (6h TTL), soft rate-limit counters and daily metrics. Payments run on two rails: Stripe Payment Links (card), and a fully self-custody USDC-on-Base flow where a cron poller watches public-RPC transfer logs and auto-issues license keys. No email anywhere: keys are delivered on-page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #1: GoPlus returns HTTP 200 when it rate-limits you
&lt;/h2&gt;

&lt;p&gt;The failure mode that cost me the most debugging time: from Cloudflare's shared egress IPs, GoPlus sometimes answers &lt;code&gt;200 OK&lt;/code&gt; with body &lt;code&gt;{"code": 4029}&lt;/code&gt; — rate limited. If you check HTTP status only, everything "works" while every scan dies.&lt;/p&gt;

&lt;p&gt;Fixes that made it robust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retry once with jitter on &lt;code&gt;code: 4029&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stale-if-error&lt;/strong&gt;: serve the last stored report (flagged as stale in the UI) rather than failing&lt;/li&gt;
&lt;li&gt;versioned cache keys (&lt;code&gt;v2:&lt;/code&gt;) so schema changes invalidate cleanly&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Surprise #2: workers.dev embeds your account name
&lt;/h2&gt;

&lt;p&gt;The default &lt;code&gt;*.workers.dev&lt;/code&gt; URL contains your &lt;em&gt;account-level&lt;/em&gt; subdomain. For a pseudonymous product that is an identity leak you cannot rename (the subdomain is set once per account). Solution: attach a custom domain to the Worker — free on any zone already in your account — and disable the workers.dev route.&lt;/p&gt;

&lt;h2&gt;
  
  
  Surprise #3: You do not need D1
&lt;/h2&gt;

&lt;p&gt;KV gets dismissed for anything relational, but licenses map naturally to keys: &lt;code&gt;lic:&amp;lt;key&amp;gt;&lt;/code&gt; for records, &lt;code&gt;idx_stripe:&amp;lt;session&amp;gt;&lt;/code&gt; for idempotent fulfillment lookups, &lt;code&gt;inv:&amp;lt;id&amp;gt;&lt;/code&gt; for invoices. Rate limiting on KV is eventually consistent — useless for billing precision, perfectly fine for abuse ceilings.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoring honestly
&lt;/h2&gt;

&lt;p&gt;Every finding maps to a mechanism (honeypot simulation, mint capability, top-LP concentration, modifiable taxes...). Unknowns &lt;em&gt;lower&lt;/em&gt; the score instead of being ignored — conservative by design. A clean renounced token scores ~80-100; a mintable token with single-wallet liquidity lands in DANGER territory regardless of how nice its chart looks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Costs
&lt;/h2&gt;

&lt;p&gt;$0/month: Workers free tier (100k req/day), KV free tier, GoPlus free endpoint, Stripe has no fixed fee. The only non-zero thing ever spent was my time.&lt;/p&gt;

&lt;p&gt;If you want to poke at it: &lt;a href="https://rugradar.datanest-stores.com" rel="noopener noreferrer"&gt;rugradar.datanest-stores.com&lt;/a&gt;. Scan your favorite bag; disagree with the machine in the comments — the findings link their reasoning precisely so you can.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>cloudflare</category>
      <category>showdev</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>How to Spot a Honeypot Token Before You Buy (2026 Field Guide)</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Sat, 22 Aug 2026 20:40:10 +0000</pubDate>
      <link>https://dev.to/datanestdigital/how-to-spot-a-honeypot-token-before-you-buy-2026-field-guide-3dn3</link>
      <guid>https://dev.to/datanestdigital/how-to-spot-a-honeypot-token-before-you-buy-2026-field-guide-3dn3</guid>
      <description>&lt;p&gt;Every week, someone loses savings to a token they could not sell. Not because they picked wrong on fundamentals — because the contract was designed so that selling was never possible.&lt;/p&gt;

&lt;p&gt;Here is the mechanics-first guide I wish those people had read at 1 a.m., plus the exact checks I run before any trade.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a honeypot actually is
&lt;/h2&gt;

&lt;p&gt;A honeypot is a smart contract that allows buys but blocks sells for everyone except privileged wallets. The buy button works. The chart goes up. The sell button fails with vague errors — or silently does nothing.&lt;/p&gt;

&lt;p&gt;The trick is caller inspection: &lt;code&gt;transfer&lt;/code&gt; behaves differently depending on &lt;em&gt;who&lt;/em&gt; calls it. Sellers revert or get taxed at 100%; the owner's wallets move freely. Modern versions hide this behind proxies and external calls, which is why reading bytecode rarely saves you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 60-second pre-buy checklist
&lt;/h2&gt;

&lt;p&gt;Run these five checks on &lt;em&gt;every&lt;/em&gt; new token, especially ones trending on social feeds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Sell simulation.&lt;/strong&gt; Can an unrelated wallet actually sell? This single test kills most traps. Simulation beats code reading because obfuscated logic still has to behave one way or another.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mint authority.&lt;/strong&gt; If the contract can mint unlimited supply, your position can be diluted to dust in one transaction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Owner concentration.&lt;/strong&gt; An active owner holding double-digit percentages of supply — or hidden ownership — means every rule you read today can change tonight.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Liquidity control.&lt;/strong&gt; $2M of liquidity held by one wallet is more dangerous than $20k burned forever. Concentration answers "who can pull"; depth only answers slippage.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tax mutability.&lt;/strong&gt; Buy/sell taxes that are owner-modifiable are levers waiting for maximum holder count. Per-wallet slippage modification is the precision instrument of targeted traps.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Three or more red flags together are a pattern, not noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why your wallet never warns you
&lt;/h2&gt;

&lt;p&gt;Wallets sign transactions; they do not simulate outcomes. Nothing in MetaMask distinguishes a tradable token from a trap. Social proof does not help either — on meme-heavy chains, social consensus is literally the product being sold to you.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tools
&lt;/h2&gt;

&lt;p&gt;You can do all five checks manually against explorers, or automate them. I use &lt;a href="https://rugradar.datanest-stores.com" rel="noopener noreferrer"&gt;RugRadar&lt;/a&gt; — full disclosure, I built it — which runs the sell-simulation, mint/ownership, LP-concentration and tax-mutability checks across Ethereum, BSC, Polygon, Arbitrum and Base, and explains every finding in plain language instead of raw flags. The scanner is free; there is a paid tier for bulk/API use, which matters if you degen more than occasionally.&lt;/p&gt;

&lt;p&gt;Whatever tool you use, the discipline is the same: &lt;strong&gt;simulation first, concentration second, taxes third — before size goes on.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The uncomfortable summary
&lt;/h2&gt;

&lt;p&gt;Most rug victims were not unlucky; they skipped thirty seconds of checking under hype pressure. The market pays for speed, but it collects from people who confuse speed with diligence.&lt;/p&gt;

&lt;p&gt;Stay safe out there. Paste the address before you paste your seed phrase into regret.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;This post is informational, not financial advice. Always do your own research.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>crypto</category>
      <category>security</category>
      <category>blockchain</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>I built a fake logistics company to stop myself buying things — how Out for Never works under the hood</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Fri, 21 Aug 2026 18:30:34 +0000</pubDate>
      <link>https://dev.to/datanestdigital/i-built-a-fake-logistics-company-to-stop-myself-buying-things-how-out-for-never-works-under-the-okb</link>
      <guid>https://dev.to/datanestdigital/i-built-a-fake-logistics-company-to-stop-myself-buying-things-how-out-for-never-works-under-the-okb</guid>
      <description>&lt;p&gt;A few weeks ago I shipped &lt;a href="https://outfornever.com" rel="noopener noreferrer"&gt;&lt;strong&gt;Out for Never&lt;/strong&gt;&lt;/a&gt;, a web app whose entire premise is: &lt;em&gt;what if the fun part of online shopping — the checkout, the tracking page, the "it's on the way" dopamine — existed without the spending?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;You enter the thing you nearly bought, a price, how strong the urge was. You run a theatrical fake checkout. Then a fictional logistics network takes over and ships your imaginary parcel to nowhere, with courier lore, absurd incidents and manual reroutes along the way. You keep the money.&lt;/p&gt;

&lt;p&gt;This post is for the dev crowd: the small stories of it being fun, then the parts I actually had to think about.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fun stories first
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The pigeon that wouldn't stop.&lt;/strong&gt; One courier is Pip, of &lt;em&gt;Pigeon Express&lt;/em&gt; — "fast in a straight line, has never chosen one." You pick a courier when you create a parcel, and their pickup line, mood and bio all render on the tracking page. Watching a pigeon with 3,500+ documented courier failures carry your imaginary €89 headphones through Luxembourg is genuinely better than the real tracking page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The reroutes are deterministic chaos.&lt;/strong&gt; Every parcel gets a &lt;code&gt;seed&lt;/code&gt;. Incidents, "nearby updates" and final delays are all picked from static arrays via a seeded PRNG — so a given parcel always tells the same story, but different parcels diverge. One of mine got &lt;em&gt;"Paused for a tiny parade — seven ducks and an admirably small brass band have priority."&lt;/em&gt; You can also manually reroute it to a parade, an extra moon orbit, or duck traffic.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The friends who don't know it's a joke.&lt;/strong&gt; The paid tier sends a cinematic tracking journey with a real personal message at the end — &lt;em&gt;"For someone lovely"&lt;/em&gt;, custom reveal chapters, no address, nothing ships. Sending one is the most chaotic thing I've done all year.&lt;/p&gt;

&lt;h2&gt;
  
  
  The engineering
&lt;/h2&gt;

&lt;p&gt;It's a &lt;strong&gt;framework-free SPA&lt;/strong&gt; (&lt;code&gt;index.html&lt;/code&gt; + &lt;code&gt;app.js&lt;/code&gt; + &lt;code&gt;styles.css&lt;/code&gt;, no build step) served by a &lt;strong&gt;Cloudflare Worker&lt;/strong&gt; (&lt;code&gt;src/worker.js&lt;/code&gt;) running as Pages advanced-mode. Around 4,400 lines total. Here are the bits worth stealing.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Every parcel is an AES-GCM sealed token
&lt;/h3&gt;

&lt;p&gt;A tracking link like &lt;code&gt;/track/&amp;lt;token&amp;gt;&lt;/code&gt; is a self-contained, encrypted parcel. There's no server-side parcel table — the token &lt;em&gt;is&lt;/em&gt; the parcel. The Worker seals it like this:&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;// derive a purpose-scoped key from the master secret&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SHA-256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`out-for-never:v2:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;purpose&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;secret&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="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;importKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;raw&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="kc"&gt;false&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="s2"&gt;encrypt&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="s2"&gt;decrypt&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getRandomValues&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Uint8Array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="c1"&gt;// AAD binds the ciphertext to the version + purpose, so a token&lt;/span&gt;
&lt;span class="c1"&gt;// minted for one purpose can't be replayed against another&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;additionalData&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;encoder&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`out-for-never:v2:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;purpose&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtle&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;encrypt&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;AES-GCM&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;iv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;additionalData&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="c1"&gt;// wire format: v2.&amp;lt;base64url(iv || ciphertext)&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The details that matter:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Purpose-separated keys&lt;/strong&gt; — &lt;code&gt;parcel&lt;/code&gt;, &lt;code&gt;stripe-draft&lt;/code&gt; and &lt;code&gt;membership&lt;/code&gt; tokens each derive a &lt;em&gt;different&lt;/em&gt; key from the same master secret. A gift token can't be replayed as a membership token.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AAD as a poor-man's type system&lt;/strong&gt; — the version + purpose string is passed as AES-GCM &lt;em&gt;additional authenticated data&lt;/em&gt;, so even a ciphertext minted with the right key but the wrong purpose fails to authenticate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Canonical base64url only&lt;/strong&gt; — on open, the decoder re-encodes the bytes and rejects the token if the string isn't canonical (&lt;code&gt;bytesToBase64Url(bytes) !== value&lt;/code&gt;). That kills the classic mutability-token bug where two encodings decode to the same payload.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bounded input&lt;/strong&gt; — tokens must be 29–14,000 bytes; all user text is normalized and length-limited server-side; locked gift answers are stored only as a hash inside the token.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I wrote this deliberately, because the free tier has no accounts. The &lt;em&gt;link&lt;/em&gt; is the identity, so the link has to be unforgeable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The logistics are deterministic, not random
&lt;/h3&gt;

&lt;p&gt;The client has static arrays — &lt;code&gt;COURIERS&lt;/code&gt;, &lt;code&gt;INCIDENTS&lt;/code&gt;, &lt;code&gt;NEARBY_UPDATES&lt;/code&gt;, &lt;code&gt;FINAL_DELAYS&lt;/code&gt;, &lt;code&gt;REROUTES&lt;/code&gt; — and picks from them with a seeded PRNG:&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;function&lt;/span&gt; &lt;span class="nf"&gt;seededPick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;offset&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="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;random&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;seededRandom&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;seed&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;random&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;list&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&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;Progress itself is pure time math against two timestamps baked into the parcel:&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;progress&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;clamp&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startsAt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;arrivesAt&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;startsAt&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SVG route, the courier marker, the timeline stages and the status text all derive from that one &lt;code&gt;0..1&lt;/code&gt; value. The ETA countdown never hits zero — it renders &lt;code&gt;NEVER&lt;/code&gt; at completion. Non-delivery is the only guaranteed delivery.&lt;/p&gt;

&lt;p&gt;This design buys you a lot for free: parcels are stateless (shareable, cacheable), the story is stable across reloads and recipients, and there's no progress row to store anywhere.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The voucher system lives in KV
&lt;/h3&gt;

&lt;p&gt;Since the whole thing is one Worker, discount codes are just KV records — no Stripe coupon API needed for the product's own promo flow. A voucher is &lt;code&gt;ofn:voucher:&amp;lt;CODE&amp;gt;&lt;/code&gt; in the &lt;code&gt;OUT_FOR_NEVER_KV&lt;/code&gt; namespace:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"percentOff"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"maxUses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"uses"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"expiresAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The Worker validates (&lt;code&gt;percentOff&lt;/code&gt; 0–100, &lt;code&gt;uses &amp;lt; maxUses&lt;/code&gt;, not expired), applies it to the computed price, and bumps &lt;code&gt;uses&lt;/code&gt; when the order actually confirms. A &lt;code&gt;scripts/voucher.mjs&lt;/code&gt; CLI creates, lists and deletes them against the Cloudflare API. Fun side effect: a 100%-off voucher is how you test the paid flow without touching real money — the Worker detects &lt;code&gt;totalCents &amp;lt;= 0&lt;/code&gt; and issues the gift directly.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The vault is aggressively local
&lt;/h3&gt;

&lt;p&gt;Every avoided purchase lands in a &lt;code&gt;localStorage&lt;/code&gt; vault — money kept per currency, streaks, a 28-day heatmap, category charts, CSV/JSON export. No account, no analytics, nothing phones home. That was a product decision as much as a technical one: a "spend less" tool that tracks you would be a joke at its own expense.&lt;/p&gt;

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

&lt;p&gt;The core ritual is free, no signup, no card. Build a fictional parcel, pick a courier, and watch it get lost somewhere south of reason.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For dev.to readers:&lt;/strong&gt; the launch voucher &lt;code&gt;DEVTOFOREVER&lt;/code&gt; gives &lt;strong&gt;50% off&lt;/strong&gt; any premium fictional gift (the shareable tracking story you send to someone). 25 redemptions, drop it in the &lt;em&gt;Voucher code&lt;/em&gt; field on the Send page.&lt;/p&gt;

&lt;p&gt;→ &lt;strong&gt;&lt;a href="https://outfornever.com" rel="noopener noreferrer"&gt;outfornever.com&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And if you send one to someone who also doesn't need a fourth pair of headphones — that's the whole point.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>Out for Never — we built an anti-commerce app that helps you buy less</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Mon, 17 Aug 2026 12:35:00 +0000</pubDate>
      <link>https://dev.to/datanestdigital/out-for-never-we-built-an-anti-commerce-app-that-helps-you-buy-less-2bga</link>
      <guid>https://dev.to/datanestdigital/out-for-never-we-built-an-anti-commerce-app-that-helps-you-buy-less-2bga</guid>
      <description>&lt;p&gt;There is a whole class of purchases you make not because you need them, but because the moment of buying feels good. The checkout rush. The "it's on the way" dopamine. The tiny progress bar marching toward the shipping confirmation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out for Never&lt;/strong&gt; is the app I built to break that loop — and instead of a savings app that nags you, it gives you the &lt;em&gt;whole buying experience without the buying&lt;/em&gt;.&lt;/p&gt;

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

&lt;p&gt;You type in the thing you almost bought: the gadget, the impulse Amazon cart, the thing in your size at 2am. Pick a price, a category, how strong the temptation was.&lt;/p&gt;

&lt;p&gt;Then you place the order. Theatrically.&lt;/p&gt;

&lt;p&gt;Instead of a real charge, a completely fictional logistics network kicks in. Your "parcel" gets a tracking number. A courier picks it up. It travels across a route that doesn't exist, hits absurd incidents, gets manually rerouted, and eventually — beautifully — is never delivered.&lt;/p&gt;

&lt;p&gt;You keep the real money. The parcel goes nowhere. And somehow that scratch is &lt;em&gt;itched&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;Every avoided purchase is recorded in a local vault: money kept, streaks, temptation intensity, a 28-day heatmap, categories, savings goals, even a CSV export and JSON backup. All in your browser's &lt;code&gt;localStorage&lt;/code&gt;. No account, no tracking, nothing leaves your device.&lt;/p&gt;

&lt;p&gt;You can also quarantine a temptation for 1–30 days instead of deciding right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it (it's free)
&lt;/h2&gt;

&lt;p&gt;The core intervention is completely free — no signup, no card. Go to &lt;strong&gt;&lt;a href="https://outfornever.com" rel="noopener noreferrer"&gt;outfornever.com&lt;/a&gt;&lt;/strong&gt;, run through your first fake order, and watch your parcel get lost somewhere south of reason.&lt;/p&gt;

&lt;h2&gt;
  
  
  The voucher for dev.to readers
&lt;/h2&gt;

&lt;p&gt;Because I'd rather you gift the experience than buy more stuff, there's a &lt;strong&gt;launch voucher&lt;/strong&gt; for the premium fictional gifts (a cinematic reveal story you can send to someone):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;&lt;code&gt;DEVTOFOREVER&lt;/code&gt;&lt;/strong&gt; — &lt;strong&gt;50% off&lt;/strong&gt; any premium gift, valid for the first &lt;strong&gt;25 redemptions&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Drop it into the &lt;em&gt;Voucher code&lt;/em&gt; field on the Send page. It applies to the Spark, Cinematic and Grand Gesture tiers — no physical item is ever shipped, because nothing is shipped, because nothing exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  A weird but real behaviour change
&lt;/h2&gt;

&lt;p&gt;The product is deliberately absurd. But the mechanism is real: a lot of impulse buying is about the &lt;em&gt;anticipation&lt;/em&gt;, not the object. Giving that anticipation a fictional outlet — with courier lore and "ESTIMATED NON-ARRIVAL" countdowns — is, frankly, better entertainment than the real parcel will ever be.&lt;/p&gt;

&lt;p&gt;It's a Cloudflare Worker + a framework-free SPA, one of those "why not build the joke" weekend projects that refused to stay a joke.&lt;/p&gt;

&lt;p&gt;If you try it, send a fictional gift to someone who also needs to not buy a fourth pair of headphones. Use the code, keep the streak.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://outfornever.com" rel="noopener noreferrer"&gt;Try Out for Never →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>productivity</category>
      <category>webdev</category>
      <category>javascript</category>
    </item>
    <item>
      <title>AgentStack MCP: one deterministic reasoning stack for AI agents (simulate + decide + compute)</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Tue, 11 Aug 2026 12:30:22 +0000</pubDate>
      <link>https://dev.to/datanestdigital/agentstack-mcp-one-deterministic-reasoning-stack-for-ai-agents-simulate-decide-compute-597p</link>
      <guid>https://dev.to/datanestdigital/agentstack-mcp-one-deterministic-reasoning-stack-for-ai-agents-simulate-decide-compute-597p</guid>
      <description>&lt;p&gt;&lt;em&gt;The fourth in a suite of deterministic MCP servers for AI agents — and the one that ties the first three together.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Over the last stretch I shipped three focused, deterministic MCP servers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://scenariosim-mcp.pages.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;ScenarioSim&lt;/strong&gt;&lt;/a&gt; — what-if / scenario simulation&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://decisionmatrix-mcp.pages.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;DecisionMatrix&lt;/strong&gt;&lt;/a&gt; — multi-criteria decision analysis&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://precisioncalc-mcp.pages.dev" rel="noopener noreferrer"&gt;&lt;strong&gt;PrecisionCalc&lt;/strong&gt;&lt;/a&gt; — exact finance / business math&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They're great on their own, but agents kept needing all three in the same task — and installing three servers, juggling three keys, and hand-gluing their outputs is friction. So here's &lt;strong&gt;AgentStack MCP&lt;/strong&gt;: one endpoint, one key, all three — plus composite tools that chain them.&lt;/p&gt;

&lt;h2&gt;
  
  
  simulate → decide → compute
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"agentstack"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://agentstack-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier: no key, 20 calls/day. The tools are namespaced so an agent always knows which engine it's calling:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sim_*&lt;/code&gt; — ScenarioSim (run, sensitivity, break-even, compare, templates)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decide_*&lt;/code&gt; — DecisionMatrix (decide, score, sensitivity, compare_two, methods)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;calc_*&lt;/code&gt; — PrecisionCalc (metrics, currency, NPV, IRR, loan, depreciation, …)&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The part that's actually new: composite tools
&lt;/h2&gt;

&lt;p&gt;These chain the engines to do reasoning &lt;strong&gt;no single server can&lt;/strong&gt;, deterministically end-to-end:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;evaluate_options_with_scenarios&lt;/code&gt;&lt;/strong&gt; (simulate → decide) — project each option as its own scenario, then rank the &lt;em&gt;outcomes&lt;/em&gt; against weighted criteria:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"evaluate_options_with_scenarios"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"saas_growth"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"horizon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Aggressive"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new_customers_per_period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"churn_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Lean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new_customers_per_period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"churn_rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.02&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ending_mrr"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"benefit"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"total_churned_customers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cost"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;&lt;code&gt;plan_to_valuation&lt;/code&gt;&lt;/strong&gt; (simulate → compute) — project a plan, then value its cash-flow line: NPV, IRR, undiscounted total.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;stress_test_decision&lt;/code&gt;&lt;/strong&gt; (simulate × decide) — stress one scenario assumption across every option and report how often the chosen option survives (robustness) and where it flips.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fighting tool bloat with profiles
&lt;/h2&gt;

&lt;p&gt;Bundling 24 tools risks drowning an agent's tool-selection. So the endpoint takes a &lt;code&gt;?profile=&lt;/code&gt; filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://agentstack-mcp.pages.dev/mcp?profile=finance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;finance&lt;/code&gt; → &lt;code&gt;calc_*&lt;/code&gt; + &lt;code&gt;plan_to_valuation&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;decision&lt;/code&gt; → &lt;code&gt;decide_*&lt;/code&gt; + the two decision composites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;simulation&lt;/code&gt; → &lt;code&gt;sim_*&lt;/code&gt; + all composites&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;all&lt;/code&gt; (default) → everything&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it's built this way
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Deterministic&lt;/strong&gt;: everything runs through decimal.js at 40-digit precision. Same inputs → byte-identical output, across all three engines and the composites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No proxying&lt;/strong&gt;: AgentStack imports the &lt;em&gt;same&lt;/em&gt; engines directly, so there's zero added latency and no cascading failure — not three network hops behind one URL.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Additive, not a replacement&lt;/strong&gt;: the three standalone servers keep running for single-domain use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One key, one quota&lt;/strong&gt;: ~half the price of subscribing to the three separately.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stateless + MIT&lt;/strong&gt;: self-host on Cloudflare Pages, Node, Deno, or Bun.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live endpoint:&lt;/strong&gt; &lt;a href="https://agentstack-mcp.pages.dev/mcp" rel="noopener noreferrer"&gt;https://agentstack-mcp.pages.dev/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site + docs:&lt;/strong&gt; &lt;a href="https://agentstack-mcp.pages.dev" rel="noopener noreferrer"&gt;https://agentstack-mcp.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub (MIT):&lt;/strong&gt; &lt;a href="https://github.com/inity13/agentstack-mcp" rel="noopener noreferrer"&gt;https://github.com/inity13/agentstack-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Registry:&lt;/strong&gt; &lt;code&gt;io.github.inity13/agentstack-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agents plan, choose, and do the numbers, give them one calculator that does all three — and never drifts. Feedback welcome.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>ScenarioSim MCP: a deterministic what-if &amp; scenario simulation engine for AI agents</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Tue, 11 Aug 2026 11:11:13 +0000</pubDate>
      <link>https://dev.to/datanestdigital/scenariosim-mcp-a-deterministic-what-if-scenario-simulation-engine-for-ai-agents-3dle</link>
      <guid>https://dev.to/datanestdigital/scenariosim-mcp-a-deterministic-what-if-scenario-simulation-engine-for-ai-agents-3dle</guid>
      <description>&lt;p&gt;&lt;em&gt;The third in a suite of deterministic MCP servers for AI agents — after &lt;a href="https://precisioncalc-mcp.pages.dev" rel="noopener noreferrer"&gt;PrecisionCalc MCP&lt;/a&gt; (high-precision finance math) and &lt;a href="https://decisionmatrix-mcp.pages.dev" rel="noopener noreferrer"&gt;DecisionMatrix MCP&lt;/a&gt; (multi-criteria decision analysis).&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Ask an LLM to "project 12 months of SaaS growth at 3% churn" and it will happily produce a table of numbers. Ask it twice and you'll get two different tables. Compounding, churn, elasticity, break-even — these are exactly the kind of multi-period arithmetic that language models drift on, and they can't show their work.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ScenarioSim MCP&lt;/strong&gt; offloads that to an exact, explainable engine. Your agent hands over assumptions; it gets back projections over time, sensitivity analysis, and break-even solving — every number computed with 40-digit decimal precision, so identical inputs always produce &lt;strong&gt;byte-identical output&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Connect (free, no install)
&lt;/h2&gt;

&lt;p&gt;It runs as a remote MCP server on Cloudflare's edge over Streamable HTTP:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scenariosim"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://scenariosim-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Free tier is 20 calls/day with no API key. Works in Cursor, Claude Desktop (via &lt;code&gt;mcp-remote&lt;/code&gt;), VS Code, Windsurf, and any Streamable-HTTP client.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six tools
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;run_scenario&lt;/code&gt;&lt;/strong&gt; — the main tool: project a template or a free-form model over time → per-period projections, headline key results, the exact assumptions used, methodology, and a plain-language explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;sensitivity_analysis&lt;/code&gt;&lt;/strong&gt; — vary one or more inputs and see the impact on a target metric, with an elasticity estimate and a "most influential" ranking.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;break_even&lt;/code&gt;&lt;/strong&gt; — solve for the input value that makes a metric hit a target (deterministic bisection).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;compare_scenarios&lt;/code&gt;&lt;/strong&gt; — run 2–3 scenarios side by side with deltas and an optional winner.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;list_templates&lt;/code&gt;&lt;/strong&gt; / &lt;strong&gt;&lt;code&gt;health_check&lt;/code&gt;&lt;/strong&gt; — discovery + status.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Nine templates (plus a free-form model)
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;saas_growth&lt;/code&gt;, &lt;code&gt;pricing_change&lt;/code&gt;, &lt;code&gt;churn_impact&lt;/code&gt;, &lt;code&gt;cost_reduction&lt;/code&gt;, &lt;code&gt;hiring_plan&lt;/code&gt;, &lt;code&gt;cash_runway&lt;/code&gt;, &lt;code&gt;unit_economics&lt;/code&gt;, &lt;code&gt;marketing_funnel&lt;/code&gt;, &lt;code&gt;compound_growth&lt;/code&gt; — or bring your own &lt;code&gt;metrics&lt;/code&gt; array for anything else.&lt;/p&gt;

&lt;h2&gt;
  
  
  Example: solve for the churn you can tolerate
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"break_even"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"arguments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"template"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"churn_impact"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"starting_customers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"new_customers_per_period"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"solve_for"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"churn_rate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"target_metric"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"retention_pct"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"target_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"horizon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;"To reach retention_pct = 0.9, 'churn_rate' must be 0.008742 (a change of -0.041258 from the baseline 0.05)."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Roughly 0.87%/month churn to keep 90% of customers over a year — solved exactly, with the residual reported.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why deterministic matters for agents
&lt;/h2&gt;

&lt;p&gt;Every response is one consistent JSON envelope — &lt;code&gt;status&lt;/code&gt;, &lt;code&gt;key_results&lt;/code&gt;, &lt;code&gt;projections&lt;/code&gt;, &lt;code&gt;assumptions_used&lt;/code&gt;, &lt;code&gt;methodology&lt;/code&gt;, &lt;code&gt;notes&lt;/code&gt;, &lt;code&gt;explanation&lt;/code&gt; — and errors come back as structured, actionable data (&lt;code&gt;{type, message, hint}&lt;/code&gt;), never raw exceptions. That's what lets an agent &lt;em&gt;reliably parse and act on&lt;/em&gt; the result instead of re-reading a prose paragraph.&lt;/p&gt;

&lt;p&gt;The engine is pure and stateless: no database, no sessions, no clocks, no randomness. Self-host it on Cloudflare Pages, Node, Deno, or Bun in one command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Open source
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live endpoint:&lt;/strong&gt; &lt;a href="https://scenariosim-mcp.pages.dev/mcp" rel="noopener noreferrer"&gt;https://scenariosim-mcp.pages.dev/mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Site + docs:&lt;/strong&gt; &lt;a href="https://scenariosim-mcp.pages.dev" rel="noopener noreferrer"&gt;https://scenariosim-mcp.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub (MIT):&lt;/strong&gt; &lt;a href="https://github.com/inity13/scenariosim-mcp" rel="noopener noreferrer"&gt;https://github.com/inity13/scenariosim-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP Registry:&lt;/strong&gt; &lt;code&gt;io.github.inity13/scenariosim-mcp&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agents make plans, give them a calculator that never drifts. Feedback welcome.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>opensource</category>
      <category>typescript</category>
    </item>
    <item>
      <title>DecisionMatrix MCP: give your AI agent a transparent, deterministic decision engine</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Mon, 10 Aug 2026 11:34:33 +0000</pubDate>
      <link>https://dev.to/datanestdigital/decisionmatrix-mcp-give-your-ai-agent-a-transparent-deterministic-decision-engine-imh</link>
      <guid>https://dev.to/datanestdigital/decisionmatrix-mcp-give-your-ai-agent-a-transparent-deterministic-decision-engine-imh</guid>
      <description>&lt;p&gt;Ask an AI agent to pick between three vendors, or a database, or a job offer, and it will happily give you an answer. Ask it to &lt;em&gt;weigh five options against six weighted criteria&lt;/em&gt; and it quietly falls apart: inconsistent weights, arithmetic that drifts, and no way to see how it got there. "Decision-making" is exactly the kind of multi-step scoring LLMs are bad at — and exactly the kind of thing you don't want a black box for.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;DecisionMatrix MCP&lt;/strong&gt; — a deterministic &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server that turns "which option is best?" into a transparent, reproducible calculation. You give it options and weighted criteria plus a score matrix; it returns a &lt;strong&gt;scored, ranked, and explained&lt;/strong&gt; result: the winner, the full ranking, per-criterion breakdowns, the method used, the weights applied, and a plain-language explanation. Every number runs through &lt;code&gt;decimal.js&lt;/code&gt; (&lt;strong&gt;never floats&lt;/strong&gt;), so identical inputs always produce identical output.&lt;/p&gt;

&lt;p&gt;It's live, free to start, and takes ~30 seconds to add.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add it to your agent
&lt;/h2&gt;

&lt;p&gt;Remote server over Streamable HTTP — no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://decisionmatrix-mcp.pages.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generic client (Cursor, etc.):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"decisionmatrix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://decisionmatrix-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude Desktop (via the &lt;code&gt;mcp-remote&lt;/code&gt; bridge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"decisionmatrix"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://decisionmatrix-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Listed in the official MCP Registry as &lt;code&gt;io.github.inity13/decisionmatrix-mcp&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;Six tools:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;create_decision&lt;/strong&gt; — the main one: rank options against weighted criteria, return the winner + full ranking + per-criterion breakdown + explanation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;score_options&lt;/strong&gt; — the normalized scored matrix and ranking, without the narrative&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;sensitivity_analysis&lt;/strong&gt; — how robust is the winner? Sweeps each criterion's weight ±20% and tells you which criteria could flip the result, and at what weight&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compare_two&lt;/strong&gt; — head-to-head of two options with a per-criterion breakdown&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;list_methods&lt;/strong&gt; / &lt;strong&gt;health_check&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Three scoring methods: &lt;strong&gt;weighted_sum&lt;/strong&gt;, &lt;strong&gt;weighted_product&lt;/strong&gt;, and &lt;strong&gt;TOPSIS&lt;/strong&gt; (distance to the ideal/anti-ideal solution). Criteria can be &lt;code&gt;benefit&lt;/code&gt; (higher is better) or &lt;code&gt;cost&lt;/code&gt; (lower is better).&lt;/p&gt;

&lt;h2&gt;
  
  
  What a call looks like
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;create_decision&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"options"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Postgres"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MongoDB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DynamoDB"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"criteria"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"direction"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cost"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scalability"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"team_familiarity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"weight"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scores"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"Postgres"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scalability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"team_familiarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"MongoDB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scalability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"team_familiarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"DynamoDB"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cost"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"scalability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"team_familiarity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back the winner, a ranked list with exact scores, a per-criterion breakdown showing where each option gained or lost, the weights used, and a sentence explaining &lt;em&gt;why&lt;/em&gt;. Change a weight and the result changes predictably — and you can prove it with &lt;code&gt;sensitivity_analysis&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why deterministic matters
&lt;/h2&gt;

&lt;p&gt;The whole point of offloading a decision to a tool is trust. DecisionMatrix is &lt;strong&gt;stateless&lt;/strong&gt; (no database, no sessions) and &lt;strong&gt;byte-for-byte reproducible&lt;/strong&gt;. The hosted endpoint is a Cloudflare Pages Function; the same engine also runs as a local stdio server you can self-host with a one-line Docker build. MIT licensed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — 15 calls/day, no key needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starter — $12/mo&lt;/strong&gt; — 5,000 calls/day&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro — $39/mo&lt;/strong&gt; — 50,000 calls/day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an agent hits the free limit, the tool returns a structured error with the checkout URL, so an autonomous agent can surface the paywall and the user is two clicks from a key. Prefer to self-host? It's open source with a Dockerfile — run it with unlimited calls and your own keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Site + docs: &lt;a href="https://decisionmatrix-mcp.pages.dev" rel="noopener noreferrer"&gt;https://decisionmatrix-mcp.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub (MIT): &lt;a href="https://github.com/inity13/decisionmatrix-mcp" rel="noopener noreferrer"&gt;https://github.com/inity13/decisionmatrix-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📇 &lt;code&gt;llms.txt&lt;/code&gt;: &lt;a href="https://decisionmatrix-mcp.pages.dev/llms.txt" rel="noopener noreferrer"&gt;https://decisionmatrix-mcp.pages.dev/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your agents make choices — vendor selection, architecture, prioritization, hiring — give it a try. I'm considering adding AHP (with a consistency ratio) and Pareto/efficiency-frontier tools next; tell me what you'd want.&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>showdev</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built an MCP server that gives AI agents exact, high-precision finance math</title>
      <dc:creator>DatanestDigital</dc:creator>
      <pubDate>Mon, 10 Aug 2026 11:13:46 +0000</pubDate>
      <link>https://dev.to/datanestdigital/i-built-an-mcp-server-that-gives-ai-agents-exact-high-precision-finance-math-40pl</link>
      <guid>https://dev.to/datanestdigital/i-built-an-mcp-server-that-gives-ai-agents-exact-high-precision-finance-math-40pl</guid>
      <description>&lt;p&gt;LLMs are shockingly bad at arithmetic. Ask an agent to chain a CAC payback with a churn-adjusted LTV, convert it to EUR, and discount three years of cash flows, and you will get an answer that &lt;em&gt;looks&lt;/em&gt; right and is quietly wrong. Floating point, dropped steps, and confident hallucination are a bad combination when the output is a number someone makes a decision on.&lt;/p&gt;

&lt;p&gt;So I built &lt;strong&gt;PrecisionCalc MCP&lt;/strong&gt; — a deterministic &lt;a href="https://modelcontextprotocol.io" rel="noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt; server that gives AI agents a calculator they can actually trust. Every monetary/financial value is computed with arbitrary-precision decimals (&lt;strong&gt;never floats&lt;/strong&gt;), and every response includes the exact value, the &lt;strong&gt;formula used&lt;/strong&gt;, the &lt;strong&gt;inputs&lt;/strong&gt;, the unit, and any assumptions — so the agent (and you) can audit it.&lt;/p&gt;

&lt;p&gt;It's live, free to start, and takes about 30 seconds to add.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add it to your agent
&lt;/h2&gt;

&lt;p&gt;It's a remote server over Streamable HTTP — no install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://precisioncalc-mcp.pages.dev/mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Cursor&lt;/strong&gt; (&lt;code&gt;~/.cursor/mcp.json&lt;/code&gt;) or any generic client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"precisioncalc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://precisioncalc-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Claude Desktop&lt;/strong&gt; (uses the &lt;code&gt;mcp-remote&lt;/code&gt; bridge):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"precisioncalc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mcp-remote"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://precisioncalc-mcp.pages.dev/mcp"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also works with VS Code, Windsurf, Cline, Zed, and anything speaking MCP. It's listed in the official MCP Registry as &lt;code&gt;io.github.inity13/precisioncalc-mcp&lt;/code&gt;.&lt;/p&gt;

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

&lt;p&gt;11 tools, all returning the same clean, parseable envelope:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;calculate_metric&lt;/strong&gt; — 14 SaaS/business metrics: LTV, CAC, LTV:CAC, payback, gross margin, churn, MRR growth, ARR, break-even units, NRR, GRR, Rule of 40, magic number&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;currency_convert&lt;/strong&gt; — 9 major currencies, live + historical ECB rates (with an offline fallback)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;business_days&lt;/strong&gt; — add/count/next/previous business days with US/UK/EU holidays + custom holidays&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compound_growth&lt;/strong&gt; — future value, present value, CAGR (7 compounding frequencies incl. continuous)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;net_present_value&lt;/strong&gt; / &lt;strong&gt;internal_rate_of_return&lt;/strong&gt; — NPV/DCF and IRR (Newton + bisection)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;loan_amortization&lt;/strong&gt; — payment, total interest, payoff, full schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;depreciation&lt;/strong&gt; — straight-line, declining-balance, sum-of-years-digits&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;batch_calculate&lt;/strong&gt;, &lt;strong&gt;list_metrics&lt;/strong&gt;, &lt;strong&gt;health_check&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why it's trustworthy
&lt;/h2&gt;

&lt;p&gt;A call to &lt;code&gt;net_present_value&lt;/code&gt; with &lt;code&gt;rate=0.10, cashflows=[-10000, 3000, 4200, 6800]&lt;/code&gt; returns:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"1307.2877535687..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formatted_value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"$1,307.29"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"formula"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"NPV = sum(CF_t / (1 + rate)^t) for t = 0..n"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"inputs_used"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"0.10"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"cashflows"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-10000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"3000"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"4200"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"6800"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"notes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Period 0 cashflow is not discounted."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The full-precision &lt;code&gt;value&lt;/code&gt; is serialized as a string so no precision is lost in JSON transport. The engine is pure and deterministic — same inputs, same output, every time. It ships with a unit-test suite plus Hypothesis property tests that assert invariants like PV↔FV round-trips and NPV(IRR) ≈ 0.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Canonical server:&lt;/strong&gt; Python 3.11+ using the &lt;code&gt;decimal&lt;/code&gt; module and the official MCP SDK. Runs over stdio or streamable HTTP, with optional API-key auth, rate limiting, structured logging, and OpenTelemetry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosted edge server:&lt;/strong&gt; a Cloudflare Pages Function that mirrors the Python engine in &lt;code&gt;decimal.js&lt;/code&gt; — verified with 17/17 exact output parity against the Python implementation. That's what powers the free public endpoint.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Free&lt;/strong&gt; — 15 calls/day, no key needed (static FX, no batch)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Starter — $12/mo&lt;/strong&gt; — 5,000 calls/day, live FX + batch&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro — $39/mo&lt;/strong&gt; — 50,000 calls/day&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When an agent hits the free limit, the tool returns a structured error containing the checkout URL — so an autonomous agent can surface the paywall and the user is two clicks from a key. Prefer to self-host? The whole thing is MIT-licensed with a Docker image and Fly.io/Render blueprints — run it with unlimited calls and your own keys.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🌐 Site + docs: &lt;a href="https://precisioncalc-mcp.pages.dev" rel="noopener noreferrer"&gt;https://precisioncalc-mcp.pages.dev&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 GitHub (MIT): &lt;a href="https://github.com/inity13/precisioncalc-mcp" rel="noopener noreferrer"&gt;https://github.com/inity13/precisioncalc-mcp&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📇 &lt;code&gt;llms.txt&lt;/code&gt;: &lt;a href="https://precisioncalc-mcp.pages.dev/llms.txt" rel="noopener noreferrer"&gt;https://precisioncalc-mcp.pages.dev/llms.txt&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you build agents that touch money, give it a try and tell me what tool you'd want next. I'm considering bond pricing, WACC, and options (Black-Scholes).&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
  </channel>
</rss>
