<?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: Prajwal Hallale</title>
    <description>The latest articles on DEV Community by Prajwal Hallale (@prajwal89).</description>
    <link>https://dev.to/prajwal89</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%2F1013346%2F480bb6bb-3a68-48ff-ba61-9316831aa17e.png</url>
      <title>DEV Community: Prajwal Hallale</title>
      <link>https://dev.to/prajwal89</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/prajwal89"/>
    <language>en</language>
    <item>
      <title>Your HAR file is a bug report. Turn one request into a cURL that runs</title>
      <dc:creator>Prajwal Hallale</dc:creator>
      <pubDate>Wed, 04 Nov 2026 09:48:06 +0000</pubDate>
      <link>https://dev.to/prajwal89/your-har-file-is-a-bug-report-turn-one-request-into-a-curl-that-runs-3fcd</link>
      <guid>https://dev.to/prajwal89/your-har-file-is-a-bug-report-turn-one-request-into-a-curl-that-runs-3fcd</guid>
      <description>&lt;p&gt;A screenshot of a failed request tells the next person almost nothing. A HAR capture tells them everything except which of the four hundred entries matters and how to replay it.&lt;/p&gt;

&lt;p&gt;There are two ways this handoff goes wrong: a message that says "it returns 500, here is a picture", and a 12 MB archive with no note attached. The second version gets fixed faster, but only if the person receiving it can turn the archive back into a command. That conversion is mechanical, and the mechanical part breaks in ways that look like product bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a HAR file actually contains
&lt;/h2&gt;

&lt;p&gt;A HAR is JSON. The spec defines &lt;code&gt;log.entries&lt;/code&gt;, and each entry holds one request and its response:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;log.entries[n].request   method, url, httpVersion, headers[], cookies[],
                         queryString[], postData{ mimeType, text, params[] }
log.entries[n].response  status, statusText, headers[], content{ size,
                         mimeType, text, encoding }, redirectURL
log.entries[n].timings   blocked, dns, connect, send, wait, receive, ssl
log.entries[n].time      total milliseconds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things in there are worth more than the rest. &lt;code&gt;request.postData.text&lt;/code&gt; is the exact body that was sent, byte for byte, which is usually what a screenshot cannot show. &lt;code&gt;response.content&lt;/code&gt; carries the bytes that came back, and when those bytes are not text the entry sets &lt;code&gt;content.encoding&lt;/code&gt; to &lt;code&gt;base64&lt;/code&gt; so the archive stays valid JSON.&lt;/p&gt;

&lt;h2&gt;
  
  
  Capture the small window, not the whole tab
&lt;/h2&gt;

&lt;p&gt;The archive is only as useful as the session you recorded it in, and a browsing session holds your cookies for everything you visited.&lt;/p&gt;

&lt;p&gt;Open a fresh window, log in, reproduce the failure once, stop. Then export from the Network panel. In Chrome, since version 130, the export is sanitized by default: &lt;code&gt;Cookie&lt;/code&gt;, &lt;code&gt;Set-Cookie&lt;/code&gt; and &lt;code&gt;Authorization&lt;/code&gt; headers are dropped unless you turn on Settings &amp;gt; Preferences &amp;gt; Network &amp;gt; "Allow to generate HAR with sensitive data" and pick the matching export option. That default is good for your secrets and bad for reproduction, because a request replayed without its auth header returns 401 and the bug report becomes a mystery again. Export the sensitive version on your own machine, then redact the specific values before it leaves your laptop. The steps further down do that part.&lt;/p&gt;

&lt;p&gt;Firefox exports the same full log from its own Network panel. Either way, the file includes response bodies, which is the part that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  Find the one entry
&lt;/h2&gt;

&lt;p&gt;Do not read the archive. Query it. This prints every entry that failed, with its index, which is the only number you need from here on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python3 - &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;PY&lt;/span&gt;&lt;span class="sh"&gt;'
import json
har = json.load(open("capture.har"))
for i, e in enumerate(har["log"]["entries"]):
    if e["response"]["status"] &amp;gt;= 400:
        print(i, e["response"]["status"], e["request"]["method"],
              e["request"]["url"][:110], e["time"], "ms")
&lt;/span&gt;&lt;span class="no"&gt;PY
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the failing call returned 200 with a wrong body, filter on the URL path instead of the status. Slow calls are the third case, and the &lt;code&gt;wait&lt;/code&gt; timing is the one that names them: &lt;code&gt;timings.wait&lt;/code&gt; is time to first byte, &lt;code&gt;timings.receive&lt;/code&gt; is the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  Convert it, with the fields a naive converter forgets
&lt;/h2&gt;

&lt;p&gt;Copying the request as a dictionary loses duplicate headers and picks up fields the client must set itself. This converter drops those and keeps everything else:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;DROP&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content-length&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;transfer-encoding&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;connection&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_curl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;chunks&lt;/span&gt; &lt;span class="o"&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;-X &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;method&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;upper&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url&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;header&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;req&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;headers&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="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;header&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;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DROP&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-H &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&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="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;value&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="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;post&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;req&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;postData&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&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;post&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;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--data-binary&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;post&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;mimeType&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;multipart/&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--data-raw&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;shlex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;

    &lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--compressed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;curl &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="se"&gt;\\\n&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;chunks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;har&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&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;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;to_curl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;har&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run against a POST entry captured from &lt;code&gt;httpbin.org/anything&lt;/code&gt;, it prints:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  https://httpbin.org/anything &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer sk_test_...'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s1"&gt;'Accept: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{"order_id": "ord_1042", "refund": true}'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--compressed&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That command replayed the request and returned the same JSON body, 537 bytes, with the server echoing the exact payload. The four decisions in the script are the ones that turn a broken replay into a working one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Content-Length&lt;/code&gt; is gone. The archive records the length the browser sent, and once you change the body, or the shell rewrites it, the header lies. A server waiting for 99 bytes that never arrive holds the connection until curl gives up (exit 28 over HTTP/1.1), and over HTTP/2 curl aborts the stream (exit 92). Neither error mentions the header that caused it.&lt;/p&gt;

&lt;p&gt;Pseudo-headers are gone. Entries from HTTP/2 sites carry &lt;code&gt;:authority&lt;/code&gt;, &lt;code&gt;:method&lt;/code&gt;, &lt;code&gt;:path&lt;/code&gt; and &lt;code&gt;:scheme&lt;/code&gt;. They are protocol instructions rather than headers, and curl maps or discards them depending on the target protocol, so a command that behaves locally can behave differently on a colleague's machine. Anything starting with a colon is not a header.&lt;/p&gt;

&lt;p&gt;Duplicates survive because headers are a list. Building a dict collapses repeated &lt;code&gt;Accept&lt;/code&gt; or &lt;code&gt;Cookie&lt;/code&gt; values and silently changes what the server sees.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--compressed&lt;/code&gt; is added because browsers send &lt;code&gt;Accept-Encoding&lt;/code&gt; and expect a compressed response. Without it the body may arrive as bytes that look like garbage in your terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Redact before you paste it into a ticket
&lt;/h2&gt;

&lt;p&gt;Sanitizing by hand is how tokens end up in issue trackers. Walk the headers and replace values instead:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;SECRET&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cookie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;set-cookie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-api-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="n"&gt;har&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capture.har&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;entry&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;har&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entries&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;header&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request&lt;/span&gt;&lt;span class="sh"&gt;"&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;headers&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="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&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;headers&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;header&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="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SECRET&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;header&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;value&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;REDACTED&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;har&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capture.redacted.har&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;w&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Keep the header names. A maintainer needs to know an &lt;code&gt;Authorization&lt;/code&gt; header was present and roughly what shape it had, not its value. Redact query strings too, since analytics and API keys travel there often, and check &lt;code&gt;postData.text&lt;/code&gt; for a password or a session token before attaching the file.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the body is not text
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;content.encoding&lt;/code&gt; is &lt;code&gt;base64&lt;/code&gt;, the response is binary and the text field is an encoded blob. Decode it to a real file rather than reading base64 out loud:&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="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;
&lt;span class="n"&gt;har&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capture.har&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;har&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;log&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;entries&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;response&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;content&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;content&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;encoding&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;base64&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;payload.bin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;text&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The round trip is worth checking once on a known file. Decoding a 482,855 byte PNG back from its base64 text produced an identical hash. For one-off decoding without a terminal, &lt;a href="https://onlineminitools.com/base64-to-file" rel="noopener noreferrer"&gt;Base64 to file&lt;/a&gt; does the same thing in the browser and guesses the MIME type and filename.&lt;/p&gt;

&lt;p&gt;Multipart uploads are the other shape worth knowing. The entry still holds &lt;code&gt;postData.text&lt;/code&gt; with the full boundary and parts, so the &lt;code&gt;--data-binary&lt;/code&gt; branch in the converter reproduces the request as long as you keep the &lt;code&gt;Content-Type&lt;/code&gt; header that carries the boundary. Do not strip it.&lt;/p&gt;

&lt;h2&gt;
  
  
  GraphQL requests are just POSTs
&lt;/h2&gt;

&lt;p&gt;A GraphQL call in a HAR looks like an ordinary POST to &lt;code&gt;/graphql&lt;/code&gt; with a JSON body holding &lt;code&gt;query&lt;/code&gt;, &lt;code&gt;variables&lt;/code&gt; and &lt;code&gt;operationName&lt;/code&gt;. Replaying it is the same command. What usually goes wrong is that people read the &lt;code&gt;operationName&lt;/code&gt; and hand-write a smaller query, which changes the response. Keep the captured query, including the fields you do not recognise, or the response will differ for a reason that has nothing to do with the bug. If you are composing one from scratch rather than replaying one, &lt;a href="https://onlineminitools.com/graphql-to-curl" rel="noopener noreferrer"&gt;GraphQL to cURL&lt;/a&gt; builds the POST body for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you would rather not run a script
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://onlineminitools.com/har-to-curl" rel="noopener noreferrer"&gt;Convert HAR to cURL&lt;/a&gt; takes the archive, or a single entry copied out of it, and prints cURL commands in the browser, which is handy when the archive is already on someone else's machine. For reading the archive itself, &lt;a href="https://onlineminitools.com/json-formatter" rel="noopener noreferrer"&gt;JSON formatter&lt;/a&gt; collapses a 12 MB file into something you can navigate. Those pages process the input in the browser rather than uploading it, which matters when a redacted capture is still sensitive.&lt;/p&gt;

&lt;h2&gt;
  
  
  What goes in the report
&lt;/h2&gt;

&lt;p&gt;The archive, redacted. The one-line cURL, replayed on your machine with the status and the response time you got. And one sentence naming what you expected instead. Three items, and the person reading it can reproduce the failure in under a minute without asking you a question.&lt;/p&gt;

&lt;p&gt;The archive is not the report. The command is the report. The file is the evidence behind it.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>api</category>
      <category>debugging</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why 2.675 Rounds to 2.67: A Field Guide to Money Math Bugs</title>
      <dc:creator>Prajwal Hallale</dc:creator>
      <pubDate>Sun, 01 Nov 2026 18:27:18 +0000</pubDate>
      <link>https://dev.to/prajwal89/why-2675-rounds-to-267-a-field-guide-to-money-math-bugs-5eg9</link>
      <guid>https://dev.to/prajwal89/why-2675-rounds-to-267-a-field-guide-to-money-math-bugs-5eg9</guid>
      <description>&lt;p&gt;Open a Python REPL and type three lines:&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="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;
&lt;span class="mf"&gt;0.30000000000000004&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="mf"&gt;2.67&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;3.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All three results look wrong. None of them is a bug in Python. Each one follows a rule that is working exactly as designed, and the rules are not the ones most people assume their computer uses. Money is where those assumptions get expensive.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov28totp1lygpctxfjl5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fov28totp1lygpctxfjl5.png" alt="Infographic: why 2.675 rounds to 2.67, showing binary float precision, three rounding rules, and three production failures" width="800" height="1000"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  0.1 has no exact binary form
&lt;/h2&gt;

&lt;p&gt;Computers store fractional numbers in base 2, the same way we write integers in base 10. A value like 0.125 is easy in binary: it is 1/8, one bit after the point. A value like 0.1 is not. Its binary expansion repeats forever:&lt;br&gt;
&lt;/p&gt;

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

&lt;/div&gt;



&lt;p&gt;Stop at any finite number of bits and you have an approximation. A Python float keeps 53 bits of precision, so the stored value of 0.1 is the fraction 3602879701896397 / 2**55, which is close to 1/10 but not equal to it. The &lt;a href="https://docs.python.org/3/tutorial/floatingpoint.html" rel="noopener noreferrer"&gt;Python tutorial&lt;/a&gt; prints the exact stored value:&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="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;from_float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;0.1000000000000000055511151231257827021181583404541015625&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;Add two of those approximations and the error surfaces at the 17th digit, which is why the result prints as 0.30000000000000004. JavaScript uses the same IEEE 754 binary64 format and returns the same answer.&lt;/p&gt;

&lt;p&gt;This is also why 2.675 rounds down. The literal 2.675 is stored as 2.67499999999999982236431605997495353221893310546875, a number strictly below 2.675. Any rounding rule applied to it will produce 2.67, because that is the correct rounding of the value actually in memory.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding is a policy, and languages disagree
&lt;/h2&gt;

&lt;p&gt;Representation explains the first problem. The second one is that "round" is not a single operation. Python 3 rounds halfway cases to the nearest even integer, a rule called banker's rounding. That is why &lt;code&gt;round(2.5)&lt;/code&gt; is 2 and &lt;code&gt;round(3.5)&lt;/code&gt; is 4. The &lt;a href="https://docs.python.org/3/library/functions.html#round" rel="noopener noreferrer"&gt;language reference&lt;/a&gt; states it directly: when two multiples are equally close, the result goes to the even choice.&lt;/p&gt;

&lt;p&gt;JavaScript rounds halfway cases toward positive infinity. The &lt;a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round" rel="noopener noreferrer"&gt;MDN documentation&lt;/a&gt; notes that this differs from languages that round half away from zero, and the asymmetry shows up on negative numbers:&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="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// 1&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// -0&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&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;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// -Infinity&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then &lt;code&gt;toFixed&lt;/code&gt; adds a third behavior, because it rounds the decimal expansion of the stored binary value. That produces results that look like a different bug entirely:&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="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;2.675&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '2.67'&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.005&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// '1.00'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of these engines is wrong. They implement different rounding policies, and each policy is reasonable until the number is a price, a tax line, or an invoice total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three failures that reached production
&lt;/h2&gt;

&lt;h3&gt;
  
  
  The Vancouver Stock Exchange index, 1982 to 1983
&lt;/h3&gt;

&lt;p&gt;The exchange started an index at 1000 in January 1982 and updated it after every trade. Each update was truncated to three decimal places, and the truncated value was used to compute the next one. With roughly 3,000 updates a day, the truncation compounded into a loss of about 25 points per month. By late November 1983 the published index read 524.811. Correcting the arithmetic over the weekend of November 25 to 28 raised it to 1098.892. The index had been understating the market by more than half for nearly two years, not because of a crash, but because a rounding error was fed back into its own input. &lt;a href="https://en.wikipedia.org/wiki/Vancouver_Stock_Exchange" rel="noopener noreferrer"&gt;Wikipedia has the figures&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The lesson is not "use more decimals". The lesson is that a rounded value must never become the input to the next calculation in a chain.&lt;/p&gt;

&lt;h3&gt;
  
  
  The Patriot missile battery, 1991
&lt;/h3&gt;

&lt;p&gt;A Patriot battery in Dhahran tracked time in tenths of a second. The value 0.1 was stored in a 24-bit register, chopping the repeating binary expansion and introducing an error of about 0.000000095 seconds per tenth. That error was multiplied by the number of tenths since boot. After roughly 100 hours of uptime the clock was off by about 0.34 seconds, which was enough for the system to miscalculate a Scud's position. The missile was not intercepted, and 28 soldiers died. The &lt;a href="https://www.gao.gov/assets/imtec-92-26.pdf" rel="noopener noreferrer"&gt;GAO report&lt;/a&gt; is public, and the numerical walkthrough is widely taught.&lt;/p&gt;

&lt;p&gt;The failure mode here is a small bias in one direction, accumulated over a long time. Financial systems have the same shape: a per-transaction rounding bias of one cent is invisible in a test and material after ten million transactions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Excel 2007 and the number 65,535
&lt;/h3&gt;

&lt;p&gt;In September 2007, users found that &lt;code&gt;=77.1*850&lt;/code&gt; displayed 100000 instead of 65535. Microsoft's own &lt;a href="https://web.archive.org/web/20071006040406/http://blogs.msdn.com/excel/archive/2007/09/25/calculation-issue-update.aspx" rel="noopener noreferrer"&gt;post-mortem&lt;/a&gt; confirmed the pattern: many calculations that should have produced 65,535 displayed 100000, while others such as &lt;code&gt;=65535*1&lt;/code&gt; worked. The bug sat at the boundary where the calculated value was converted to the text shown on screen, and it was patched.&lt;/p&gt;

&lt;p&gt;This one is about the display layer rather than the calculation. The stored value was correct; the text rendered from it was not, and for a spreadsheet user the number on screen is the number that matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  What actually holds up
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Store money as integers in the smallest unit
&lt;/h3&gt;

&lt;p&gt;Stripe's API makes the rule explicit: the &lt;code&gt;amount&lt;/code&gt; field is "a positive integer representing how much to charge in the smallest currency unit" (see the &lt;a href="https://docs.stripe.com/api/charges/object" rel="noopener noreferrer"&gt;Charge object&lt;/a&gt;). 100 means $1.00, and a zero-decimal currency like JPY is already in its smallest unit. Integer arithmetic inside a safe range is exact, which removes representation error from every addition, subtraction, and comparison. JavaScript's safe integer limit is 9,007,199,254,740,991, or about $90 trillion in cents, which is enough for any ordinary ledger.&lt;/p&gt;

&lt;h3&gt;
  
  
  Use a decimal type when the language has one
&lt;/h3&gt;

&lt;p&gt;Python's &lt;code&gt;decimal&lt;/code&gt; module and Java's &lt;code&gt;BigDecimal&lt;/code&gt; store decimal digits rather than binary fractions, so 0.1 is exactly 0.1. The catch is that the rounding mode is an argument, not a default you can ignore:&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;decimal&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;quantize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;0.01&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rounding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;ROUND_HALF_UP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.675&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Decimal('2.68')
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass a string, not a float. &lt;code&gt;Decimal(2.675)&lt;/code&gt; imports the representation error and gives 2.67499999999999982236431605997495353221893310546875, which rounds back to 2.67. &lt;code&gt;Decimal("2.675")&lt;/code&gt; is the decimal you meant.&lt;/p&gt;

&lt;p&gt;Pick the rounding mode the domain requires. Tax authorities usually specify half-up or a floor at a particular step. Banks often use half-even. The important part is that the choice is written down and tested, not inherited from whichever language happened to run the code.&lt;/p&gt;

&lt;h3&gt;
  
  
  Round once, at the boundary
&lt;/h3&gt;

&lt;p&gt;Compute in exact units, and round only when you produce a value a human will see or a system will store as a final amount. Interest accrual, allocation, and tax calculations should run on unrounded values; the invoice line rounds once. If you round at every step, the errors compound in the same way as the Vancouver index.&lt;/p&gt;

&lt;h3&gt;
  
  
  Allocate the remainder instead of losing it
&lt;/h3&gt;

&lt;p&gt;Splitting 100.00 three ways does not divide evenly. Rounding each share independently gives 33.33 three times, and one cent vanishes. The largest remainder method assigns the leftover units to the first shares:&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;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;divmod&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total_cents&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;remainder&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&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;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parts&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [3334, 3333, 3333]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sum always equals the total, which is the property an accounting system actually checks.&lt;/p&gt;

&lt;h3&gt;
  
  
  Test the invariants, not just the examples
&lt;/h3&gt;

&lt;p&gt;Two assertions catch most money bugs before they ship:&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;assert&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nf"&gt;money&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.675&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="nc"&gt;Decimal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;2.68&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first one is a property: every split sums back to its input. The second one pins the rounding policy in a place where a language upgrade or a refactor will break the build if it changes. Add a case for negative amounts, because refunds and credits are where half-up and half-even diverge most visibly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Binary floats cannot represent most decimal fractions exactly. That is arithmetic, not a language defect.&lt;/li&gt;
&lt;li&gt;Rounding modes differ between languages and between industries. Choose one, name it, and test it.&lt;/li&gt;
&lt;li&gt;Never feed a rounded value into the next calculation. Round at the boundary.&lt;/li&gt;
&lt;li&gt;Integers in the smallest unit, or a decimal type, for anything that has to balance.&lt;/li&gt;
&lt;li&gt;Split totals with a remainder method so the parts still sum to the whole.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single cent looks like noise. The same cent, applied a few million times without a rule, is a line item nobody can reconcile.&lt;/p&gt;

</description>
      <category>python</category>
      <category>javascript</category>
      <category>fintech</category>
      <category>programming</category>
    </item>
    <item>
      <title>Your best engineer writes the least code. Standup makes them look useless.</title>
      <dc:creator>Prajwal Hallale</dc:creator>
      <pubDate>Sun, 01 Nov 2026 18:25:27 +0000</pubDate>
      <link>https://dev.to/prajwal89/your-best-engineer-writes-the-least-code-standup-makes-them-look-useless-320</link>
      <guid>https://dev.to/prajwal89/your-best-engineer-writes-the-least-code-standup-makes-them-look-useless-320</guid>
      <description>&lt;p&gt;One person in standup lists nine tickets. Another says they spent the day reading. Six months later, the area the second person owns has no incidents, no midnight pages and no rewrite.&lt;/p&gt;

&lt;p&gt;The board rewards the first person. The system rewards the second one. Those are not the same measurement, and the gap explains a lot of bad promotion decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deletion does not produce a ticket
&lt;/h2&gt;

&lt;p&gt;The visible unit of work is code added. The valuable unit is often code removed: a feature nobody uses, a service that existed for one customer, a dependency that pulled in forty more, a config flag that has been true since 2023.&lt;/p&gt;

&lt;p&gt;Deleting all of that shows up as a small diff and a line in the changelog. It also removes a permanent maintenance cost. Nobody puts "deleted 4,000 lines" on a performance review, which is why so few people optimise for it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8vt4ymidp9uz97tpavf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fn8vt4ymidp9uz97tpavf.jpg" alt="Two scoreboards: what standup measures and what the system rewards" width="800" height="367"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The first day is reading, not typing
&lt;/h2&gt;

&lt;p&gt;A senior engineer starts by finding out where the data comes from, who calls the function, what the last three commits touched and whether the bug reports share a cause. That looks like nothing is happening. It is the cheapest way to avoid building the wrong thing quickly.&lt;/p&gt;

&lt;p&gt;Juniors are usually told to start coding to show progress. The advice is well meant and expensive. Progress that has to be thrown away is not progress, it is a rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cheapest bug is the feature you refuse
&lt;/h2&gt;

&lt;p&gt;Scope negotiation gets a bad name because it sounds like obstruction. In practice, asking "do we need this at all" is the most valuable review available. A shipped feature costs maintenance, documentation, support, tests and attention for as long as the product exists. A feature that never ships costs one conversation.&lt;/p&gt;

&lt;p&gt;The engineer who kills a bad idea in a planning meeting has saved more work than the one who ships it cleanly. Only one of them has something to demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Boring choices are a feature
&lt;/h2&gt;

&lt;p&gt;Postgres. A cron job. A single server. A monolith. The senior picks whatever the team can operate at 3am, not whatever looks best in a design doc. Interesting technology is a cost that gets paid by whoever is on call, usually someone who did not choose it.&lt;/p&gt;

&lt;p&gt;This is also why the most experienced person in the room is often the least excited about the new framework. They have watched a migration before, and they remember who did the work.&lt;/p&gt;

&lt;h2&gt;
  
  
  The work with no diff
&lt;/h2&gt;

&lt;p&gt;A lot of senior output never reaches a commit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Answering the question that stops a junior from building the wrong abstraction.&lt;/li&gt;
&lt;li&gt;Reviewing the pull request that would have taken production down.&lt;/li&gt;
&lt;li&gt;Writing the paragraph in the README that ends ten future questions.&lt;/li&gt;
&lt;li&gt;Fixing the flaky test everyone else retries.&lt;/li&gt;
&lt;li&gt;Sitting with the new hire until the codebase stops feeling hostile.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that appears in a commit graph. All of it compounds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to tell slow from senior
&lt;/h2&gt;

&lt;p&gt;Both look the same in a standup. The difference is what the system looks like afterwards. Drift leaves the mess where it was and adds a bit more. Seniority leaves the system quieter: fewer alerts, fewer surprises at release, fewer places where one person is the only one who understands it.&lt;/p&gt;

&lt;p&gt;So ask three questions about the last month: what did you remove, what did you talk the team out of, and who did you unblock? If all three answers are "nothing", it is not seniority you are looking at. It is someone waiting for a ticket.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaz8nn4s1zhs50oo65v6.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fsaz8nn4s1zhs50oo65v6.jpg" alt="Three questions to ask about last month: what did you remove, what did you talk the team out of, who did you unblock" width="799" height="383"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The obvious counterpoint
&lt;/h2&gt;

&lt;p&gt;None of this excuses missing deadlines. A senior who ships nothing is just expensive, and "I was thinking about architecture" is not a deliverable. Output still matters. The argument is about where the effort lands: less new surface area, more reliability, fewer things that need babysitting later.&lt;/p&gt;

&lt;p&gt;The best engineer on your team probably cannot prove what they did last sprint. Look at what stopped breaking.&lt;/p&gt;

&lt;p&gt;What is the last thing you deleted at work? The answers usually say more about a team than any velocity chart.&lt;/p&gt;

</description>
      <category>career</category>
      <category>productivity</category>
      <category>discuss</category>
    </item>
    <item>
      <title>How to compute the real APR on a loan (derivation + 20 lines of Python)</title>
      <dc:creator>Prajwal Hallale</dc:creator>
      <pubDate>Sun, 01 Nov 2026 17:48:21 +0000</pubDate>
      <link>https://dev.to/prajwal89/how-to-compute-the-real-apr-on-a-loan-derivation-20-lines-of-python-12m0</link>
      <guid>https://dev.to/prajwal89/how-to-compute-the-real-apr-on-a-loan-derivation-20-lines-of-python-12m0</guid>
      <description>&lt;p&gt;Two loan offers land in your inbox. Both say 6.5%. One has a $1,200 origination fee and the other has none. The monthly payment on the first is not a cent higher than on the second, because the fee is taken out of the money before it reaches you. Compare the quoted rates and the two offers look identical. On a $20,000 five-year loan, the fee turns 6.5% into a 9.12% APR.&lt;/p&gt;

&lt;p&gt;The APR is the number that makes them comparable, and it is just the internal rate of return on the loan's cash flows. This post derives it, implements it, and shows the two places where a quoted rate misleads: front-loaded fees and flat-rate interest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the quoted rate actually is
&lt;/h2&gt;

&lt;p&gt;A loan quote gives you a nominal annual rate &lt;code&gt;r&lt;/code&gt; and a term of &lt;code&gt;n&lt;/code&gt; months. The payment follows the annuity formula:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment = P * (r/12) / (1 - (1 + r/12)^(-n))
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For $20,000 at 6.5% over 60 months that is $391.32 a month, or $23,479.38 in total, of which $3,479.38 is interest. So far so ordinary. The quoted rate prices the principal on the assumption that you receive all of it.&lt;/p&gt;

&lt;h2&gt;
  
  
  APR is the rate where the cash flows net to zero
&lt;/h2&gt;

&lt;p&gt;Now add a $1,200 origination fee, deducted at closing. You still owe payments on $20,000, but only $18,800 ever hits your account. Economically you borrowed $18,800 and are paying $391.32 a month for 60 months.&lt;/p&gt;

&lt;p&gt;The APR is the annualised discount rate that makes the present value of those payments equal the money you actually received:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;18,800 = sum(payment / (1 + i)^k)    for k = 1..60
APR = 12 * i
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no closed-form solution for &lt;code&gt;i&lt;/code&gt;, since the equation is a polynomial of degree 60, but there is exactly one positive root: the present value falls monotonically as &lt;code&gt;i&lt;/code&gt; rises, and that is all a bisection search needs.&lt;/p&gt;

&lt;p&gt;Using &lt;code&gt;12 * i&lt;/code&gt; rather than compounding to &lt;code&gt;(1+i)^12 - 1&lt;/code&gt; is deliberate. US Regulation Z defines APR as a nominal annual rate, so a monthly IRR is multiplied by twelve. Comparing it to an APY from a savings account is apples to oranges: APY compounds and APR does not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The implementation
&lt;/h2&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;payment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;principal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annual_rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Fixed monthly payment on an amortising loan.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;annual_rate&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;principal&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;apr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;net_advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monthly_payment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;months&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Annualised IRR of the loan&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s cash flows, as a decimal (0.0912 = 9.12%).&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;3.0&lt;/span&gt;                      &lt;span class="c1"&gt;# 0% .. 300% monthly, plenty of room
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;                   &lt;span class="c1"&gt;# 200 halvings is far past float precision
&lt;/span&gt;        &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monthly_payment&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&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;span class="n"&gt;months&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;pv&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;net_advanced&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;              &lt;span class="c1"&gt;# rate too low: payments are worth too much
&lt;/span&gt;            &lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;hi&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;lo&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;hi&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details matter in that function. Bisection rather than Newton, because &lt;code&gt;pv(i)&lt;/code&gt; is smooth and strictly decreasing, so the search cannot diverge; a hundred iterations is already overkill and two hundred costs microseconds. And &lt;code&gt;net_advanced&lt;/code&gt; is the money you can spend, not the face value. Fees deducted at closing shrink it, and that is the whole mechanism by which APR exceeds the quoted rate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The worked example
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;payment(20_000, 0.065, 60)                  -&amp;gt; 391.32
apr(20_000, 391.32, 60)                     -&amp;gt; 0.0650   (6.50%)
apr(18_800, 391.32, 60)                     -&amp;gt; 0.0912   (9.12%)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same $1,200 fee, at the same rate, priced against different terms:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Term&lt;/th&gt;
&lt;th&gt;Payment&lt;/th&gt;
&lt;th&gt;APR, no fee&lt;/th&gt;
&lt;th&gt;APR, $1,200 fee&lt;/th&gt;
&lt;th&gt;APR, $2,400 fee&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;36 mo&lt;/td&gt;
&lt;td&gt;$612.98&lt;/td&gt;
&lt;td&gt;6.50%&lt;/td&gt;
&lt;td&gt;10.72%&lt;/td&gt;
&lt;td&gt;15.33%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;48 mo&lt;/td&gt;
&lt;td&gt;$474.30&lt;/td&gt;
&lt;td&gt;6.50%&lt;/td&gt;
&lt;td&gt;9.72%&lt;/td&gt;
&lt;td&gt;13.24%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;60 mo&lt;/td&gt;
&lt;td&gt;$391.32&lt;/td&gt;
&lt;td&gt;6.50%&lt;/td&gt;
&lt;td&gt;9.12%&lt;/td&gt;
&lt;td&gt;11.98%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;72 mo&lt;/td&gt;
&lt;td&gt;$336.20&lt;/td&gt;
&lt;td&gt;6.50%&lt;/td&gt;
&lt;td&gt;8.71%&lt;/td&gt;
&lt;td&gt;11.13%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things show up in that table. A fee hurts more on a short loan, because it is spread over fewer dollars of interest, so the effective price per borrowed dollar rises. At 36 months the same $1,200 fee is worth 4.2 percentage points of APR; at 72 months it is worth 2.2. That is why "no fee, slightly higher rate" is usually the better trade on a short term and often the worse one on a long term.&lt;/p&gt;

&lt;p&gt;The other is that APR measures price, not the bill. The 72-month loan has the lowest APR of the fee-paying rows and the highest total interest ($4,206 versus $3,479 at 60 months). APR answers how expensive each dollar-year of borrowing is, not how much you will pay in total. You need both numbers: APR to compare offers, total cost to decide whether to borrow at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap two: the flat rate
&lt;/h2&gt;

&lt;p&gt;Some lenders, car finance and personal loans especially, quote a flat rate: interest is charged on the original principal for the whole term, and the monthly payment is &lt;code&gt;(principal + principal * rate * years) / months&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;6.5% flat on $20,000 over 60 months
interest = 20,000 * 0.065 * 5      = $6,500
payment  = 26,500 / 60             = $441.67
apr(20_000, 441.67, 60)            = 11.68%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same "6.5%" is nearly twice as expensive as a 6.5% reducing-balance loan, because flat-rate interest ignores that the balance falls every month. Add the $1,200 fee and the APR reaches 14.43%. Any quote expressed as a flat rate has to be converted before it can be compared with anything, and the conversion is the same twenty lines above.&lt;/p&gt;

&lt;h2&gt;
  
  
  What APR leaves out
&lt;/h2&gt;

&lt;p&gt;APR is a good comparator with a narrow definition. It does not include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Late fees and penalty rates, which are contingent and therefore outside the finance charge.&lt;/li&gt;
&lt;li&gt;Optional products such as payment protection, gap insurance and extended warranties, unless they are required to get the loan.&lt;/li&gt;
&lt;li&gt;Rate resets on variable loans. The APR of an adjustable-rate mortgage is computed from the initial rate and an assumed schedule; it is not a prediction.&lt;/li&gt;
&lt;li&gt;Prepayment. The calculation assumes you hold the loan to maturity. Pay it off early and the fee is spread over a shorter period, which raises the effective cost above the quoted APR.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also varies by jurisdiction. Reg Z in the US, the APRC under the EU Consumer Credit Directive and the UK's APR all use slightly different fee baskets and rounding rules. The core identity, net proceeds equal the present value of payments, is the same everywhere.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checking a real quote
&lt;/h2&gt;

&lt;p&gt;To run a quote through this without writing code, the &lt;a href="https://financecalckit.com/tools/apr-calculator" rel="noopener noreferrer"&gt;APR calculator&lt;/a&gt; on Finance Calc Kit takes the loan amount, rate, term and fees and prints the APR together with the formula and each step of the cash-flow table; the &lt;a href="https://financecalckit.com/tools/emi-calculator" rel="noopener noreferrer"&gt;EMI calculator&lt;/a&gt; gives the payment and amortisation schedule. Both run client-side, so the numbers stay in the browser, and neither asks for an account. I build Finance Calc Kit, and the code above is the same maths the calculator runs, so you can check it against your own numbers.&lt;/p&gt;

&lt;p&gt;The rule I use on any loan offer: compare APR first, because it is the only figure that prices fees; compare total payments second, because a lower APR over a longer term can still cost more; then go back to the fee list and ask which fees you can negotiate away. Every dollar removed from the fee moves the APR down more than a dollar of rate does.&lt;/p&gt;

&lt;p&gt;If you want to check the implementation against your own loan, the function is short enough to paste into a REPL. If the APR a lender quotes you disagrees with the one it returns, ask which fees they left out of the finance charge.&lt;/p&gt;

</description>
      <category>python</category>
      <category>fintech</category>
      <category>programming</category>
    </item>
    <item>
      <title>Before you install that MCP server, check these six things</title>
      <dc:creator>Prajwal Hallale</dc:creator>
      <pubDate>Tue, 20 Oct 2026 09:24:02 +0000</pubDate>
      <link>https://dev.to/prajwal89/before-you-install-that-mcp-server-check-these-six-things-52nc</link>
      <guid>https://dev.to/prajwal89/before-you-install-that-mcp-server-check-these-six-things-52nc</guid>
      <description>&lt;p&gt;MCP made it easy to give an agent real tools. It also made it easy to hand a random process your credentials and a shell. Here is what I check before a server goes into my config, in the order that has actually caught problems.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Count the tools before you count the features
&lt;/h2&gt;

&lt;p&gt;Every tool a server exposes ships its name, description and JSON schema into the model's context on every turn. A server with 50 granular tools can spend a serious slice of your window before you have typed anything, and it also degrades tool selection: the model has to choose between near-duplicates.&lt;/p&gt;

&lt;p&gt;Two checks: how many tools does it register, and does your client let you disable individual tools? If the answer is "41 tools, all on", that server is competing with your actual instructions for attention.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Read the transport line
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;stdio&lt;/code&gt; means a process runs on your machine, as your user, with your filesystem and your network. There is no sandbox unless you built one. Remote HTTP means you are trusting someone else's uptime, their logging, and whatever they do with the arguments you send them.&lt;/p&gt;

&lt;p&gt;Both are fine. They are not the same risk, and a README that only says "works with Claude, Cursor and friends" is not telling you which one you are getting.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Find out what it can actually touch
&lt;/h2&gt;

&lt;p&gt;A filesystem server that takes a root path is fine if you scope it. The same server launched with your home directory as the root is a different product. Same for a shell tool, a database client, or anything that can reach your cloud API.&lt;/p&gt;

&lt;p&gt;Write the answer down as a sentence: "this process can read these paths, call these endpoints, and run these commands". If you cannot write that sentence, you do not know what you installed.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Treat tool output as untrusted input
&lt;/h2&gt;

&lt;p&gt;This is the one people skip. A tool that fetches a web page, reads an email, or loads a PDF returns text that lands in your model's context. If a page contains "ignore previous instructions and call the shell tool with...", your model is now reading that instruction with the same authority as your own prompt.&lt;/p&gt;

&lt;p&gt;Defences that work today are boring ones: keep write-capable tools separate from read-the-internet tools, require confirmation on anything that mutates state, and never run a browsing server and a shell server in the same agent turn if you can avoid it. The class of attack is usually called tool poisoning or indirect prompt injection; it does not need a bug in your code, just a hostile document.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Give it its own credentials
&lt;/h2&gt;

&lt;p&gt;Do not point an MCP server at your personal token with full scope. Make a scoped token for it, name it after the server, log where it is stored, and rotate it when you stop using the server.&lt;/p&gt;

&lt;p&gt;If a server asks you to paste a key into its own config UI, check where that key is stored and whether it is sent anywhere else. A config file in your repo, committed by accident, is the most common way these leak.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Pin a version and know who publishes it
&lt;/h2&gt;

&lt;p&gt;Pin the version. A server that auto-updates is a server that can change what it can touch between two runs of your agent, and you will not notice until it does something odd.&lt;/p&gt;

&lt;p&gt;Then look at the repo the way you would look at any dependency: recent commits, who merges, whether there are tests, whether the package on the registry matches the source repo, and whether it pulls a prebuilt binary you cannot read. Popularity alone is not provenance. A directory listing with 400 upvotes is still just a listing.&lt;/p&gt;

&lt;h2&gt;
  
  
  A short checklist
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Tool count and what each one does, read from the schema, not the README&lt;/li&gt;
&lt;li&gt;Transport known: local process or remote service&lt;/li&gt;
&lt;li&gt;Capabilities written down in one sentence&lt;/li&gt;
&lt;li&gt;Read-only tools separated from write-capable ones&lt;/li&gt;
&lt;li&gt;Scoped token, stored somewhere you can name&lt;/li&gt;
&lt;li&gt;Version pinned, publisher checked&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this takes long. The first time a server surprises you, it takes a lot longer.&lt;/p&gt;

&lt;p&gt;Full disclosure: I run &lt;a href="https://eliteai.tools" rel="noopener noreferrer"&gt;EliteAI.tools&lt;/a&gt;, a curated directory of AI tools, which has an MCP section listing servers with their descriptions alongside an index of agent skills pulled from public GitHub repos. It is one place to start looking, not a substitute for the six checks above.&lt;/p&gt;

&lt;p&gt;If you have a server that passed all six and earned its place in your config, I would like to hear which one and why.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>llm</category>
      <category>mcp</category>
      <category>security</category>
    </item>
  </channel>
</rss>
