<?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: Ujwal Vanjare</title>
    <description>The latest articles on DEV Community by Ujwal Vanjare (@ujwal240).</description>
    <link>https://dev.to/ujwal240</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%2F3717448%2Fc1944409-c9df-4fa3-a96a-9b4df814231e.png</url>
      <title>DEV Community: Ujwal Vanjare</title>
      <link>https://dev.to/ujwal240</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ujwal240"/>
    <language>en</language>
    <item>
      <title>The request has to end before the work does</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Fri, 28 Aug 2026 22:53:29 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-thought-i-had-built-an-autonomous-agent-i-had-built-a-very-long-function-call-3io9</link>
      <guid>https://dev.to/ujwal240/i-thought-i-had-built-an-autonomous-agent-i-had-built-a-very-long-function-call-3io9</guid>
      <description>&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon.&lt;/p&gt;

&lt;h2&gt;
  
  
  The thing I got wrong first
&lt;/h2&gt;

&lt;p&gt;I was building an agent for a small, annoying problem: your power goes out, the fridge with your insulin in it gets warm, and the alarm on the sensor is where the help stops. Somebody still has to work out which medicine it was, how long it was warm, reach a pharmacist, arrange a replacement, and confirm it arrived.&lt;/p&gt;

&lt;p&gt;My first version did all of that. You clicked a button, and about fifteen seconds later the case was closed. It looked great in a screen recording.&lt;/p&gt;

&lt;p&gt;It was not autonomous. It was one HTTP request that happened to do nine things. If you closed the tab halfway through, nothing finished. There was no moment where the system was on its own, because there was no moment where I was not standing there holding it.&lt;/p&gt;

&lt;p&gt;That distinction turned out to be the whole project.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "on its own" actually requires
&lt;/h2&gt;

&lt;p&gt;The rewrite was simple to describe and annoying to build: &lt;strong&gt;the request must end before the work does.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the workflow now stops after dispatching a courier, and writes down what it intends to do next:&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;register_followups&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&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;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Register the wakes appropriate to the case&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s current state.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;registered&lt;/span&gt; &lt;span class="o"&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;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;awaiting_professional_review&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;review_followup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="n"&gt;registered&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;review_followup&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;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&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;delivery_dispatched&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;eta&lt;/span&gt; &lt;span class="o"&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;case&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;delivery&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="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;eta_minutes&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;courier_status_poll&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;eta&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="nf"&gt;_record&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;receipt_followup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;minutes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&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;registered&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those "wakes" are rows in Firestore. Cloud Scheduler calls a worker every minute with a Google-signed OIDC token. The worker verifies the identity, claims each due wake inside a transaction with a lease, runs an idempotent action, and marks it done. If the action throws, the wake goes back to pending with a bounded retry count and eventually a dead letter, instead of retrying forever.&lt;/p&gt;

&lt;p&gt;The result is a page you can close. A minute after a pharmacist records a decision, the case closes itself, and the browser finds out later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three bugs that only appear when nobody is watching
&lt;/h2&gt;

&lt;p&gt;Synchronous code hides a lot. Here is what fell out once the work moved to a background worker.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The courier that always said yes.&lt;/strong&gt; My sandbox courier returned "delivered" whenever the poll fired. That is not a check, it is a timer with extra steps. I rewrote it as a stateful connector with a poll count and an injectable delay. Now "still in transit" causes a bounded re-poll, and a courier that never confirms leaves the case open with a visible hold for a human. No receipt is ever invented.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Two clocks that disagreed.&lt;/strong&gt; A utility publishes an outage with its own timestamp. A household's sensor has its own clock. A baseline reading stamped 110 milliseconds &lt;em&gt;after&lt;/em&gt; the outage started was being counted as evidence from during the outage, which meant a household whose sensor had gone completely silent looked like it was reporting fine, and never reached its safe stop. The fix was to stop comparing timestamps from different sources and judge by arrival order 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="c1"&gt;# Evidence is judged by arrival order, not by comparing timestamps from different clocks.
&lt;/span&gt;&lt;span class="n"&gt;outage&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;readings_at_outage&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;case&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sensor&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;readings&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;&lt;strong&gt;A falsy zero.&lt;/strong&gt; For the demo I wanted a courier ETA of zero minutes, meaning "poll on the very next scheduler tick." The dispatch code read &lt;code&gt;case.get("delivery_eta_minutes") or DEFAULT_ETA&lt;/code&gt;, and &lt;code&gt;0 or 34&lt;/code&gt; is &lt;code&gt;34&lt;/code&gt;. Everything passed. The case simply never closed while anyone was watching. Classic, and it only showed up when I timed the live path with a stopwatch instead of trusting the tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Letting a model help without letting it decide
&lt;/h2&gt;

&lt;p&gt;The medicine question, "is this insulin still safe," is not mine to answer and not the model's either. FDA and CDC guidance both point at a pharmacist, and a Cochrane review on insulin thermal stability shows that stability varies by formulation, temperature and duration, which is exactly why a threshold rule would be wrong to automate.&lt;/p&gt;

&lt;p&gt;So the rule I designed to is: &lt;strong&gt;the agent acts where a mistake can be undone, and stops where it cannot.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Reading a label wrong is recoverable, so Gemini reads the package, but a field is only kept if it appears word for word in the model's own transcription. Reserving inventory is recoverable, so that is automatic. Telling somebody their insulin is fine is not recoverable, so a named human owns it and the state machine physically cannot proceed without it.&lt;/p&gt;

&lt;p&gt;A Google ADK agent assembles the pharmacist's packet through three read-only tools, and then a plain function checks its homework:&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;verify_packet&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;truth&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;str&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Return the names of fields the model got wrong. Empty means the packet is accepted.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&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;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;PACKET_FIELDS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;actual&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;truth&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;candidate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;,&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;,&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="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;rejected&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;actual&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;rejected&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;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidate&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;question&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="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;disposition&lt;/span&gt;&lt;span class="sh"&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;question&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;or&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;question&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;FORBIDDEN_TERMS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;rejected&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;question&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="n"&gt;rejected&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the agent invents a number, or slips in a sentence implying the medicine is safe, the whole packet is thrown away and a deterministic one is used instead. The workflow never depends on the model being right. Only on it being checkable.&lt;/p&gt;

&lt;p&gt;Gemma 4 does something similar for prompt injection on the package label. It may only return spans that exist verbatim in the text, so its output is verifiable rather than trusted, and it has no route to a decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  Proving it, instead of claiming it
&lt;/h2&gt;

&lt;p&gt;Every agent demo says "autonomous." I wanted a number that could not be fudged.&lt;/p&gt;

&lt;p&gt;Each case emits a receipt derived from the stored timeline: operator clicks after the human decision, background wakes fired, human decisions, and whether the case was closed by a scheduler wake. Then it is signed with HMAC using a key from Secret Manager. Change one field in a copy and the verify endpoint rejects it.&lt;/p&gt;

&lt;p&gt;There is a fail-closed detail I like: if a timeline entry has an actor the classifier does not recognise, the proof reports it as unclassified and marks itself invalid, rather than quietly counting it as an agent action. I found that one the hard way when a new background actor made a genuinely autonomous run report &lt;code&gt;proof_integrity: incomplete&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it looks like now
&lt;/h2&gt;

&lt;p&gt;One click starts a synthetic case. Live models read the package, screen it, and build the packet. It stops with &lt;code&gt;AI DISPOSITION: NONE&lt;/code&gt;. A pharmacist records a decision. Then you take your hands off the keyboard, and about a minute later the page says:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Delivered. Closed by a Cloud Scheduler wake, no operator.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;with zero operator clicks on a signed receipt.&lt;/p&gt;

&lt;p&gt;One utility outage event reaches every monitored household at once, and each one gets judged separately from its own readings: warm goes to a pharmacist, still-in-range keeps being watched, silent sensor stops safely and asks for a person.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;Everything downstream is a labelled sandbox connector. No real pharmacy, courier or insurer. Every person and medicine lot is fictional, the demo clock is simulated and says so on screen, and no pharmacist has reviewed the packet design yet. It demonstrates workflow execution, not clinical benefit, and I have been careful not to claim otherwise anywhere in the project.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://cold-clock-109051079423.us-central1.run.app" rel="noopener noreferrer"&gt;https://cold-clock-109051079423.us-central1.run.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/usv240/cold-clock" rel="noopener noreferrer"&gt;https://github.com/usv240/cold-clock&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Executable proofs, if you like that sort of thing: &lt;code&gt;/api/proof&lt;/code&gt; and &lt;code&gt;/api/hardening/proof&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built on Cloud Run, Firestore, Cloud Scheduler, Pub/Sub, and Vertex AI with Gemini 3.5 Flash, Gemini Embedding 001 and Gemma 4, using the Google ADK and the Gen AI SDK.&lt;/p&gt;

&lt;p&gt;If you take one thing from this: if your agent finishes inside the request that started it, you have written a function. The interesting part starts when the request ends and the work does not.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>python</category>
      <category>showdev</category>
    </item>
    <item>
      <title>One Advisory: a fleet of agents that acts after the water warning, with nobody at the keyboard</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Tue, 25 Aug 2026 15:10:39 +0000</pubDate>
      <link>https://dev.to/ujwal240/one-advisory-building-a-fleet-of-agents-that-acts-after-the-water-warning-with-nobody-at-the-5eo2</link>
      <guid>https://dev.to/ujwal240/one-advisory-building-a-fleet-of-agents-that-acts-after-the-water-warning-with-nobody-at-the-5eo2</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon. #AllThingsAgenticHackathon&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A boil-water advisory reaches a dialysis clinic, a school and a long-term-care home in the same second. The message is identical. The work is not. Dialysis has to verify treatment water and a continuity plan. The school has to secure drinking and hygiene water and hold food service. The care home has to protect resident care and environmental services.&lt;/p&gt;

&lt;p&gt;And today, nobody verifies that any of it happened. Existing tools define zones, send the alert, log the approval, and stop. An alert is treated as a response.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One Advisory&lt;/strong&gt; is the fleet that starts &lt;em&gt;after&lt;/em&gt; the warning. It's my entry for the Fortified Enterprise Fleet track, built on Gemini, Google ADK and Google Cloud, and the whole point of it is that it works when no human is watching.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Live: &lt;a href="https://one-advisory-109051079423.us-central1.run.app" rel="noopener noreferrer"&gt;https://one-advisory-109051079423.us-central1.run.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Code: &lt;a href="https://github.com/usv240/one-advisory" rel="noopener noreferrer"&gt;https://github.com/usv240/one-advisory&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything in the sandbox is fictional: facilities, people, advisories, contacts. It proves architecture and execution, not public-health outcomes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What "works without a human" actually means here
&lt;/h2&gt;

&lt;p&gt;I set myself one rule for the demo: the operator gets &lt;strong&gt;zero "continue" clicks&lt;/strong&gt;. The system had to advance on real events and real time, and stop only at decisions a human is legally supposed to make. Here's the loop that runs for one incident:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An authorized advisory arrives (an image or pasted text).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Armor&lt;/strong&gt; screens the text inline (&lt;code&gt;sanitizeUserPrompt&lt;/code&gt;). A prompt-injection or PII match returns HTTP 422 with the receipt and nothing is stored.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini 3.5 Flash&lt;/strong&gt; extracts only fields it can quote verbatim from its own transcription. Gemini Embedding 001 orders the facility playbooks. &lt;strong&gt;Agent Engine Memory Bank&lt;/strong&gt; recalls how each facility behaved last time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemini synthesizes facility-specific tasks&lt;/strong&gt; from the advisory wording, the facility's capacity note and its remembered history. A verifier keeps only tasks that cite a registered public-health source (CDC toolkit, CDC dialysis guidance, published incident studies). Anything else is dropped; a facility with fewer than two survivors keeps its standing playbook, and the fallback is recorded.&lt;/li&gt;
&lt;li&gt;Four &lt;strong&gt;ADK role agents on Vertex AI Agent Engine&lt;/strong&gt; (Facility Fleet, Policy Gateway, Resource Coordinator, Recovery Verifier), each with its own Agent Identity behind one governed Agent Gateway, authorize each typed command. More on that below.&lt;/li&gt;
&lt;li&gt;Standing playbooks are delivered and &lt;strong&gt;three durable acknowledgement wakes&lt;/strong&gt; are written to Firestore.&lt;/li&gt;
&lt;li&gt;Facilities answer asynchronously: from the UI, a keyed &lt;code&gt;/v1&lt;/code&gt; API, or a &lt;strong&gt;Pub/Sub push subscription&lt;/strong&gt;. &lt;strong&gt;Gemma 4&lt;/strong&gt; triages what they said and flags replies that don't match the declared event. A facility can attach a &lt;strong&gt;photo&lt;/strong&gt;; Gemini describes what is visible and says whether it supports the claim. An unsupported photo never becomes evidence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Scheduler fires the wakes on wall-clock time.&lt;/strong&gt; A facility that answered is confirmed. A facility that stayed silent is escalated by the wake itself, a recheck is scheduled, and the governed fleet resumes.&lt;/li&gt;
&lt;li&gt;Two assistance requests become a resource conflict with options. The AI chooses nobody. A &lt;strong&gt;named incident commander&lt;/strong&gt; allocates.&lt;/li&gt;
&lt;li&gt;After an authorized rescission the fleet verifies recovery per facility, writes each facility's outcome to Memory Bank, drafts the &lt;strong&gt;after-action briefing&lt;/strong&gt; from the audit trace, and schedules a &lt;strong&gt;42-day follow-up wake&lt;/strong&gt; so a closed incident resumes weeks later.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Every step produces a receipt. &lt;code&gt;GET /api/incidents/{id}/autonomy-proof&lt;/code&gt; classifies the whole trace: automatic actions, human-authority events, external events, durable wakes, managed commands, and the operator continue-click count, which is zero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm proudest of: agents that propose, schemas that decide
&lt;/h2&gt;

&lt;p&gt;The first version of the managed runtimes was a lookup table: a role/state/command matrix. It was safe, auditable, and honestly a bit embarrassing as an "agent". The final version keeps the table but moves it &lt;em&gt;behind&lt;/em&gt; the model:&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;class&lt;/span&gt; &lt;span class="nc"&gt;OneAdvisoryRuntimeAgent&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;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;guard&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;armor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;screen_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# structural screen
&lt;/span&gt;        &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;expected_command&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;schema_allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;COMMAND_STATES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;role&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reasoner&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;propose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incident_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# ADK LlmAgent
&lt;/span&gt;        &lt;span class="n"&gt;model_allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;allowed&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proposed_command&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;command&lt;/span&gt;
        &lt;span class="n"&gt;allowed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;schema_allowed&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;model_allowed&lt;/span&gt;          &lt;span class="c1"&gt;# both must agree
&lt;/span&gt;        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The ADK &lt;code&gt;LlmAgent&lt;/code&gt; (Gemini 3.5 Flash, a pydantic &lt;code&gt;output_schema&lt;/code&gt;, low thinking) reads the bounded incident state and proposes a command with a one-sentence rationale. The registered schema then checks it. A hallucinated &lt;code&gt;close_every_facility&lt;/code&gt; is simply not in the table; a model outage fails closed; an out-of-role request is refused with the reason. You get real reasoning and a provable ceiling on what it can do, and the rationale rides along in every receipt.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part that surprised me: making the background worker matter
&lt;/h2&gt;

&lt;p&gt;Halfway through I audited my own project and found the worst kind of gap: the Cloud Scheduler job fired every minute, the Firestore wakes were claimed transactionally, retries and dead letters all worked, and the handler appended one log row and did nothing. Perfect plumbing, no water.&lt;/p&gt;

&lt;p&gt;The fix changed how I think about "autonomous". The wake now reads the facility's state, escalates silence with &lt;code&gt;trigger: durable_wake&lt;/code&gt;, schedules a recheck, and calls the same &lt;code&gt;advance_safe_automation&lt;/code&gt; the API surfaces call. The proof endpoint has a check called &lt;em&gt;silent facility is escalated by the wake itself&lt;/em&gt;, and I verified it live: created an incident, answered for one facility, walked away, and came back to two escalations and a stage that had verified itself.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; background autonomy is only credible when the background worker changes state. The wake that escalates silence is worth more than any dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that cost me hours (so they don't cost you)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent Engine reserves &lt;code&gt;GOOGLE_CLOUD_PROJECT&lt;/code&gt; / &lt;code&gt;GOOGLE_CLOUD_LOCATION&lt;/code&gt;&lt;/strong&gt; as env var names. Gemini 3.5 Flash is served from the &lt;code&gt;global&lt;/code&gt; endpoint, not the runtime's region, so I set my own variable and point the ADK client at &lt;code&gt;global&lt;/code&gt; at import time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent Engine calls &lt;code&gt;query()&lt;/code&gt; inside a running event loop.&lt;/strong&gt; &lt;code&gt;asyncio.run()&lt;/code&gt; on an ADK turn raises &lt;code&gt;RuntimeError&lt;/code&gt;; run it on its own loop in a worker thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ADK wants &lt;code&gt;LlmAgent(output_schema=PydanticModel)&lt;/code&gt;&lt;/strong&gt;, not &lt;code&gt;response_schema&lt;/code&gt; in the generate config. And Gemini 3.5 spends tokens thinking: a 200-token cap returned "Here is the" and nothing else. &lt;code&gt;thinking_level="LOW"&lt;/code&gt; cut a 5 s call to ~1.6 s.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run has two URLs&lt;/strong&gt; and Scheduler and Pub/Sub may sign OIDC tokens for different ones. Accept both audiences or your worker returns 401 after a redeploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloud Run traffic can be pinned to a named revision.&lt;/strong&gt; My new revisions were "ready" and serving nothing. &lt;code&gt;update-traffic --to-latest&lt;/code&gt; after every deploy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Model Armor's &lt;code&gt;NO_MATCH_FOUND&lt;/code&gt; contains the substring &lt;code&gt;MATCH_FOUND&lt;/code&gt;.&lt;/strong&gt; Parse the nested &lt;code&gt;matchState&lt;/code&gt; fields, don't grep.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Gemma 4 on the Gemini API&lt;/strong&gt; (&lt;code&gt;gemma-4-26b-a4b-it&lt;/code&gt;) returns thinking parts by default; pass &lt;code&gt;ThinkingConfig(include_thoughts=False)&lt;/code&gt; and a system instruction.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What it is not
&lt;/h2&gt;

&lt;p&gt;It cannot issue or rescind an advisory, close a facility, or allocate a scarce resource. Three fictional facility classes cannot establish general coverage. No emergency manager, dialysis expert or accessibility specialist has validated it. No health, speed or compliance outcome is claimed. The public sandbox accepts fictional data only.&lt;/p&gt;

&lt;p&gt;What it does prove: routing, governance, memory, guardrails and failure behaviour on a real Google Cloud fleet, and that "the agent handled it while you were away" can be a receipt, not a promise.&lt;/p&gt;

&lt;p&gt;Try it: &lt;a href="https://one-advisory-109051079423.us-central1.run.app" rel="noopener noreferrer"&gt;https://one-advisory-109051079423.us-central1.run.app&lt;/a&gt; · Code: &lt;a href="https://github.com/usv240/one-advisory" rel="noopener noreferrer"&gt;https://github.com/usv240/one-advisory&lt;/a&gt;&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>ai</category>
      <category>agents</category>
      <category>gemini</category>
    </item>
    <item>
      <title>Five things 15 days with the YouCam API taught me that the docs didn't</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Mon, 17 Aug 2026 07:16:53 +0000</pubDate>
      <link>https://dev.to/ujwal240/five-things-15-days-with-the-youcam-api-taught-me-that-the-docs-didnt-bm7</link>
      <guid>https://dev.to/ujwal240/five-things-15-days-with-the-youcam-api-taught-me-that-the-docs-didnt-bm7</guid>
      <description>&lt;p&gt;I spent the last two weeks building &lt;strong&gt;PatternProof&lt;/strong&gt;, a small app that tries to get a customer and a tailor to agree on a garment before anyone cuts fabric. It uses Perfect Corp's YouCam APIs for the visual side.&lt;/p&gt;

&lt;p&gt;This post is not a tutorial. It is the list of things I got wrong, plus a few results I did not expect. If you are about to integrate a generative visual API into anything, some of this will probably save you a day.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version of what I built
&lt;/h2&gt;

&lt;p&gt;Virtual try-on exists to answer &lt;em&gt;"should I buy this finished garment?"&lt;/em&gt; But in a lot of the world, clothing is made, not bought. India alone has around 12 million custom tailors, 99% of them informal, per the Periodic Labour Force Survey 2023-24.&lt;/p&gt;

&lt;p&gt;In that transaction, a customer holds up a phone and says "like this." The tailor interprets, and cuts. Off the rack, a mistake is a return. Made to order, a mistake is destroyed fabric.&lt;/p&gt;

&lt;p&gt;So PatternProof renders the garment on the customer's actual body, lets the tailor mark each requirement as &lt;em&gt;can make as shown&lt;/em&gt;, &lt;em&gt;can make with adjustment&lt;/em&gt;, or &lt;em&gt;not feasible&lt;/em&gt;, freezes the whole thing with a SHA-256 digest, and only then lets the customer approve it.&lt;/p&gt;

&lt;p&gt;That is the product. Now the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. I refused to write product code until the API proved itself
&lt;/h2&gt;

&lt;p&gt;I gave myself one rule for day one: no application code until a numbered list of tests passed. Boring, and easily the best decision I made.&lt;/p&gt;

&lt;p&gt;The one that mattered most I called T3. My assumption was that Clothes VTO would need a clean catalogue-style product image, and that a real customer's screenshot from social media would fail. If that was true, my entire product premise collapsed into "pick from our curated library," which is a much weaker thing.&lt;/p&gt;

&lt;p&gt;So I threw three deliberately awful references at it: a garment worn by a different person, an angled and cropped shot, and one in bad light.&lt;/p&gt;

&lt;p&gt;All three transferred recognizably. 22.7s, 13.2s, 22.2s.&lt;/p&gt;

&lt;p&gt;That single result kept real customer inspiration photos in scope. Had I discovered it on day nine instead of day one, I would have built two days of the wrong thing first.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The takeaway:&lt;/strong&gt; find the assumption that would break your product, and test only that, before you write anything.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. A generative API returned byte-identical output
&lt;/h2&gt;

&lt;p&gt;This is the one I still find slightly startling.&lt;/p&gt;

&lt;p&gt;For my latency test I sent the same request twice, expecting to compare rough similarity. Instead I got the same file. Not similar. Identical.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attempt 1: sha256 b53062e7e436dbd9...  141,631 bytes
Attempt 5: sha256 b53062e7e436dbd9...  141,631 bytes
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For most projects this is trivia. For mine it mattered a lot, because the entire premise is a &lt;strong&gt;frozen&lt;/strong&gt; agreement. If a customer approves a Cut Card and it re-renders as a slightly different dress on reload, the artifact is worthless.&lt;/p&gt;

&lt;p&gt;I built the full content-addressed cache anyway, with a database reservation and lease around every call. Observed determinism is not a documented guarantee, and I am not willing to bet an approval record on behaviour I inferred from five requests. But it was a nice thing to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. "Success" from the provider is not success for your users
&lt;/h2&gt;

&lt;p&gt;I ran a test with a deliberately dark, half-cropped body photo, expecting a clean error I could catch and surface.&lt;/p&gt;

&lt;p&gt;The API returned HTTP success after 31.5 seconds. The image was technically a result and absolutely not something you would ever show a paying customer. Nothing in the response distinguished it from a good render.&lt;/p&gt;

&lt;p&gt;That reframed how I thought about the whole integration. Quality judgement could not live at the provider boundary, so it moved into my app, ahead of the spend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Reject before we pay, not after.&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;edge&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;MIN_EDGE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;resolution&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grayscaleMean&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;lighting&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;heightRatio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;categoryMinimum&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;framing&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Rejected uploads cost zero API units, because they never reach the provider. That turned out to be a nice property while filming a demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Signed URLs work, and I nearly designed around a myth
&lt;/h2&gt;

&lt;p&gt;Before starting I read another dev.to writeup on the same API which stated that Perfect Corp requires &lt;em&gt;publicly accessible URLs&lt;/em&gt; for both the user photo and the reference image. Genuinely useful post, and I am glad it existed. But that specific line worried me, because body photos are the most sensitive thing my app touches and I wanted them in a private bucket.&lt;/p&gt;

&lt;p&gt;I had already sketched an ugly workaround: copy each image into a short-lived public staging bucket, pass that URL, then purge it.&lt;/p&gt;

&lt;p&gt;Before building it, I tested. &lt;strong&gt;Supabase expiring signed URLs were accepted without complaint.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So I got to keep the design I wanted. Canonical inputs stay in a private bucket, the provider gets short-lived signed access, and every returned asset is host-allowlisted, size-bounded, hashed, and re-hosted privately before it is shown to anyone.&lt;/p&gt;

&lt;p&gt;If you are integrating this API, test that yourself before you build a staging-bucket dance. Mine may not be your configuration, but the assumption is worth thirty seconds of checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. The black video player, and my three wrong theories
&lt;/h2&gt;

&lt;p&gt;This one cost me the most time, and the debugging path is the useful part, so here is all of it including the mistakes.&lt;/p&gt;

&lt;p&gt;After approval, the app generates a five-second motion proof with Image-to-Video V2. In production, the player rendered but sat black at &lt;code&gt;0:00&lt;/code&gt;. No duration. No error dialog.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong theory one: the signed URL expired.&lt;/strong&gt; The video sits inside a collapsed &lt;code&gt;&amp;lt;details&amp;gt;&lt;/code&gt;, so the browser only fetches it when you expand. My signed URLs lasted five minutes. Plausible! I raised the TTL to an hour. Still black.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Wrong theory two: the file is broken.&lt;/strong&gt; So I pulled the actual bytes and looked:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;HTTP 200
Content-Type: video/mp4
Length: 1,021,595 bytes
ftyp: isom, codecs avc1 (H.264) + mp4a (AAC)
mvhd: timescale 1000, duration 5063  -&amp;gt;  5.063 seconds
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfectly valid file. But I did notice this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mdat (media data) at offset          44
moov (the index)  at offset   1,017,142   &amp;lt;- at the END
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Wrong theory three: it is not faststart.&lt;/strong&gt; True, and worth fixing. With &lt;code&gt;moov&lt;/code&gt; after &lt;code&gt;mdat&lt;/code&gt;, &lt;code&gt;preload="metadata"&lt;/code&gt; cannot resolve duration without pulling the whole file. I switched it to &lt;code&gt;preload="auto"&lt;/code&gt;, which is correct for a 1 MB clip. Still black.&lt;/p&gt;

&lt;p&gt;At this point I had a valid file, correct headers, a reachable URL, and a player that refused to initialise. So I finally looked at the thing I should have checked first: &lt;strong&gt;response headers.&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt;   &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;: &lt;span class="n"&gt;blob&lt;/span&gt;: &lt;span class="n"&gt;https&lt;/span&gt;://*.&lt;span class="n"&gt;supabase&lt;/span&gt;.&lt;span class="n"&gt;co&lt;/span&gt;   &amp;lt;- &lt;span class="n"&gt;images&lt;/span&gt; &lt;span class="n"&gt;fine&lt;/span&gt;
&lt;span class="n"&gt;media&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; (&lt;span class="n"&gt;not&lt;/span&gt; &lt;span class="n"&gt;declared&lt;/span&gt;)                             &amp;lt;- &lt;span class="n"&gt;falls&lt;/span&gt; &lt;span class="n"&gt;back&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There it was. My Content-Security-Policy declared &lt;code&gt;img-src&lt;/code&gt; with the Supabase host but had no &lt;code&gt;media-src&lt;/code&gt;, so video inherited &lt;code&gt;default-src 'self'&lt;/code&gt; and the browser silently refused to load an MP4 from a different origin. Images kept working through their own directive, which is exactly why it looked like a video problem.&lt;/p&gt;

&lt;p&gt;One line:&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="s2"&gt;"media-src 'self' blob: https://*.supabase.co"&lt;/span&gt;&lt;span class="err"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the detail that made it hard: my CSP was applied only when &lt;code&gt;NODE_ENV === "production"&lt;/code&gt;. Local dev had no CSP at all, so this could never reproduce on my machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson I actually took:&lt;/strong&gt; when one media type loads and another does not, that is a policy problem, not a file problem. Check headers before you start parsing atom offsets. I did it in exactly the wrong order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The feature I killed on day one
&lt;/h2&gt;

&lt;p&gt;I wanted the customer's &lt;em&gt;own fabric swatch&lt;/em&gt; rendered onto the garment. It was the most exciting thing in my original pitch.&lt;/p&gt;

&lt;p&gt;Fabric VTO takes a provider-defined template, not an arbitrary upload.&lt;/p&gt;

&lt;p&gt;I could have fudged it and shown a plausible fabric direction while implying it was the customer's cloth. Instead I cut the claim entirely. The app now says, on every render:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Visual intent reference. Not a fit, construction, or fabric-drape guarantee.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That disclaimer is my favourite thing in the product. Generative try-on can make intent concrete. It cannot certify measurements, cloth behaviour, or whether a seam is physically constructible. Saying so out loud made everything else more believable.&lt;/p&gt;

&lt;p&gt;Which leads to the last thing, and it is not about the API at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three of my own statistics were fake
&lt;/h2&gt;

&lt;p&gt;I did a lot of research with AI assistance. Late on, I went back to verify every number at source rather than trusting a summary. Three did not survive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A "35% of tailoring clients" figure attributed to an industry body. No authoritative source carries it.&lt;/li&gt;
&lt;li&gt;A "73%" consumer statistic I had recorded as coming from a government ministry. It traces to a fashion brand's blog.&lt;/li&gt;
&lt;li&gt;A "2 to 3 billion people" figure. I made that one up myself, early, as a plausible-sounding framing, and never went back to check it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three had survived multiple review passes. The mis-attributions are the scary ones, because search summaries do not invent numbers so much as invent &lt;strong&gt;provenance&lt;/strong&gt;. A real figure from a blog gets confidently relabelled as coming from a ministry, and it reads as authoritative right up until you click through.&lt;/p&gt;

&lt;p&gt;The rule I ended up with, which I would now apply to any AI-assisted work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If it was not fetched at source, it does not ship.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I applied the same rule to code. No endpoint, parameter, or response shape got written from memory or a search result. Given how the CSP bug went, I am glad about that.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you are building on this API
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Test your riskiest assumption on day one, in isolation, before any product code.&lt;/li&gt;
&lt;li&gt;Read unit costs from the feature-cost endpoint rather than estimating. Mine came back at exactly 2 units per Clothes VTO V3 result, which made real budget enforcement possible instead of guesswork.&lt;/li&gt;
&lt;li&gt;Provider success is not product success. Put a quality gate in front of the spend.&lt;/li&gt;
&lt;li&gt;Test signed URLs yourself before designing around public buckets.&lt;/li&gt;
&lt;li&gt;Declare &lt;code&gt;media-src&lt;/code&gt; in your CSP if you are going to serve video from object storage. Please learn this from my afternoon rather than your own.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The app is live and needs no login if you want to poke at it: &lt;a href="https://patternproof-nu.vercel.app" rel="noopener noreferrer"&gt;patternproof-nu.vercel.app&lt;/a&gt;. Code is at &lt;a href="https://github.com/usv240/patternproof" rel="noopener noreferrer"&gt;github.com/usv240/patternproof&lt;/a&gt;, including the raw day-one validation record with the timings and failures in it.&lt;/p&gt;

&lt;p&gt;Happy to answer anything in the comments, particularly if you have hit different behaviour on the same endpoints. I would genuinely like to know whether the byte-identical result reproduces for anyone else.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>webdev</category>
      <category>hackathon</category>
    </item>
    <item>
      <title>The Antibiotic Review That Software Quietly Forgets</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:28:27 +0000</pubDate>
      <link>https://dev.to/ujwal240/the-antibiotic-review-that-software-quietly-forgets-2ane</link>
      <guid>https://dev.to/ujwal240/the-antibiotic-review-that-software-quietly-forgets-2ane</guid>
      <description>&lt;p&gt;Nothing has to crash for a workflow to fail.&lt;/p&gt;

&lt;p&gt;Sometimes the right information arrives, the right person is capable of acting on it, and the review still happens late because nobody carried the context from one moment to the next.&lt;/p&gt;

&lt;p&gt;That is the problem I kept thinking about while building Day Three.&lt;/p&gt;

&lt;p&gt;A broad antibiotic can be started before a culture is final. Later, when the report is complete, a pharmacist may have better evidence for a review. I did not want to build another chatbot that summarized the report and disappeared. I wanted to build something that could remember why the review mattered, wake when it was due, gather the supporting evidence, and then get out of the pharmacist's way.&lt;/p&gt;

&lt;h2&gt;
  
  
  My first idea was wrong
&lt;/h2&gt;

&lt;p&gt;I began with a dramatic premise: small hospitals do not have stewardship programs, so software should bring one to them.&lt;/p&gt;

&lt;p&gt;The research did not support it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.cdc.gov/antibiotic-use/hcp/data-research/stewardship-report.html" rel="noopener noreferrer"&gt;CDC's 2025 stewardship update&lt;/a&gt; says that almost all reporting acute-care hospitals had adopted the seven Core Elements in 2024. Smaller studies of selected rural and critical-access hospitals instead described practical constraints such as limited time, staffing, expertise, and electronic-record support.&lt;/p&gt;

&lt;p&gt;That changed the project. Day Three was no longer about replacing a missing program. It became a way to help an existing team carry one recurring piece of work across time.&lt;/p&gt;

&lt;p&gt;The distinction matters. It kept me focused on coordination instead of pretending that a model should practice medicine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent's real job is remembering
&lt;/h2&gt;

&lt;p&gt;In the public demo, a synthetic microbiology report arrives before the review is due. Gemini transcribes the report into structured fields. Direct identifiers are removed before model review, and retained facts stay connected to the words that support them.&lt;/p&gt;

&lt;p&gt;The workflow then creates a durable course record and registers future work. At the review point, it wakes, retrieves the latest evidence, checks the local context, and prepares a draft for a pharmacist.&lt;/p&gt;

&lt;p&gt;The draft is not an order. Day Three cannot prescribe, choose a dose, change therapy, contact a patient, or write to a medical chart. Its job is to bring the pharmacist back to a review with the evidence still attached.&lt;/p&gt;

&lt;p&gt;That is what makes it agentic to me. It is not the amount of text it produces. It is the fact that it can carry state and responsibility from now to later without quietly taking authority along with it.&lt;/p&gt;

&lt;p&gt;You can try the &lt;a href="https://day-three-109051079423.us-central1.run.app" rel="noopener noreferrer"&gt;live Day Three workflow&lt;/a&gt; or read the &lt;a href="https://github.com/usv240/day-three" rel="noopener noreferrer"&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  A local number can look more certain than it is
&lt;/h2&gt;

&lt;p&gt;I also wanted the pharmacist to see local susceptibility context, but this introduced another trap.&lt;/p&gt;

&lt;p&gt;A tiny isolate count can still produce a neat percentage. The number looks scientific even when the sample is too small to deserve that confidence. A &lt;a href="https://pmc.ncbi.nlm.nih.gov/articles/PMC9927543/" rel="noopener noreferrer"&gt;study of low-isolate antibiograms&lt;/a&gt; made this risk concrete.&lt;/p&gt;

&lt;p&gt;So Day Three sometimes shows less. When the count is below the declared threshold, the percentage is suppressed and the reason is visible. The interface does not fill empty space with false precision.&lt;/p&gt;

&lt;p&gt;This became a broader product rule: uncertainty should remain visible, even when hiding it would make the demo look cleaner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hardest bug was an empty string
&lt;/h2&gt;

&lt;p&gt;The most important rule in Day Three is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;No quote, no claim, no rendered sentence.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every factual sentence in the pharmacist draft needs a source reference, and the quoted text must be present in the stored artifact.&lt;/p&gt;

&lt;p&gt;I thought that boundary was solid until I found a wonderfully ordinary Python problem. An empty string is considered to be inside every other string. A source object with &lt;code&gt;quoted_text = ""&lt;/code&gt; could therefore pass a naive containment check.&lt;/p&gt;

&lt;p&gt;The claim looked sourced. The reference object existed. The verifier said yes. But there was no evidence at all.&lt;/p&gt;

&lt;p&gt;Fixing that one case led me to probe the whole evidence chain. I found another path where the application created a fallback quote when extraction returned none. The text could appear in the report, but it was not the model's extracted evidence. The application had silently repaired the gap and broken the chain of custody.&lt;/p&gt;

&lt;p&gt;Both shortcuts are gone. Missing evidence now stays missing, and unsupported prose does not reach the pharmacist.&lt;/p&gt;

&lt;p&gt;That work changed how I think about grounded generation. Finding a sentence somewhere in a document is not enough. Provenance includes how that sentence entered the system.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture follows the boundary
&lt;/h2&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%2Fp5lpye8wufxdxjskr1dx.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%2Fp5lpye8wufxdxjskr1dx.png" alt="Day Three architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Day Three runs on Cloud Run with durable records in Firestore and scheduled work driven by Cloud Scheduler. Gemini handles the bounded transcription task. Gemma performs a second privacy review after deterministic redaction. Cloud Trace and Logging make the workflow inspectable.&lt;/p&gt;

&lt;p&gt;The roles are deliberately narrow: intake, redaction, transcription, curation, scheduling, reconciliation, verification, and pharmacist review. They share one workflow, but they do not share unlimited authority.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://www.cdc.gov/antibiotic-use/media/pdfs/core-elements-small-critical-508.pdf" rel="noopener noreferrer"&gt;CDC Core Elements for Small and Critical Access Hospitals&lt;/a&gt; informed the human role and local context. A &lt;a href="https://www.ncbi.nlm.nih.gov/pmc/articles/PMC11776815/" rel="noopener noreferrer"&gt;review of antimicrobial de-escalation timing&lt;/a&gt; informed the review window. Neither source proves that this software improves clinical outcomes, and the project does not make that claim.&lt;/p&gt;

&lt;p&gt;The full source-to-decision trail is in the repository's &lt;a href="https://github.com/usv240/day-three/blob/main/docs/research-traceability.md" rel="noopener noreferrer"&gt;research ledger&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would carry into the next build
&lt;/h2&gt;

&lt;p&gt;I started this project thinking the interesting part would be the recommendation.&lt;/p&gt;

&lt;p&gt;It was not.&lt;/p&gt;

&lt;p&gt;The interesting part was deciding what the system must remember, what it must be able to prove, and exactly where it must stop. A useful agent can be proactive without being sovereign. It can wake, gather, check, and prepare while leaving the consequential judgment with the person who actually has the authority to make it.&lt;/p&gt;

&lt;p&gt;That boundary made Day Three smaller than my first idea, but much more real.&lt;/p&gt;

&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon.&lt;/p&gt;

&lt;p&gt;If you were designing an agent for a high-stakes workflow, where would you draw the line between useful preparation and human authority?&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>gemini</category>
      <category>ai</category>
      <category>allthingsagentichackathon</category>
    </item>
    <item>
      <title>A Deadline Does Not Care That You Are Still Recovering</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:28:22 +0000</pubDate>
      <link>https://dev.to/ujwal240/a-deadline-does-not-care-that-you-are-still-recovering-3080</link>
      <guid>https://dev.to/ujwal240/a-deadline-does-not-care-that-you-are-still-recovering-3080</guid>
      <description>&lt;p&gt;A deadline can be clear on paper and still be hard to meet.&lt;/p&gt;

&lt;p&gt;Imagine opening a disaster-assistance letter while you are also dealing with damage, insurance, repairs, and displacement. The letter may tell you what is missing and start a 60-day appeal window. The records you need may be held by an insurer, a public office, or a contractor. Some may not exist yet.&lt;/p&gt;

&lt;p&gt;The clock does not pause while you find them.&lt;/p&gt;

&lt;p&gt;That was the starting point for Sixty Days. I wanted to build an agent that could carry the checklist and the deadline across several weeks. I did not want it to impersonate a lawyer, decide eligibility, contact people without permission, or submit anything on an applicant's behalf.&lt;/p&gt;

&lt;h2&gt;
  
  
  The letter comes before the catalogue
&lt;/h2&gt;

&lt;p&gt;My first design began with a general list of documents someone might need. It was organized and looked helpful. It was also the wrong source of truth.&lt;/p&gt;

&lt;p&gt;FEMA's &lt;a href="https://www.fema.gov/sites/default/files/documents/fema_ia-quick-reference_appeals.pdf" rel="noopener noreferrer"&gt;appeals guidance&lt;/a&gt; and its &lt;a href="https://www.fema.gov/fact-sheet/8-tips-appealing-femas-decision-1" rel="noopener noreferrer"&gt;tips for appealing a decision&lt;/a&gt; kept pointing back to the same thing: the decision letter explains the reason and the relevant supporting documents.&lt;/p&gt;

&lt;p&gt;So the workflow now starts there.&lt;/p&gt;

&lt;p&gt;Gemini reads a synthetic decision letter after direct identifiers have been removed. The system keeps the stated reason as a quotation, calculates the deadline from the letter date, and routes only the evidence needs supported by that letter.&lt;/p&gt;

&lt;p&gt;If the reason cannot be mapped safely, the workflow does not guess. It asks for review.&lt;/p&gt;

&lt;p&gt;That one change made the product feel less like a generic checklist and more like a case-specific companion.&lt;/p&gt;

&lt;p&gt;You can walk through the &lt;a href="https://sixty-days-109051079423.us-central1.run.app" rel="noopener noreferrer"&gt;live Sixty Days demo&lt;/a&gt; or inspect the &lt;a href="https://github.com/usv240/sixty-days" rel="noopener noreferrer"&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remembering is the autonomous part
&lt;/h2&gt;

&lt;p&gt;Once the letter is understood, Sixty Days creates a durable series of future checkpoints. Early wakes make sure the evidence plan exists. Later wakes notice missing replies and unfinished items. Near the deadline, the agent assembles whatever is available without pretending the packet is complete.&lt;/p&gt;

&lt;p&gt;The public demo uses a clearly labelled simulated clock, because waiting two real months would not make for a useful demonstration. The same deadline logic runs, but a judge can advance the synthetic case in seconds.&lt;/p&gt;

&lt;p&gt;What I like about this design is that the autonomy is quiet. The agent does not need to perform a dramatic action. It needs to remember work that a browser session would forget and bring it back at the useful moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preparing is not sending
&lt;/h2&gt;

&lt;p&gt;One design choice kept coming back: should the agent contact an insurer to request a missing record?&lt;/p&gt;

&lt;p&gt;It would make the product look more autonomous. It would also cross a line I could not justify.&lt;/p&gt;

&lt;p&gt;Instead, Sixty Days prepares a request for the applicant to review. It explains what is being requested, leaves visible blanks where the applicant needs to add or verify information, and tracks whether a response has arrived. There is no send endpoint.&lt;/p&gt;

&lt;p&gt;The same boundary applies to the appeal packet. The system can assemble a draft, list the missing items, and keep the deadline visible. It cannot submit the packet.&lt;/p&gt;

&lt;p&gt;This is not a technical limitation disguised as a principle. It is a product decision. Preparing useful work is different from acting with someone else's legal and administrative authority.&lt;/p&gt;

&lt;h2&gt;
  
  
  A photo can show framing, not truth
&lt;/h2&gt;

&lt;p&gt;Evidence photos created another tempting shortcut. A model can observe whether a room is visible, whether an image is too dark, or whether the relevant damage is in frame. That does not mean it can prove authenticity, causation, value, or acceptance by an agency.&lt;/p&gt;

&lt;p&gt;The vocabulary in Sixty Days stays narrow. It can ask for a clearer photo or route an image to manual review. It never labels evidence as accepted by FEMA.&lt;/p&gt;

&lt;p&gt;That boundary came directly from treating public guidance as a design input. FEMA's page on &lt;a href="https://www.fema.gov/fact-sheet/verifying-home-ownership-or-occupancy" rel="noopener noreferrer"&gt;verifying ownership or occupancy&lt;/a&gt; lists several possible records and different date contexts. The &lt;a href="https://www.fema.gov/sites/default/files/documents/fema_insurance_qrg_20241010.pdf" rel="noopener noreferrer"&gt;insurance guidance&lt;/a&gt; distinguishes settlement information, denials, and policy evidence. Neither supports a universal document shortcut, so the interface offers examples without promising sufficiency.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest packet is sometimes incomplete
&lt;/h2&gt;

&lt;p&gt;There is an uncomfortable moment near any deadline when polishing the output can hide the real state of the work.&lt;/p&gt;

&lt;p&gt;Sixty Days does the opposite. If evidence is missing, the packet remains visibly partial. Each draft says it has not been submitted. Missing records stay on the checklist. The final deadline wake preserves the work already assembled and raises the remaining gap instead of quietly turning partial into complete.&lt;/p&gt;

&lt;p&gt;That behavior became the emotional center of the project for me. The goal is not to create a reassuring PDF. It is to help an applicant see exactly what exists, what is still missing, and what they control next.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is put together
&lt;/h2&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%2Fm88p1yhc2wx6t58m5jax.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%2Fm88p1yhc2wx6t58m5jax.png" alt="Sixty Days architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application runs on Cloud Run. Firestore keeps structured cases, requirements, requests, wakes, and packet snapshots. Cloud Scheduler invokes due work. Gemini handles bounded letter transcription and observable photo review. Gemma provides a second privacy check after deterministic redaction.&lt;/p&gt;

&lt;p&gt;Raw letters, image bytes, transcriptions, and free-form applicant narratives are deliberately not placed in the durable case record.&lt;/p&gt;

&lt;p&gt;The source hierarchy and the decisions it changed are documented in the public &lt;a href="https://github.com/usv240/sixty-days/blob/main/docs/research-traceability.md" rel="noopener noreferrer"&gt;research ledger&lt;/a&gt;. For broader context, &lt;a href="https://www.gao.gov/products/gao-20-503" rel="noopener noreferrer"&gt;GAO-20-503&lt;/a&gt; describes the scale and complexity of the historical assistance process. Those figures provide context, not a prediction that this tool changes an applicant's outcome.&lt;/p&gt;

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

&lt;p&gt;I began by thinking the packet was the product.&lt;/p&gt;

&lt;p&gt;The real product is continuity. It is the ability to keep the reason, deadline, requests, evidence, missing pieces, and applicant's choices connected over time.&lt;/p&gt;

&lt;p&gt;The most helpful agent was not the one that did everything. It was the one that prepared the next safe step and made the boundary unmistakable:&lt;/p&gt;

&lt;p&gt;Prepare, track, remind, and assemble. Never decide, contact, or submit.&lt;/p&gt;

&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon.&lt;/p&gt;

&lt;p&gt;Where would you want an assistant like this to stop and hand control back to you?&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>gemini</category>
      <category>ai</category>
      <category>allthingsagentichackathon</category>
    </item>
    <item>
      <title>The Registry Said 28 Feet. The Drawing Said 31.</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Tue, 11 Aug 2026 21:28:14 +0000</pubDate>
      <link>https://dev.to/ujwal240/the-registry-said-28-feet-the-drawing-said-31-4dma</link>
      <guid>https://dev.to/ujwal240/the-registry-said-28-feet-the-drawing-said-31-4dma</guid>
      <description>&lt;p&gt;The registry said the dam was 28 feet high.&lt;/p&gt;

&lt;p&gt;The legacy drawing said 31.&lt;/p&gt;

&lt;p&gt;Three feet is not the interesting part of that story. The interesting part is what software does when two plausible sources disagree.&lt;/p&gt;

&lt;p&gt;An early version of Downstream displayed both values and then kept going. I told myself that was transparent because the conflict was visible on screen. But the agent never asked anyone about it. It behaved as if showing uncertainty was the same as helping resolve it.&lt;/p&gt;

&lt;p&gt;That realization changed the whole project.&lt;/p&gt;

&lt;h2&gt;
  
  
  This is not a form-filling problem
&lt;/h2&gt;

&lt;p&gt;Downstream helps assemble a reviewable draft Emergency Action Plan for a synthetic dam.&lt;/p&gt;

&lt;p&gt;The facts needed for that work do not live in one place. A public record may have basic inventory fields. A legacy drawing may contain different measurements. An owner knows which access road washes out and who can answer after hours. Emergency managers, engineers, and state reviewers bring knowledge and authority that the software does not have.&lt;/p&gt;

&lt;p&gt;I wanted the agent to help those pieces meet over several short sessions. I did not want it to manufacture an engineering answer just because the interface had an empty section.&lt;/p&gt;

&lt;p&gt;You can try the &lt;a href="https://downstream-109051079423.us-central1.run.app/?guided=1#workspace" rel="noopener noreferrer"&gt;guided Downstream workspace&lt;/a&gt;, inspect its &lt;a href="https://downstream-109051079423.us-central1.run.app/evidence" rel="noopener noreferrer"&gt;evidence dashboard&lt;/a&gt;, or read the &lt;a href="https://github.com/usv240/downstream" rel="noopener noreferrer"&gt;source on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Research removed the original headline
&lt;/h2&gt;

&lt;p&gt;The first concept had a strong statistic: count dams with a blank public Emergency Action Plan field and call them dams without a plan.&lt;/p&gt;

&lt;p&gt;It was catchy. It was also unsupported.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://nid.sec.usace.army.mil/nid/" rel="noopener noreferrer"&gt;USACE National Inventory of Dams&lt;/a&gt; exposes useful public inventory fields, but a blank selected field does not prove that a plan does not exist. It proves only that the value is unreported in that field.&lt;/p&gt;

&lt;p&gt;So I removed the bigger claim.&lt;/p&gt;

&lt;p&gt;I also had to be careful with hazard classifications. High hazard describes the potential consequences if a failure occurs. It does not say that a dam is in poor condition or likely to fail.&lt;/p&gt;

&lt;p&gt;Those corrections did more than clean up the landing page. They gave the product a governing rule: preserve what a source actually says, including its silence, without upgrading it into a more dramatic conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  The disagreement became a question
&lt;/h2&gt;

&lt;p&gt;The 28-foot and 31-foot values now remain side by side with their source fragments. Instead of silently selecting one, Downstream opens a focused question for the owner.&lt;/p&gt;

&lt;p&gt;An owner can add context or identify the controlling record. The agent then marks that context as recorded. It does not mark the measurement as resolved, because confirmation may still belong to a qualified engineer or an authoritative record.&lt;/p&gt;

&lt;p&gt;That wording took more thought than the feature itself.&lt;/p&gt;

&lt;p&gt;"Resolved" would make the workflow look satisfyingly complete. "Owner context recorded" is less impressive and more accurate.&lt;/p&gt;

&lt;p&gt;This is what collaboration means in the project. The agent should move uncertainty toward the right person, not rename uncertainty as confidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  A later session has to be genuinely later
&lt;/h2&gt;

&lt;p&gt;The workspace asks one question at a time and explains why it matters. A user can ask for simpler language, answer, correct an earlier answer, or hold a question for later.&lt;/p&gt;

&lt;p&gt;At one point, "hold for later" was mostly theatre. The interface accepted held questions, but they did not reliably return in the next session. A user could also hit an error when trying to hold the dynamically created source-conflict question.&lt;/p&gt;

&lt;p&gt;That was a useful failure. It exposed the difference between a conversational interface and a collaborative system.&lt;/p&gt;

&lt;p&gt;Now a held question reopens in a later saved session. Corrections create a new version instead of overwriting the old answer. The affected plan section is rebuilt, while the earlier statement and the reason for the correction remain visible.&lt;/p&gt;

&lt;p&gt;Refreshing the shareable workspace URL restores the same state. The collaboration belongs to the workspace, not to one lucky browser tab.&lt;/p&gt;

&lt;h2&gt;
  
  
  The most important map is the one it refuses to draw
&lt;/h2&gt;

&lt;p&gt;An inundation map would make the demo visually stronger. It would also make a claim the available evidence cannot support.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://www.fema.gov/sites/default/files/2020-08/fema_dam-safety_emergency-action-planning_P-64.pdf" rel="noopener noreferrer"&gt;FEMA P-64&lt;/a&gt; informed the purpose and expected contents of an Emergency Action Plan. The &lt;a href="https://damsafety.org/dam-owners/emergency-action-planning" rel="noopener noreferrer"&gt;ASDSO planning guidance&lt;/a&gt; reinforces the collaborative roles of owners and emergency managers. The &lt;a href="https://www.damsafety.org/sites/default/files/files/EAPWG%20Final%20SIMS.pdf" rel="noopener noreferrer"&gt;Simplified Inundation Maps methodology&lt;/a&gt; describes circumstances in which simplified approaches may be useful while retaining engineering and regulatory responsibilities.&lt;/p&gt;

&lt;p&gt;The synthetic case does not prove that a particular mapping method is applicable. It has no approved reference map and no jurisdictional acceptance.&lt;/p&gt;

&lt;p&gt;So Downstream shows a documented flow-path input, explains what is missing, and stops. It does not generate an inundation boundary, depth, velocity, arrival time, or evacuation zone.&lt;/p&gt;

&lt;p&gt;The blank space is intentional. It tells the next reviewer what evidence or expertise must enter the process before the section can continue.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it is put together
&lt;/h2&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%2Fjdddzjntk7d3ltjtxexe.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%2Fjdddzjntk7d3ltjtxexe.png" alt="Downstream architecture" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The application runs on Cloud Run. It combines a public USACE record with a synthetic legacy drawing read by Gemini. Facts keep their provenance and source text. A conflict policy decides when the agent must ask instead of infer.&lt;/p&gt;

&lt;p&gt;Firestore preserves sessions, held questions, owner answers, revisions, preferences, and draft sections. Gemma performs a second privacy review of synthetic owner notes after deterministic redaction. A bounded context builder keeps long-running workspaces from turning into an ever-growing prompt.&lt;/p&gt;

&lt;p&gt;Each rendered plan section carries an evidence class such as published requirement, owner answer, unresolved gap, or fail-closed mapping policy. The human-readable evidence ledger makes those distinctions visible outside the code.&lt;/p&gt;

&lt;p&gt;The complete mapping from public sources to product decisions is in the repository's &lt;a href="https://github.com/usv240/downstream/blob/main/docs/research-traceability.md" rel="noopener noreferrer"&gt;research ledger&lt;/a&gt;.&lt;/p&gt;

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

&lt;p&gt;I started with a document I wanted the agent to complete.&lt;/p&gt;

&lt;p&gt;I ended with a workspace designed to preserve the reasons it cannot complete some parts yet.&lt;/p&gt;

&lt;p&gt;The strongest behavior is not eloquent conversation. It is continuity with restraint: ask for the missing fact, explain why it matters, remember the answer, preserve the correction, and stop when the next step belongs to someone with evidence or authority the system does not have.&lt;/p&gt;

&lt;p&gt;The registry still says 28 feet. The drawing still says 31. Downstream does not hide that disagreement. It helps the people responsible decide what should happen next.&lt;/p&gt;

&lt;p&gt;I created this piece of content for the purposes of entering the All Things Agentic Hackathon.&lt;/p&gt;

&lt;p&gt;When two credible sources disagree, what would you want an agent to do before moving on?&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>gemini</category>
      <category>ai</category>
      <category>allthingsagentichackathon</category>
    </item>
    <item>
      <title>I built an AI agent that tells you if you're overpaying property tax</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Thu, 23 Jul 2026 04:41:42 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-built-an-ai-agent-that-tells-you-if-youre-overpaying-property-tax-376d</link>
      <guid>https://dev.to/ujwal240/i-built-an-ai-agent-that-tells-you-if-youre-overpaying-property-tax-376d</guid>
      <description>&lt;p&gt;Most people are overpaying property tax and never find out. In the US, 30 to 60% of homes are valued too high for tax. In the UK, hundreds of thousands of homes sit in the wrong council tax band, still based on a rushed 1991 valuation. The proof is public record. The catch is that nobody could ever actually query it.&lt;/p&gt;

&lt;p&gt;So for the ClickHouse x Trigger.dev Summer Hackathon 2026 (theme: "Beyond the Wall of Text"), I built &lt;strong&gt;Overtaxed&lt;/strong&gt;: a chat agent where you type your home address and, instead of a paragraph, you get a picture.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Live demo:&lt;/strong&gt; &lt;a href="https://overtaxed-mauve.vercel.app" rel="noopener noreferrer"&gt;https://overtaxed-mauve.vercel.app&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/usv240/overtaxed" rel="noopener noreferrer"&gt;https://github.com/usv240/overtaxed&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The idea: the answer IS the product
&lt;/h2&gt;

&lt;p&gt;The hackathon brief was clear. Don't reply with walls of text. Reply with maps, charts, and interactive things you can explore. If your best answer is a paragraph, you missed the point.&lt;/p&gt;

&lt;p&gt;Overtaxed leans all the way into that. Type an address and you get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A one line verdict ("you're overpaying about $3,793 a year")&lt;/li&gt;
&lt;li&gt;A map of your street, with your home glowing red against neighbours&lt;/li&gt;
&lt;li&gt;The comparable sales that prove it&lt;/li&gt;
&lt;li&gt;Two AI advocates that debate whether you should appeal&lt;/li&gt;
&lt;li&gt;A real, filled-in appeal document, ready to review&lt;/li&gt;
&lt;li&gt;And you can just ask the data anything and watch it answer live&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It also works for the UK (live council tax band checks) and covers a second US county, with no code changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  ClickHouse is the star, not the storage
&lt;/h2&gt;

&lt;p&gt;This was the fun part. ClickHouse is the primary database and it does all the actual thinking, live, over about 8 million real rows (6M UK Land Registry sales and 1.6M Chicago properties).&lt;/p&gt;

&lt;p&gt;A few of my favourite uses:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Comparable sales with &lt;code&gt;geoDistance()&lt;/code&gt;&lt;/strong&gt; to find the nearest real sales to any home.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fairness science live:&lt;/strong&gt; the standard IAAO uniformity metrics (PRD and COD) computed over 60k+ sold homes, sub-second.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A "Tax Divide" heatmap:&lt;/strong&gt; one spatial query turns 1.6 million homes into about 1,800 map cells (&lt;code&gt;round(lat, 2), round(lng, 2), avg(ratio)&lt;/code&gt;) in roughly 200ms. No external GIS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero-ETL ingestion&lt;/strong&gt; with &lt;code&gt;INSERT ... SELECT FROM url(...)&lt;/code&gt;, so ClickHouse reads the raw government CSVs straight off HTTP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A materialized view&lt;/strong&gt; (&lt;code&gt;AggregatingMergeTree&lt;/code&gt; with &lt;code&gt;argMaxState&lt;/code&gt;) for each home's latest sale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ask the data anything:&lt;/strong&gt; the agent turns your plain-English question into a safe, read-only ClickHouse query, runs it, and picks a chart. This is the judges' own "ask my logs anything" idea, pointed at a real civic dataset. The generated SQL and the run time stay on screen, so every answer is auditable.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every result shows its query latency, so the speed is proven, not claimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trigger.dev runs the whole show
&lt;/h2&gt;

&lt;p&gt;The experience is a Trigger.dev &lt;code&gt;chat.agent()&lt;/code&gt;. The agent routes each message to tools that query ClickHouse and return visual specs (a map, a chart, a card), never prose.&lt;/p&gt;

&lt;p&gt;But it does more than chat:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Durable, long-running ingestion tasks&lt;/strong&gt; stream millions of rows into ClickHouse, retryable, no timeouts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A durable child sub-task&lt;/strong&gt; runs the "should you appeal?" debate: two advocates argue in parallel, then a verdict. Even if the browser closes, it keeps running.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheduled cron task&lt;/strong&gt; re-checks saved homes and flags changes, surfaced by a "Watch this home" button in the UI.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The bonus round: OLTP + OLAP in one query
&lt;/h2&gt;

&lt;p&gt;For the OLTP + OLAP category, saved appeals live in Postgres. The portfolio page runs a single ClickHouse query that pulls those Postgres rows in via the &lt;code&gt;postgresql()&lt;/code&gt; table function and joins them against the analytics tables. One statement, two engines, with the query and its latency shown right on the page.&lt;/p&gt;

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

&lt;p&gt;This is not a toy. In Cook County alone we measure about $460 million a year of unfair over-assessment, live over real homes. Scaled nationally, and consistent with published research across 118 million homes, that points to roughly $20 billion a year, quietly taken from the people who can least afford it.&lt;/p&gt;

&lt;p&gt;Overtaxed turns those buried public records into a verdict, visible evidence, and a path to act, in about ten seconds.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://overtaxed-mauve.vercel.app" rel="noopener noreferrer"&gt;https://overtaxed-mauve.vercel.app&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code (MIT):&lt;/strong&gt; &lt;a href="https://github.com/usv240/overtaxed" rel="noopener noreferrer"&gt;https://github.com/usv240/overtaxed&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built by team Agent Forge for the ClickHouse x Trigger.dev Virtual Summer Hackathon 2026. Thanks for reading.&lt;/p&gt;

</description>
      <category>clickhouse</category>
      <category>ai</category>
      <category>showdev</category>
    </item>
    <item>
      <title>I built a spell-checker for the statistics in research papers</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Fri, 03 Jul 2026 16:49:52 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-built-a-spell-checker-for-the-statistics-in-research-papers-3a9m</link>
      <guid>https://dev.to/ujwal240/i-built-a-spell-checker-for-the-statistics-in-research-papers-3a9m</guid>
      <description>&lt;p&gt;I kept running into the same uncomfortable fact. Somewhere around half of published&lt;br&gt;
psychology papers have at least one number that doesn't add up. It's usually not&lt;br&gt;
fraud. It's tired people making small mistakes. A p-value typed wrong. An average&lt;br&gt;
that can't exist for the sample size. A result called "significant" that, when you&lt;br&gt;
actually check it, isn't.&lt;/p&gt;

&lt;p&gt;The strange thing is that these slip through. Reviewers read for the ideas and the&lt;br&gt;
argument, not the arithmetic. Almost nobody sits down and recomputes a p-value by&lt;br&gt;
hand. And the tools that can do it are expensive, closed, and sold to journals, so&lt;br&gt;
they only run after you've already submitted. The one person who'd benefit the most,&lt;br&gt;
the author about to hit submit, gets nothing.&lt;/p&gt;

&lt;p&gt;So I built Rigor. You paste your paper, or drop in a PDF, and it checks the numbers&lt;br&gt;
for you in a few seconds.&lt;/p&gt;

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

&lt;p&gt;Six checks now, and they each catch a different kind of slip.&lt;/p&gt;

&lt;p&gt;It recomputes every p-value from the test statistic. If you wrote p &amp;lt; .001 but the&lt;br&gt;
math says p = .06, it tells you, and it flags the ones that flip a result from&lt;br&gt;
significant to not.&lt;/p&gt;

&lt;p&gt;It runs the GRIM and GRIMMER tests. GRIM catches an average that can't exist: the mean&lt;br&gt;
of ten whole-number answers can't be 3.45. GRIMMER does the same for the standard&lt;br&gt;
deviation. Small checks like that catch typos, and sometimes worse.&lt;/p&gt;

&lt;p&gt;It compares the degrees of freedom to the sample size. If a test needs more people&lt;br&gt;
than the study actually collected, something is mislabeled.&lt;/p&gt;

&lt;p&gt;It recomputes a reported effect size (Cohen's d) from the t-statistic, and flags a&lt;br&gt;
mismatch that rounding can't explain.&lt;/p&gt;

&lt;p&gt;And it reads the claims, checking whether the words match the numbers, like calling a&lt;br&gt;
result significant when the recomputed value says it isn't.&lt;/p&gt;

&lt;p&gt;Every finding comes with a plain explanation and a suggestion for what to do next,&lt;br&gt;
not just a red mark.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I'm proudest of
&lt;/h2&gt;

&lt;p&gt;Most tools stop at "something is wrong." Rigor goes one step further and tells you&lt;br&gt;
which single number is most likely the mistake. A paper's statistics are all linked,&lt;br&gt;
the sample size, the degrees of freedom, the test statistic, the p-value, the mean. So&lt;br&gt;
when several checks fail, Rigor looks for the one correction that would resolve the&lt;br&gt;
most of them, and proves it by re-running the checks with that value substituted. On&lt;br&gt;
the demo paper it says something like "the sample size is the likely typo; fix it and&lt;br&gt;
every flagged test lines up." That is the difference between finding a problem and&lt;br&gt;
knowing how to fix it.&lt;/p&gt;

&lt;p&gt;It also puts each paper in context. Every audit compares it to a published baseline&lt;br&gt;
(about one in ten reported p-values is inconsistent, from a study of roughly 250,000 of&lt;br&gt;
them), and estimates the time it just saved you against checking by hand.&lt;/p&gt;

&lt;h2&gt;
  
  
  The question I was afraid of
&lt;/h2&gt;

&lt;p&gt;Early on I kept asking myself whether this was just ChatGPT with a nice screen on&lt;br&gt;
top. That question kills a lot of AI projects, and it deserved a real answer.&lt;/p&gt;

&lt;p&gt;Here's how I made sure it wasn't. Qwen, the model, only reads. Its one job is to pull&lt;br&gt;
the numbers and claims out of messy writing. It never decides if something is right&lt;br&gt;
or wrong. Every verdict comes from plain math, exact statistics that can't be made up.&lt;br&gt;
You can even prove it: turn the AI off and run the math engine over 530 test cases, and&lt;br&gt;
it still scores 100 percent. If the model misreads something, the worst case is a flag&lt;br&gt;
you can dismiss. It can never invent a wrong answer, because it never gives an answer at&lt;br&gt;
all. The math does.&lt;/p&gt;

&lt;p&gt;That split, where the model reads and the math judges, is the whole idea.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Qwen came in
&lt;/h2&gt;

&lt;p&gt;I used Qwen through Alibaba Cloud's Model Studio. The part only a good model can do is&lt;br&gt;
read real, free-form scientific writing and pull the statistics out as clean data. I&lt;br&gt;
used Qwen's function calling for this, so instead of hoping it hands back tidy text I&lt;br&gt;
can parse, it fills in a proper structured form.&lt;/p&gt;

&lt;p&gt;Because reading is the only uncertain step, I also let it read a paper a few times and&lt;br&gt;
keep only the numbers the runs agree on, and it shows that agreement as a score. It&lt;br&gt;
turns the one shaky part into a measured number.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turning it into a real agent
&lt;/h2&gt;

&lt;p&gt;For a while Rigor was a straight line. Extract, check, report. Useful, but that's a&lt;br&gt;
pipeline, not an agent. So I gave Qwen the checks as tools and let it run its own loop.&lt;br&gt;
Now it reads the paper, decides what to check, calls the tools itself, thinks about the&lt;br&gt;
results, and then tells you whether the problems look like one-off typos or something&lt;br&gt;
more systematic.&lt;/p&gt;

&lt;p&gt;My favorite part is that you can watch it happen. There's a live log in the app that&lt;br&gt;
streams each step as it goes: reading, calling grim_test, calling recompute_pvalue,&lt;br&gt;
reasoning, then the verdict.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mistake that taught me the most
&lt;/h2&gt;

&lt;p&gt;I ran an early version on some real papers, feeling pretty smart, and it flagged a&lt;br&gt;
measurement of 6.07 micrometres as an impossible average. That's nonsense. GRIM only&lt;br&gt;
applies to whole-number ratings, not physical measurements. My extraction was too&lt;br&gt;
eager. I tightened it so it only checks what it should, and now it stays quiet when a&lt;br&gt;
paper is out of its depth. Being careful turned out to matter as much as being clever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Does it work on real papers?
&lt;/h2&gt;

&lt;p&gt;Yes, and I have a favorite example. I ran it on a published geology paper about rock&lt;br&gt;
erosion. It flagged a correlation the authors reported "across the four rock types" as&lt;br&gt;
significant. With only four data points that is a shaky claim, and it recomputes to&lt;br&gt;
about p = 0.08. I read the paper's own methods to be sure, and yes, four rock types,&lt;br&gt;
four points. Rigor caught it on its own.&lt;/p&gt;

&lt;p&gt;Then I ran it over 26 real published papers in a single pass, the editorial-review&lt;br&gt;
shape. Recomputing all their reported statistics by hand would take about three hours.&lt;br&gt;
Rigor did it in under six minutes and narrowed the pile to the three papers that needed&lt;br&gt;
a human look, ranking the one I had verified worst of all.&lt;/p&gt;

&lt;p&gt;It's honest about its limits, too. On long, messy papers extraction can be noisier,&lt;br&gt;
which is exactly why there's a human review step and the agreement score. It flags, you&lt;br&gt;
decide, and you dismiss anything it got wrong before you export the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  Running it on Alibaba Cloud
&lt;/h2&gt;

&lt;p&gt;The whole thing runs on Alibaba Cloud, in a container on a small server in Singapore,&lt;br&gt;
with Qwen doing the reading. It's open source, and it goes where you work: a website, a&lt;br&gt;
command-line tool, a GitHub Action that screens papers on every push, and an MCP server&lt;br&gt;
so other AI agents can call the checks too. Rigor ended up being a small building&lt;br&gt;
block, not just a website.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd pass on
&lt;/h2&gt;

&lt;p&gt;If you build with a model, use it for the one thing only it can do, which is making&lt;br&gt;
sense of human writing, and let ordinary code own anything that has to be correct. That&lt;br&gt;
line is what turns a demo into something people can actually trust. And for a tool about&lt;br&gt;
integrity, being honest about what it can't do, and only ever showing numbers I can&lt;br&gt;
prove, made it more credible, not less.&lt;/p&gt;

&lt;p&gt;Rigor is free and open source. The code is at &lt;a href="https://github.com/usv240/rigor" rel="noopener noreferrer"&gt;https://github.com/usv240/rigor&lt;/a&gt;, and you&lt;br&gt;
can try the live version, paste a paper, and watch it work.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Making product recalls executable with Aurora DSQL and Vercel</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Thu, 25 Jun 2026 03:53:41 +0000</pubDate>
      <link>https://dev.to/ujwal240/making-product-recalls-executable-with-aurora-dsql-and-vercel-2nja</link>
      <guid>https://dev.to/ujwal240/making-product-recalls-executable-with-aurora-dsql-and-vercel-2nja</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;Live demo: &lt;a href="https://safestate.vercel.app" rel="noopener noreferrer"&gt;https://safestate.vercel.app&lt;/a&gt; , code: &lt;a href="https://github.com/usv240/safestate" rel="noopener noreferrer"&gt;https://github.com/usv240/safestate&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;A product recall today is basically a notice. It lives on a webpage, or a PDF, or an email that somebody is supposed to read. Say the problem out loud and it gets uncomfortable fast. A recalled crib can be listed and sold to another family, and nobody in that sale ever sees the recall. Reselling recalled goods is actually illegal, and recalled infant products have killed kids.&lt;/p&gt;

&lt;p&gt;I spent this hackathon building something to close that gap. I called it SafeState, and the idea is small: make the recall do something. When a second-hand item is listed or sold, the marketplace checks SafeState first, and recalled units get blocked right at checkout. It is precise down to the serial number, so safe units still sell.&lt;/p&gt;

&lt;p&gt;It runs on the stack this hackathon is about. A Next.js front end on Vercel, with Amazon Aurora DSQL behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why DSQL is the whole point here
&lt;/h2&gt;

&lt;p&gt;The promise SafeState has to keep is this: the moment a recall lands in any region, no marketplace anywhere should ever read that product as "safe" again.&lt;/p&gt;

&lt;p&gt;That is a strong consistency problem, not a nice-to-have. If there is any window where a recalled product still looks safe, that is exactly when it gets sold. An eventually consistent store or a nightly sync leaves that window open. DSQL's active-active, multi-region setup with strong consistency is what closes it.&lt;/p&gt;

&lt;p&gt;I set up a real peered cluster across us-east-1 and us-east-2, with us-west-2 as the witness. Write a recall through one region's endpoint and you can read it back from the other region right away. There is a page in the app that lets you run that yourself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one trick that makes it work
&lt;/h2&gt;

&lt;p&gt;DSQL runs on snapshot isolation (PostgreSQL REPEATABLE READ) with optimistic concurrency. It catches write-write conflicts at commit time. Snapshot isolation will not protect you from write skew, so I had to design around that.&lt;/p&gt;

&lt;p&gt;To guarantee that a recall and a sale of the same product actually collide, I make both of them write the same row. Every model has one &lt;code&gt;safety_guard&lt;/code&gt; row that holds its status and an epoch number.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// authorize-transfer, simplified. The AUTHORIZED path touches the SAME guard&lt;/span&gt;
&lt;span class="c1"&gt;// row a concurrent recall writes, so DSQL is forced to detect the conflict.&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;BEGIN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT epoch FROM safety_guard WHERE model_id = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="c1"&gt;// ...evaluate every active directive against THIS unit's serial...&lt;/span&gt;
&lt;span class="c1"&gt;// if it is covered, return BLOCKED. otherwise:&lt;/span&gt;

&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;INSERT INTO ownership_transfers (...) VALUES (...)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE product_instances SET current_owner_id = $1 WHERE id = $2&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;buyer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;UPDATE safety_guard SET updated_at = now() WHERE model_id = $1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;modelId&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// the conflict-forcing write&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;COMMIT&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// the loser throws SQLSTATE 40001 / OC000 here&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the recall commits first, the sale's COMMIT throws &lt;code&gt;SQLSTATE 40001&lt;/code&gt; (&lt;code&gt;OC000&lt;/code&gt;). A small wrapper catches it, backs off with some jitter, and runs the whole transaction again. The second time around it reads the recalled state and returns BLOCKED. So there is no version of events where a recalled product slips through as safe.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RETRYABLE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;40001&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;OC000&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;OC001&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
&lt;span class="c1"&gt;// retry the WHOLE transaction on conflict, backoff plus jitter, max 3 attempts&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Proving it under load
&lt;/h2&gt;

&lt;p&gt;A guarantee you cannot see is just a claim, so I put a stress test right in the app: a hundred concurrent attempts to buy a recalled unit, fired at the live cluster at once. Every one comes back blocked. Zero recalled units sell, no matter the concurrency.&lt;/p&gt;

&lt;p&gt;Getting there taught me something. My first version put a &lt;code&gt;SELECT ... FOR UPDATE&lt;/code&gt; on the guard row in every check. That was overkill. Two blocked checks on the same model would each take a write intent on that one row and conflict with each other for no reason. The conflict I actually care about is between a recall and an authorized sale, and both of those already write the guard row. So I dropped the &lt;code&gt;FOR UPDATE&lt;/code&gt; from the read. The blocked path stopped fighting itself, the load test went clean, and the recall versus sale conflict still fires exactly as before.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two databases, on purpose
&lt;/h2&gt;

&lt;p&gt;Not everything belongs in the transactional store. Every public check, verify, and scan is an event worth counting, and every Safety Receipt is a small durable record. That stream is write-heavy and key-accessed, and it does not need a distributed transaction. So it lives in Amazon DynamoDB, while Aurora DSQL keeps the transactional core. Picking the right database per workload, instead of forcing one to do both, kept the hot path clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Vercel side
&lt;/h2&gt;

&lt;p&gt;Route handlers talk to DSQL over the normal Postgres protocol, but auth is a short-lived IAM token minted per connection with &lt;code&gt;@aws-sdk/dsql-signer&lt;/code&gt;. There is no database password sitting in an env var anywhere.&lt;/p&gt;

&lt;p&gt;A Vercel Cron job pulls real recalls from the public CPSC API once a day. And Claude reads messy second-hand listings, the kind a person actually writes ("used baby sleeper, works fine"), and figures out which recall they match, with a confidence score. The uncertain ones go to a review queue instead of being auto-blocked.&lt;/p&gt;

&lt;p&gt;Two more things the same app does. When a recall is issued, it walks live ownership and emails the people who own one now, not the original buyers. And the public check fans out to CPSC, FDA, and NHTSA at once, so you can look up a product, a bag of spinach, or a car.&lt;/p&gt;

&lt;p&gt;One thing that cost me an hour. Vercel functions run on Lambda, and Lambda reserves &lt;code&gt;AWS_ACCESS_KEY_ID&lt;/code&gt;, &lt;code&gt;AWS_SECRET_ACCESS_KEY&lt;/code&gt; and &lt;code&gt;AWS_REGION&lt;/code&gt;. You cannot set those as env vars. So I pass the DSQL credentials under different names and hand them to the signer directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_ACCESS_KEY_ID&lt;/span&gt;
  &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;accessKeyId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_ACCESS_KEY_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;secretAccessKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;SAFESTATE_AWS_SECRET_ACCESS_KEY&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// local dev falls back to the default AWS provider chain&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DsqlSigner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;creds&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;region&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  A few things that helped
&lt;/h2&gt;

&lt;p&gt;If you build on DSQL, pick a problem where being correct under concurrency is the actual product, not a side detail. That is where it earns its keep. Make your conflicting operations write the same row so OCC has something to catch. And write the retry-on-40001 wrapper before anything else, because you will lean on it constantly.&lt;/p&gt;

&lt;p&gt;Recalls should stop being PDFs and start being decisions. Aurora DSQL and Vercel got me there over a weekend.&lt;/p&gt;

&lt;p&gt;Live: &lt;a href="https://safestate.vercel.app" rel="noopener noreferrer"&gt;https://safestate.vercel.app&lt;/a&gt; , code: &lt;a href="https://github.com/usv240/safestate" rel="noopener noreferrer"&gt;https://github.com/usv240/safestate&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I built this for the H0: Hack the Zero Stack hackathon. #H0Hackathon&lt;/em&gt;&lt;/p&gt;

</description>
      <category>aws</category>
      <category>database</category>
      <category>webdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>I built a GitLab flow that tells you what a diff actually means</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Mon, 22 Jun 2026 23:08:24 +0000</pubDate>
      <link>https://dev.to/ujwal240/i-built-a-gitlab-flow-that-tells-you-what-a-diff-actually-means-3bfp</link>
      <guid>https://dev.to/ujwal240/i-built-a-gitlab-flow-that-tells-you-what-a-diff-actually-means-3bfp</guid>
      <description>&lt;p&gt;Every code review starts the same way. You open a diff and try to figure out&lt;br&gt;
what it actually means - not just what changed, but what depends on it,&lt;br&gt;
whether it breaks something downstream, and whether someone else is about to&lt;br&gt;
step on the same code. That investigation usually falls on the reviewer,&lt;br&gt;
by hand, every single time.&lt;/p&gt;

&lt;p&gt;I spent the last two weeks building something to change that.&lt;/p&gt;

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

&lt;p&gt;&lt;strong&gt;Orbit Change Passport&lt;/strong&gt; is a GitLab Duo Agent Platform flow that fires&lt;br&gt;
automatically when a merge request is marked ready for review. It queries&lt;br&gt;
GitLab Orbit's Knowledge Graph and posts a structured comment before anyone&lt;br&gt;
reads a single line of the diff.&lt;/p&gt;

&lt;p&gt;Here is what the comment covers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Changed Surface&lt;/strong&gt; - the exact functions and methods the diff touched,
identified by name, not line numbers&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency graph&lt;/strong&gt; - a live Mermaid diagram of everything that imports
the changed modules, rendered inline in GitLab&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conflict Radar&lt;/strong&gt; - every other open MR right now that touches the same
code, caught before merge instead of at it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-project blast radius&lt;/strong&gt; - files in other projects in the group that
depend on what changed. This one is only possible because Orbit's graph
spans the whole group, not just one repo&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test gaps&lt;/strong&gt; - test files that import the changed module but were not
touched in this MR&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Suggested reviewers&lt;/strong&gt; - based on recent authorship of dependent files&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reviewer Brief&lt;/strong&gt; - a second shorter comment posted immediately after:
one paragraph with the single most important thing before reading the diff&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A companion Duo Chat agent lets reviewers keep asking questions against the&lt;br&gt;
same graph after the passport posts - "what else calls this?",&lt;br&gt;
"who imports this file?" - answered live, not restated from the comment.&lt;/p&gt;

&lt;h2&gt;
  
  
  A concrete example
&lt;/h2&gt;

&lt;p&gt;Here is what the impact line looks like on a real run:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Impact: HIGH - 2 definitions changed - 2 within-project dependents -&lt;br&gt;
cross-project blast radius (3 projects) - 1 conflict&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single line tells me: this change has dependents outside this repo,&lt;br&gt;
and another open MR is already touching the same functions. Both of those&lt;br&gt;
things would normally take 10 minutes to find manually. They showed up&lt;br&gt;
automatically before I opened the diff.&lt;/p&gt;

&lt;p&gt;The cross-project result is the one that surprised me most. A docstring&lt;br&gt;
change to &lt;code&gt;engine/orbit_client.py&lt;/code&gt; surfaced 14 dependent files across three&lt;br&gt;
other projects in the group. Nobody catches that by scrolling through a diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;The flow uses a three-tier strategy to identify which functions a diff&lt;br&gt;
actually touched:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DEFINES traversal in the Orbit graph (File to Definition edge)&lt;/li&gt;
&lt;li&gt;fqn-fragment query as a fallback for Rust crates&lt;/li&gt;
&lt;li&gt;Bounded page scan as a last resort&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each tier produces a different confidence level in the output. This redundancy&lt;br&gt;
matters because query availability on the live Orbit instance is not guaranteed.&lt;br&gt;
DEFINES, IMPORTS, CALLS, and Definition filtering have each independently been&lt;br&gt;
unavailable within a two-hour window during testing. A flow that only works&lt;br&gt;
when one specific query is live is not useful.&lt;/p&gt;

&lt;p&gt;For dependents, the flow runs ImportedSymbol lookups in both FQN and&lt;br&gt;
crate-relative forms for Rust, and by identifier_name stem for Python files.&lt;br&gt;
That last one took a while to figure out. Python relative imports&lt;br&gt;
(&lt;code&gt;from . import module&lt;/code&gt;) are stored in Orbit with &lt;code&gt;import_path&lt;/code&gt; set to the&lt;br&gt;
package name and &lt;code&gt;identifier_name&lt;/code&gt; set to the module stem, not as a dotted&lt;br&gt;
path. We found that by reading the actual graph data.&lt;/p&gt;

&lt;p&gt;The Reviewer Brief is a second call to &lt;code&gt;create_merge_request_note&lt;/code&gt; within the&lt;br&gt;
same agent turn. We arrived at this after two other approaches failed: a&lt;br&gt;
router-chained second AgentComponent that never started, and a second ambient&lt;br&gt;
flow that turned out to share the trigger silently with the first. Only the&lt;br&gt;
most-recently-enabled one fires - that was not documented anywhere and took a&lt;br&gt;
while to diagnose.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is in the repo
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flows/change-passport/change-passport.yaml   - the Duo Agent Platform flow
skills/ask-orbit-passport/SKILL.md           - companion Duo Chat agent
engine/passport_runner.py                    - CLI runner, posts to any MR
engine/orbit_client.py                       - Orbit query layer
research/findings.md                         - every query pattern confirmed
                                               against the live graph
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;p&gt;The flow is live in the AI Catalog. The repo is public and MIT licensed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Repo: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Flow: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/flows/1011552/" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/flows/1011552/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Agent: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/agents/1011562" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/automate/agents/1011562&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Demo MR: &lt;a href="https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/merge_requests/5" rel="noopener noreferrer"&gt;https://gitlab.com/gitlab-ai-hackathon/transcend/38491653/-/merge_requests/5&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for the GitLab Transcend Hackathon.&lt;/p&gt;

&lt;p&gt;A diff shows what changed. Orbit Change Passport shows what that change means.&lt;/p&gt;

</description>
      <category>gitlab</category>
      <category>opensource</category>
      <category>ai</category>
      <category>devtools</category>
    </item>
    <item>
      <title>Scaling Astrolord: Our Journey with Serverless on AWS</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Wed, 04 Feb 2026 03:23:53 +0000</pubDate>
      <link>https://dev.to/ujwal240/scaling-astrolord-our-journey-with-serverless-on-aws-2j78</link>
      <guid>https://dev.to/ujwal240/scaling-astrolord-our-journey-with-serverless-on-aws-2j78</guid>
      <description>&lt;p&gt;When we started building &lt;a href="https://astro-lord.com" rel="noopener noreferrer"&gt;Astrolord&lt;/a&gt;, we knew we had a tricky engineering problem on our hands. We were building an AI-powered astrology platform that needed to perform heavy astronomical calculations one second and stream personalized AI responses the next. We needed infrastructure that could handle a sudden spike in traffic, like during a major planetary transit, without burning a hole in our pocket during quiet hours.&lt;/p&gt;

&lt;p&gt;We decided early on to go all-in on AWS Serverless. It wasn't just about avoiding server management; it was about building an architecture that could scale to zero when no one was using it, and scale up infinitely when they were. Here is a look at how we pieced it all together.&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.amazonaws.com%2Fuploads%2Farticles%2F690st14e065ha0u7eqmi.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.amazonaws.com%2Fuploads%2Farticles%2F690st14e065ha0u7eqmi.png" alt="High Level Architecture Diagram - Showing React/S3 and FastAPI/Lambda flows" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Compute Layer: AWS Serverless
&lt;/h2&gt;

&lt;p&gt;The heart of our backend is Python. The challenge was deploying our application in a way that didn't require maintaining a fleet of servers or paying for idle time.&lt;/p&gt;

&lt;p&gt;We chose AWS Lambda for its efficiency. By adapting our web application to run in a serverless environment, we can write modern, clean code while letting AWS handle the underlying infrastructure.&lt;/p&gt;

&lt;p&gt;There were trade-offs, of course. We had to be careful with "cold starts", the initial delay when a function wakes up. We spent time optimizing our application startup to keep latency minimal. But the benefit of paying literally zero dollars when no traffic is hitting the API made it an easy choice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Front Door: API Gateway
&lt;/h2&gt;

&lt;p&gt;Sitting in front of those Lambda functions is Amazon API Gateway. Connective tissue is often overlooked, but for us, this piece is critical. It acts as our secure entry point, routing requests to the correct backend services.&lt;/p&gt;

&lt;p&gt;Beyond just routing, we rely on it for stability. We configured usage plans and throttling rules directly at the gateway level. This means if a bad actor tries to hammer our API, AWS blocks them before they even wake up our compute layer, saving us money and processing power.&lt;/p&gt;

&lt;h2&gt;
  
  
  Delivering the UI: S3 and CloudFront
&lt;/h2&gt;

&lt;p&gt;For the frontend, we built a static React application. We didn't want to run a web server or manage containers just to serve HTML and JavaScript files.&lt;/p&gt;

&lt;p&gt;Instead, we use what I consider the "gold standard" for static hosting: Amazon S3 paired with CloudFront. We upload our build artifacts into an S3 bucket, which offers incredible durability. Then, CloudFront sits in front of that bucket, caching our application at edge locations all over the world.&lt;/p&gt;

&lt;p&gt;The result is that a user in Mumbai loads the app just as fast as a user in New York. We also configured strict security controls, ensuring that no one can bypass the CDN to hit our bucket directly.&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.amazonaws.com%2Fuploads%2Farticles%2Fmj1477mqexsmtjstpeur.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.amazonaws.com%2Fuploads%2Farticles%2Fmj1477mqexsmtjstpeur.png" alt="Astrolord Dashboard - The result of our React + Vite frontend" width="800" height="407"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Observability
&lt;/h2&gt;

&lt;p&gt;One thing they don't tell you about distributed systems is that debugging can be a nightmare if you aren't prepared. Since we don't have a single server to SSH into, we rely heavily on centralized logging and monitoring.&lt;/p&gt;

&lt;p&gt;We capture all standard output from our functions into CloudWatch Logs. We also set up specific metric filters to track errors and throttling events. If our error rate crosses a certain threshold, an alarm triggers and we get notified immediately. It gives us the confidence to deploy on a Friday (well, almost).&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.amazonaws.com%2Fuploads%2Farticles%2Fjiywon5fouw85rfq0nox.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.amazonaws.com%2Fuploads%2Farticles%2Fjiywon5fouw85rfq0nox.png" alt="AWS CloudWatch Metrics - Showing Lambda invocations or low latency" width="800" height="528"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The Economics of Serverless
&lt;/h2&gt;

&lt;p&gt;One of the biggest wins for us wasn't just technical, it was financial.&lt;/p&gt;

&lt;p&gt;With a traditional EC2 or container-based architecture, you are paying for capacity 24/7. Even if no one visits your site at 3 AM, that server is still running, and you are still receiving a bill.&lt;/p&gt;

&lt;p&gt;With this serverless setup, our infrastructure cost aligns perfectly with our business growth.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Zero Idle Costs&lt;/strong&gt;: When our app is quiet, our compute bill is literally $0.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Generous Free Tier&lt;/strong&gt;: AWS offers a substantial free tier for Lambda and API Gateway, meaning we effectively run our development and testing environments for free.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;No Over-Provisioning&lt;/strong&gt;: We never have to guess how many servers we need. The system just scales to meet demand, whether that's 5 users or 5,000.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why This Architecture Works for Us
&lt;/h2&gt;

&lt;p&gt;Usage patterns in our app are unpredictable. Some days are quiet; other days, everyone wants to know what the full moon means for them.&lt;/p&gt;

&lt;p&gt;This serverless architecture on AWS absorbs that volatility perfectly. We don't have to provision for peak capacity and pay for idle time. We just pay for the milliseconds of compute we actually use. It has allowed us to focus less on "keeping the lights on" and more on improving the actual product.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>serverless</category>
      <category>python</category>
      <category>react</category>
    </item>
    <item>
      <title>Time-Travel Debugging for Python: A Complete Tutorial</title>
      <dc:creator>Ujwal Vanjare</dc:creator>
      <pubDate>Fri, 23 Jan 2026 05:10:45 +0000</pubDate>
      <link>https://dev.to/ujwal240/-time-travel-debugging-for-python-a-complete-tutorial-1dip</link>
      <guid>https://dev.to/ujwal240/-time-travel-debugging-for-python-a-complete-tutorial-1dip</guid>
      <description>&lt;h1&gt;
  
  
  Time-Travel Debugging for Python: A Complete Tutorial
&lt;/h1&gt;

&lt;p&gt;Building web applications means dealing with external APIs, databases, and the inevitable production bugs. I'm going to show you how to capture production issues and debug them locally without ever hitting those external services again.&lt;/p&gt;

&lt;p&gt;This is a complete walkthrough using Timetracer with a Starlette application. By the end, you'll have a working example and understand how to apply this to your own projects.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; If you're new to Timetracer, you might want to check out &lt;a href="https://dev.to/ujwal240/i-got-tired-of-it-works-on-my-machine-so-i-built-a-time-travel-debugger-235h"&gt;my initial post&lt;/a&gt; about why I built this tool, or &lt;a href="https://dev.to/ujwal240/timetracer-v14-native-django-support-and-easiest-pytest-integration-5e4f"&gt;the v1.4 release post&lt;/a&gt; covering Django and pytest integration.&lt;/p&gt;

&lt;p&gt;This tutorial focuses specifically on Starlette integration and shows the complete debugging workflow with the new v1.6.0 dashboard features.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem
&lt;/h2&gt;

&lt;p&gt;You know that moment when a bug happens in production? You spend hours trying to reproduce it locally. You're making API calls to third-party services, dealing with rate limits, stale data, and that nagging feeling you're not testing the exact scenario that failed.&lt;/p&gt;

&lt;p&gt;Traditional debugging flow:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Bug reported in production&lt;/li&gt;
&lt;li&gt;Try to reproduce locally (often fails)&lt;/li&gt;
&lt;li&gt;Add logging and redeploy (slow)&lt;/li&gt;
&lt;li&gt;Hope you captured enough context (usually didn't)&lt;/li&gt;
&lt;li&gt;Repeat until fixed (hours or days)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;There's a better way. With Timetracer, you capture the entire request context in production and replay it locally. Think of it as a flight recorder for your web application.&lt;/p&gt;




&lt;h2&gt;
  
  
  Setting Up the Project
&lt;/h2&gt;

&lt;p&gt;Let's build a simple API that proxies GitHub user data. First, install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install &lt;/span&gt;starlette uvicorn httpx timetracer
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Create a file called &lt;code&gt;app.py&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.applications&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Starlette&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.routing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Route&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;starlette.responses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;JSONResponse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&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;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;message&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;Welcome to the Starlette + Timetracer example&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;endpoints&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="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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&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;/repos/{username}&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_repos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;user_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&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;user_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="n"&gt;repos_resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;repos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repos_resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&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;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total_repos&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;user_data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;public_repos&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;top_repos&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="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="n"&gt;r&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stars&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stargazers_count&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;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repos&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;stargazers_count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Starlette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Route&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="n"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/repos/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_repos&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;This gives us three endpoints: a homepage, a user lookup, and a repo list. The last two hit the GitHub API.&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.amazonaws.com%2Fuploads%2Farticles%2Fj6qgndsut4d8bjqahmbx.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.amazonaws.com%2Fuploads%2Farticles%2Fj6qgndsut4d8bjqahmbx.png" alt="Code Example" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The Starlette application with three endpoints&lt;/em&gt;&lt;/p&gt;


&lt;h2&gt;
  
  
  Integrating Timetracer
&lt;/h2&gt;

&lt;p&gt;Now add Timetracer. Import the integration and call &lt;code&gt;auto_setup()&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;timetracer.integrations.starlette&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;

&lt;span class="c1"&gt;# ... your routes ...
&lt;/span&gt;
&lt;span class="n"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Starlette&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;debug&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;routes&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="nc"&gt;Route&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="n"&gt;homepage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/user/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nc"&gt;Route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/repos/{username}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;get_repos&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# This is the only line you need for Timetracer
&lt;/span&gt;&lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plugins&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;httpx&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;That's it. One line of code. This adds middleware that captures every request and tracks all httpx calls to external APIs.&lt;/p&gt;




&lt;h2&gt;
  
  
  Recording Requests
&lt;/h2&gt;

&lt;p&gt;Start the server in record mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;record
uvicorn app:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your terminal should show Timetracer capturing requests:&lt;br&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.amazonaws.com%2Fuploads%2Farticles%2F4drra4t2z14zdvhgdc6v.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.amazonaws.com%2Fuploads%2Farticles%2F4drra4t2z14zdvhgdc6v.png" alt="Terminal Record Mode" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Terminal output showing Timetracer recording requests with timing information&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Now let's make some requests and see what gets captured.&lt;/p&gt;
&lt;h3&gt;
  
  
  Request 1: Homepage
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Response:&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;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Welcome to the Starlette + Timetracer example"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"endpoints"&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;"/"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/user/{username}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/repos/{username}"&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;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.amazonaws.com%2Fuploads%2Farticles%2F9zncr9odxstots4qmowp.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.amazonaws.com%2Fuploads%2Farticles%2F9zncr9odxstots4qmowp.png" alt="Homepage Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Browser showing the homepage JSON response&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Terminal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [OK] recorded GET /  id=cddb  status=200  total=9ms  deps=none
  cassette: cassettes/2026-01-23/GET__root__cddb6be9.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice &lt;code&gt;deps=none&lt;/code&gt; because this endpoint doesn't make any external calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request 2: User Lookup
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/user/octocat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&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;"login"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"octocat"&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;"The Octocat"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bio"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"public_repos"&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;"followers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21594&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;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.amazonaws.com%2Fuploads%2Farticles%2Fvlrvx0mad9xyki0e570l.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.amazonaws.com%2Fuploads%2Farticles%2Fvlrvx0mad9xyki0e570l.png" alt="User Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;GitHub user data returned through our API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Terminal output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [OK] recorded GET /user/octocat  id=88d7  status=200  total=472ms  deps=http.client:1
  cassette: cassettes/2026-01-23/GET__user_octocat__88d76871.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time &lt;code&gt;deps=http.client:1&lt;/code&gt; shows one external HTTP call was tracked. The duration is 472ms instead of 9ms because we're waiting for GitHub's API.&lt;/p&gt;

&lt;h3&gt;
  
  
  Request 3: Repository List
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/repos/octocat
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Fu8y58gt92u8fld7x2l9u.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.amazonaws.com%2Fuploads%2Farticles%2Fu8y58gt92u8fld7x2l9u.png" alt="Repos Response" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Top repositories for the octocat user&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This endpoint makes two GitHub API calls: one for the user data and one for the repository list.&lt;/p&gt;
&lt;h3&gt;
  
  
  What Got Saved?
&lt;/h3&gt;

&lt;p&gt;Each cassette is a JSON file containing your request, response, and all external dependencies with timing information.&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.amazonaws.com%2Fuploads%2Farticles%2F8xtfavn8n9i89igk0r8g.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.amazonaws.com%2Fuploads%2Farticles%2F8xtfavn8n9i89igk0r8g.png" alt="Cassette JSON Structure" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Cassette file showing the captured request, response, and external API calls&lt;/em&gt;&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Request details (method, path, headers, body)&lt;/li&gt;
&lt;li&gt;Response details (status, headers, body, duration)&lt;/li&gt;
&lt;li&gt;All external dependencies (each GitHub API call with its own timing)&lt;/li&gt;
&lt;li&gt;Metadata about the session (framework, timestamp, etc.)&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;
  
  
  Using the Dashboard
&lt;/h2&gt;

&lt;p&gt;Now for the interactive part. Timetracer includes a web dashboard to browse and analyze your captured requests.&lt;/p&gt;

&lt;p&gt;Start the dashboard server:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer serve &lt;span class="nt"&gt;--dir&lt;/span&gt; cassettes &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open &lt;a href="http://localhost:3000" rel="noopener noreferrer"&gt;http://localhost:3000&lt;/a&gt; in your browser:&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.amazonaws.com%2Fuploads%2Farticles%2Fcfvfdwjihspdthiaq37l.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.amazonaws.com%2Fuploads%2Farticles%2Fcfvfdwjihspdthiaq37l.png" alt="Dashboard Overview" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard showing all captured requests with statistics&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Total requests, success count, error count&lt;/li&gt;
&lt;li&gt;Every captured request with method, path, status, duration, and dependencies&lt;/li&gt;
&lt;li&gt;Search and filter capabilities&lt;/li&gt;
&lt;li&gt;View details or replay any request&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Viewing Request Details
&lt;/h3&gt;

&lt;p&gt;Click "View" on the &lt;code&gt;/repos/octocat&lt;/code&gt; request:&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.amazonaws.com%2Fuploads%2Farticles%2F3e0yws9uu0lq9bk1whky.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.amazonaws.com%2Fuploads%2Farticles%2F3e0yws9uu0lq9bk1whky.png" alt="Dashboard Detail View" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Detailed view showing request, response, and external API dependencies&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The detail view shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Request metadata: Path, method, timestamp&lt;/li&gt;
&lt;li&gt;Response: Status 200, duration 524ms&lt;/li&gt;
&lt;li&gt;Dependency Events: Both GitHub API calls with individual timings

&lt;ul&gt;
&lt;li&gt;GET &lt;a href="https://api.github.com/users/torvalds" rel="noopener noreferrer"&gt;https://api.github.com/users/torvalds&lt;/a&gt; (104ms)&lt;/li&gt;
&lt;li&gt;GET &lt;a href="https://api.github.com/users/torvalds/repos" rel="noopener noreferrer"&gt;https://api.github.com/users/torvalds/repos&lt;/a&gt; (95ms)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Ready-to-use replay command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This view tells you exactly what happened during the request, including all external services that were called.&lt;/p&gt;
&lt;h3&gt;
  
  
  Filtering Requests
&lt;/h3&gt;

&lt;p&gt;Type "repos" in the search box:&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.amazonaws.com%2Fuploads%2Farticles%2Fh8mkoq456od1mniictws.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.amazonaws.com%2Fuploads%2Farticles%2Fh8mkoq456od1mniictws.png" alt="Filtered Dashboard" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard filtered to show only repository-related requests&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The dashboard now shows "Showing 5 of 19 cassettes" with only the matching requests visible.&lt;/p&gt;

&lt;p&gt;You can also filter by HTTP method or status code to focus on specific types of requests.&lt;/p&gt;
&lt;h3&gt;
  
  
  Inspecting the Raw Data
&lt;/h3&gt;

&lt;p&gt;For technical inspection, the dashboard includes a Raw JSON viewer:&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.amazonaws.com%2Fuploads%2Farticles%2Fddjccf2x0t9fhbf50d7d.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.amazonaws.com%2Fuploads%2Farticles%2Fddjccf2x0t9fhbf50d7d.png" alt="Raw JSON Tab" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Raw JSON view showing the complete cassette structure&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;This gives you direct access to the underlying cassette data, making it easy to verify exactly what state is being captured and will be replayed.&lt;/p&gt;


&lt;h2&gt;
  
  
  Debugging a Real Bug
&lt;/h2&gt;

&lt;p&gt;Now let's use Timetracer for what it's really good at: debugging production issues without touching production.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Bug Appears
&lt;/h3&gt;

&lt;p&gt;Imagine a user reports that requesting a non-existent GitHub user crashes the server with a 500 error. The problematic code:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&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="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;  &lt;span class="c1"&gt;# Crashes on 404
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When someone requests a user that doesn't exist, GitHub returns 404, but our code assumes success and tries to parse the error response.&lt;/p&gt;

&lt;p&gt;Even though the app crashes, Timetracer still captures the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer [ERROR] recorded GET /user/nonexistent-user-12345  id=bad1  status=500  total=156ms  deps=http.client:1
  cassette: cassettes/2026-01-23/GET__user_nonexistent-user-12345__bad1234.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Inspecting the Error
&lt;/h3&gt;

&lt;p&gt;In the dashboard, click "View" on the failed request:&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.amazonaws.com%2Fuploads%2Farticles%2Fhitycmxkis4k6lqk02fl.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.amazonaws.com%2Fuploads%2Farticles%2Fhitycmxkis4k6lqk02fl.png" alt="Error Detail View" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Dashboard detail view showing a 404 error from GitHub API&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The detail view clearly shows that GitHub returned a 404, which propagated to our endpoint as a 500 error. You can see exactly what happened: the external API call failed, and our code didn't handle it properly.&lt;/p&gt;
&lt;h3&gt;
  
  
  The Fix
&lt;/h3&gt;

&lt;p&gt;Looking at the dashboard detail view, you can see GitHub returned 404. Fix the code:&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;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path_params&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;username&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;httpx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;AsyncClient&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.github.com/users/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;username&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="c1"&gt;# Check status code before parsing
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;404&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;JSONResponse&lt;/span&gt;&lt;span class="p"&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;error&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;User not found&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;404&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;JSONResponse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Getting the Replay Command
&lt;/h3&gt;

&lt;p&gt;The dashboard provides a ready-to-copy replay command:&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.amazonaws.com%2Fuploads%2Farticles%2Ffed3or0hsicw7py160ti.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.amazonaws.com%2Fuploads%2Farticles%2Ffed3or0hsicw7py160ti.png" alt="Replay Command" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Ready-to-use replay command for testing the fix&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Just copy this command to test your fix with the exact scenario that failed in production.&lt;/p&gt;


&lt;h2&gt;
  
  
  Testing the Fix Without Network
&lt;/h2&gt;

&lt;p&gt;This is where Timetracer shows its real value. You can test the fix using the captured cassette without making any real API calls to GitHub.&lt;/p&gt;

&lt;p&gt;Stop the server and restart in replay mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replay
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_CASSETTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cassettes/2026-01-23/GET__user_nonexistent-user-12345__bad1234.json
uvicorn app:app &lt;span class="nt"&gt;--reload&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Ff047gpky6ldickd6xsor.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.amazonaws.com%2Fuploads%2Farticles%2Ff047gpky6ldickd6xsor.png" alt="Replay Mode" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Server running in replay mode with mocked external API responses&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Make the same request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:8000/user/nonexistent-user-12345
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Terminal shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;timetracer replay GET /user/nonexistent-user-12345  mocked=1  matched=OK  runtime=5ms
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Response:&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"User not found"&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;Status: 404&lt;/p&gt;

&lt;p&gt;The fix works. Notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;No network call - the response came from the cassette&lt;/li&gt;
&lt;li&gt;Fast: 5ms instead of the original 156ms&lt;/li&gt;
&lt;li&gt;Exact scenario: Same 404 from GitHub that caused the original crash&lt;/li&gt;
&lt;li&gt;Offline: This works with no internet connection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You just debugged and fixed a production bug without touching production or making a single external API call.&lt;/p&gt;




&lt;h2&gt;
  
  
  Performance Comparison
&lt;/h2&gt;

&lt;p&gt;Let's compare the timing differences:&lt;/p&gt;

&lt;h3&gt;
  
  
  Record Mode vs Replay Mode
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Endpoint&lt;/th&gt;
&lt;th&gt;Record Duration&lt;/th&gt;
&lt;th&gt;Replay Duration&lt;/th&gt;
&lt;th&gt;Speedup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;9ms&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;1.1x&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/user/octocat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;472ms&lt;/td&gt;
&lt;td&gt;8ms&lt;/td&gt;
&lt;td&gt;59x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;/repos/octocat&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;524ms&lt;/td&gt;
&lt;td&gt;10ms&lt;/td&gt;
&lt;td&gt;52x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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.amazonaws.com%2Fuploads%2Farticles%2Ftj67pf1soolio3ra5u2w.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.amazonaws.com%2Fuploads%2Farticles%2Ftj67pf1soolio3ra5u2w.png" alt="Performance Comparison" width="800" height="800"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Comparison of request durations in record mode versus replay mode&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;For endpoints without external calls, the times are similar. But anything that touches an external API or database becomes dramatically faster in replay mode.&lt;/p&gt;

&lt;p&gt;This isn't just about speed. It's about reliability. Tests that depend on external APIs can be flaky due to network issues, rate limiting, or changing data. Replay mode eliminates all those problems.&lt;/p&gt;


&lt;h2&gt;
  
  
  When to Use This
&lt;/h2&gt;

&lt;p&gt;I've found Timetracer most useful in these scenarios:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Debugging Production Bugs&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When a user reports an issue, capture the failing request in production. Download the cassette and debug locally with the exact same conditions. No need to reproduce complex scenarios or guess at what data caused the problem.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Integration Testing&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tests that hit real APIs are slow and unreliable. Record your test scenarios once, then replay them. Tests run in milliseconds instead of seconds, and they never fail due to network issues or rate limiting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Offline Development&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Working on a plane or anywhere without internet? Load up cassettes with the API responses you need. Everything works normally without network access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Performance Analysis&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The dashboard shows you exactly how long each external dependency takes. If your endpoint is slow, you can see whether it's your code or a slow external API.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Preventing Regressions&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you fix a bug, keep the cassette and add it to your test suite. That specific scenario is now covered forever.&lt;/p&gt;


&lt;h2&gt;
  
  
  Framework Support
&lt;/h2&gt;

&lt;p&gt;Timetracer works with:&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.amazonaws.com%2Fuploads%2Farticles%2F1kl89ercgs5vvdvb1trd.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.amazonaws.com%2Fuploads%2Farticles%2F1kl89ercgs5vvdvb1trd.png" alt="Framework Support" width="800" height="393"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Supported web frameworks and external service integrations&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Web Frameworks:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI&lt;/li&gt;
&lt;li&gt;Starlette (new in v1.6.0)&lt;/li&gt;
&lt;li&gt;Flask&lt;/li&gt;
&lt;li&gt;Django&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;External Services:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;httpx and requests (HTTP clients)&lt;/li&gt;
&lt;li&gt;Motor and PyMongo (MongoDB)&lt;/li&gt;
&lt;li&gt;SQLAlchemy (SQL databases)&lt;/li&gt;
&lt;li&gt;Redis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The integration is similar across all frameworks. Usually just &lt;code&gt;auto_setup(app)&lt;/code&gt; or adding middleware.&lt;/p&gt;


&lt;h2&gt;
  
  
  Trade-offs to Consider
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Storage&lt;/strong&gt;: Each cassette is a JSON file. If you have many unique requests, you'll accumulate files. Clean up old cassettes periodically or store them in S3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Sensitive data&lt;/strong&gt;: Cassettes contain your actual request and response data. Review what's being captured, especially in production. Timetracer has built-in redaction for common sensitive fields like passwords and tokens, but verify this for your use case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cassette maintenance&lt;/strong&gt;: API responses change over time. You'll need to re-record cassettes when your external dependencies change their response format.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not a replacement&lt;/strong&gt;: This isn't trying to replace your testing framework or mocking library. It's a debugging tool that captures production context and lets you work with it locally.&lt;/p&gt;


&lt;h2&gt;
  
  
  Getting Started
&lt;/h2&gt;

&lt;p&gt;Install Timetracer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# For Starlette&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[starlette]

&lt;span class="c"&gt;# For FastAPI&lt;/span&gt;
pip &lt;span class="nb"&gt;install &lt;/span&gt;timetracer[fastapi]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Integrate into your app:&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;timetracer.integrations.starlette&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;auto_setup&lt;/span&gt;
&lt;span class="nf"&gt;auto_setup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;plugins&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;httpx&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;Run in record mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;record
uvicorn app:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;View the dashboard:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;timetracer serve &lt;span class="nt"&gt;--dir&lt;/span&gt; cassettes &lt;span class="nt"&gt;--port&lt;/span&gt; 3000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Test in replay mode:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_MODE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;replay
&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;TIMETRACER_CASSETTE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;path/to/cassette.json
uvicorn app:app
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The workflow I showed here - capturing a failing production request, viewing it in the dashboard, fixing the bug, and testing the fix in replay mode - saves hours compared to traditional debugging.&lt;/p&gt;

&lt;p&gt;Instead of:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Trying to reproduce the bug&lt;/li&gt;
&lt;li&gt;Adding logging&lt;/li&gt;
&lt;li&gt;Redeploying&lt;/li&gt;
&lt;li&gt;Hoping you captured enough context&lt;/li&gt;
&lt;li&gt;Repeating until fixed&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;You can:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Download the cassette&lt;/li&gt;
&lt;li&gt;View it in the dashboard&lt;/li&gt;
&lt;li&gt;Fix the code&lt;/li&gt;
&lt;li&gt;Verify the fix in replay mode&lt;/li&gt;
&lt;li&gt;Deploy with confidence&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The complete example code is on GitHub at &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;github.com/usv240/timetracer&lt;/a&gt;. All 174 tests are passing, and version 1.6.0 just added Starlette support and PyMongo integration.&lt;/p&gt;

&lt;p&gt;If you work with external APIs, spend time debugging production issues, or want faster integration tests, give it a try.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Resources&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/usv240/timetracer" rel="noopener noreferrer"&gt;https://github.com/usv240/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/timetracer" rel="noopener noreferrer"&gt;https://pypi.org/project/timetracer&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Documentation: &lt;a href="https://github.com/usv240/timetracer#readme" rel="noopener noreferrer"&gt;GitHub README&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Example code: See &lt;code&gt;examples/starlette_example/&lt;/code&gt; in the repo&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; #python #starlette #fastapi #debugging #testing #devtools&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
