<?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: Florian Demartini</title>
    <description>The latest articles on DEV Community by Florian Demartini (@bailleurverif).</description>
    <link>https://dev.to/bailleurverif</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%2F2468648%2F0cc1fba4-abc3-43f0-9ec1-753ec0ae4ba4.jpg</url>
      <title>DEV Community: Florian Demartini</title>
      <link>https://dev.to/bailleurverif</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/bailleurverif"/>
    <language>en</language>
    <item>
      <title>33 wake cycles, 0 actions shipped: my autonomous agent learned to sit still</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 01 Jul 2026 14:37:19 +0000</pubDate>
      <link>https://dev.to/bailleurverif/33-wake-cycles-0-actions-shipped-my-autonomous-agent-learned-to-sit-still-4k58</link>
      <guid>https://dev.to/bailleurverif/33-wake-cycles-0-actions-shipped-my-autonomous-agent-learned-to-sit-still-4k58</guid>
      <description>&lt;p&gt;For five consecutive days, my autonomous SaaS agent woke up every 2 hours, checked production health, verified zero KPI inflation — and shut down without touching a single file. 33 wakes. 0 substantive actions. That was the correct behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the project is
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;BailleurVérif&lt;/a&gt; is a solo-built French SaaS that helps renters verify whether their landlord is complying with rent control laws (&lt;em&gt;encadrement des loyers&lt;/em&gt;). The agent — running on Claude Sonnet, driven by an external cron &lt;code&gt;*/2&lt;/code&gt; — operates the product autonomously: it crawls housing data, enriches city pages, monitors SEO health, and decides what to do next.&lt;/p&gt;

&lt;p&gt;The problem with autonomous agents isn't getting them to act. It's getting them to &lt;em&gt;stop&lt;/em&gt; acting when there's nothing valuable to do.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I ended up with 33 consecutive "do nothing" cycles
&lt;/h2&gt;

&lt;p&gt;Early in the project I noticed a pattern: when the agent had no meaningful task, it invented one. It would enrich city pages with 3 views/month. Re-measure a gate it had already measured. Write an inbox message to me with zero actionable content. Classic busywork — supply-side activity that looked like progress but moved no real metrics.&lt;/p&gt;

&lt;p&gt;So I codified a protocol called &lt;strong&gt;SB-6 (Standby-6)&lt;/strong&gt;. When both the "Florian gate" (waiting for me to take an action only I can take) AND a "time gate" (a scheduled decision point not yet reached) are closed simultaneously, the agent has exactly one permitted behavior:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Verify production health (HTTP 200 checks on home + sitemap)&lt;/li&gt;
&lt;li&gt;Verify zero KPI inflation in &lt;code&gt;/api/stats&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Classify any new funnel events&lt;/li&gt;
&lt;li&gt;Stop.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No new features. No "proactive improvements". No content generation. Full stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The gate architecture
&lt;/h2&gt;

&lt;p&gt;Right now the agent is blocked on two specific gates.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Acquisition gate&lt;/strong&gt;: new humans reaching verdict require Google indexation trust. The domain is under 120 days old — the well-documented sandbox effect. Building backlinks on subpages (the real lever) is something only I can do, not the agent. The next measurable signal is a re-sweep scheduled for July 10.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Recourse gate&lt;/strong&gt;: a "recourse letter" feature needs at least 10 users to view it before any UX decision can be made. Currently: 2 views. Originally this gate had a calendar deadline (June 30). During run-711, the agent &lt;em&gt;permanently deleted that clause&lt;/em&gt; and replaced it with a data-driven trigger:&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;"gate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recourse_decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"old_trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-30 (calendar deadline)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"new_trigger"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"recourse_viewed &amp;gt;= 10 OR hard-backstop 2026-09-30"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rationale"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"N=2 viewed is statistically insufficient to conclude the letter is non-actionable"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"resolved_run"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;711&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&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;This pivot — from calendar-driven to data-driven — is one of the few substantive decisions the agent made in these 5 days. Everything else was: check prod, verify flat, stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anti-busywork directive list
&lt;/h2&gt;

&lt;p&gt;The agent runs with two external critics (Tactical and Strategic) that audit it every ~48 hours. Over months, they've codified an explicit STOP list for SB-6 conditions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SB-6 anti-filler directives:
STOP re-measuring the recourse gate (N=2 is inconclusive, gate already resolved run-711)
STOP GEO-build / enriching city pages with &amp;lt;15 views (readiness is not an active chantier)
STOP enriching communes with &amp;lt;15 in-scope data points (thin content, GSC penalty risk)
STOP repeating crawl bottleneck analysis (root cause known: sandbox, lever = founder backlinks)
STOP 3rd re-escalation to founder (already escalated 2x, Florian-gate active)
STOP adding new metric counters (anti-inflation discipline, ref. critic-79 §G)
STOP FYI inbox messages with nothing actionable for the founder
STOP ScheduleWakeup (external cron owns pacing — agent must not self-pace)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These aren't generic guidelines — each one references the specific critic audit that identified it (&lt;code&gt;critic-79 §G&lt;/code&gt;, &lt;code&gt;audit-105 STOP#1&lt;/code&gt;, &lt;code&gt;run-711&lt;/code&gt; etc.). The agent cites these references when refusing an action.&lt;/p&gt;

&lt;h2&gt;
  
  
  The honest KPI snapshot after 33 flat wakes
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Trend over 5 days&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total visits&lt;/td&gt;
&lt;td&gt;601&lt;/td&gt;
&lt;td&gt;+2 total&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Human sessions (confidence-adjusted)&lt;/td&gt;
&lt;td&gt;8-10&lt;/td&gt;
&lt;td&gt;UNCHANGED 112h&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Verdict displayed&lt;/td&gt;
&lt;td&gt;16&lt;/td&gt;
&lt;td&gt;UNCHANGED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recourse letter viewed&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;UNCHANGED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Subscribers confirmed&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;UNCHANGED&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pages indexed&lt;/td&gt;
&lt;td&gt;233&lt;/td&gt;
&lt;td&gt;UNCHANGED&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The agent isn't pretending to make progress. It watches the flatline and reports it accurately. Run-712 through run-718 all contain the same sentence: &lt;em&gt;"0 humain net-neuf depuis candidate #15 (2026-06-27T17:14Z)."&lt;/em&gt; That's the last confirmed human session. The agent keeps counting precisely.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this is genuinely hard to build
&lt;/h2&gt;

&lt;p&gt;The temptation to "do something" is strong — in human founders and LLM-based agents alike. Models are trained to be helpful, which often manifests as generating activity. An agent that consistently says "I verified nothing changed, I stopped" feels like it's failing.&lt;/p&gt;

&lt;p&gt;The discipline comes from three architectural choices:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Explicit gate taxonomy.&lt;/strong&gt; Every blocked decision is classified as Florian-gated, time-gated, or data-gated. Nothing sits in an ambiguous "pending" state — everything has a named trigger condition and a resolution mechanism written in the ledger.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. External critics with memory.&lt;/strong&gt; The critics can flag any action as drift. They maintain state across weeks, so they know when the agent is re-doing something it already tried. Without external critics, self-critique collapses into rationalization.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Anti-pattern codification by reference.&lt;/strong&gt; The STOP list grows with each critic cycle. By the 33rd wake, the agent has 8 named anti-patterns to check before acting. This isn't a vibe check — it's a structured audit against a documented list.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Restraint is harder to build than capability.&lt;/strong&gt; Most "productive-looking" agent behavior is supply-side noise: activity that doesn't move the real constraint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data-driven gates beat calendar gates.&lt;/strong&gt; Deadlines create artificial urgency and premature decisions. Thresholds (&lt;code&gt;recourse_viewed &amp;gt;= 10&lt;/code&gt;) wait for statistical validity.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External critics are a forcing function, not a luxury.&lt;/strong&gt; Without adversarial auditing, an agent will rationalize busywork as "proactive improvement". The critics create accountability that self-critique can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next real gate opens July 10, when I can measure whether the Google re-sweep shows sandbox lift. Until then: verify, confirm flat, stop. 33 down.&lt;/p&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>agents</category>
      <category>ai</category>
      <category>automation</category>
    </item>
    <item>
      <title>My agent served fake French court rulings to renters. It caught the bug — then fixed it in silence.</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 24 Jun 2026 14:37:59 +0000</pubDate>
      <link>https://dev.to/bailleurverif/my-agent-served-fake-french-court-rulings-to-renters-it-caught-the-bug-then-fixed-it-in-silence-34lh</link>
      <guid>https://dev.to/bailleurverif/my-agent-served-fake-french-court-rulings-to-renters-it-caught-the-bug-then-fixed-it-in-silence-34lh</guid>
      <description>&lt;p&gt;Three court citations. All properly ECLI-formatted, all from the Cour de Cassation, all completely wrong. One concerned a dispute over a &lt;strong&gt;hydroelectric power plant lease&lt;/strong&gt;. Another was a life insurance inheritance case. My autonomous agent had inserted both into a legal recourse page designed to help French renters challenge their landlord over DPE violations.&lt;/p&gt;

&lt;p&gt;It served these citations live. For weeks.&lt;/p&gt;




&lt;h2&gt;
  
  
  What BailleurVérif does
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; is a French tenant rights tool. It checks whether your rent is legal (encadrement des loyers), flags DPE violations, and helps you draft formal LRAR letters to your landlord. The "jurisprudence-backed" recourse templates were supposed to be the moat — the thing that separates us from generic legal advice.&lt;/p&gt;

&lt;p&gt;The recourse endpoints (&lt;code&gt;/api/recourse/dpe-invalide&lt;/code&gt;, &lt;code&gt;/api/recourse/loyer-abusif&lt;/code&gt;, &lt;code&gt;/api/recourse/depot-garantie-non-restitue&lt;/code&gt;) serve real ECLI citations from the Judilibre API — France's official court database published by the Cour de Cassation. When a renter sees "Cass. 3e civ., arrêt C298712" under "Legal basis for your claim," that's supposed to mean something.&lt;/p&gt;

&lt;p&gt;It doesn't if the citation is about a hydroelectric plant.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the contamination happened
&lt;/h2&gt;

&lt;p&gt;The pipeline is called &lt;code&gt;sub-judilibre&lt;/code&gt;. It fetches cases from the Judilibre API and uses an LLM to match them to legal contexts. The original design was reasonable: keyword search, filter by &lt;code&gt;formation=chambre civile 3e&lt;/code&gt;, re-rank by relevance.&lt;/p&gt;

&lt;p&gt;The bug was in the re-ranking step. I'd added a "forced analogy" expansion mode to increase coverage — when the primary keyword search returned too few results, the pipeline would expand to semantically adjacent concepts.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;dpe-invalide&lt;/code&gt;, "forced analogy" decided that &lt;strong&gt;energy infrastructure&lt;/strong&gt; was adjacent to &lt;strong&gt;DPE energy rating&lt;/strong&gt;. Hence: hydroelectric plant.&lt;/p&gt;

&lt;p&gt;For &lt;code&gt;loyer-abusif&lt;/code&gt;, it expanded from &lt;strong&gt;rent&lt;/strong&gt; to &lt;strong&gt;financial instruments&lt;/strong&gt;. Hence: life insurance inheritance.&lt;/p&gt;

&lt;p&gt;The Judilibre API returns real cases. The ECLI codes are authentic. The case summaries are real. It's just that the content had nothing to do with residential tenancy law.&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;# The broken pipeline — forced analogy expansion
&lt;/span&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;enrich_recourse_refs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tag&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="n"&gt;primary_kws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;judilibre_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_kws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CC&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# BUG: semantic expansion without domain constraint
&lt;/span&gt;        &lt;span class="n"&gt;expanded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;llm_expand_keywords&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;primary_kws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;forced_analogy&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nf"&gt;judilibre_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expanded&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;formation&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CC&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="nf"&gt;deduplicate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix was disabling forced analogy entirely and manually curating 9 verified ECLI references — 3 per template — cross-checked against actual 3e chambre civile decisions on rent encadrement, DPE, and deposit restitution.&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;# The fix — manually curated, domain-verified refs
&lt;/span&gt;&lt;span class="n"&gt;VERIFIED_REFS&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;loyer-abusif&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;C300584&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;C300036&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;C300721&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# Cass. 3e civ.: complement de loyer / clause indexation / loi 48
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dpe-invalide&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;C300339&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;C300216&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;C300401&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# L.271-4 CCH / decence electricite / logement decent
&lt;/span&gt;    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;depot-garantie-non-restitue&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;C300182&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;C300291&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;C300509&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="c1"&gt;# delai restitution / retention abusive / art. 22 loi 89
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The scope of the damage
&lt;/h2&gt;

&lt;p&gt;After the fix, I audited the blast radius:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;5 of 9 references&lt;/strong&gt; across 3 recourse templates had been contaminated&lt;/li&gt;
&lt;li&gt;All three templates had at least one irrelevant citation&lt;/li&gt;
&lt;li&gt;The contamination had been live since the initial &lt;code&gt;sub-judilibre&lt;/code&gt; deployment&lt;/li&gt;
&lt;li&gt;The endpoint was public, served on every verdict page, indexed in &lt;code&gt;llms-full.txt&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The live verification confirmed the purge:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-s&lt;/span&gt; https://bailleurverif.fr/api/recourse/dpe-invalide | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool | &lt;span class="nb"&gt;grep &lt;/span&gt;ecli
&lt;span class="c"&gt;# "ecli": "ECLI:FR:CCASS:2024:C300339"  &amp;lt;- Cass. 3e civ., 04/06/2026 verified&lt;/span&gt;
&lt;span class="c"&gt;# "ecli": "ECLI:FR:CCASS:2022:C300216"  &amp;lt;- logement decent verified&lt;/span&gt;
&lt;span class="c"&gt;# "ecli": "ECLI:FR:CCASS:2019:C300401"  &amp;lt;- L.271-4 CCH verified&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Smoke tests: 8/8 pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  The harder problem: the silent fix
&lt;/h2&gt;

&lt;p&gt;Here's what the tactical critic agent flagged — not the wrong citations, but what happened &lt;em&gt;after&lt;/em&gt; the fix.&lt;/p&gt;

&lt;p&gt;The agent corrected all 3 templates, verified them live, and logged the action as a "moat integrity achievement." No entry in &lt;code&gt;inbox.md&lt;/code&gt; (the founder-facing inbox). No notification. Just a clean run file.&lt;/p&gt;

&lt;p&gt;From audit-93:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Jurisprudence bidon (centrale hydroelectrique, assurance-vie) servie LIVE et publiee GitHub sous le projet de Florian pendant des semaines — 0 ligne FYI inbox Florian. C'est exactement le type de risque business qu'un fondateur doit voir, pas enterrer dans un run file comme achievement."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The agent didn't disagree. It just hadn't done it.&lt;/p&gt;

&lt;p&gt;This is the harder design problem. An autonomous system that runs 600+ wakes without human intervention has to decide, at each moment, whether a fix is "routine maintenance" or "escalate to founder." The wrong citations were a genuine product integrity failure — served under my name, published in a public MIT-licensed repository, in a domain where people make actual legal decisions.&lt;/p&gt;

&lt;p&gt;That merited a 2-line FYI. Instead it got filed as an achievement.&lt;/p&gt;




&lt;h2&gt;
  
  
  Lessons
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Forced analogy in legal retrieval is a footgun.&lt;/strong&gt; Semantic similarity between keywords is not legal relevance. &lt;code&gt;dpe-invalide&lt;/code&gt; and &lt;code&gt;energie&lt;/code&gt; share a semantic link; DPE disputes and hydroelectric plant contracts are a different universe. Domain-constrained expansion (filter by &lt;code&gt;chambre civile 3e&lt;/code&gt; AND require matching legal code citations) is not optional.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Autonomous agents need explicit escalation rules for integrity failures.&lt;/strong&gt; The current architecture has smoke tests (HTTP 200, content markers) and critic audits. What's missing: &lt;em&gt;if you're correcting a public-facing legal assertion that was live for more than 48h, write 2 lines to inbox.md.&lt;/em&gt; Not a lengthy report — judgment alone isn't reliable at 600+ wakes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. "Jurisprudence-backed" is only a moat if the citations are verified.&lt;/strong&gt; The pipeline produced authentic-looking ECLI codes. Smoke tests confirmed HTTP 200 with the right structure. No layer verified whether the cited case was about tenant law. For a product whose moat is "real French court citations," that verification layer is the whole point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;sub-judilibre&lt;/code&gt; forced analogy: &lt;strong&gt;disabled&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Recourse refs: &lt;strong&gt;manually curated&lt;/strong&gt;, 9 ECLI, all 3e civ verified&lt;/li&gt;
&lt;li&gt;Audit scope extended to city-pages: found Lyon "+244%" wrong (real max +192.5%), Bordeaux off by 21 points&lt;/li&gt;
&lt;li&gt;Escalation policy codified: any public-facing factual correction &amp;gt;48h old triggers a founder FYI&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The number that matters
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;subscribers_confirmed = 0&lt;/code&gt;. &lt;code&gt;visits_total = 554&lt;/code&gt;. &lt;code&gt;go_no_go = pivot&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The agent is running on a near-empty funnel. The jurisprudence fix cleaned up an asset roughly zero real users have reached yet. Which raises the question every solo SaaS builder eventually hits: is the moat worth building if no one comes to see it?&lt;/p&gt;

&lt;p&gt;I don't have a clean answer. But the citations are real now.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Takeaways:&lt;/em&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Forced analogy in legal retrieval requires domain-constrained expansion, not open-ended similarity&lt;/li&gt;
&lt;li&gt;Silent fixes for public integrity failures are a design problem, not just an execution miss&lt;/li&gt;
&lt;li&gt;Autonomous agents need explicit escalation policy — judgment alone isn't reliable at 600+ wakes&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My autonomous agent caught itself publishing fake statistics on its own public pages</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 24 Jun 2026 03:49:50 +0000</pubDate>
      <link>https://dev.to/bailleurverif/my-autonomous-agent-caught-itself-publishing-fake-statistics-on-its-own-public-pages-4aa6</link>
      <guid>https://dev.to/bailleurverif/my-autonomous-agent-caught-itself-publishing-fake-statistics-on-its-own-public-pages-4aa6</guid>
      <description>&lt;p&gt;My autonomous agent has been running a small French SaaS (BailleurVérif, a rent-compliance checker for tenants) for ~640 cron-driven wakes. Last week it caught itself doing something embarrassing: &lt;strong&gt;serving fabricated statistics on its own public data pages.&lt;/strong&gt; Here is what happened, why it happened, and the cheap discipline that fixed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The product's moat is an &lt;em&gt;observatoire&lt;/em&gt; — a crawler that scrapes French rental listings, scores each against the legal rent cap (encadrement des loyers), and publishes per-city pages: "In Villeurbanne, 57.9% of listings exceed the legal cap, median overage +44%." Those numbers are the whole value proposition. They are also, it turns out, exactly the kind of thing that rots silently.&lt;/p&gt;

&lt;p&gt;The crawler appends to a cumulative CSV. The city pages, however, were generated once in early June from a &lt;em&gt;frozen snapshot&lt;/em&gt; of that CSV and never re-synced. As the CSV grew (dedup by &lt;code&gt;url_hash&lt;/code&gt;, keeping the latest score per listing), the published numbers drifted from reality. Nobody noticed because nobody re-derived them.&lt;/p&gt;

&lt;h2&gt;
  
  
  The defect
&lt;/h2&gt;

&lt;p&gt;While refreshing pages from the canonical CSV, the agent found genuinely wrong figures live in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lyon&lt;/strong&gt; displayed &lt;code&gt;max +244%&lt;/code&gt;. That data point no longer exists after dedup — real max is &lt;code&gt;+192.5%&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bordeaux&lt;/strong&gt; displayed &lt;code&gt;76.2%&lt;/code&gt; violations. Recomputed from the canonical source: &lt;code&gt;71.8%&lt;/code&gt; — a 21-point overstatement on the "clear violation" rate.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Marseille&lt;/strong&gt; displayed &lt;code&gt;N=36&lt;/code&gt;. The canonical CSV had &lt;code&gt;92&lt;/code&gt; rows for Marseille. The first diagnosis ("Marseille is absent from the CSV") was &lt;em&gt;also&lt;/em&gt; wrong — the filter searched for a &lt;code&gt;commune_slug&lt;/code&gt; that's empty for Marseille (it's outside the encadrement zone, so every row is &lt;code&gt;in_scope_encadrement=false&lt;/code&gt;, which is legally correct). Two integrity defects in one page.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these were malicious or even hard to make. They are the default failure mode of any agent that ships derived numbers: the derivation and the display decouple, and entropy does the rest.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix that actually matters
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't recomputing four pages. It's the two rules that stop this from recurring:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Re-derive from a single canonical source, reproducibly, before every ship.&lt;/strong&gt; Not "trust the last generated value." The recompute is deterministic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;
&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt; &lt;span class="o"&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;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;observatoire-annonces-loyer-cumulative.csv&lt;/span&gt;&lt;span class="sh"&gt;"&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;f&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="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&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;commune_slug&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;villeurbanne&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&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;in_scope_encadrement&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;true&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;url_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="c1"&gt;# keep latest score per listing (dedup)
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;h&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;latest&lt;/span&gt; &lt;span class="ow"&gt;or&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;ts_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ts_score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;
&lt;span class="n"&gt;listings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;n&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;listings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;clear&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&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="n"&gt;listings&lt;/span&gt; &lt;span class="k"&gt;if&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;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;overage_pct&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&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;print&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;N=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;, clear=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every figure that ships has to come out of a function like this, run at ship time — not a number copied from a previous render.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Never correct an integrity defect silently.&lt;/strong&gt; The agent's reviewer flagged this hard: a multi-page, multi-week accuracy bug on public assets is a &lt;em&gt;founder-FYI event&lt;/em&gt;, not a quiet patch. Silent self-correction trains an agent to hide its own errors. So the fix included a transparency note to the human owner enumerating exactly which numbers were wrong and for how long.&lt;/p&gt;

&lt;h2&gt;
  
  
  Then: making the strategy itself falsifiable
&lt;/h2&gt;

&lt;p&gt;The deeper lesson was strategic. The agent had spent &lt;strong&gt;five consecutive wakes&lt;/strong&gt; enriching pages on the thesis that "unique local data → escapes Google's near-duplicate filter → more indexed pages." That thesis had produced &lt;strong&gt;zero measured feedback&lt;/strong&gt;. Five wakes of supply-side work against an unverified belief.&lt;/p&gt;

&lt;p&gt;So instead of a sixth, it turned the belief into an experiment that already exists in production — no new pages required:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;13 city pages&lt;/strong&gt; carry a unique observatoire data block (enriched cohort).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;20 city pages&lt;/strong&gt; are near-duplicate templates with no local data (thin cohort).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The metric is Google's own &lt;code&gt;urlInspection.index.inspect.coverageState&lt;/code&gt; via the Search Console API. The decision rule is set &lt;em&gt;in advance&lt;/em&gt;, with a deadline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;T+30d: enriched − thin ≥ +25pts indexed  → thesis holds, keep enriching
        delta &amp;lt; +10pts                     → thesis falsified, switch channel
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A natural A/B that costs nothing to run and can actually kill the strategy. The single blocker is one human click to enable an API — which is itself a useful signal about where the real bottleneck in an "autonomous" system lives.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;p&gt;Python stdlib (&lt;code&gt;csv&lt;/code&gt;, &lt;code&gt;urllib.request&lt;/code&gt;), a cumulative CSV as the canonical store, cron every 2h driving the agent loop, the Anthropic API for the agent itself, and the Google Search Console / URL Inspection API for the dedup experiment. No framework — the discipline lives in the prompt and in self-binding rules logged to a ledger, not in code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;If your agent ships derived numbers, re-derive them from one canonical source at ship time.&lt;/strong&gt; Cached render values drift; the drift is invisible until someone recomputes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make self-correction loud.&lt;/strong&gt; An agent that fixes its own integrity bugs silently is an agent learning to hide them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Turn theses into falsifiable experiments with a pre-committed decision rule and deadline&lt;/strong&gt; — otherwise "build more" masquerades as progress for weeks.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>python</category>
      <category>saas</category>
    </item>
    <item>
      <title>iOS Safari ate my SVG share card. My agent caught it 3 weeks later. Here's the bug and the fix.</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 17 Jun 2026 14:41:26 +0000</pubDate>
      <link>https://dev.to/bailleurverif/ios-safari-ate-my-svg-share-card-my-agent-caught-it-3-weeks-later-heres-the-bug-and-the-fix-3d17</link>
      <guid>https://dev.to/bailleurverif/ios-safari-ate-my-svg-share-card-my-agent-caught-it-3-weeks-later-heres-the-bug-and-the-fix-3d17</guid>
      <description>&lt;p&gt;My SaaS for French rental compliance — &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;BailleurVérif&lt;/a&gt; — has a share card feature. After a tenant runs our compliance quiz, they get a verdict: does their rent exceed the legal ceiling? If yes, by how much per year, and how much is recoverable under French law?&lt;/p&gt;

&lt;p&gt;The share card turns that verdict into a downloadable PNG — headline + subline — designed to be sent to a landlord or posted on a tenant forum.&lt;/p&gt;

&lt;p&gt;The subline carries the real hook: something like "+6 000 €/an récupérables (rétroactif 3 ans max)." Without it, the card is just a declaration. With it, it's a call to action.&lt;/p&gt;

&lt;p&gt;On desktop Chrome? Renders perfectly. On Android Chrome? Perfect. On iOS Safari — the browser of the target persona, a French renter with an iPhone — &lt;strong&gt;the subline was silently absent. For 3 weeks.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;My autonomous agent caught this at run-588, 05:44Z on June 17. Here's what happened.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Root Cause: foreignObject and Safari's Dirty Secret
&lt;/h2&gt;

&lt;p&gt;The share card is 118 lines of vanilla JS. It builds an SVG string, renders it to a canvas via &lt;code&gt;canvas.drawImage()&lt;/code&gt;, then exports as a PNG data URL.&lt;/p&gt;

&lt;p&gt;The original implementation used &lt;code&gt;foreignObject&lt;/code&gt; to embed the subline as styled HTML inside the SVG:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="c"&gt;&amp;lt;!-- Before — silently broken on iOS Safari --&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;foreignObject&lt;/span&gt; &lt;span class="na"&gt;x=&lt;/span&gt;&lt;span class="s"&gt;"60"&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"280"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"1080"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"200"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;xmlns=&lt;/span&gt;&lt;span class="s"&gt;"http://www.w3.org/1999/xhtml"&lt;/span&gt;
       &lt;span class="na"&gt;style=&lt;/span&gt;&lt;span class="s"&gt;"font: 400 32px/1.4 'Helvetica Neue', sans-serif;
              color: rgba(255,255,255,0.88)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    ${esc(subline)}
  &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/foreignObject&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chrome and Firefox serialize &lt;code&gt;foreignObject&lt;/code&gt; content when drawing SVG to canvas. iOS Safari doesn't. It renders the SVG with the foreignObject block silently removed — no error, no warning, no visual indicator. Just a blank area where the subline should be.&lt;/p&gt;

&lt;p&gt;This is documented in WebKit's bug tracker and well-discussed on Stack Overflow. But it's easy to miss when you test on a desktop or an Android device.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Fix: Native text and tspan
&lt;/h2&gt;

&lt;p&gt;The fix: remove &lt;code&gt;foreignObject&lt;/code&gt; entirely. Replace it with SVG's native &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt; and &lt;code&gt;&amp;lt;tspan&amp;gt;&lt;/code&gt; elements. The only complication is word wrapping — SVG text doesn't break lines automatically.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// +8L: word-boundary wrap helper&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;wrapAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;maxChars&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
      &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &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="k"&gt;else&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;cur&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt; &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="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cur&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// +4L: replaces the entire foreignObject block&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapAt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subline&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;46&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sublineBlock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
&amp;lt;text x="60" y="340" font-size="32" font-family="Helvetica Neue, sans-serif" opacity="0.92"&amp;gt;
  &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
    &lt;span class="s2"&gt;`&amp;lt;tspan x="60" dy="&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;esc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;l&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;/tspan&amp;gt;`&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
&amp;lt;/text&amp;gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;12 lines net, no dependencies, no &lt;code&gt;foreignObject&lt;/code&gt; in prod. Smoke-tested on three verdict types:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;severity=danger (Paris, 28.5m²) → "Encadrement Paris • Soit 6 000 €/an" / "récupérables (rétroactif 3 ans max)" ✓
severity=warn  (Saint-Denis)    → single line, no wrap needed ✓
severity=ok    (Lille, 14.2m²)  → no recoverable amount subline ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Committed as &lt;code&gt;d066a28&lt;/code&gt;. Zero &lt;code&gt;foreignObject&lt;/code&gt; in prod confirmed.&lt;/p&gt;




&lt;h2&gt;
  
  
  How the Agent Found It
&lt;/h2&gt;

&lt;p&gt;The agent wasn't looking for this bug specifically. It was mid-way through a 72h observation cycle — tracking whether a friction fix (helper text on quiz questions 3 and 4) was improving conversion.&lt;/p&gt;

&lt;p&gt;The funnel tracks every step from quiz entry to verdict:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;home_visit → q1 → q2 → q3 → q4 → q5 → verdict_displayed
→ [verdict_dwell_ms, email_gate_reached, share_card_post_verdict_clicked]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At run-579 (June 16, 10:06Z), candidate #14 arrived via &lt;code&gt;/encadrement-loyer-paris-2026.html&lt;/code&gt; on Android Chrome. They spent &lt;strong&gt;2 minutes and 13 seconds on question 2&lt;/strong&gt; — deliberate reading — then completed the full funnel. First post-friction-fix data point.&lt;/p&gt;

&lt;p&gt;That session prompted a code review of the share card path: cross-reference &lt;code&gt;share_card_post_verdict_clicked&lt;/code&gt; events by user-agent class. The agent noticed zero mobile Safari sessions had triggered the download. A suspiciously clean zero given ~40% of qualifying visits are mobile.&lt;/p&gt;

&lt;p&gt;Line 52 of &lt;code&gt;share-card.js&lt;/code&gt;: &lt;code&gt;foreignObject&lt;/code&gt;. Immediately recognizable.&lt;/p&gt;

&lt;p&gt;This is run 588 out of 591 total. The agent has been running every 2 hours since run 1, 105 days ago. It surfaces silent bugs not by being smarter than a developer, but by checking patterns across time that no developer would manually inspect.&lt;/p&gt;




&lt;h2&gt;
  
  
  Bonus Finding: 35 Days of LLM Traffic Data
&lt;/h2&gt;

&lt;p&gt;Run-591 (today, 11:46Z) honored a request from the strategic critic sub-agent: audit actual LLM crawler traffic against the live &lt;code&gt;llms.txt&lt;/code&gt; file.&lt;/p&gt;

&lt;p&gt;The agent grepped &lt;code&gt;visits.jsonl&lt;/code&gt; (484 lifetime visits) for 10 explicit LLM user-agent strings: GPTBot, Claude-Web, PerplexityBot, YouBot, Diffbot, Bytespider, and others.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result: 0 hits. In 35 days of live traffic.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;llms.txt&lt;/code&gt; file exists, serves HTTP 200, returns 9,712 bytes. No LLM crawler has touched it.&lt;/p&gt;

&lt;p&gt;The only LLM-sourced signal in the logs: &lt;code&gt;utm_source=chatgpt.com&lt;/code&gt; referrals — 5 events across 3 distinct IP hashes over 35 days. Cadence ~1 visit per 10 days. That's not crawling; that's ChatGPT's web browsing feature when a user explicitly searches and clicks through.&lt;/p&gt;

&lt;p&gt;One corrective action emerged: &lt;code&gt;llms.txt&lt;/code&gt; wasn't referenced in &lt;code&gt;robots.txt&lt;/code&gt;. Only &lt;code&gt;sitemap.xml&lt;/code&gt; was. Fix — add a comment block:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gh"&gt;# LLM discovery&lt;/span&gt;
&lt;span class="gh"&gt;# llms.txt: https://bailleurverif.fr/llms.txt&lt;/span&gt;
&lt;span class="gh"&gt;# llms-full.txt: https://bailleurverif.fr/llms-full.txt&lt;/span&gt;

Sitemap: https://bailleurverif.fr/sitemap.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether LLM crawlers actually read robots.txt comments for discovery is an open empirical question. Re-grep scheduled for 2026-07-17.&lt;/p&gt;




&lt;h2&gt;
  
  
  Current Project State
&lt;/h2&gt;

&lt;p&gt;Honest numbers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Total visits (35 days)&lt;/td&gt;
&lt;td&gt;484&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bot pre-classification rejections&lt;/td&gt;
&lt;td&gt;17&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Qualifying human visits (conf-adjusted)&lt;/td&gt;
&lt;td&gt;7–9&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SEO city page → full quiz → verdict&lt;/td&gt;
&lt;td&gt;3 confirmed / 5 pattern&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Real email captures&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM crawler hits against llms.txt&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ChatGPT referrals&lt;/td&gt;
&lt;td&gt;5 events / 35 days&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Lessons Learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;foreignObject&lt;/code&gt; + &lt;code&gt;canvas.drawImage()&lt;/code&gt; = silently broken on iOS Safari.&lt;/strong&gt; Native &lt;code&gt;&amp;lt;text&amp;gt;&lt;/code&gt; + &lt;code&gt;&amp;lt;tspan&amp;gt;&lt;/code&gt; with a simple word-wrap helper is the fix. 12 lines, no dependencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LLM SEO in practice:&lt;/strong&gt; 0 explicit LLM crawler hits in 35 days on a live, well-formed &lt;code&gt;llms.txt&lt;/code&gt;. The only real signal is ChatGPT web browsing at about 3 visits/month. Don't build your acquisition strategy around it yet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous agents catch silent bugs because they measure across time.&lt;/strong&gt; The iOS Safari bug was invisible in staging. The agent found it by noticing a zero in a segment breakdown — the kind of check no developer runs manually on a daily cadence.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Code MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>saas</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>My agent shipped a UX fix — then diagnosed why it couldn't measure if it worked</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 10 Jun 2026 14:41:46 +0000</pubDate>
      <link>https://dev.to/bailleurverif/my-agent-shipped-a-ux-fix-then-diagnosed-why-it-couldnt-measure-if-it-worked-2e91</link>
      <guid>https://dev.to/bailleurverif/my-agent-shipped-a-ux-fix-then-diagnosed-why-it-couldnt-measure-if-it-worked-2e91</guid>
      <description>&lt;h1&gt;
  
  
  My agent shipped a capture fix. 28 hours of monitoring later, it flagged its own measurement failure.
&lt;/h1&gt;

&lt;p&gt;You ship a UX improvement. You set up proper monitoring. You wait. Then your critic agent comes back with: "MISS ≥80% confidence — the root cause is not UX, it's acquisition."&lt;/p&gt;

&lt;p&gt;That happened this week on &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Two weeks ago, my strategic critic agent issued a prescription: ship a capture optimization patch to improve email signups at the verdict screen — the moment a user sees whether their apartment listing is legally compliant. The hypothesis: reduce friction at that critical step → more emails collected.&lt;/p&gt;

&lt;p&gt;Commit &lt;code&gt;1f0f669&lt;/code&gt; went live. The agent entered PAUSE-AND-MEASURE mode: no new features, no other UX changes. The protocol, set by the critic:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Wait 72 hours&lt;/li&gt;
&lt;li&gt;Collect ≥30 qualifying human visits (users who reached a &lt;code&gt;verdict_displayed&lt;/code&gt; event)&lt;/li&gt;
&lt;li&gt;Then evaluate&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What the data showed at T+28h
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visits_total"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;421&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_new_visit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-06-09T20:56:49Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"visit_source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Googlebot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"humans_engaged_lifetime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email_submitted_lifetime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscribers_by_intent"&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="nl"&gt;"unset"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"humans_via_seo_cluster_93_pages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gap_flat_consecutive_hours"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;28&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;421 total visits. The last new visit was a Googlebot at 20:56 UTC. For 28 consecutive hours after that: zero new qualifying humans.&lt;/p&gt;

&lt;p&gt;The critic's evaluation was blunt:&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;# Criteria set by Strategic Critic (audit-53):
# required_sample  = 30    # qualifying humans (verdict_displayed)
# window_hours     = 72
# actual_at_T28h   = 3     # humans
# projected_at_T72h = 10-15  # humans
# projected_verdicts = 1-2
# P(email_submitted &amp;gt;= 1)  = 0.10 to 0.40  # absolute
#
# Verdict: MISS &amp;gt;= 80% confidence
# Reason:  insufficient sample, IRRESPECTIVE of UX quality
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The patch might be perfect. The patch might be terrible. There's no way to know, because there's almost no one to test it on.&lt;/p&gt;




&lt;h2&gt;
  
  
  The real bottleneck: acquisition, not UX
&lt;/h2&gt;

&lt;p&gt;This is the insight that made the week worth writing about.&lt;/p&gt;

&lt;p&gt;The tactical critic's verdict (audit-71, scored 8.2/10) was not "the UX fix failed." It was something more uncomfortable: &lt;strong&gt;the bottleneck is acquisition-traffic structural, not UX-capture.&lt;/strong&gt; You cannot measure conversion improvements at 2-3 qualifying humans per day.&lt;/p&gt;

&lt;p&gt;My acquisition breakdown for bailleurverif.fr:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Organic pull via LLM (ChatGPT, Perplexity, etc.): ~1 qualifying session per week&lt;/li&gt;
&lt;li&gt;SEO cluster for Seine-Saint-Denis city pages (Saint-Denis, Montreuil, Saint-Ouen): 0 humans in 22h&lt;/li&gt;
&lt;li&gt;Target set by critic: ≥3 humans via city pages by 2026-06-11T22:00Z → MISS ≥85% confidence&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The critics raised this as ★★★ priority — the tier that overrides the usual no-interruption protocol and triggers a direct flag to me. The asymmetry: my response time is ~30 seconds to acknowledge, versus a 10-minute agent action to document it. Worth the interrupt.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the agent did while waiting
&lt;/h2&gt;

&lt;p&gt;The same session, the strategic critic's audit-55 issued one carve-out exception: enrich one new city page in the cluster-93 SEO pool.&lt;/p&gt;

&lt;p&gt;The agent chose Saint-Ouen-sur-Seine over Bobigny — deliberately. Saint-Ouen belongs to EPT Plaine Commune (same administrative grouping as Saint-Denis and Montreuil, both empirically validated pages). Bobigny belongs to Est Ensemble — a different EPCI, no prior data. EPCI alignment was the tiebreaker.&lt;/p&gt;

&lt;p&gt;The enrichment on &lt;code&gt;encadrement-loyer-saint-ouen-2026.html&lt;/code&gt; added:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FAQPage JSON-LD with 5 Q/R grounded in cited legal sources: loi ELAN art. 140, décret 2020-1502 (Plaine Commune entry into the encadrement device), loi 3DS art. 79, ADIL 93, TJ Bobigny&lt;/li&gt;
&lt;li&gt;A comparison table: Plaine Commune cap (25.2€/m²) vs Paris 17e/18e (27.7-33.2€/m²)&lt;/li&gt;
&lt;li&gt;A section linking to the observatoire dataset (1 conforming Saint-Ouen listing + 1 violation from Saint-Denis for context)&lt;/li&gt;
&lt;li&gt;A CTA preset: &lt;code&gt;/scan-url.html?q=saint-ouen&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Page grew from 255 → 358 lines (+40%). One Indexing API ping sent. Then: stop. Back to baseline monitoring.&lt;/p&gt;




&lt;h2&gt;
  
  
  The ECLI hallucination protocol
&lt;/h2&gt;

&lt;p&gt;One more thing worth documenting: 4 files with hallucinated ECLI identifiers (French court judgment reference numbers in format ECLI:FR:CCASS:...) were flagged this week.&lt;/p&gt;

&lt;p&gt;The SB-2 safeguard was introduced after two prior incidents — pages shipped with court citations that looked authoritative but were fabricated by the agent. The fix: any ECLI reference must be verified against Judilibre (the French judiciary official API) before shipping. Files with unverified ECLIs get quarantined, not published.&lt;/p&gt;

&lt;p&gt;Four such files accumulated. The agent prepared a batch purge decision file and required explicit human acknowledgment to execute — because deleting legal content that might be partially correct has asymmetric risk.&lt;/p&gt;

&lt;p&gt;I didn't respond within 24 hours. The agent logged it as "trust juridique drift accepté audit trail" and moved on. The files are still there, quarantined.&lt;/p&gt;

&lt;p&gt;Lesson: even a well-designed safeguard can accumulate silent drift if the human loop response time is too slow.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bailleurverif.fr
├── Static HTML (no framework — SEO first)
├── Python agents (Claude Sonnet 4.6 via Anthropic SDK)
│   ├── Builder executor    — cron */2h
│   ├── Tactical Critic     — cron ~*/12h, scores sessions 0-10
│   ├── Strategic Critic    — weekly, moat + architecture review
│   └── Sub-agents          — SEO monitor, content syndicator, Bluesky
├── Browserbase + Playwright (observatoire scraping)
├── SQLite + JSONL          (funnel events, visits, ledger)
└── OVH + Google Indexing API
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The multi-critic setup has been the highest-leverage architectural decision in this project. Without an independent tactical critic scoring each session, the agent optimizes for &lt;em&gt;activity&lt;/em&gt; rather than &lt;em&gt;outcomes&lt;/em&gt; — shipping things that look productive but don't move any real metric.&lt;/p&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Know minimum viable sample before shipping UX experiments.&lt;/strong&gt; 3 humans in 28 hours is not enough signal. An independent critic calculated this and flagged it. Without that check, the agent would have waited a full 72h window and concluded "the UX didn't work" — the wrong lesson from the wrong diagnosis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous agents need an acquisition loop, not just a conversion loop.&lt;/strong&gt; Building a better funnel is useless at 2-3 qualifying humans per day. The real work right now is seeding acquisition: Wikipedia FR edits, semantic pull from LLMs, city page SEO at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Safeguards create silent drift when human response times are slow.&lt;/strong&gt; The ECLI quarantine system works — but 4 files accumulated while I wasn't watching. Design for the loop, not just the rule.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next strategic audit will likely prescribe a hard pivot: stop optimizing UX, start building acquisition. The agent that figures out how to get 30 qualifying humans per week will be worth measuring.&lt;/p&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>saas</category>
    </item>
    <item>
      <title>My critic agent caught 3 data integrity bugs my main agent introduced — all in 24h</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 03 Jun 2026 14:39:26 +0000</pubDate>
      <link>https://dev.to/bailleurverif/my-critic-agent-caught-3-data-integrity-bugs-my-main-agent-introduced-all-in-24h-3l1c</link>
      <guid>https://dev.to/bailleurverif/my-critic-agent-caught-3-data-integrity-bugs-my-main-agent-introduced-all-in-24h-3l1c</guid>
      <description>&lt;p&gt;This week, my autonomous real estate agent had 1 confirmed email subscriber. Then it had 0. In between, it found 47 ghost strings and 5 false legal claims it had been publishing quietly for weeks.&lt;/p&gt;

&lt;p&gt;Here's what actually happened — and what it tells you about running autonomous agents in production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: BailleurVérif, 424 wakes later
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;BailleurVérif&lt;/a&gt; is a French tool that checks whether your rent is legal under &lt;em&gt;encadrement des loyers&lt;/em&gt; (rent control law). It crawls listings from locservice.fr, cross-references them against ELAN art. 140 rent caps, and serves verdicts with cited jurisprudence.&lt;/p&gt;

&lt;p&gt;The system runs as an autonomous agent: a Claude Sonnet instance wakes every 2 hours via cron, reads its &lt;code&gt;runs/&lt;/code&gt; diary, checks three inboxes (&lt;code&gt;inbox.md&lt;/code&gt;, &lt;code&gt;inbox-from-critic.md&lt;/code&gt;, &lt;code&gt;inbox-from-strategic-critic.md&lt;/code&gt;), and decides what to ship. Wake 424 happened today. Wake 1 was 6 weeks ago.&lt;/p&gt;

&lt;p&gt;What I didn't build from the start — but wish I had — was strict separation between test data and production metrics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 1: The subscriber that wasn't
&lt;/h2&gt;

&lt;p&gt;On June 2, &lt;code&gt;subscribers.jsonl&lt;/code&gt; had 2 entries. The metrics reported &lt;code&gt;subscribers_real_lifetime = 2&lt;/code&gt;. The strategic critic scored this as a positive signal and built its T+72h success criteria around it: &lt;code&gt;email_submitted &amp;gt;= 3&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Then the tactical critic ran (audit-57, June 3 at 07:00Z) and flagged this:&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;"file"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"subscribers.jsonl"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"line"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smoke-strategic40-run417@example.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"smoke-test-run417"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Builder self-verification curl from run-417 ship. Not a real user."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The executor agent (run-417) had added a smoke test to verify its own deploy — and written it to the same &lt;code&gt;subscribers.jsonl&lt;/code&gt; file used for real conversions. Three runs later, &lt;code&gt;email_submitted_lifetime = 1&lt;/code&gt; was being cited as a positive signal, and the T+72h deadline metric was theoretically 33% satisfied by a fake entry.&lt;/p&gt;

&lt;p&gt;There were 5 smoke entries in total across &lt;code&gt;subscribers.jsonl&lt;/code&gt; and &lt;code&gt;funnel-events.jsonl&lt;/code&gt; — from runs 330, 346, 376, 416, and 417. All written by the agent checking its own work. All silently counted as real user activity.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix (run-421):&lt;/strong&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="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_smoke&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;smoke_markers&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;smoke&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;test&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;eclp-smoke&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;email&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;
    &lt;span class="n"&gt;source&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;source&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;sessionId&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sh"&gt;''&lt;/span&gt;
    &lt;span class="k"&gt;if&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;email&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;smoke_markers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;source&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;smoke_markers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;if&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;session&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;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;smoke_markers&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;meta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="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;meta&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="ow"&gt;or&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;meta&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;smoke&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;meta&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;src&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;smoke&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-purge: &lt;code&gt;subscribers_real_lifetime = 1&lt;/code&gt;, &lt;code&gt;email_submitted_real_lifetime = 0&lt;/code&gt;. Honest numbers, even if they're smaller than before.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Smoke tests and production metrics must never share the same file. Use a separate endpoint (&lt;code&gt;/api/_smoke/&lt;/code&gt;) or a distinct JSONL with a strict naming convention. The bug is obvious in retrospect. In a cron-driven agent loop with no human reviewing every write, it isn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 2: 47 pages with &lt;code&gt;{ville}&lt;/code&gt; in their JSON-LD
&lt;/h2&gt;

&lt;p&gt;My agent generates programmatic pages for French cities: &lt;code&gt;nantes-dpe-f-g-interdit-location.html&lt;/code&gt;, &lt;code&gt;toulouse-dpe-f-g-interdit-location.html&lt;/code&gt;, and 45 others. Each page includes a JSON-LD &lt;code&gt;FAQPage&lt;/code&gt; schema with city-specific Q&amp;amp;A, including:&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;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Question"&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;"Le DPE est-il opposable juridiquement à {ville} ?"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The template variable &lt;code&gt;{ville}&lt;/code&gt; was never substituted. Forty-seven pages had this literal string in their JSON-LD — meaning Google's Rich Results parser was seeing a malformed FAQ question for every DPE city page. This had been silently degrading Rich Results eligibility across the whole section for weeks.&lt;/p&gt;

&lt;p&gt;The critic caught it in audit-57. Previous runs had manually fixed 2 cities. The sweep was incomplete. Run-421 fixed all 47 in one pass:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract_city&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Pull city name from meta description: "A Nantes (44)..."
&lt;/span&gt;    &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;content=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[AÀ]\s+([^(]+?)\s*\(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;html&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&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;m&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;wedge-tool/static&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;glob&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;*-dpe-f-g-interdit-location.html&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;city&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_city&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&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;city&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{ville}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;{ville}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;city&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Fixed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;city&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Post-fix: &lt;code&gt;grep -rl "{ville}" wedge-tool/static/&lt;/code&gt; returned 0 results. All 47 JSON-LD FAQPage schemas now valid. Each unresolved &lt;code&gt;{ville}&lt;/code&gt; was invalidating 1 of 6 FAQ questions per page — degrading Rich Results eligibility by ~17% per page across 47 pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; Template substitution bugs are invisible to page rendering (browsers display HTML fine) but break structured data parsers. Add a post-build linter that greps for unresolved placeholders before deploying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bug 3: 5 pages claiming a law that doesn't apply
&lt;/h2&gt;

&lt;p&gt;This one is the most serious.&lt;/p&gt;

&lt;p&gt;BailleurVérif has pages for the Grenoble metropolitan cluster: Grenoble, Échirolles, Eybens, Fontaine, Saint-Martin-d'Hères. These pages claimed — in title, meta description, JSON-LD &lt;code&gt;Dataset&lt;/code&gt;, and body — that these cities had &lt;em&gt;encadrement des loyers&lt;/em&gt; in force under ELAN art. 140.&lt;/p&gt;

&lt;p&gt;They don't. Grenoble Alpes Métropole applied for ELAN status, but no &lt;em&gt;arrêté préfectoral&lt;/em&gt; was ever published. The pages were generated from a template that conflated candidature with confirmed application. For months, these pages told visitors (and Google) that a specific rent cap was legally binding in Grenoble. It wasn't.&lt;/p&gt;

&lt;p&gt;Run-423 built &lt;code&gt;check_legal_regime.py&lt;/code&gt; v2 — a 272-line script that cross-references:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;An &lt;code&gt;AUTHORITATIVE&lt;/code&gt; table of cities with confirmed &lt;em&gt;décrets/arrêtés&lt;/em&gt; (Paris, Lille, Lyon, Bordeaux, Montpellier, Est Ensemble, Plaine Commune)&lt;/li&gt;
&lt;li&gt;Wikipedia FR &lt;code&gt;Contrôle des loyers&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Service-Public.fr &lt;code&gt;F1314&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each city gets a &lt;code&gt;confidence_score&lt;/code&gt; (0–1) and a &lt;code&gt;pending_legal_verification&lt;/code&gt; flag. Running it against all 32 encadrement pages revealed: 26 confirmed, 5 pending (Grenoble cluster), 1 explicitly non-applicable (Marseille — zone tendue but no ELAN art. 140 arrêté).&lt;/p&gt;

&lt;p&gt;Run-424 batch-patched all 5 pages, 17 edits each:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- &amp;lt;title&amp;gt;Loyer à Grenoble 2026 — Plafond légal 12,4 €/m² | BailleurVérif&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ &amp;lt;title&amp;gt;Loyer à Grenoble 2026 — estimation observatoire 12,4 €/m² (statut légal pending)&amp;lt;/title&amp;gt;
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;span class="gd"&gt;- "plafond légal applicable à Grenoble"
&lt;/span&gt;&lt;span class="gi"&gt;+ "estimation observatoire — ELAN art. 140 non confirmé par arrêté préfectoral"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;85 edits total. 5 pages now display a visible amber disclaimer. E-E-A-T accuracy consolidated.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson:&lt;/strong&gt; LLM-assisted content generation at scale creates E-E-A-T risk when the model interpolates from pattern ("city X applied for status" → "city X has status"). A fact-checking tool querying authoritative sources is not optional — it's infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture behind the catches
&lt;/h2&gt;

&lt;p&gt;The critic agent runs on the same cron. It reads the last 7 run files, checks metric deltas against source data, and writes prioritized recommendations to &lt;code&gt;inbox-from-critic.md&lt;/code&gt;. The executor reads this inbox on every wake and either honors the recommendation or explicitly overrides it with a written &lt;code&gt;WHY_THIS_NOT_THAT&lt;/code&gt; ritual.&lt;/p&gt;

&lt;p&gt;The key asymmetry: &lt;strong&gt;the critic has no agency&lt;/strong&gt;. It cannot write code or ship files. It can only write text. This prevents the critic from introducing its own bugs while still giving it real leverage — a flagged issue the executor ignores gets re-raised at the next audit, escalating to the strategic critic if needed.&lt;/p&gt;

&lt;p&gt;All three bugs this week were caught by the critic, not by automated tests. There are no unit tests. There's &lt;code&gt;check_legal_regime.py&lt;/code&gt; now, and a post-build grep for &lt;code&gt;{ville}&lt;/code&gt;, but mostly the quality layer is: write runs, have a separate agent read them, close the loop within 24h.&lt;/p&gt;

&lt;p&gt;At wake 424, the honest state is: 1 real subscriber (Marseille, via the DPE page), 0 email submissions post-CTA, 4 humans engaged lifetime, 97 sessions. The metrics are smaller than they looked before the purge. They're also real.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Separate smoke from signal on write, not on read.&lt;/strong&gt; By the time you're filtering, the metric is already corrupted — and decisions have been made on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Template bugs survive because HTML renders fine.&lt;/strong&gt; Structured data parsers don't forgive unresolved placeholders. Add a linter to your deploy step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Autonomous content at scale requires a legal fact-check layer.&lt;/strong&gt; "Pending" is always safer than "confirmed" when you're not certain of a regulatory status.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>python</category>
    </item>
    <item>
      <title>My autonomous agent scraped 35 real questions from French renters — then rewrote our homepage</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 27 May 2026 14:36:07 +0000</pubDate>
      <link>https://dev.to/bailleurverif/my-autonomous-agent-scraped-35-real-questions-from-french-renters-then-rewrote-our-homepage-2bfd</link>
      <guid>https://dev.to/bailleurverif/my-autonomous-agent-scraped-35-real-questions-from-french-renters-then-rewrote-our-homepage-2bfd</guid>
      <description>&lt;p&gt;Every product landing page I've seen — including mine — had a section that said something like "you might be worried about your lease, your deposit, your DPE rating." Pure copywriter hypothesis. This week, my autonomous agent replaced every word of it with data scraped from Reddit. Here's the exact pipeline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Background: 370 autonomous cycles, one French housing rights tool
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;BailleurVérif&lt;/a&gt; is a French housing rights SaaS I run solo. The agent wakes every 2 hours via cron, reads a strategic critic audit every 12 hours, and executes prescriptions autonomously — no human in the loop per wake cycle. As of today: 370 wakes, 27/27 strategic prescriptions honored, 0 ScheduleWakeup calls (external cron handles pacing).&lt;/p&gt;

&lt;p&gt;Stack: Python + Anthropic Claude API for the critic/executor pattern + SQLite funnel tracker + static HTML server. The agent commits to GitHub, pings the Indexing API, publishes datasets on data.gouv.fr. This week it made a product decision I'd been procrastinating on for weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: our homepage copy was entirely hypothetical
&lt;/h2&gt;

&lt;p&gt;Strategic critic audit-26 (2026-05-26T21:55Z) flagged it directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Homepage copy is 100% hypothetical — 'vous vous demandez peut-être si votre loyer est légal...' — while Reddit has thousands of real renter questions publicly accessible."&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The prescription: scrape 30+ questions from French subreddits, build a data-driven page, publish the JSON dataset as CC-BY 4.0, then use that corpus to swap the homepage hero. Builder-only. Zero human action required from me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The scraping pipeline
&lt;/h2&gt;

&lt;p&gt;The agent wrote &lt;code&gt;scrape_reddit_locataires_run367.py&lt;/code&gt;. Two rounds, rate-limited at 2s/request, with an identified bot UA:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;urllib.request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

&lt;span class="n"&gt;SUBREDDITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;france+paris+immobilier+vosfinances+AskFrance&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;UA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BailleurVerifBot/0.1 (+bailleurverif.fr; contact@bailleurverif.fr)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

&lt;span class="n"&gt;TAG_QUERIES&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;loyer-abusif&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;loyer abusif encadrement refus propriétaire&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;loyer trop élevé que faire locataire&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;dpe-invalide&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;DPE faux fraude loyer augmenté&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;dpe invalide recours locataire&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;depot-garantie-non-restitue&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;caution non rendue délai légal&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;dépôt garantie non restitué propriétaire&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="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;results&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;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;TAG_QUERIES&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&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;q&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;queries&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.reddit.com/search.json&lt;/span&gt;&lt;span class="sh"&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;?q=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;quote&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;amp;subreddit=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;SUBREDDITS&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&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;&amp;amp;sort=top&amp;amp;limit=25&amp;amp;t=month&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;req&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nc"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&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;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UA&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;urllib&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="nf"&gt;urlopen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&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;resp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read&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;post&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;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;data&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;children&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;post&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;results&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;tag&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&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;subreddit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;subreddit&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;score&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;score&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;num_comments&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;num_comments&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;id_hash&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# no username stored
&lt;/span&gt;            &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Round 1 produced 50 raw results. Round 2 added &lt;code&gt;r/Locataires&lt;/code&gt; and tightened the queries. &lt;strong&gt;Final dataset: 35 questions.&lt;/strong&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The quality filter: why 50 → 35 matters
&lt;/h2&gt;

&lt;p&gt;The filter pass mattered more than the scrape volume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Title-anchor required&lt;/strong&gt;: must contain a tag-specific word (loyer, DPE, caution, garantie, encadrement)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Renter scope only&lt;/strong&gt;: not a landlord asking about their property&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Anti-noise blacklist&lt;/strong&gt;: "copropriété", "syndic", "locaux commerciaux" — adjacent topics, out of scope&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What got cut: 8 landlord questions, 4 commercial lease questions, 3 homeowner renovation questions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Final distribution: loyer-abusif: 26 / dpe-invalide: 6 / depot-garantie: 3.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Score range: 8 to 347 upvotes. The agent sorted by score within each tag and picked the top 3 for the homepage hero.&lt;/p&gt;

&lt;h2&gt;
  
  
  The output: 776 lines of HTML + a CC-BY 4.0 JSON dataset
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;build_questions_reelles_page_run367.py&lt;/code&gt; generated &lt;code&gt;/questions-reelles-locataires-fr.html&lt;/code&gt; with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;JSON-LD: WebPage + BreadcrumbList + FAQPage (3 legal questions) + Dataset + Organization&lt;/li&gt;
&lt;li&gt;35 questions by category, anonymized (id_hash only, subreddit + score shown)&lt;/li&gt;
&lt;li&gt;Direct links to the legal templates matching each question tag&lt;/li&gt;
&lt;li&gt;CC-BY 4.0 downloadable JSON dataset (27 KB)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Dataset schema:&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;"metadata"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Questions réelles de locataires FR — Reddit 2026-05"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Reddit public search API"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"license"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CC-BY 4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scrape_date"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-27"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"filter_methodology"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"tag-anchor title match + renter scope + blacklist anti-noise"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"total_questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;35&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"questions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"id_hash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"e95b9fed0029"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"tag"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"loyer-abusif"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Mais qui respecte l'encadrement des loyers pour les petites surfaces ?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"subreddit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"paris"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"score"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;93&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"num_comments"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The homepage hero swap — 12 hours later
&lt;/h2&gt;

&lt;p&gt;Strategic audit-27 arrived at 10:00Z the next morning with a single prescription: use the corpus to replace the hypothetical homepage intro. The agent picked 3 questions (one per tag, highest score), added citation badges, and modified 22 lines of &lt;code&gt;index.html&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Before:&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;p&amp;gt;&lt;/span&gt;Vous vous demandez peut-être si votre loyer respecte l'encadrement légal,
si votre DPE est valide, si votre propriétaire peut garder votre caution...&lt;span class="nt"&gt;&amp;lt;/p&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;After:&lt;/strong&gt; 3 real questions with &lt;code&gt;r/paris · 93 votes&lt;/code&gt; badges. A CTA linking to all 35 questions. Commit &lt;code&gt;47404ed&lt;/code&gt;, smoke-tested via &lt;code&gt;curl -s https://bailleurverif.fr/ | grep "loyer abusif"&lt;/code&gt; ✅.&lt;/p&gt;

&lt;p&gt;The whole cycle — audit-26 prescribed, agent built and shipped, audit-27 prescribed homepage swap, agent shipped — completed in under 16 hours, with zero messages from me.&lt;/p&gt;

&lt;h2&gt;
  
  
  The side discovery: 60% of "direct" visits were bots
&lt;/h2&gt;

&lt;p&gt;While auditing the funnel this week, the agent cross-referenced session IDs against raw UA strings in &lt;code&gt;visits.jsonl&lt;/code&gt;. The result: &lt;strong&gt;159 raw "direct" sessions → 63 plausibly human after UA filtering.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The filter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;

&lt;span class="n"&gt;BOT_UA_PATTERNS&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;Googlebot&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;Applebot&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;Yandex&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;HeadlessChrome&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;GoogleOther&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;PerplexityBot&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;GPTBot&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;ClaudeBot&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;CCBot&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;Bytespider&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;bot&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;crawler&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;spider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;HEADLESS_CLEAN_VERSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Chrome/14[5-9]\.0\.0\.0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# Real Chrome 148 reports "148.0.7778.96" — headless Chrome reports "148.0.0.0"
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;is_bot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ua_lower&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ua&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;if&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;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ua_lower&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BOT_UA_PATTERNS&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;HEADLESS_CLEAN_VERSION&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ua&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two visits the agent had labeled "plausible human" in earlier runs turned out to be Googlebot Nexus 5X and YandexRenderResourcesBot. The agent now tracks &lt;code&gt;direct_humans_after_ua_filter_lifetime = 63&lt;/code&gt; as a separate metric from raw visit counts.&lt;/p&gt;

&lt;p&gt;The 40% noise estimate is probably conservative — sessionId=null visits (JS not executed) add another layer of bot signal the agent is now checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons from this cycle
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reddit public JSON is underused for user research.&lt;/strong&gt; No auth needed, 2s/request rate limit is acceptable, and you get score + comment count as engagement proxy for free. Query it before writing your landing page copy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Filter harder than you scrape.&lt;/strong&gt; 50 → 35 with strict title-anchor + scope criteria. The 15 removed would have diluted the page signal and made the homepage swap weaker. Quality over volume.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Real user questions beat copywriter hypothesis every time.&lt;/strong&gt; We had no surveys, no user interviews. But 26 questions tagged &lt;code&gt;loyer-abusif&lt;/code&gt; with scores ranging from 8 to 347 are more actionable than "you might be wondering if..."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Track &lt;code&gt;humans_after_ua_filter&lt;/code&gt; from day one.&lt;/strong&gt; Raw visit counts are noise-heavy. Cross-reference UA strings. Ship a derived counter early.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>ai</category>
      <category>automation</category>
      <category>saas</category>
    </item>
    <item>
      <title>90 pages with broken Rich Results. My autonomous agent found them, fixed them, and rewrote its own monitoring.</title>
      <dc:creator>Florian Demartini</dc:creator>
      <pubDate>Wed, 20 May 2026 14:35:48 +0000</pubDate>
      <link>https://dev.to/bailleurverif/90-pages-with-broken-rich-results-my-autonomous-agent-found-them-fixed-them-and-rewrote-its-own-k0</link>
      <guid>https://dev.to/bailleurverif/90-pages-with-broken-rich-results-my-autonomous-agent-found-them-fixed-them-and-rewrote-its-own-k0</guid>
      <description>&lt;p&gt;Twelve days into running my solo SaaS on an autonomous agent, I watched it silently fix a structured data bug across 90 pages — then patch its own monitoring sub-agent to detect the same pattern forever.&lt;/p&gt;

&lt;p&gt;Here's the actual log.&lt;/p&gt;




&lt;h2&gt;
  
  
  The product
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;BailleurVérif&lt;/a&gt; is a French rental compliance checker built on open data (data.gouv.fr + ADIL jurisprudence). It generates programmatic HTML pages to answer questions like "is my Paris apartment rent legally capped?" — no account needed.&lt;/p&gt;

&lt;p&gt;The agent running it fires every hour via cron, reads server logs and memory files, makes decisions, ships code, and commits to GitHub. No human in the loop unless I send an explicit brief.&lt;/p&gt;




&lt;h2&gt;
  
  
  The bug that hid for 11+ cycles
&lt;/h2&gt;

&lt;p&gt;For 11+ wake cycles — roughly 11 consecutive hours — 90 HTML files were serving invalid &lt;code&gt;BreadcrumbList&lt;/code&gt; JSON-LD. The missing field: the &lt;code&gt;item&lt;/code&gt; property on &lt;code&gt;ListItem&lt;/code&gt; position 2.&lt;/p&gt;

&lt;p&gt;Google's Rich Results parser requires &lt;em&gt;both&lt;/em&gt; &lt;code&gt;name&lt;/code&gt; AND &lt;code&gt;item&lt;/code&gt; for every breadcrumb position. Without &lt;code&gt;item&lt;/code&gt;, the breadcrumb rich result is silently dropped from SERP — no error, no warning, just quietly gone.&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="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;BROKEN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;was&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;live)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"Encadrement des loyers Paris"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;VALID&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(what&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;it&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;should&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;be)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"@type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ListItem"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"position"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"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;"Encadrement des loyers Paris"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"item"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://bailleurverif.fr/loyer-legal-paris.html"&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;I noticed it via GSC URL Inspection — the tool showed "BreadcrumbList is invalid" for the Paris page I'd shipped 9 hours earlier. I sent a brief to the agent at 09:45Z: &lt;em&gt;"Fix missing &lt;code&gt;item&lt;/code&gt; field on BreadcrumbList position 2 — 81+ pages."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The agent's response in one wake cycle
&lt;/h2&gt;

&lt;p&gt;Run-321 started at 10:00Z. By 11:00Z:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Committed the fix across 90 files&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent ran a Python &lt;code&gt;str.replace&lt;/code&gt; pass — turned out 90 files had the bug, not just 81 (the wider grep caught more templates). One commit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;commit 3ee81da
fix: add missing item field on BreadcrumbList position 2 (81+ pages)

90 files: 31 encadrement-loyer + 50 DPE F/G + 9 connexes
(guide-bailleur, scanner-arnaque, irl-revision-loyer, etc.)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;2. Wrote a permanent discipline document&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The agent created &lt;code&gt;memory-agent/concepts/seo-discipline.md&lt;/code&gt; (+80 lines): the correct JSON-LD pattern, 6 canonical hub URLs, 4 anti-patterns, and a rule that sub-seo-monitor should detect this automatically going forward.&lt;/p&gt;

&lt;p&gt;Not as a one-time note — as a &lt;em&gt;concept file&lt;/em&gt; that every future wake loads as context.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. PATCHed its own monitoring sub-agent&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;This is the part I find genuinely interesting. The agent sent an HTTP PATCH to &lt;code&gt;sub-seo-monitor&lt;/code&gt; — a Haiku sub-agent running nightly — to add a new audit task between existing tasks 2 and 3:&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;audit_breadcrumbs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html_content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;
    &lt;span class="n"&gt;results&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;script&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;script type=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/ld\+json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;(.*?)&amp;lt;/script&amp;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;html_content&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DOTALL&lt;/span&gt;
    &lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;script&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;data&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;@type&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;BreadcrumbList&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;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&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;itemListElement&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[]):&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;item&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;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;results&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;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;pages_with_missing_item&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&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;results&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="c1"&gt;# Alert rule: if pages_with_missing_item &amp;gt;= 1 → prepend inbox.md HEAD
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The sub-seo-monitor prompt went from 3,301 → 5,766 characters (+2,465 chars). The backup hash was logged: &lt;code&gt;81a0184d8f687290&lt;/code&gt;. The sub-agents registry was updated with &lt;code&gt;last_update_run=run-321&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;From now on: any HTML template regression that reintroduces a missing &lt;code&gt;item&lt;/code&gt; field gets caught within 24 hours.&lt;/p&gt;




&lt;h2&gt;
  
  
  What happened in the 12 hours after the fix
&lt;/h2&gt;

&lt;p&gt;Independently that same day, the SEO infrastructure closed a loop I'd been waiting on for weeks:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Googlebot WRS Mobile rendered the homepage with JavaScript for the first time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The proof is in &lt;code&gt;server.log&lt;/code&gt;. Three consecutive requests from IP &lt;code&gt;66.249.73.129&lt;/code&gt; (verified Googlebot):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;2026-05-20T06:40:00Z  GET /                        200
2026-05-20T06:40:01Z  GET /api/changelog?limit=5   200   ← JS-only endpoint
2026-05-20T06:40:02Z  POST /api/visit               200
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/api/changelog&lt;/code&gt; is called exclusively by client-side JavaScript on the homepage. A plain HTML crawler never hits it. Googlebot hitting it means Googlebot is actually executing our JS.&lt;/p&gt;

&lt;p&gt;That same day, 9 distinct bot crawls hit the Paris page within 12 hours of it going live — from 4 independent channels:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Googlebot Mobile WRS (rendered JS, see above)&lt;/li&gt;
&lt;li&gt;Google-InspectionTool/1.0 (rare signal, likely GSC quality check)&lt;/li&gt;
&lt;li&gt;GPTBot/1.3 (OpenAI LLM ingestion pipeline)&lt;/li&gt;
&lt;li&gt;Generic AWS/Bing crawlers
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;dashboard-extras.json,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="err"&gt;h&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;post-ship&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_hits_24h"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bot_hits_lifetime"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;118&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"gptbot_today"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"last_googlebot"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-05-20T08:43:24Z"&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;Same week: the agent added Wikidata entity &lt;code&gt;Q139857638&lt;/code&gt; to the site's &lt;code&gt;Organization&lt;/code&gt; JSON-LD &lt;code&gt;sameAs&lt;/code&gt; array, and made the footer links to GitHub and Wikidata visible. Moat category-4 count went from 2 → 3 substantive components.&lt;/p&gt;




&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Agent runtime&lt;/strong&gt;: Claude claude-opus-4-6 (Builder Opus) running the main cron wake; Claude claude-haiku-4-5 (sub-agents: &lt;code&gt;sub-seo-monitor&lt;/code&gt;, &lt;code&gt;sub-observatoire-publisher&lt;/code&gt;, &lt;code&gt;sub-critic&lt;/code&gt;, &lt;code&gt;sub-linkedin-drafter&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Memory&lt;/strong&gt;: flat &lt;code&gt;.md&lt;/code&gt; files in &lt;code&gt;memory-agent/&lt;/code&gt; (concepts, decisions, kpis, snapshots) — no vector DB, no embeddings, just structured Markdown loaded at wake start&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Orchestration&lt;/strong&gt;: cron &lt;code&gt;0 * * * *&lt;/code&gt; on a Linux VPS, each wake = 1 Claude API call, time-boxed 15 min&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sub-agent management&lt;/strong&gt;: local Node.js &lt;code&gt;agent-browser&lt;/code&gt; server with PATCH/GET API, agents registered in &lt;code&gt;sub-agents-registry.json&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTML generation&lt;/strong&gt;: Python &lt;code&gt;str.replace&lt;/code&gt; on templates, 90 static files, committed and pushed via GitHub PAT&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SEO signals&lt;/strong&gt;: JSON-LD (Organization, BreadcrumbList, FAQPage, Dataset), IndexNow pings, sitemap.xml auto-generated&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Data&lt;/strong&gt;: data.gouv.fr reuse &lt;code&gt;6a0c30a&lt;/code&gt;, ADIL jurisprudence scraping, observatoire 121-wave cross-analysis (57.6% violation rate nationally)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Silent structured data bugs are insidious.&lt;/strong&gt; Google drops invalid Rich Results without noise. The only detection path is GSC URL Inspection or a dedicated nightly audit — not your server logs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Patching your own monitoring is the actual fix.&lt;/strong&gt; The breadcrumb code fix took 3 minutes. Writing the discipline doc and PATCHing the sub-agent prompt took 12 more. But now any template regression is caught within 24h automatically, forever.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Googlebot rendering JS is a measurable milestone.&lt;/strong&gt; The gap between "crawls HTML" and "executes JavaScript" matters for JS-heavy pages. &lt;code&gt;server.log&lt;/code&gt; is your proof: look for client-side-only API endpoints in the Googlebot user agent trail.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Flat Markdown memory beats vector stores for small autonomous agents.&lt;/strong&gt; The agent's &lt;code&gt;memory-agent/concepts/&lt;/code&gt; directory is just structured &lt;code&gt;.md&lt;/code&gt; files. It loads relevant files at wake start, writes new ones when it learns something. No embedding pipeline, no retrieval latency. For under 200 files, simple grep and read is fast enough.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Code source MIT &lt;a href="https://github.com/Creariax5/bailleurverif" rel="noopener noreferrer"&gt;github.com/Creariax5/bailleurverif&lt;/a&gt; · Site &lt;a href="https://bailleurverif.fr" rel="noopener noreferrer"&gt;bailleurverif.fr&lt;/a&gt; · Wikidata &lt;a href="https://www.wikidata.org/wiki/Q139857638" rel="noopener noreferrer"&gt;Q139857638&lt;/a&gt;&lt;/p&gt;

</description>
      <category>claude</category>
      <category>ai</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
