<?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: Odilon HUGONNOT</title>
    <description>The latest articles on DEV Community by Odilon HUGONNOT (@ohugonnot).</description>
    <link>https://dev.to/ohugonnot</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%2F3833552%2F48d32eab-68ed-4496-8ba6-f01e32806723.png</url>
      <title>DEV Community: Odilon HUGONNOT</title>
      <link>https://dev.to/ohugonnot</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/ohugonnot"/>
    <language>en</language>
    <item>
      <title>Pass Commerce BFC: 30% back on your website project</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 29 Jun 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/pass-commerce-bfc-30-back-on-your-website-project-33he</link>
      <guid>https://dev.to/ohugonnot/pass-commerce-bfc-30-back-on-your-website-project-33he</guid>
      <description>&lt;p&gt;A charcutier from Pontarlier called me in January. He wanted a simple website with click &amp;amp; collect — something he'd been postponing for two years. Tight budget, as always. I told him about the Pass Commerce et Artisanat from the Bourgogne-Franche-Comté region. He frowned. "Never heard of it." He'd been eligible for four years.&lt;/p&gt;

&lt;p&gt;Out of my last thirty prospects in Franche-Comté — shopkeepers, craftsmen, restaurants, independent professionals — &lt;strong&gt;twenty-six had no idea this subsidy existed&lt;/strong&gt;. Of the four who'd heard of it, two assumed it was reserved for larger companies.&lt;/p&gt;

&lt;p&gt;The result: a regional programme that reimburses 30% of your digital project, capped at EUR 7,500, sitting there mostly unused. Here's how it works.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pass Commerce et Artisanat in two sentences
&lt;/h2&gt;

&lt;p&gt;The Bourgogne-Franche-Comté region covers &lt;strong&gt;30% of the pre-tax cost&lt;/strong&gt; of eligible digitisation expenses. The maximum eligible spend is EUR 25,000 HT, meaning the reimbursement caps at &lt;strong&gt;EUR 7,500&lt;/strong&gt;. It covers website creation, redesign, SEO, business software, and even some IT equipment.&lt;/p&gt;

&lt;p&gt;In practice: you commission a website for EUR 5,000 HT. You file the application &lt;em&gt;before&lt;/em&gt; signing the quote. If approved, the region transfers EUR 1,500 after delivery. Your website cost EUR 3,500.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who qualifies
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Small businesses&lt;/strong&gt; with fewer than 7 employees (FTE), under EUR 1M turnover&lt;/li&gt;
&lt;li&gt;  Registered in one of the &lt;strong&gt;8 BFC départements&lt;/strong&gt;: Côte-d'Or, Doubs, Jura, Nièvre, Haute-Saône, Saône-et-Loire, Yonne, Territoire de Belfort&lt;/li&gt;
&lt;li&gt;  Sectors: retail, crafts, personal services, restaurants, tourist accommodation&lt;/li&gt;
&lt;li&gt;  The business must be at least 1 year old&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The most common blocker is the NAF code (French activity classification). If your registered main activity doesn't match the eligible sectors, even if your actual work qualifies, the application can be rejected.&lt;/p&gt;

&lt;h2&gt;
  
  
  The critical timing rule
&lt;/h2&gt;

&lt;p&gt;This is the rule that kills most applications: &lt;strong&gt;you must file before signing the quote and before work begins&lt;/strong&gt;. Not before invoicing — before the &lt;em&gt;start&lt;/em&gt; of work. Sign on March 10, file on March 12? Too late.&lt;/p&gt;

&lt;p&gt;Allow &lt;strong&gt;2 to 3 months&lt;/strong&gt; between first CCI/CMA contact and the green light. It's slow. It's non-negotiable. But it's 30% of the project that stays in your pocket.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does and doesn't cover
&lt;/h2&gt;

&lt;p&gt;Covered: website creation, redesign, SEO audit, business software, first-year hosting, training on your new tools, professional photo shoots (if related to the web project).&lt;/p&gt;

&lt;p&gt;Not covered: recurring hosting fees, social media management, Facebook ads, second-hand equipment, expenses under EUR 500.&lt;/p&gt;

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

&lt;p&gt;The Pass Commerce et Artisanat is one of the rare public subsidies that actually works simply — provided you play by the rules: file before starting, provide a detailed quote, keep all payment receipts. If you're a small business owner in Bourgogne-Franche-Comté and you've been putting off your website because "it's too expensive": 30% of the cost may already be budgeted by the region.&lt;/p&gt;

</description>
      <category>subsidy</category>
      <category>website</category>
      <category>besancon</category>
      <category>sme</category>
    </item>
    <item>
      <title>Stop Asking the LLM Whether Its Source Is Real</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:39:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/stop-asking-the-llm-whether-its-source-is-real-2oaa</link>
      <guid>https://dev.to/ohugonnot/stop-asking-the-llm-whether-its-source-is-real-2oaa</guid>
      <description>&lt;p&gt;You ask the AI for a bibliography. It hands you a title, authors, a journal, a year, a well-formed DOI. Everything is plausible, everything is clean. And one reference in two doesn't exist. Not "approximate": nonexistent. The DOI resolves to nothing, the paper was never written.&lt;/p&gt;

&lt;p&gt;The reflex is to ask the model again: "are you sure this source is real?" It says yes. Always. You just asked the forger about the authenticity of his forgery.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hallucination is plausible by construction
&lt;/h2&gt;

&lt;p&gt;An LLM doesn't store a database of publications. It generates likely sequences of words. A citation, to it, is a shape: a surname, an initial, two more names, a capitalized journal, a recent year, ten DOI digits. It produces that shape perfectly, because that's exactly what it's good at. The content doesn't need to be true to be plausible, it just needs to resemble.&lt;/p&gt;

&lt;p&gt;That's why a hallucinated reference is so vicious: it doesn't look like an error. A wrong calculation jumps out. An invented citation looks like a real one, until you click.&lt;/p&gt;

&lt;h2&gt;
  
  
  Don't ask the culprit
&lt;/h2&gt;

&lt;p&gt;The golden rule fits in one sentence: never ask the model that hallucinated a citation whether that citation is real. For two reasons that compound. First, it doesn't have the information: it has no access to a registry, it can only regenerate something plausible. Second, even if it doubted, its self-evaluation bias pushes it to confirm what it already produced. You get a "yes" worth nothing.&lt;/p&gt;

&lt;p&gt;Verification has to come from elsewhere. From a source the model neither controls nor can invent: a metadata API.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three filters: existence, credibility, fidelity
&lt;/h2&gt;

&lt;p&gt;In my pipeline for writing technical dossiers, no reference enters the document before clearing three filters, in this order.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Existence.&lt;/strong&gt; The DOI must resolve. It's binary, and it's free. Crossref exposes its whole database:&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; &lt;span class="s2"&gt;"https://api.crossref.org/works/10.1145/3290605.3300233"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | jq &lt;span class="s1"&gt;'.message.title[0], .message.author[0].family, .message["published"]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the API returns a title and authors, the paper exists. If it returns a 404, the reference is out, full stop. For preprints, same logic with the arXiv API (&lt;code&gt;export.arxiv.org/api/query&lt;/code&gt;) or HAL for French research. This step alone removes the bulk of hallucinations, because an invented DOI never resolves.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Credibility.&lt;/strong&gt; Existing isn't enough. A predatory journal, one that publishes anything for a fee, gives a valid DOI to a worthless paper. This filter checks that the journal or conference is real and recognized, not a shell. The DOI proves the source exists, not that it's worth anything.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fidelity.&lt;/strong&gt; The most demanding filter, and the one the API won't do for you. The source exists, it's serious, but does it actually say what you make it say? You have to read the paper, spot what's measured versus what's merely asserted, and not extrapolate past its abstract. A real citation slapped onto a claim it doesn't support is still false evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  The same principle for any RAG
&lt;/h2&gt;

&lt;p&gt;This pipeline is nothing specific to academic dossiers. The moment an agent cites a source, a ticket, a CVE number, a doc page, a commit, the same discipline applies: the reference must resolve against the authoritative system, not against the model's memory. An agent that says "per ticket JIRA-1242" must have resolved JIRA-1242; otherwise it may have invented the number with as much confidence as a DOI.&lt;/p&gt;

&lt;p&gt;The most common architecture mistake in RAG is trusting the generation layer to self-verify. It can't. Verification is a separate step, wired to an external truth, run before the output reaches the user.&lt;/p&gt;

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

&lt;p&gt;There's a lot of talk about lowering models' hallucination rate. That's the wrong fight: a plausible-text generator will always hallucinate a little, it's its nature. The real lever isn't making the model more honest, it's ceasing to take it at its word. A citation you can't resolve against an external registry isn't a citation. It's a guess in a lab coat.&lt;/p&gt;

</description>
      <category>hallucination</category>
      <category>ai</category>
      <category>rag</category>
      <category>citations</category>
    </item>
    <item>
      <title>No Agent Grades Its Own Homework</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:38:30 +0000</pubDate>
      <link>https://dev.to/ohugonnot/no-agent-grades-its-own-homework-8lb</link>
      <guid>https://dev.to/ohugonnot/no-agent-grades-its-own-homework-8lb</guid>
      <description>&lt;p&gt;You ask Claude to review your code. It says "looks good, clean, well factored". Of course it does. It wrote that code five minutes ago. You just asked the author to grade his own paper, and he gave himself an A.&lt;/p&gt;

&lt;p&gt;Having an AI review code works. But not by asking the one who just wrote it. Quality doesn't come from a smarter model, it comes from an architecture where no role checks itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The self-preference bias
&lt;/h2&gt;

&lt;p&gt;This isn't a hunch, it's measured. A model evaluating its own output rates it higher than others' at equal quality: the &lt;em&gt;self-preference bias&lt;/em&gt;, documented by Panickssery and co-authors in 2024, and it's causal, not correlational. The model recognizes its own style and prefers it.&lt;/p&gt;

&lt;p&gt;In practice that means the naive loop "write, then review what you just wrote" is broken by construction. You don't get a review, you get a justification. The agent already decided its code was good the moment it produced it; asking again only confirms.&lt;/p&gt;

&lt;h2&gt;
  
  
  The blind reviewer
&lt;/h2&gt;

&lt;p&gt;So the first rule: the reviewer is never the author. In my config, the review agents run in a &lt;strong&gt;clean context&lt;/strong&gt;. They don't see the implementation prompt, they don't know what constraints the author set, they meet the diff like a colleague on Monday morning. And when the author is a known model, the reviewer is from a &lt;strong&gt;different family&lt;/strong&gt;, to break style recognition.&lt;/p&gt;

&lt;p&gt;One detail matters as much as the rest: the developer's name never enters the reviewer's prompt. No "this was written by a senior", no "review this model's work". The author's identity is exactly the information that triggers the bias. We take it off the table.&lt;/p&gt;

&lt;h2&gt;
  
  
  No finding without a receipt
&lt;/h2&gt;

&lt;p&gt;The second trap is the opposite of the first. An AI reviewer, especially in a clean context, tends to over-flag: it invents problems to look useful, it flags "vulnerabilities" that aren't. A review that cries wolf on every line is no better than a complacent one: either way, you stop listening.&lt;/p&gt;

&lt;p&gt;Hence the receipt rule. Every finding must cite a &lt;code&gt;file:line&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; pass a check before it's surfaced: a grep proving the occurrence, a sandbox run, a failing test, a data-flow trace. A finding nobody can prove is dropped silently, no debate.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Finding: "non-parameterized SQL call, injection risk"
  → receipt required: grep the user-input → query flow
  → if the value is a code constant: dropped
  → if it comes from the HTTP request: kept, with the line
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Proof comes before judgment. The reviewer isn't allowed to bother you over a hunch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The refute panel
&lt;/h2&gt;

&lt;p&gt;For critical findings, the ones that would block a merge, I add a last layer: a panel of independent skeptics whose instruction isn't to confirm but to &lt;strong&gt;refute&lt;/strong&gt;. Each one gets the finding and tries to tear it down: "here's why this isn't a bug". If a majority is needed to &lt;em&gt;keep&lt;/em&gt; the finding, plausible false alarms don't survive. The ones that remain took a demolition attempt and held.&lt;/p&gt;

&lt;p&gt;It's the exact opposite of the naive loop. Instead of one model trying to be right, several trying to contradict each other. The truth that comes out has been attacked, not self-proclaimed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separation of powers
&lt;/h2&gt;

&lt;p&gt;Put end to end, this gives a team of agents where the roles never overlap. The one who writes the code isn't the one who writes the tests, which are written from the spec only, not from the code. The one who reviews didn't write. And before a human or an LLM even gives an opinion, an objective gate (build, lint, tests) has to be green: the model's judgment only comes after the machine, never instead of it.&lt;/p&gt;

&lt;p&gt;This isn't gratuitous distrust of AI. It's the same principle that governs a newsroom, an accounting team, a court: you let no one sign off on their own work, because no one is a good judge of themselves. LLMs, with a proven self-preference bias, even less than the rest.&lt;/p&gt;

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

&lt;p&gt;The temptation, with a model that codes well, is to hand it the whole cycle: write, test, review, sign off. That's exactly what you mustn't do, because each of those steps corrects the previous one, and a corrector that corrects itself corrects nothing. The quality of an AI review isn't measured by the model's intelligence. It's measured by how many times you stop it from grading itself.&lt;/p&gt;

</description>
      <category>codereview</category>
      <category>ai</category>
      <category>agents</category>
      <category>selfpreferencebias</category>
    </item>
    <item>
      <title>Your AI Writes Tests That Can Never Fail</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:38:00 +0000</pubDate>
      <link>https://dev.to/ohugonnot/your-ai-writes-tests-that-can-never-fail-3i57</link>
      <guid>https://dev.to/ohugonnot/your-ai-writes-tests-that-can-never-fail-3i57</guid>
      <description>&lt;p&gt;You ask the AI for tests. It hands you twelve, all green. CI passes. You merge. Three days later a bug ships, on a function those tests were supposed to cover. You reopen the test file and it clicks: it ran, it passed, and it tested nothing.&lt;/p&gt;

&lt;p&gt;A green test isn't a proof. It's a hypothesis. And an AI, left to its own devices, is very good at writing hypotheses that can never be disproved.&lt;/p&gt;

&lt;h2&gt;
  
  
  The phantom test
&lt;/h2&gt;

&lt;p&gt;Take a dead-simple function, a discount above 100 euros:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;Discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;int&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;total&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="m"&gt;100&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;total&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="m"&gt;10&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;total&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here's the kind of test an AI produces when you ask "write me a test for this" with no further framing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;150&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;got&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"result should not be negative"&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This test is green. It does run the discount branch (so your coverage climbs). But look at the assertion: &lt;code&gt;got &amp;lt; 0&lt;/code&gt; is never true, whatever &lt;code&gt;Discount&lt;/code&gt; does. Replace &lt;code&gt;total - 10&lt;/code&gt; with &lt;code&gt;total + 10&lt;/code&gt;, with &lt;code&gt;total * 2&lt;/code&gt;, with &lt;code&gt;42&lt;/code&gt;: the test stays green. It doesn't check behavior, it checks that the lights are on.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage doesn't measure what you think
&lt;/h2&gt;

&lt;p&gt;The trap is that this phantom test inflates your coverage. Coverage counts lines &lt;em&gt;executed&lt;/em&gt;, not assertions that &lt;em&gt;bite&lt;/em&gt;. A line crossed by a test that asserts nothing useful counts as much as a line genuinely verified. So a 90% coverage report can hide half a suite of tests that will never fall, even if you break the code on purpose.&lt;/p&gt;

&lt;p&gt;That's exactly an LLM's playground. Its reward signal is "the tests pass". Not "the tests catch a bug". With no external oracle to stop it, it drifts toward the shortest path to green: soft assertions, mocks that test themselves, cases that never exercise the risky branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red-check: break the code, demand the red
&lt;/h2&gt;

&lt;p&gt;The counter is one move, and it's as old as TDD: before trusting a test, check that it knows how to fail. Mutate the line it's meant to protect, rerun, and expect to see it go red. If it stays green, it's vacant.&lt;/p&gt;

&lt;p&gt;On our function, I change the discount for one second:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="c"&gt;// temporary mutation: - becomes +&lt;/span&gt;
&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The phantom test stays green. Verdict: bin it. Here's the one that earns your trust:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;TestDiscount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;testing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;Discount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;150&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="n"&gt;got&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="m"&gt;140&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Discount(150) = %d, want 140"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;got&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With the same mutation, &lt;code&gt;Discount(150)&lt;/code&gt; returns 160, the test goes red instantly. It bites. That's a test: not one that passes, one that knows why it might not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automating the red-check: mutation testing
&lt;/h2&gt;

&lt;p&gt;Doing this by hand on every test doesn't scale. That's precisely what &lt;strong&gt;mutation testing&lt;/strong&gt; automates: the tool applies hundreds of small mutations to your code (a &lt;code&gt;&amp;gt;&lt;/code&gt; that becomes &lt;code&gt;&amp;gt;=&lt;/code&gt;, a &lt;code&gt;+&lt;/code&gt; that becomes &lt;code&gt;-&lt;/code&gt;, a gutted &lt;code&gt;return&lt;/code&gt;) and reruns your suite after each one. Every mutation that makes no test go red is a &lt;em&gt;surviving mutant&lt;/em&gt;: a hole your tests can't see.&lt;/p&gt;

&lt;p&gt;In Go, &lt;a href="https://github.com/go-gremlins/gremlins" rel="noopener noreferrer"&gt;gremlins&lt;/a&gt; does the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;go &lt;span class="nb"&gt;install &lt;/span&gt;github.com/go-gremlins/gremlins/cmd/gremlins@latest
gremlins unleash ./...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It gives you a &lt;em&gt;mutation score&lt;/em&gt;: the percentage of mutants killed. Where coverage tells you "this line is crossed", the mutation score tells you "this line is actually tested". The two numbers have nothing to do with each other, and it's the second that counts.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I wire it into an AI loop
&lt;/h2&gt;

&lt;p&gt;When I let an agent write code and its tests, I don't let it declare itself done. Before any review, an objective gate runs: build, lint, test suite, then a red-check on the critical tests. The agent mutates the target line itself, checks the test goes red, restores it. A test still green after mutation gets rewritten, not negotiated. The LLM doesn't get a vote on "does this actually test something": the mutation decides, it only observes.&lt;/p&gt;

&lt;p&gt;The rule that falls out is simple: no generated test enters the suite without proving it can fail. The cost is tiny, the payoff huge, because a vacant test is worse than no test. The absence, you see it. The vacant one lulls you.&lt;/p&gt;

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

&lt;p&gt;We've learned to distrust AI-written code, so we review it. We still extend blind trust to the tests it writes, because they're green. But green doesn't prove itself: a test is only worth the red it's able to produce. Until you've watched a test fail at least once, you don't have a test, you have a decoration.&lt;/p&gt;

</description>
      <category>tests</category>
      <category>mutationtesting</category>
      <category>ai</category>
      <category>go</category>
    </item>
    <item>
      <title>I Versioned the Way I Think. Then I Forced It to Comply.</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 28 Jun 2026 12:37:30 +0000</pubDate>
      <link>https://dev.to/ohugonnot/i-versioned-the-way-i-think-then-i-forced-it-to-comply-ddk</link>
      <guid>https://dev.to/ohugonnot/i-versioned-the-way-i-think-then-i-forced-it-to-comply-ddk</guid>
      <description>&lt;p&gt;One morning I pasted four principles into my &lt;code&gt;CLAUDE.md&lt;/code&gt;, the global instruction file Claude Code reads at the start of every session. "Think before you code", "simplicity first", that kind of maxim you see fly by on X, credited to Andrej Karpathy. I felt clever for about a day.&lt;/p&gt;

&lt;p&gt;Then I watched Claude read the file, nod, and carry on exactly as before. A &lt;code&gt;CLAUDE.md&lt;/code&gt; is a suggestion box. The model nods, then does whatever it wants. If I wanted it to code my way, writing it down wasn't going to cut it. I had to enforce it.&lt;/p&gt;

&lt;p&gt;What follows is what that frustration turned into: a config in four layers, reinstallable in one command, and a discovery that runs through everything else. The only rigor that counts is the one a model can't grant itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four layers, and only one really changes the behavior
&lt;/h2&gt;

&lt;p&gt;My config has four floors, from softest to hardest.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;brain&lt;/strong&gt; is &lt;code&gt;CLAUDE.md&lt;/code&gt;: how I work, not the docs for my code. The rule that sums it up lives inside it: "what not to add: anything Claude rediscovers by reading the code." It holds my design principles, my stance on orchestrating subagents (I size up, I delegate, I verify: "I stay the brain, they're the hands"), and one line that becomes the thread running through the whole thing.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;references&lt;/strong&gt;: a &lt;code&gt;go-best-practices.md&lt;/code&gt; file the brain points to in plain text whenever Go is involved.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;skills&lt;/strong&gt;: ten of them. A skill is a folder with a playbook that Claude loads on demand for a specific job: review code, write an article, distill a book. Mine are packaged as a &lt;a href="https://github.com/ohugonnot/claude-skills" rel="noopener noreferrer"&gt;marketplace, in a public GitHub repo&lt;/a&gt;, with a changelog and a version number. That's the real differentiator: versioned tooling, not just rules scribbled in a file.&lt;/p&gt;

&lt;p&gt;The &lt;strong&gt;guardrails&lt;/strong&gt;, finally. And this is the only layer that reliably changes behavior. The first three, the model can read and ignore. The fourth, it can't.&lt;/p&gt;

&lt;p&gt;The four config layers, from softest (the model can ignore) to hardest (the model is bound by the guardrail) Brain CLAUDE.md: how I work References go-best-practices.md Skills 10 versioned skills Guardrails hooks: settings.json + guard.sh read, then ignored enforced&lt;/p&gt;

&lt;p&gt;Three layers the model can ignore, one it can't.&lt;/p&gt;

&lt;h2&gt;
  
  
  From the "FORBIDDEN" you ignore to the block you can't
&lt;/h2&gt;

&lt;p&gt;My &lt;code&gt;CLAUDE.md&lt;/code&gt; said, in capitals: never read &lt;code&gt;.env&lt;/code&gt; files. A suggestion. The model honored it when convenient.&lt;/p&gt;

&lt;p&gt;So I wrote a hook. A hook is a script Claude Code runs automatically at a precise point in its cycle. &lt;code&gt;guard.sh&lt;/code&gt; attaches to &lt;code&gt;PreToolUse&lt;/code&gt;, intercepts every &lt;code&gt;Read&lt;/code&gt; and every &lt;code&gt;Bash&lt;/code&gt; before it runs, and decides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$tool&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="k"&gt;in
  &lt;/span&gt;Read&lt;span class="p"&gt;)&lt;/span&gt;
    is_secret_path &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; block &lt;span class="s2"&gt;"reading a secrets file (&lt;/span&gt;&lt;span class="nv"&gt;$path&lt;/span&gt;&lt;span class="s2"&gt;)."&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;
  Bash&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$cmd&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-Eq&lt;/span&gt; &lt;span class="nt"&gt;--&lt;/span&gt; &lt;span class="s1"&gt;'--no-verify|core\.hooksPath='&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
      &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; block &lt;span class="s2"&gt;"bypassing git hooks is forbidden."&lt;/span&gt;
    &lt;span class="p"&gt;;;&lt;/span&gt;
&lt;span class="k"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;block&lt;/code&gt; exits with code 2. For Claude Code, a &lt;code&gt;PreToolUse&lt;/code&gt; hook exiting with 2 is a veto: the tool never runs. Reading a &lt;code&gt;.env&lt;/code&gt;, bypassing git hooks with &lt;code&gt;--no-verify&lt;/code&gt;: these are no longer things I ask it not to do, they're things that don't happen.&lt;/p&gt;

&lt;p&gt;The hook is &lt;em&gt;fail-open&lt;/em&gt;. The slightest doubt, a missing &lt;code&gt;jq&lt;/code&gt;, a malformed JSON, and it allows. A bug in the guardrail has never blocked a legitimate tool. It only shuts the door on a confirmed violation.&lt;/p&gt;

&lt;h2&gt;
  
  
  The proof is outside, never inside the model
&lt;/h2&gt;

&lt;p&gt;Here's the &lt;code&gt;CLAUDE.md&lt;/code&gt; line that feeds everything else:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;External rigor: the only proof that counts is verifiable from the outside (a test, a command, a grep, real output), never the model's self-assessment. "Are you sure?" → an LLM always says yes; demand a fact, not a claim.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Ask a model if it's sure, it says yes. Always. So my skills are built so the proof is executable, not declared.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;feature-loop&lt;/code&gt; ships a feature in a loop, but before trusting a critical test, it mutates it, checks that it &lt;strong&gt;goes red&lt;/strong&gt;, then restores it. A test that stays green after mutation tests nothing: it gets rewritten. The reason is spelled out in the skill: 76% of LLM-written tests miss the red-to-green transition. A test that always passes is as reassuring as it is dishonest.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rediger-cir&lt;/code&gt;, my skill for French R&amp;amp;D tax-credit dossiers, pushes the same logic to the extreme. No reference enters a dossier without resolving through a metadata API: the DOI has to resolve, the journal can't be predatory, the paper has to actually say what you make it say. The golden rule fits in one sentence: never ask the model that just hallucinated a citation whether that citation is real. It's precisely the one you shouldn't ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the reviewer is never the author
&lt;/h2&gt;

&lt;p&gt;If I had to keep a single decision out of this whole config, it'd be this one.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;senior-review&lt;/code&gt; reviews code before merge. The obvious trap would be to ask the model that just wrote the code to review itself. Except a model judging its own output over-rates it: self-preference bias is documented and causal (Panickssery, 2024). An honest reviewer has to be blind. My reviewer agents run in a clean context, never see the implementation prompt, and when the author is a known model, they're from a different family. The developer's name never enters the prompt.&lt;/p&gt;

&lt;p&gt;Then comes the gate that does the cleanup: no finding without a receipt. Every flag cites a &lt;code&gt;file:line&lt;/code&gt; and passes a check, a grep, a sandbox run, a failing test. An unverifiable finding is dropped silently. Models over-flag by reflex; grounding each flag in a real execution removes around 60% of false positives. The goal isn't to find the most problems. It's to surface only the real ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  Ten skills, from scoping to wrap-up
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;feature-loop&lt;/code&gt;, &lt;code&gt;senior-review&lt;/code&gt; and &lt;code&gt;rediger-cir&lt;/code&gt;, I've already dissected. But they don't live alone. The ten skills form a chain: four cover the life cycle of a dev task, from scoping to commit, and each knows when to hand off to the next. The other six are orthogonal, pulled out when needed.&lt;/p&gt;

&lt;p&gt;Skill&lt;/p&gt;

&lt;p&gt;Its marrow&lt;/p&gt;

&lt;p&gt;&lt;code&gt;issue-mr&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Scope. Turns a fuzzy task into a clean issue, branch and PR. An analyse mode settles the design before a line is written.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;feature-loop&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Build. A quality loop where author, tester and reviewer are separate agents: an objective gate (build, tests, red-check) before any review, a mandatory live smoke test.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;senior-review&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Review. Blind reviewers per dimension, no finding without a receipt, an adversarial panel on the critical ones.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;branch-wrap-up&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Close out. Proposes a conventional commit, push and PR, captures knowledge. Never commits without my go-ahead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;code-mentor&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Teach. Slows down at each decision, explains the why, makes me predict before it reveals. The opposite of copy-paste.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;skill-builder&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Design. The principles for a skill that actually triggers and stays focused: one anchor word, a predictable process.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tech-article&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Write. An article that sounds human, not AI. The one you're reading came out of it.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;book-distill&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Distill. A faithful reading note of a book, every quote verified word-for-word against the text.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;rediger-cir&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Justify. A French R&amp;amp;D tax-credit dossier where every reference resolves through an API, zero hallucination.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;vide-contexte&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;Remember. Before a context reset, persists the non-obvious insights into memory files.&lt;/p&gt;

&lt;p&gt;One thread runs through all of them: the author is never the reviewer, the objective gate comes before the model's judgment, and nothing commits without my consent. These aren't ten tools, they're one way of working, declined ten times.&lt;/p&gt;

&lt;h2&gt;
  
  
  How I work, held together by the CLAUDE.md
&lt;/h2&gt;

&lt;p&gt;The brain of the config comes down to a few rules I apply to myself as much as to the model.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;The mother matrix.&lt;/strong&gt; I don't run everything myself. I size up the difficulty, then delegate to calibrated subagents: a Haiku for the mechanical, an Opus for the hard reasoning. I stay the brain, they're the hands.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Expose, don't guess.&lt;/strong&gt; Before coding, I state my assumptions. Several readings possible, I lay them all out instead of silently picking one.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Surgical.&lt;/strong&gt; Every changed line traces back to the request. No opportunistic refactor on code that isn't broken, no style drifting in on the back of a fix.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;The simplest thing that works.&lt;/strong&gt; YAGNI. A one-implementation interface, a just-in-case abstraction, one more file for no reason: all stop signals.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Comments for a senior dev.&lt;/strong&gt; He gets the what in five seconds. A comment survives only if it carries a non-obvious why, a trap, an invariant.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sixth rule you already know: the proof is external. It's the one that turns all the rest from good intentions into guarantees.&lt;/p&gt;

&lt;h2&gt;
  
  
  222,793 stars prove nothing
&lt;/h2&gt;

&lt;p&gt;While building this config, I studied the big repos in the space. The numbers, checked against the GitHub API the day I'm writing this, are dizzying. "Everything Claude Code" tops out at 222,793 stars. Next to it, a very clean skills repo by Matt Pocock has 148,737, and another, sold as "Karpathy's skills", 183,652.&lt;/p&gt;

&lt;p&gt;That last one deserves a warning, because it illustrates the theme better than any test. The repo is nothing official: it's a third party's reconstruction of a single tweet, duplicated into four formats. The label says Karpathy. The source says a copied tweet. Trusting the label is exactly the mistake my whole config tries to make impossible.&lt;/p&gt;

&lt;p&gt;As for the 222,793-star behemoth, its traction comes from distribution, not code: a won Anthropic hackathon, threads with millions of views, an umbrella name, a README turned into a landing page. Under the hood, two or three solid hooks, whose &lt;code&gt;.env&lt;/code&gt;-blocking and &lt;code&gt;--no-verify&lt;/code&gt; idea I borrowed, by the way, a "learning" loop disabled by default, and internal counters that contradict each other from one section to the next.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ohugonnot/claude-skills" rel="noopener noreferrer"&gt;My own repo&lt;/a&gt; sits at zero stars. And its guardrails are stricter than the average of the one with 222,793. The lesson isn't bitter, it's useful: stars measure a well-told story, not rigor. The two have almost nothing to do with each other.&lt;/p&gt;

&lt;h2&gt;
  
  
  The check an LLM can't grant itself
&lt;/h2&gt;

&lt;p&gt;Before publishing, I had my own skills audited. The verdict: the scientific references were 98% real, but four falsely precise numbers and one misattributed quote had slipped into the batch. Fixed. Even an author who thinks he's rigorous can't certify himself alone.&lt;/p&gt;

&lt;p&gt;This article is the final proof. I had one of the ten skills write it, from the public repo, on the strength of a brief I'd written myself. That brief claimed "Everything Claude Code" was a lone outlier, five times bigger than everything else. The check against the GitHub API, right before writing, showed there's actually a whole pack of six-figure repos. My own brief was wrong, and only a call to an external source caught it.&lt;/p&gt;

&lt;p&gt;That's the whole point. The model doesn't know it's wrong. Neither do I, half the time. The only thing that knows is the test that goes red, the DOI that resolves, the &lt;code&gt;file:line&lt;/code&gt; you can open. A Claude Code config worth having isn't a list of good intentions. It's the place where proof stops being a claim.&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>config</category>
      <category>hooks</category>
      <category>skills</category>
    </item>
    <item>
      <title>Go in Production: patterns that survive fintech</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sun, 28 Jun 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/go-in-production-patterns-that-survive-fintech-1184</link>
      <guid>https://dev.to/ohugonnot/go-in-production-patterns-that-survive-fintech-1184</guid>
      <description>&lt;p&gt;When you write Go for a regulated financial platform, there's an unspoken rule everyone understands after the first incident: &lt;strong&gt;the code running on Friday evening must still be running on Monday morning, exactly the same way&lt;/strong&gt;. Not "roughly the same." Not "after a restart." Exactly the same.&lt;/p&gt;

&lt;p&gt;That changes how you write Go. You stop looking for the most elegant solution — you look for the one that won't break at 3 AM on a bank holiday. The patterns described here didn't come from tutorials or conference talks. They survived months of production, post-mortems, and code reviews with people who have very little patience for code that "should work."&lt;/p&gt;

&lt;h2&gt;
  
  
  Graceful shutdown — the non-negotiable pattern
&lt;/h2&gt;

&lt;p&gt;First pattern, and by far the most critical. If your service can't stop cleanly, everything else is decoration.&lt;/p&gt;

&lt;p&gt;The scenario: a deployment in progress. Kubernetes sends SIGTERM. Your service has 30 seconds to finish what it's doing. If you're in the middle of a financial transaction — a fund transfer, a reconciliation, a ledger entry — you can't just cut. You also can't take 5 minutes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;srv&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Server&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;Addr&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;    &lt;span class="s"&gt;":8080"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;Handler&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="n"&gt;newRouter&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;errCh&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="nb"&gt;make&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;chan&lt;/span&gt; &lt;span class="kt"&gt;error&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;go&lt;/span&gt; &lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;errCh&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt; &lt;span class="n"&gt;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ListenAndServe&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}()&lt;/span&gt;

    &lt;span class="k"&gt;select&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;errCh&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fmt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Errorf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"server stopped: %w"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;-&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Done&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;shutCtx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cancel&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Background&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="m"&gt;25&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Second&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;defer&lt;/span&gt; &lt;span class="n"&gt;cancel&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;srv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shutCtx&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;  &lt;code&gt;context.Background()&lt;/code&gt; for shutdown, not the parent ctx — the parent is already cancelled, that's why we're here&lt;/li&gt;
&lt;li&gt;  25 seconds, not 30 — keep a 5-second margin before Kubernetes sends kill -9&lt;/li&gt;
&lt;li&gt;  The error channel is buffered — if shutdown arrives before &lt;code&gt;ListenAndServe&lt;/code&gt; returns, the goroutine doesn't leak&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The real difficulty isn't the HTTP server — it's everything else. Kafka consumers, background workers, gRPC connections, tickers. Every component with a lifecycle must stop in the right order. In practice, we use &lt;code&gt;errgroup&lt;/code&gt; with a shared context: the first component to die cancels the others.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ctx&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;errgroup&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WithContext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;httpServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;grpcServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;kafkaConsumer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Go&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;func&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="kt"&gt;error&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;metricsServer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Simple. Testable. Every component implements &lt;code&gt;Run(ctx context.Context) error&lt;/code&gt;. When the context is cancelled, everything shuts down in reverse startup order. It's boring, verbose, and has worked for two years without surprises.&lt;/p&gt;

&lt;h2&gt;
  
  
  HTTP middleware — the production stack
&lt;/h2&gt;

&lt;p&gt;Every HTTP request goes through the same middleware chain. The order is non-negotiable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="k"&gt;func&lt;/span&gt; &lt;span class="n"&gt;newRouter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NewServeMux&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"GET /health"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handleHealth&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;mux&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HandleFunc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"POST /api/v1/transfers"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;handleTransfer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;var&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="n"&gt;http&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mux&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;withAuth&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withRequestID&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withRecovery&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withLogging&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="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;withMetrics&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Read bottom to top (last wrapped = first executed):&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Metrics&lt;/strong&gt; — Prometheus histogram, before everything else to capture total duration&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Logging&lt;/strong&gt; — structured request log with request ID, status, duration&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Recovery&lt;/strong&gt; — catches panics, logs the stack trace, returns 500 instead of killing the process&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Request ID&lt;/strong&gt; — UUID in context, propagated through all logs and downstream calls&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Auth&lt;/strong&gt; — token verification, identity injected into context&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The recovery middleware is the most underestimated. In dev, a panic crashes the program and you see the stack trace. In production, a panic in an HTTP handler &lt;em&gt;kills the goroutine&lt;/em&gt; but not the process — except the connection is closed cleanly by the runtime, with no log. The client gets an EOF. You see nothing. The recovery middleware turns that into a 500 + stack trace in logs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Circuit breaker — when downstream is dead
&lt;/h2&gt;

&lt;p&gt;In fintech, your services talk to banking partners, KYC APIs, payment systems. They go down. Not often, but when they do, it's rarely for 5 seconds — it's for 45 minutes, on a Saturday, with no warning.&lt;/p&gt;

&lt;p&gt;Without a circuit breaker, your service stacks pending requests, goroutines multiply, memory climbs, timeouts cascade, and your own healthcheck fails. The circuit breaker cuts the connection to the dead service &lt;em&gt;before&lt;/em&gt; it contaminates the rest.&lt;/p&gt;

&lt;p&gt;The key questions for configuration: how many failures before opening the circuit? How long before retrying? Does "failure" include timeouts or only 5xx? The answer depends on the downstream service. A banking partner that normally responds in 800ms and 30s when struggling? Timeout at 5s, circuit open after 3 failures, reset after 60s.&lt;/p&gt;

&lt;h2&gt;
  
  
  Structured logging — slog in production
&lt;/h2&gt;

&lt;p&gt;We switched from &lt;code&gt;log.Printf&lt;/code&gt; to &lt;code&gt;slog&lt;/code&gt; (standard library since Go 1.21) a year and a half ago. The gain isn't aesthetic — it's operational. When an incident hits at 2 AM, the question is never "what happened?" but "what happened for &lt;em&gt;this&lt;/em&gt; request ID, &lt;em&gt;this&lt;/em&gt; user, &lt;em&gt;this&lt;/em&gt; amount?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight go"&gt;&lt;code&gt;&lt;span class="n"&gt;slog&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"transfer processed"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reqID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"user_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;userID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"amount_cents"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s"&gt;"duration_ms"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Since&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Milliseconds&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
    &lt;span class="s"&gt;"partner"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"bank_xyz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two rules we enforce:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Never log personal data&lt;/strong&gt; — no email, no name, no IBAN. User ID yes, everything else no. It's a GDPR reflex, but mostly it's the law when you handle funds.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Request ID goes everywhere&lt;/strong&gt; — from HTTP middleware to the last downstream gRPC call. Passed through context, included in every log. When a customer calls about a stuck transaction, support provides the request ID, and in 30 seconds you have the full trace.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the code doesn't show
&lt;/h2&gt;

&lt;p&gt;The patterns above are the technical bricks. What makes the difference in financial production is everything that isn't code:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Blameless post-mortems.&lt;/strong&gt; Every incident documented, every corrective action tracked.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Shutdown tests.&lt;/strong&gt; We test graceful shutdown as seriously as features. A deployment that drops requests is a P0 bug.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;"Boring code."&lt;/strong&gt; The most reliable code is the code you don't need to re-read. No generics everywhere, no channels when a mutex will do, no abstraction for fun. Boring code is code that runs.&lt;/li&gt;
&lt;/ul&gt;

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

&lt;p&gt;After several years of Go in financial production, the patterns that survive are never the most sophisticated. They're the most boring. Graceful shutdown, middleware in the right order, circuit breaker, structured logging. Nothing spectacular. But when the banking partner goes down at 11 PM on a Friday, it's this boring code that makes the difference between "the circuit breaker cut, zero lost transactions, we go home" and "we spend the weekend reconciling ledger entries."&lt;/p&gt;

&lt;p&gt;Go "best practices 2026" isn't about language novelties. It's about the discipline of what you write — and especially what you don't.&lt;/p&gt;

</description>
      <category>go</category>
      <category>production</category>
      <category>fintech</category>
      <category>middleware</category>
    </item>
    <item>
      <title>Custom Automation for SMEs: 5 real cases, EUR 500 to 5000</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Sat, 27 Jun 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/custom-automation-for-smes-5-real-cases-eur-500-to-5000-3jf5</link>
      <guid>https://dev.to/ohugonnot/custom-automation-for-smes-5-real-cases-eur-500-to-5000-3jf5</guid>
      <description>&lt;p&gt;"We lose 4 hours a week copying stuff from one Excel file to another." That's what I hear most when a small business owner contacts me about automation. Not "we want AI" or "we need digital transformation." Just: we waste time on repetitive work and we'd like it to stop.&lt;/p&gt;

&lt;p&gt;The problem is nobody knows what it costs. Automation agencies sell "process audits" at EUR 3,000 before writing a single line of code. No-code platforms promise "everything without a developer" — until the day it isn't, and the Zapier scenario loops at 2 AM generating 47,000 emails.&lt;/p&gt;

&lt;p&gt;Here are five automations I delivered over the past two years. Real prices, real timelines, and what went wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Automated competitor price monitoring — EUR 480
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The need:&lt;/strong&gt; an auto parts e-commerce owner manually checks prices from 3 competitors across 200 references every Monday. Three hours of copy-paste into a spreadsheet.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I delivered:&lt;/strong&gt; a Node.js script that scrapes 3 competitor sites in parallel, extracts prices, and emails a comparison table every Monday at 7 AM. Hosted on his own server via cron.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time spent:&lt;/strong&gt; 1 day. The longest part: understanding each site's HTML structure and handling out-of-stock items (missing price vs zero price).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What went wrong:&lt;/strong&gt; after 3 months, one competitor redesigned their site. The scraping broke silently — the script ran without errors but returned zero prices. I added an alert when more than 10% of prices come back as zero.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Estimated ROI:&lt;/strong&gt; 3h × 4 weeks × 12 months × EUR 20/h = EUR 2,880/year. The script paid for itself in 2 months.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Automated quote follow-ups — EUR 750
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The need:&lt;/strong&gt; a carpenter sends 15-20 quotes per month, follows up on maybe 3. Conversion rate: 18%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I delivered:&lt;/strong&gt; an integration between his quoting tool and a PHP script that checks daily for quotes sent more than 7 days ago without a response. The prospect gets a personalised follow-up email. No response at day 14: second follow-up. Day 30: marked as lost automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; conversion rate went from 18% to 27% in 4 months. On an average order of EUR 3,200, that's roughly 2 more converted quotes per month — about EUR 6,400/month in additional revenue. For EUR 750.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Multi-platform stock sync — EUR 2,200
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The need:&lt;/strong&gt; a home decor shop sells on WooCommerce, Etsy, and in-store (Lightspeed POS). Stocks aren't synchronised. Result: regular oversells, unhappy customers, and an employee spending 1 hour daily correcting quantities by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I delivered:&lt;/strong&gt; a Go service that listens to webhooks from all 3 platforms and updates stock in real-time. When a sale happens on Etsy, stock drops on WooCommerce and Lightspeed within 30 seconds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Time spent:&lt;/strong&gt; 4 days. Etsy API is decent, WooCommerce REST API too. Lightspeed was the nightmare: approximate documentation, aggressive rate limiting, and a bug on their side with in-store sale webhooks that required a 5-minute polling backup.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Monthly PDF reporting — EUR 1,400
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The need:&lt;/strong&gt; an accounting firm generates monthly activity reports for 35 clients. A partner spends 2 days per month copy-pasting data from their accounting software into Word, exporting to PDF, and emailing. 35 times.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I delivered:&lt;/strong&gt; a PHP script that connects to the accounting software API, extracts key metrics per client, generates a clean PDF with the firm's logo and trend charts, and emails everything. Runs automatically on the 5th of each month.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Lead qualification bot — EUR 4,800
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The need:&lt;/strong&gt; a real estate agency receives 80-100 enquiries per month. A salesperson reviews each one, asks the same 5 questions by phone (budget, location, timeline, property type, financing), and qualifies the lead. 60% aren't serious.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What I delivered:&lt;/strong&gt; a multi-step form that asks the 5 qualification questions before human contact. Responses are scored automatically. Score &amp;gt; 7: salesperson notified by SMS immediately. Score 4-7: automated follow-up email with matching listings. Score &amp;lt; 4: generic email with search link.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Result:&lt;/strong&gt; the salesperson went from 15h/week of qualification to 4h. Hot prospects are contacted on average 12 minutes after enquiry instead of 48 hours. Conversion rate rose from 8% to 14%.&lt;/p&gt;

&lt;h2&gt;
  
  
  What all 5 projects have in common
&lt;/h2&gt;

&lt;p&gt;None required AI, machine learning, or "digital transformation." It's classical code — APIs, scraping, crons, emails. The value isn't in technical sophistication, it's in &lt;strong&gt;understanding the business process&lt;/strong&gt; and automating it without distorting it.&lt;/p&gt;

&lt;p&gt;A custom script costs more upfront. But it belongs to you, runs on your server, and does exactly what you need — not what the template allows.&lt;/p&gt;

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

&lt;p&gt;Automation isn't a big-company luxury. At EUR 480 for competitor monitoring or EUR 750 for quote follow-ups, it's the best ROI most small businesses can get from their digital budget. The condition: a developer who understands your business, not a generic tool that promises to do everything without code.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>freelance</category>
      <category>sme</category>
      <category>scripts</category>
    </item>
    <item>
      <title>Website pricing in Besançon 2026: 4 quotes, 10x gap, what to actually look at</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Fri, 26 Jun 2026 09:00:03 +0000</pubDate>
      <link>https://dev.to/ohugonnot/website-pricing-in-besancon-2026-4-quotes-10x-gap-what-to-actually-look-at-41df</link>
      <guid>https://dev.to/ohugonnot/website-pricing-in-besancon-2026-4-quotes-10x-gap-what-to-actually-look-at-41df</guid>
      <description>&lt;p&gt;The meeting takes place in a vaulted cellar on rue des Granges, in Besançon. The owner — a wine merchant who just took over his uncle's shop — lays four A4 sheets on the wine barrel serving as a coffee table. Four quotes for the same project: a brochure website with vintages presentation, a vintage blog, a reservation form for tastings.&lt;/p&gt;

&lt;p&gt;Quote n°1: &lt;strong&gt;€990 ex-VAT&lt;/strong&gt;. A well-known SaaS platform, turn-key template, monthly subscription included the first year.&lt;/p&gt;

&lt;p&gt;Quote n°2: &lt;strong&gt;€2,500 ex-VAT&lt;/strong&gt;. A small local agency from the Battant district, WordPress + premium theme.&lt;/p&gt;

&lt;p&gt;Quote n°3: &lt;strong&gt;€6,000 ex-VAT&lt;/strong&gt;. A senior freelancer — me, in this case — custom-built site, OVH hosting, GDPR-compliant, no subscription.&lt;/p&gt;

&lt;p&gt;Quote n°4: &lt;strong&gt;€15,000 ex-VAT&lt;/strong&gt;. A Paris-based agency in the 11th arrondissement, full brand overhaul, design system, 6-month SEO roadmap.&lt;/p&gt;

&lt;p&gt;"For the same site," he repeats, perplexed. Except it isn't the same site. After two hours opening each quote line by line, he understood why a factor of 15 separates the cheapest from the most expensive — without any one of them being objectively a scam. Here's what we discussed that day.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 4 quotes for the same website?
&lt;/h2&gt;

&lt;p&gt;When a business owner asks for "a website", they describe a visible outcome — a few pages, a design that matches their shop, a contact form. What each vendor hears is something completely different:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  The €990 platform hears: "rent an existing template, drag-and-drop three images, collect a monthly subscription forever".&lt;/li&gt;
&lt;li&gt;  The €2,500 local agency hears: "install WordPress, pick a theme, configure 3 plugins, ship in 3 weeks".&lt;/li&gt;
&lt;li&gt;  The €6,000 senior freelancer hears: "understand the business, write a custom-built site, optimize for Besançon local SEO, ship without technical debt, explain how to keep it alive".&lt;/li&gt;
&lt;li&gt;  The €15,000 Paris agency hears: "brand overhaul, design system, SEO audit, content plan, four-person team".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of them is lying. None of them is cheating. Each is selling what they know how to do, with their own cost structure. The trap is comparing the numbers without comparing the scopes. It's exactly like comparing a guesthouse in Vesoul to a suite at the Lutetia because "they both have a bed".&lt;/p&gt;

&lt;h2&gt;
  
  
  What the €990 packages hide
&lt;/h2&gt;

&lt;p&gt;The €990 quote seduced the wine merchant. Logical: ten times cheaper than mine. Except the first year actually costs €990, and the following years cost &lt;strong&gt;between €360 and €600 ex-VAT&lt;/strong&gt; in subscription, indefinitely. Over five years, you're between €2,800 and €3,400 — already more expensive than the local agency's quote, and still without owning your site.&lt;/p&gt;

&lt;p&gt;But that's not the worst. Three other traps, verified on his screenshots:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Frozen template&lt;/strong&gt;. Pages are fill-in-the-blank, not a site written for his business. Impossible to render the vintages catalogue the way he imagines it, because that component doesn't exist in the library.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Non-existent or miserable SEO&lt;/strong&gt;. No HTML control, no image optimization, no Schema.org markup, and a shared subdomain that crushes the domain authority. Result: his site never ranks page one for "wine shop Besançon".&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Export impossible&lt;/strong&gt;. When he wants to leave, he discovers data exports as CSV — but not the design, not the pages, not the URLs. Everything has to be rebuilt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For 70% of shopkeepers, these platforms are perfect — truly. If you just want an online presence, with three standard pages and zero SEO ambition, go for it. But that wasn't what the wine merchant wanted. He wanted to be found on Google when a tourist searches "wine cellar downtown Besançon".&lt;/p&gt;

&lt;h2&gt;
  
  
  The real cost of a custom website in 2026
&lt;/h2&gt;

&lt;p&gt;Let's break down a typical custom brochure site — 8 to 10 pages, blog, forms, SEO-optimized, GDPR-compliant. Here's what I bill, line by line, in actual hours:&lt;/p&gt;

&lt;p&gt;Item&lt;/p&gt;

&lt;p&gt;Hours&lt;/p&gt;

&lt;p&gt;Cost (at €65/h)&lt;/p&gt;

&lt;p&gt;Discovery, workshops, wireframes&lt;/p&gt;

&lt;p&gt;6-10 h&lt;/p&gt;

&lt;p&gt;€390 - €650&lt;/p&gt;

&lt;p&gt;Design (mockups, brand, responsive)&lt;/p&gt;

&lt;p&gt;12-20 h&lt;/p&gt;

&lt;p&gt;€780 - €1,300&lt;/p&gt;

&lt;p&gt;HTML/CSS/JS integration&lt;/p&gt;

&lt;p&gt;15-25 h&lt;/p&gt;

&lt;p&gt;€975 - €1,625&lt;/p&gt;

&lt;p&gt;Back-office (CMS or PHP/PostgreSQL pages)&lt;/p&gt;

&lt;p&gt;10-20 h&lt;/p&gt;

&lt;p&gt;€650 - €1,300&lt;/p&gt;

&lt;p&gt;Technical SEO + semantics + Schema.org&lt;/p&gt;

&lt;p&gt;6-10 h&lt;/p&gt;

&lt;p&gt;€390 - €650&lt;/p&gt;

&lt;p&gt;GDPR (notices, cookies, registry)&lt;/p&gt;

&lt;p&gt;2-4 h&lt;/p&gt;

&lt;p&gt;€130 - €260&lt;/p&gt;

&lt;p&gt;QA, deployment, client training&lt;/p&gt;

&lt;p&gt;4-8 h&lt;/p&gt;

&lt;p&gt;€260 - €520&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;55-97 h&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;€3,575 - €6,305&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;OVH hosting then costs between €35 and €120 per year depending on size — per year, not per month. Domain name: €12 per year. Maintenance: on-demand, around €65 per hour. No subscription, no vendor lock-in: the site belongs to the client, source code shipped, private GitHub shared.&lt;/p&gt;

&lt;p&gt;This is the breakdown I systematically send with my quotes. Clients often compare it to what they received from other vendors and realize that "€6,000 for a site" means nothing in itself — it's €6,000 for 80 hours of skilled work, exactly matching &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;my public rate sheet&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Realistic price grid in Besançon and BFC region
&lt;/h2&gt;

&lt;p&gt;To compare what's comparable, here are the ranges I observe on the Besançon and Burgundy-Franche-Comté market in 2026 — among senior freelancers and serious small agencies (not low-cost platforms, not national agencies):&lt;/p&gt;

&lt;p&gt;Site type&lt;/p&gt;

&lt;p&gt;Pages / Features&lt;/p&gt;

&lt;p&gt;Range ex-VAT&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple brochure site&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;5 pages, contact form, basic SEO, responsive&lt;/p&gt;

&lt;p&gt;€1,500 - €2,500&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Brochure + blog&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;10 pages, CMS-backed blog, advanced SEO, full GDPR&lt;/p&gt;

&lt;p&gt;€2,500 - €4,500&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Simple e-commerce&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;up to 100 products, Stripe payment, basic stock management&lt;/p&gt;

&lt;p&gt;€4,000 - €8,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Advanced e-commerce&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;multi-catalog, ERP integration, pro accounts, B2B/B2C&lt;/p&gt;

&lt;p&gt;€8,000 - €18,000&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom business app&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;complex back-office, workflows, APIs, multi-role&lt;/p&gt;

&lt;p&gt;€8,000 - €25,000+&lt;/p&gt;

&lt;p&gt;Three important notes. &lt;strong&gt;One&lt;/strong&gt;, these ranges are starting points — a 5-page brochure site with demanding design can exceed €2,500 without anything abnormal. &lt;strong&gt;Two&lt;/strong&gt;, they exclude hosting (€35-120/year), domain (€12/year) and maintenance (billed hourly or as a package). &lt;strong&gt;Three&lt;/strong&gt;, they align on French senior freelancer pricing — not a SaaS platform, not a big Paris agency, not an offshore vendor.&lt;/p&gt;

&lt;p&gt;For format-by-format detail, see &lt;a href="https://www.web-developpeur.com/site-vitrine-pme/" rel="noopener noreferrer"&gt;SME brochure website&lt;/a&gt; and &lt;a href="https://www.web-developpeur.com/creation-site-web/" rel="noopener noreferrer"&gt;website creation&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Pass Commerce Artisanat grant — 30% refunded
&lt;/h2&gt;

&lt;p&gt;This is the argument few vendors mention, because it doesn't suit them commercially: &lt;strong&gt;the Burgundy-Franche-Comté Region refunds 30% of the website cost&lt;/strong&gt;, up to €7,500 of grant, via the Pass Commerce Artisanat scheme.&lt;/p&gt;

&lt;p&gt;Concretely, on a website billed €6,000 ex-VAT, the eligible business recovers €1,800. The real cost drops to €4,200. It's no longer twice as expensive as the local €2,500 agency — it's almost the same price, for a site without subscription, custom-built and SEO-optimized for local search.&lt;/p&gt;

&lt;p&gt;Eligibility: be a shop, artisan, or business with fewer than 10 employees, located in Burgundy-Franche-Comté, and use a vendor that isn't a SaaS platform (Wix, Shopify, Squarespace are not eligible — you need an "owned" website). The file is built with the chamber of commerce or trades, typically over 4 to 8 weeks. Full details on the &lt;a href="https://www.web-developpeur.com/subvention-pass-commerce-bfc/" rel="noopener noreferrer"&gt;Pass Commerce BFC page&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;For the wine merchant on rue des Granges, this was decisive. His €6,000 quote became a €4,200 net cost — €30 per month over five years for a site he owns, versus €50/month subscription forever on the €990 platform.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to choose: 4 criteria that filter out scams
&lt;/h2&gt;

&lt;p&gt;At the end of the meeting, I gave him four simple criteria. Not to choose between him and me — to choose among all the vendors who would solicit him over the next six months.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Is the source code delivered?&lt;/strong&gt; If the answer is "no" or "depends on the subscription", it's a disguised platform. You're renting, not owning. Sometimes legitimate, but it has to be an informed choice, not a discovery two years later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. How long before the first well-ranked page on Google?&lt;/strong&gt; Nobody can guarantee a position, but a serious vendor can estimate: "3 to 6 months for local queries, 12-18 months for competitive ones". A "well, you never know" or "SEO is complicated" answer is a red flag. SEO isn't magic — it's clean HTML, relevant content, backlinks, Core Web Vitals, and patience.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Does the quote break down hours per item?&lt;/strong&gt; A serious quote decomposes: discovery, design, integration, back-office, SEO, GDPR, QA. A quote saying "complete site: €4,500" without any breakdown means the vendor doesn't know how many hours they'll spend — so they either take an enormous margin or underestimate and ship sloppy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Can you meet the vendor physically?&lt;/strong&gt; For a Besançon shop owner, a developer who can grab a coffee at Brasserie 1802 and look at the code together for 30 minutes is worth a lot more than a vendor in Paris or Manila. Not for chauvinism — for the speed of business iterations. See &lt;a href="https://www.web-developpeur.com/en/blog/dev-freelance-local-vs-offshore" rel="noopener noreferrer"&gt;local freelancer vs offshore: what nobody tells you about real cost&lt;/a&gt; for details.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to take away
&lt;/h2&gt;

&lt;p&gt;The wine merchant eventually picked quote n°3. Not because it was the cheapest (it wasn't), nor because it was the most prestigious (far from it). Because it was the only quote where he understood, line by line, what he was buying — and could calculate the total five-year cost factoring in hosting, maintenance, the absence of subscription, and the BFC grant.&lt;/p&gt;

&lt;p&gt;"The right price for a website" isn't a range. It's &lt;strong&gt;the price at which you understand what you're paying for&lt;/strong&gt;, where the quote hides nothing, where you aren't locked in after the fact, and where the vendor can explain their choices without blushing. In Besançon, in 2026, that lands between €2,500 and €8,000 for 95% of SMEs — and drops another 30% with the Pass Commerce BFC.&lt;/p&gt;

&lt;p&gt;If you want a detailed quote for your project, &lt;a href="https://www.web-developpeur.com/developpeur-freelance-besancon/" rel="noopener noreferrer"&gt;we can meet&lt;/a&gt; — I have a decent coffee 200 meters from Place de la Révolution. &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;Full rate sheets&lt;/a&gt; are public on the site, no tricks.&lt;/p&gt;

</description>
      <category>pricing</category>
      <category>freelance</category>
      <category>besancon</category>
      <category>website</category>
    </item>
    <item>
      <title>Agency vs freelancer in Besançon: who actually does the work behind the quote?</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Thu, 25 Jun 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/agency-vs-freelancer-in-besancon-who-actually-does-the-work-behind-the-quote-3gi8</link>
      <guid>https://dev.to/ohugonnot/agency-vs-freelancer-in-besancon-who-actually-does-the-work-behind-the-quote-3gi8</guid>
      <description>&lt;p&gt;The director called me on a Tuesday in September. His voice swung between irritation and polite disbelief. He had just paid €8,200 to a local agency for a WordPress brochure site. Three months of delay. The result: a $59 ThemeForest theme, two logos moved around, and a contact form that didn't send any email. When he asked for the sources, he received a .zip file and the Gmail address of a developer in Dhaka he had never met.&lt;/p&gt;

&lt;p&gt;The agency had a real storefront, a beautiful website full of "values" and "processes", three consultants in suits at the sales meeting. The dev who actually wrote the code never left Bangladesh. Nobody lied to him — they just forgot to mention where the code came from.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Upfront disclosure: I'm a freelancer.&lt;/strong&gt; What follows is openly biased. But it's also 14 years on the ground, dozens of projects taken back from agencies, and the honest urge to ask the question nobody asks: &lt;em&gt;why is there so much opacity about who actually does the work?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Agency vs freelance: what's the real difference?
&lt;/h2&gt;

&lt;p&gt;On paper, a web agency sells a multi-disciplinary team — project manager, designer, developer, integrator, SEO, sometimes a traffic manager. A freelancer sells one brain and two hands. The apparent difference is structural: the agency covers the chain, the freelancer covers one link.&lt;/p&gt;

&lt;p&gt;Except in real life, in Besançon as elsewhere, the split is fuzzier. Most agencies under 15 staff don't have all profiles in-house. They subcontract. Either to French freelancers (often the same ones you could have reached directly), or to offshore platforms. The actual production cost of a brochure site billed €8,000 by an agency is rarely above €1,500 — the rest is margin, sales, management, office space.&lt;/p&gt;

&lt;p&gt;The real difference isn't &lt;em&gt;who codes&lt;/em&gt;, but &lt;em&gt;who owns the project&lt;/em&gt;. The agency promises organization, a single point of contact, a contractual deliverable. The freelancer promises a direct relationship with the person writing the code. Both models are valid — but they don't cover the same needs, and the price doesn't reflect the same thing.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth of agency safety
&lt;/h2&gt;

&lt;p&gt;"Going through an agency is safer." That's the number one argument I hear. It deserves a piece-by-piece teardown.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turnover.&lt;/strong&gt; The project manager who sold you the project has a non-trivial chance of having left the agency before delivery. The dev assigned to your project has an even higher chance of being gone within 18 months. You talk to a stable salesperson, but the technical team rotates. When you call for a fix six months after delivery, the new project manager opens your file for the first time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Hidden subcontracting.&lt;/strong&gt; In 2023 I took over a Symfony project delivered by a Paris agency for €47,000. The code came from a Ukrainian freelancer (excellent, by the way) billed €8,000. The client had never heard of him. When the dev ended up in Lviv in 2022, the agency had nobody left who could maintain the project. Cost to take it back: another €12,000, because documentation was non-existent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Inflated quotes.&lt;/strong&gt; An agency bills "man-days" at €800-1,200. A senior freelancer bills between €500 and €700. The difference doesn't go into the code — it pays the structure. That's not illegitimate, but it's rarely explicit. And on projects where the structure adds nothing (a brochure site, a single-developer app), it's money that produces nothing.&lt;/p&gt;

&lt;p&gt;"Agency safety" exists — for very large multi-channel projects. For a standard SME web project, it's essentially a marketing argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  The myth of the fragile freelancer
&lt;/h2&gt;

&lt;p&gt;The symmetrical argument is just as persistent: "if I go with a freelancer, I depend on one person. If he has an accident, I lose everything." True in the abstract, false in practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Continuity.&lt;/strong&gt; A senior freelancer established in Besançon for ten years has a network. If I'm unavailable, I have three peers technically able to take over — and I formalize it. It's rarer than at an agency to see this written down, but it's doable and it's my case. Always ask for the continuity clause in the contract.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Code ownership.&lt;/strong&gt; With an honest freelancer, you own the code upon delivery. No license, no captive SaaS, no proprietary WordPress theme nobody else can modify. Accessible Git, minimal documentation, hosting accounts in your name. With an agency, you're statistically more likely to end up locked into an ecosystem whose exit costs a fortune.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Maintenance contract.&lt;/strong&gt; A freelancer can absolutely commit to an SLA. Response within 24 business hours, intervention within 48 hours for emergencies, monthly updates. Flat fee €80 to €250 excl. tax/month depending on criticality. That covers 90% of an SME's real needs — far better than a "premium agency package" at €600/month that only pays for a generic hotline.&lt;/p&gt;

&lt;h2&gt;
  
  
  When choosing an agency still makes sense
&lt;/h2&gt;

&lt;p&gt;I have to be honest: there are cases where the agency is objectively the right call. If one of these criteria applies to you, skip the rest of this article — call a serious agency.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Integrated marketing need.&lt;/strong&gt; You want a website + SEO strategy + editorial content + Meta/Google ads + community management — all coordinated by one team. It's doable by assembling several freelancers, but an agency does it natively.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Budget above €30,000.&lt;/strong&gt; At that level, the project justifies a real multi-disciplinary team. Agency margin becomes relatively small compared to the value produced. The risk inverts: a single freelancer can't keep the pace.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Complex multi-channel.&lt;/strong&gt; Website + native iOS/Android mobile app + back-office portal + multiple integrations. Too many specialties to coordinate for a single freelancer. Either you assemble a team, or you take an agency.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Heavy procurement procedures&lt;/strong&gt; (public sector, large groups). Many IT departments require a supplier with X years of existence, Y staff, ISO certifications. A freelancer doesn't pass the administrative filter, even when technically much stronger.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When a senior freelancer makes the difference
&lt;/h2&gt;

&lt;p&gt;Conversely, here are the cases where a well-chosen senior freelancer statistically buries the agency option — in quality, cost, and timeline.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Complex tech&lt;/strong&gt; with a modern stack (Go, Rust, Symfony 7, Vue 3, event-driven architecture). Local agencies rarely staff seniors on these stacks — they do WordPress or Webflow and subcontract the rest. A specialized senior freelancer codes better and faster, because that's all he's done for 10 years.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Custom business app.&lt;/strong&gt; Lightweight ERP, custom CRM, internal management tool, customer portal. These projects need deep business understanding and short iterations. A freelancer talks directly to the decision-maker, without the project manager layer filtering information.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Regulatory compliance&lt;/strong&gt; (GDPR, AMF finance, healthcare HDS). The legal responsibility granularity works better with a single named contact. See &lt;a href="https://www.web-developpeur.com/consulting/" rel="noopener noreferrer"&gt;my consulting missions&lt;/a&gt; on these topics.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Long-term partnership&lt;/strong&gt; (3-5 years and more). The same person on the codebase for five years is worth gold. No turnover, no relearning, no documentation that goes stale because nobody has read it since 2022.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Stuck project recovery.&lt;/strong&gt; When a project is in the wall, what you need is a senior dev diving into the code, not a kickoff meeting with four different profiles.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  5-question decision grid
&lt;/h2&gt;

&lt;p&gt;When a prospect hesitates between agency and freelance, I ask these five questions. If the majority points to freelance, that's probably the right call.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;strong&gt;Project volume.&lt;/strong&gt; Less than €25,000 total? Freelance. More than €35,000 with multiple components? Agency or assembled team.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Technical qualification of the need.&lt;/strong&gt; Stack identified and stable (PHP, JS, or modern variant)? Senior freelancer. Cross-functional design + dev + marketing need? Agency.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Level of partnership.&lt;/strong&gt; You want a partner who understands your business over time? Freelance. You want a contracted deliverable with a single entry point? Agency.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Continuity guarantee.&lt;/strong&gt; Ask in writing who codes, where, and what happens if that person disappears. If the agency dodges, that's a red flag. A freelancer who has a backup peer planned in case of unavailability is more reassuring than ten org-chart slides.&lt;/li&gt;
&lt;li&gt; &lt;strong&gt;Pricing transparency.&lt;/strong&gt; Ask for a day-by-day breakdown. If the agency bills 60 days without detail, ask yourself how much ends up with the dev who codes, and how much ends in margin. See my &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;public price grid&lt;/a&gt; for comparison.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  And in Besançon? The reality of the local landscape
&lt;/h2&gt;

&lt;p&gt;Let's be clear without naming anyone — Besançon is a beautiful city of 116,000 inhabitants, but the local digital landscape is modest. There are a few serious agencies, a number of WordPress integrators calling themselves "agency", and many freelancers of varied profiles.&lt;/p&gt;

&lt;p&gt;Senior agencies — those staffing architects, experienced full-stack devs, DevOps — are rare in Bourgogne-Franche-Comté. Most ambitious projects end up in Lyon, Paris, or Geneva. The result is paradoxical: in Besançon, many SMEs pay regional agency rates for subcontracted WordPress production, when a local senior freelancer would deliver better for less.&lt;/p&gt;

&lt;p&gt;This isn't a critique of local agencies — it's a market reality. The Besançon area doesn't have the critical mass to staff several senior agencies. For simple needs (WordPress brochure site, territorial communication), local integrators do the job well. For the rest, the gap between what's billed and what's delivered can be uncomfortable.&lt;/p&gt;

&lt;p&gt;If you're looking for custom work in Besançon, look first to &lt;a href="https://www.web-developpeur.com/developpeur-freelance-besancon/" rel="noopener noreferrer"&gt;local senior freelancers&lt;/a&gt;, then widen to the whole &lt;a href="https://www.web-developpeur.com/developpeur-bourgogne-franche-comte/" rel="noopener noreferrer"&gt;Bourgogne-Franche-Comté region&lt;/a&gt;, and only then consider national agencies. That's the reverse of the usual reflex, but it produces the best quality/price ratio in 70% of cases.&lt;/p&gt;

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

&lt;p&gt;The "agency vs freelance" debate is really an "opacity vs transparency" debate. An agency isn't bad by nature, a freelancer isn't good by nature. What matters is &lt;em&gt;who codes, at what level, with what continuity commitment, and how much margin sits between you and the keyboard&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For a strategic SME project under €30,000 in Besançon, in 80% of cases, a local senior freelancer brings more value than a regional agency. For a multi-channel project at €60,000+, the agency (or assembled team) regains the advantage. Between the two, the question to ask is: &lt;em&gt;how many effectively coded man-days do I get for my budget?&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're thinking through a project and want an honest opinion on the right format, we can &lt;a href="https://www.web-developpeur.com/creation-site-web/" rel="noopener noreferrer"&gt;talk it through&lt;/a&gt;. I give a frank assessment, even when the answer is "for this specific need, take an agency". It happens more often than people think — and that's what makes the conversation useful.&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>agency</category>
      <category>besancon</category>
      <category>comparison</category>
    </item>
    <item>
      <title>How much does a website cost in 2026?</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Wed, 24 Jun 2026 09:00:02 +0000</pubDate>
      <link>https://dev.to/ohugonnot/how-much-does-a-website-cost-in-2026-5ade</link>
      <guid>https://dev.to/ohugonnot/how-much-does-a-website-cost-in-2026-5ade</guid>
      <description>&lt;p&gt;"So, how much for a website?"&lt;/p&gt;

&lt;p&gt;The question lands like it's a butcher's stand. The guy across the table — director of a 12-person metallurgy SME near Besançon — drops it in the first two minutes of the meeting. He's already received three quotes. Numbers range from €800 to €18,000. For the "same" site, he says.&lt;/p&gt;

&lt;p&gt;There's no good price without context, and that's exactly why directors get suspicious. &lt;strong&gt;This article is what I wish I could have handed him that day.&lt;/strong&gt; Real price ranges for 2026, by site type, with the typical traps and the checklist of what should appear on a serious quote. No invented numbers — these are what I charge, what my competitors charge, and what local agencies charge.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody answers the question directly
&lt;/h2&gt;

&lt;p&gt;Imagine asking an architect: "how much for a house?". They'd ask about square footage, region, land, materials, foundation, finish level, decoration. You'd find that normal. For a website, it's exactly the same — but because it's intangible, people feel it should be guessable.&lt;/p&gt;

&lt;p&gt;Five invisible factors swing the price by 10x:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Number of pages&lt;/strong&gt;. A 5-page site isn't 5x cheaper than a 25-page one — but it's still 2-3x cheaper.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Content&lt;/strong&gt;. Do you provide the copy and photos, or does the vendor produce them? A pro photographer is €600-1200 per day. A web copywriter is €80-150 per page.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Visual customization&lt;/strong&gt;. 100% custom design or adapted template? Custom costs 2-4x more, but stops your site looking like 200,000 others.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Hidden features&lt;/strong&gt;. Multilingual, GDPR, CRM integration, advanced quote forms, customer portal, price calculators… Each line item multiplies the cost.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Post-delivery autonomy&lt;/strong&gt;. Can you edit the site yourself? That comfort is paid for — a custom back-office is 30-50% of dev time.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When a vendor gives a "ballpark" price without asking these questions, either they're a salesperson throwing a number to hook you, or they have a product so standardized they already know what they'll deliver. Both cases warrant digging before signing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Real ranges in 2026, by site type
&lt;/h2&gt;

&lt;p&gt;Three main categories cover 95% of demand. Here are the real French market ranges, in 2026, excluding premium Paris agencies (who add 30-50% in brand premium).&lt;/p&gt;

&lt;h3&gt;
  
  
  Brochure site — €1,500 to €8,000
&lt;/h3&gt;

&lt;p&gt;The classic: present your business, services, team, capture leads via a contact form. 5 to 15 pages. Mobile responsive mandatory. Basic SEO optimization.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;€1,500 – €3,000&lt;/strong&gt; — simple brochure site, clean design, 5-8 pages. For craftspeople, liberal professions, associations. This is my &lt;a href="https://www.web-developpeur.com/site-vitrine-pme/" rel="noopener noreferrer"&gt;Starter SME package&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€3,000 – €5,500&lt;/strong&gt; — site with animations, blog/news, basic multilingual, advanced SEO. For established SMEs, upscale restaurants, consulting firms.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€5,500 – €8,000&lt;/strong&gt; — "brand image" site: 15-25 pages, 100% custom design, illustrations, integrated pro photography, member or candidate portal. For 20+ employee companies.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  E-commerce site — €4,000 to €25,000
&lt;/h3&gt;

&lt;p&gt;You sell online: product catalog, cart, payment, order management, invoices. Highly variable depending on product volume and business rule complexity (promotions, subscriptions, shipping, multi-country…).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;€4,000 – €7,000&lt;/strong&gt; — simple shop, 20-100 products, Stripe payment, France delivery. For craft makers, winemakers, small brands.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€7,000 – €15,000&lt;/strong&gt; — wider catalog, multi-warehouse stock, accounting integration (Sage, EBP), B2B/B2C pricing, multilingual. For established brand, wholesale.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€15,000 – €25,000&lt;/strong&gt; — marketplace, recurring subscriptions, complex product configurator, integrated ERP, multi-country with country-specific VAT. For scale-up or group.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Custom business app — €8,000 to €100,000+
&lt;/h3&gt;

&lt;p&gt;Not a site, not really. An internal tool that solves a specific business problem: caregiver scheduling, construction site tracking, B2B customer portal with file access, automatic quote generator, integration between 4 systems that don't talk to each other.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;€8,000 – €20,000&lt;/strong&gt; — simple app solving a targeted problem. What I see most often in regional industrial SMEs.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€20,000 – €50,000&lt;/strong&gt; — app with multiple user roles, approval workflows, accounting exports, mobile.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;€50,000+&lt;/strong&gt; — full system, several developers, design system, automated tests. That's software publishing, not "website" territory anymore.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full detail and JSON-LD offers on the &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;pricing page&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 4 traps that blow up the budget
&lt;/h2&gt;

&lt;p&gt;The initial quote is rarely the final cost. Four recurring traps deserve to be named explicitly before signing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 1 — The "little addition" mid-project
&lt;/h3&gt;

&lt;p&gt;"Can we just add a page for our news?". "Can you add a calendar to book appointments?". Each "small addition" that wasn't in the initial quote is a renegotiation. Not an upward renegotiation because the vendor is dishonest — a renegotiation because they sell workdays, and those days were committed elsewhere.&lt;/p&gt;

&lt;p&gt;Solution: make an exhaustive list of what you want &lt;em&gt;before&lt;/em&gt; signing, distinguishing "must-have" from "nice-to-have". Anything requested post-delivery will be billed by the day.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 2 — Content not delivered on time
&lt;/h3&gt;

&lt;p&gt;You sign in January, delivery promised for March. The vendor starts design in February, needs your content (text, photos, HD logo, legal mentions) by the 15th. You provide it on March 30th because you had other priorities. The project slips by 6 weeks. The vendor takes another mission in the meantime. When they come back to your project, they need to reload context — billable time.&lt;/p&gt;

&lt;p&gt;Solution: prepare the content &lt;em&gt;before&lt;/em&gt; launching the project. Better: commission copywriting and pro photography at signing, in parallel with dev.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 3 — "Free hosting first year"
&lt;/h3&gt;

&lt;p&gt;Many vendors offer free hosting the first year — nice. But year 2, you discover this hosting costs €40/month (€480/year), or that migrating elsewhere costs €800 because the code is "optimized" for their infrastructure. Read the terms.&lt;/p&gt;

&lt;p&gt;Solution: request standard hosting (OVH, Scaleway, Hetzner) that you pay directly to the host. Real cost: €5-20/month for 95% of sites. Zero lock-in.&lt;/p&gt;

&lt;h3&gt;
  
  
  Trap 4 — Maintenance that becomes a rent
&lt;/h3&gt;

&lt;p&gt;A website needs regular security updates (CMS, plugins, language). Many vendors sell a maintenance contract at €100-300/month. Over 5 years, that's €6,000 to €18,000 — often as much as the site itself.&lt;/p&gt;

&lt;p&gt;Solution: for a custom site without third-party plugins, real maintenance is near-zero. For a WordPress site, €30-50/month is plenty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freelance, agency, DIY: the honest comparison
&lt;/h2&gt;

&lt;p&gt;For the same brief, the three main routes give different prices — and a different experience.&lt;/p&gt;

&lt;p&gt;Option&lt;/p&gt;

&lt;p&gt;Range&lt;/p&gt;

&lt;p&gt;When it's the right pick&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DIY (Wix, Squarespace)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;€0-50/month&lt;/p&gt;

&lt;p&gt;Test site, no-budget association, you have time&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Platform + template (Shopify, WordPress+Elementor)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;€500-2,000 setup + €30-80/month&lt;/p&gt;

&lt;p&gt;Simple e-commerce, you drive yourself&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Senior freelancer&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;€1,500-25,000&lt;/p&gt;

&lt;p&gt;Custom, direct relationship, no lock-in&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Local agency (5-15 people)&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;€4,000-50,000&lt;/p&gt;

&lt;p&gt;You want a structured contact, multi-disciplinary team (design + dev + SEO)&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Premium Paris agency&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;€20,000-200,000&lt;/p&gt;

&lt;p&gt;Premium brand, international image&lt;/p&gt;

&lt;p&gt;My obvious bias: I'm a senior freelancer. But I honestly recommend DIY or Shopify to directors who don't have the budget — a bad custom site is worth less than a good Shopify. And I point toward agencies those who need design+SEO+marketing coordination bundled.&lt;/p&gt;

&lt;h2&gt;
  
  
  The ideal quote — what should appear in it
&lt;/h2&gt;

&lt;p&gt;A serious quote runs 3-5 pages. A three-line email with "€4,200" is a trap. Here's what should appear mandatorily:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Detailed functional scope&lt;/strong&gt; — each page, each feature listed. If it's not in the quote, it's not delivered at the quoted price.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Tech stack&lt;/strong&gt; — PHP, WordPress, Vue.js, Symfony… You should be able to continue with another dev later.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Milestone-based planning&lt;/strong&gt; — not a single delivery date, but 3-5 intermediate stages (mockups validated, integration done, dev complete, UAT, go-live).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Payment terms&lt;/strong&gt; — typically 30% deposit / 40% mid-project / 30% at delivery.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Post-delivery bug warranty&lt;/strong&gt; — 30 to 90 days minimum.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Code ownership and access&lt;/strong&gt; — you get the source code, server access, domain accounts.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Recurring side costs&lt;/strong&gt; — hosting, domain, SSL certificate, third-party services (Stripe, Mailchimp).&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Off-quote modification terms&lt;/strong&gt; — precise daily rate for additions.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these is missing, ask for it in writing. The vendor's reaction to that request already tells you about the collaboration to come.&lt;/p&gt;

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

&lt;p&gt;The real cost of a website isn't the quote. It's the cost of the wrong choice: a site redone three years later, missing features that lose customers, a vendor who disappears with unrecoverable code.&lt;/p&gt;

&lt;p&gt;An €1,800 site that lasts 8 years costs less than an €800 site redone three times in 8 years. The question isn't "how much do I spend today", it's "how much do I spend over 10 years, and what will this site earn me during that time".&lt;/p&gt;

&lt;p&gt;To dig deeper based on your specific case, look at the &lt;a href="https://www.web-developpeur.com/creation-site-web/" rel="noopener noreferrer"&gt;available site formats&lt;/a&gt;, the &lt;a href="https://www.web-developpeur.com/site-vitrine-pme/" rel="noopener noreferrer"&gt;SME packages&lt;/a&gt;, or the &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;full pricing grid&lt;/a&gt;. And if you're based in &lt;a href="https://www.web-developpeur.com/developpeur-suisse-romande/" rel="noopener noreferrer"&gt;French-speaking Switzerland&lt;/a&gt; or eastern France, we can meet physically before signing anything.&lt;/p&gt;

</description>
      <category>websitecreation</category>
      <category>pricing</category>
      <category>freelance</category>
      <category>sme</category>
    </item>
    <item>
      <title>Local freelancer vs offshore: what nobody tells you about real cost</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Tue, 23 Jun 2026 09:00:01 +0000</pubDate>
      <link>https://dev.to/ohugonnot/local-freelancer-vs-offshore-what-nobody-tells-you-about-real-cost-2bkm</link>
      <guid>https://dev.to/ohugonnot/local-freelancer-vs-offshore-what-nobody-tells-you-about-real-cost-2bkm</guid>
      <description>&lt;p&gt;The director hangs up, a little dazed. His offshore dev, based in Manila, hasn't replied for three days. The "Pay" button of his e-commerce site has been returning a 500 error since Monday morning, right in the middle of a newsletter campaign. Revenue is dropping at €4,200/day in lost sales. He's calling me because we crossed paths at the Besançon Chamber of Commerce six months ago.&lt;/p&gt;

&lt;p&gt;I take over the project that afternoon. The offshore dev — serious, competent — had just been hospitalized. Nobody knew. No backup, no team, no documentation, source code in his personal Bitbucket that only he could access.&lt;/p&gt;

&lt;p&gt;This article isn't an attack on offshore developers. &lt;strong&gt;Many are technically excellent, and some are among the best in their specialty.&lt;/strong&gt; But the "offshore day rate × days = savings" math misses three-quarters of the real cost. Here's what's hiding behind it, and when local is objectively the right call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calculation everyone makes, and why it's wrong
&lt;/h2&gt;

&lt;p&gt;The offshore pitch fits on one line: "senior dev at €250/day instead of €600 in France". On a 60-day project, that's €21,000 instead of €36,000. €15,000 net savings. Any healthy director would be stupid to refuse on paper.&lt;/p&gt;

&lt;p&gt;The problem is that the cost of a dev project is never "day rate × days". It's:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  Day rate × actual dev days&lt;/li&gt;
&lt;li&gt;  + Coordination cost (your time, your team, your PO)&lt;/li&gt;
&lt;li&gt;  + Cost of variable quality (production bugs, refactoring, tech debt)&lt;/li&gt;
&lt;li&gt;  + Opportunity cost (delays that push back a go-to-market)&lt;/li&gt;
&lt;li&gt;  + Handover / end-of-mission cost (recovering code, brief, accounts)&lt;/li&gt;
&lt;li&gt;  + Legal cost (in case of dispute)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On the last five items, offshore starts with a structural handicap — not a fatality, but a statistical reality worth weighing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Time zones — the most underestimated argument
&lt;/h2&gt;

&lt;p&gt;Manila is UTC+8 — 7 hours ahead of Paris. Mumbai, UTC+5:30 — 4h30. Casablanca, UTC+1, barely 1h. Tunis, UTC+1 too. Buenos Aires, UTC-3.&lt;/p&gt;

&lt;p&gt;A dev in Manila working 9am-6pm local is reachable 2am-11am French time. Concretely, you send them a question at 2pm from Besançon, they read it at 7am local the next day, reply before 11am local (so before 4am your time). You read the reply at 9am. If the reply raises a new question, you respond. They read it at 6am local the day after that.&lt;/p&gt;

&lt;p&gt;Three back-and-forths = three days. During which the project doesn't move — it waits for the next question, the next answer. Multiplied by the hundreds of exchanges in a typical web project, the cumulative delay adds up to weeks.&lt;/p&gt;

&lt;p&gt;A dev in the same timezone can do three iterations in the day. The project moves three times faster, at equivalent day rate once normalized to effective hourly productivity. That's what I've experienced for 14 years of French remote work for European clients.&lt;/p&gt;

&lt;h2&gt;
  
  
  Business semantics — the argument nobody invoices
&lt;/h2&gt;

&lt;p&gt;You explain your business to your dev. It's biodynamic viticulture in the Jura. You speak of "millésime", "cuvée parcellaire", "SO2", "bio-coherence", "demeter biodynamics", "living vintages". Your Paris dev gets half of it, your Pontarlier-bordering dev gets all of it, your Lahore dev asks for definitions on every word.&lt;/p&gt;

&lt;p&gt;Not because they're less competent. Because French business vocabulary translates poorly and lacks direct equivalents in business English. Translation takes time, and every translation is an opportunity for misunderstanding. You think you explained "SO2", they understood "sulphur level" — not quite the same thing.&lt;/p&gt;

&lt;p&gt;Multiplied by specific regulatory compliance (AMF in fintech, ARS in health, GDPR everywhere), standard French T&amp;amp;Cs, French VAT, supplier slips, URSSAF, Sage/EBP invoicing, the business semantics of a local dev is a huge silent asset. Nobody invoices it, but it's worth weeks of coordination saved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Legal cost — the great unmentioned
&lt;/h2&gt;

&lt;p&gt;In case of dispute with an offshore freelancer, your practical recourse is limited. The competent commercial court is in Casablanca, Manila or Buenos Aires. The contract is in English (at best). T&amp;amp;Cs were written by your vendor and weren't reviewed by your lawyer. Source code is on private GitHub whose access depends on the dev's goodwill. If the project is poorly delivered, you pay a local lawyer to recover €12,000 — it's rarely worth it.&lt;/p&gt;

&lt;p&gt;With a French freelancer, you have the commercial court of your department (Besançon for mine), standard French T&amp;amp;Cs, a classic VAT invoice, a SIRET verifiable on societe.com. None of these protect you 100%, but they set the litigation barriers at the right level. Most conflicts settle amicably — because both parties know going to court would be possible.&lt;/p&gt;

&lt;p&gt;For sensitive missions (fintech, health, personal data), it's even a prerequisite. My &lt;a href="https://www.web-developpeur.com/developpeur-suisse-romande/" rel="noopener noreferrer"&gt;AMF fintech experience&lt;/a&gt; at Goin Invest over 4 years is usable for Swiss LSFin/FinSA work only because I've kept European legal traceability.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cultural argument no one dares state anymore
&lt;/h2&gt;

&lt;p&gt;Working with someone whose professional culture you understand is a quality factor — not chauvinism. When your French dev says "next week is going to be tight", you know it means "it won't be delivered next week". When a South Asian dev says the same translated sentence, you don't know if it's "tight but it'll happen" or "I don't want to tell you no head-on, so I say tight".&lt;/p&gt;

&lt;p&gt;Not better or worse. Different. Implicit communication codes, hierarchical relationships, how disagreement is handled, expressing uncertainty — all of this impacts coordination quality. With a culturally close dev, you save considerable re-interpretation time.&lt;/p&gt;

&lt;p&gt;Large offshore IT firms (TCS, Infosys, Cognizant) have structured layers of "onshore project managers" precisely to absorb this friction. It works, but it costs money — and it erases the initial price argument.&lt;/p&gt;

&lt;h2&gt;
  
  
  When offshore is the right call
&lt;/h2&gt;

&lt;p&gt;Honesty demands: offshore is sometimes &lt;em&gt;the right call&lt;/em&gt;. Here are the cases where I recommend it to my prospects without hesitation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Ultra-scoped mission&lt;/strong&gt;, detailed written specs, few iterations expected. Time-zone becomes an advantage: while you sleep, the code builds.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Niche skill unavailable locally&lt;/strong&gt; — a Solidity expert, a Three.js specialist, an ARM embedded Rust dev. The French market is small, offshore gives access to a global pool.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Budget incompatible with local market&lt;/strong&gt;. If you have €4,000 for a project that needs 60 dev days, offshore is the only honest option. Better a completed project from Manila than a never-delivered project from Besançon.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Evolutionary maintenance on a stable project&lt;/strong&gt; — small additions, bug fixes, minor integrations. Hourly cost becomes decisive, coordination is light.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When local is objectively superior
&lt;/h2&gt;

&lt;p&gt;Conversely, I discourage offshore in these cases — where the apparent local overcost is massively offset on the total:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;Strategic structural project&lt;/strong&gt; defining your business for 5+ years. The cost of the wrong choice exceeds the offshore margin.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Regulated industry&lt;/strong&gt; (health, finance, legal, personal data). French and European compliance is hard to carry remotely.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Frequent iterations&lt;/strong&gt; with a non-tech product owner. You'll need 30 micro-decisions per week. Cumulative delay kills the project.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Dense business semantics&lt;/strong&gt; (viticulture, construction, public sector, specialized liberal professions). Too much untranslatable vocabulary.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Regular physical coordination&lt;/strong&gt; needed (site visits, field demos, team workshops).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For these cases, a senior local freelancer costs 30-50% more at day rate, but the project ships faster, better, with less productivity loss on the client side. The total balance often favors local.&lt;/p&gt;

&lt;h2&gt;
  
  
  The right decoupling: not binary
&lt;/h2&gt;

&lt;p&gt;The real answer in 2026 isn't "local OR offshore". It's "how do I slice my project to put each task in the right hands".&lt;/p&gt;

&lt;p&gt;On a typical e-commerce project: design system and architecture locally (need fast iterations with the business), template integration offshore (scoped, few iterations), evolutionary maintenance offshore (hourly cost critical), strategic developments locally (sensitive deployments, business logic rewrites).&lt;/p&gt;

&lt;p&gt;This hybrid strategy requires a local lead — someone who speaks both languages, can brief offshore in precise specs, can own end-to-end delivery. It's a role I regularly play, because I'm both a senior dev and capable of coordinating a distributed team.&lt;/p&gt;

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

&lt;p&gt;The "local vs offshore" debate is mis-framed. The right question is: "who's the right partner for this specific mission, given its stakes, duration, business and legal sensitivity?"&lt;/p&gt;

&lt;p&gt;For 70% of French SME projects, the answer is: a senior local or regional freelancer, with offshore reinforcement on scoped tasks if needed. Not chauvinism — total cost of ownership math over 5 years.&lt;/p&gt;

&lt;p&gt;If you're in eastern France or &lt;a href="https://www.web-developpeur.com/developpeur-suisse-romande/" rel="noopener noreferrer"&gt;French-speaking Switzerland&lt;/a&gt;, we can meet physically. Otherwise see the &lt;a href="https://www.web-developpeur.com/creation-site-web/" rel="noopener noreferrer"&gt;available formats&lt;/a&gt; and the &lt;a href="https://www.web-developpeur.com/tarifs/" rel="noopener noreferrer"&gt;pricing grid&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>freelance</category>
      <category>offshore</category>
      <category>sme</category>
      <category>besancon</category>
    </item>
    <item>
      <title>Booster Leboncoin: the Manifest V3 Chrome extension that bumps my ads and watches prospects for me</title>
      <dc:creator>Odilon HUGONNOT</dc:creator>
      <pubDate>Mon, 22 Jun 2026 09:00:06 +0000</pubDate>
      <link>https://dev.to/ohugonnot/booster-leboncoin-the-manifest-v3-chrome-extension-that-bumps-my-ads-and-watches-prospects-for-me-1k09</link>
      <guid>https://dev.to/ohugonnot/booster-leboncoin-the-manifest-v3-chrome-extension-that-bumps-my-ads-and-watches-prospects-for-me-1k09</guid>
      <description>&lt;p&gt;I have seven active ads on Leboncoin (France's Craigslist): IT support, web dev, WordPress hosting, retrogaming, e-waste pickup. All relevant for my area, all invisible after day three. On Leboncoin, without a paid sub, the only way to climb back to the top is to &lt;strong&gt;delete and republish&lt;/strong&gt; each ad. By hand. One by one. Every week. With photo re-upload.&lt;/p&gt;

&lt;p&gt;After three Sunday mornings of doing this between two coffees, I figured out I'd rather code an extension I might never ship than do the chore one more time.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/ohugonnot/leboncoin-bumper" rel="noopener noreferrer"&gt;&lt;strong&gt;The repo: ohugonnot/leboncoin-bumper&lt;/strong&gt;&lt;/a&gt; — Manifest V3, zero deps, zero servers, MIT.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Chrome extension and not a Node script
&lt;/h2&gt;

&lt;p&gt;First option I tried: a Node script with Puppeteer that logs in with my account. I gave up after two evenings. Three concrete reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;  &lt;strong&gt;DataDome&lt;/strong&gt;. Leboncoin's anti-bot flags a headless browser within a handful of requests. A real browser, with my real session, my real cookies, my real user-agent — passes through.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Authentication&lt;/strong&gt;. I log in via Google. Reproducing that flow in Puppeteer means JWT scraping that breaks on every rotation. Reusing the browser's session via &lt;code&gt;chrome.scripting.executeScript&lt;/code&gt; is free.&lt;/li&gt;
&lt;li&gt;  &lt;strong&gt;Deployment&lt;/strong&gt;. An unpacked extension starts when I open Chrome. No server to maintain. No cron. No docker. The day I switch machines, I clone the repo and toggle developer mode.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The verdict was obvious after two hours of prototyping: the extension wins on every axis for strictly personal use.&lt;/p&gt;

&lt;h2&gt;
  
  
  Manifest V3 — the service worker that dies every 30 seconds
&lt;/h2&gt;

&lt;p&gt;MV3 has a trap nobody warns you about before you hit it: &lt;strong&gt;the service worker is killed when it goes idle&lt;/strong&gt;. No long-running daemon. No &lt;code&gt;setInterval&lt;/code&gt; that survives. To schedule anything, you need &lt;code&gt;chrome.alarms&lt;/code&gt;, which wakes the worker at the right time.&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="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;runtime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onInstalled&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bump-weekly&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="na"&gt;when&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;nextBumpSlot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;       &lt;span class="c1"&gt;// timestamp ms&lt;/span&gt;
    &lt;span class="na"&gt;periodInMinutes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;alarms&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onAlarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;addListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alarm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;alarm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bump-weekly&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;await&lt;/span&gt; &lt;span class="nf"&gt;runBumpCycle&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;        &lt;span class="c1"&gt;// opens a tab, scrapes, deletes, reposts&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Practical consequence: &lt;strong&gt;no in-memory state survives between two wake-ups&lt;/strong&gt;. Everything goes through &lt;code&gt;chrome.storage.local&lt;/code&gt; (watch profiles, ads already seen, cycle history, reply template). It's IndexedDB for lazy people — you write JSON, you read JSON, that's it.&lt;/p&gt;

&lt;p&gt;The lesson it took me three days to internalize: &lt;em&gt;never assume the service worker is running&lt;/em&gt;. Every piece of synchronous logic must be resumable from storage. It's painful for the first few hours, and trivial after.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bumper — driving a real tab in the background
&lt;/h2&gt;

&lt;p&gt;The cycle: &lt;em&gt;fetch active ads → pick one → scrape it in detail (title, description, price, location, photos, contact preferences) → delete it → open the new-ad wizard → fill it → re-upload photos → submit&lt;/em&gt;. Seven steps per ad, across five different pages of the Leboncoin back-office.&lt;/p&gt;

&lt;p&gt;It all rests on two MV3 primitives:&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;// 1. Open a tab and run arbitrary code in it&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AD_LIST_URL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;ads&lt;/span&gt; &lt;span class="p"&gt;}]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeScript&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;func&lt;/span&gt;&lt;span class="p"&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="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;[data-test-id="ad-card"]&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="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;card&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelector&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;h3&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;textContent&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;dataset&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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="c1"&gt;// 2. Navigate and re-inject at each step&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tabs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;EDIT_URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;waitForLoad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeScript&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="na"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;fillFormStep1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DOM selectors are the fragile zone. Every Leboncoin redesign, I rewire them. I made it a rule to centralize all selectors in a single &lt;code&gt;selectors.js&lt;/code&gt; file, with a comment about the last audit date. When someone opens an issue because it broke, I know what to grep.&lt;/p&gt;

&lt;p&gt;The detail that cost me an hour: photo upload. Leboncoin expects a &lt;code&gt;File&lt;/code&gt; object in the &lt;code&gt;&amp;lt;input type="file"&amp;gt;&lt;/code&gt;. Except you can't create a &lt;code&gt;File&lt;/code&gt; from an image URL in pure JS — you have to &lt;code&gt;fetch()&lt;/code&gt; the CDN URL, grab the &lt;code&gt;Blob&lt;/code&gt;, wrap it in a &lt;code&gt;File&lt;/code&gt;, then use &lt;code&gt;DataTransfer&lt;/code&gt; to plant it in the input:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;blob&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;photoUrl&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blob&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;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;File&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="nx"&gt;blob&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;photo.jpg&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="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;image/jpeg&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;dt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;DataTransfer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;file&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;input&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dispatchEvent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&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="na"&gt;bubbles&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I found the &lt;code&gt;DataTransfer&lt;/code&gt; trick in a 2019 Stack Overflow answer with 23 upvotes. Without it, the input stays empty and Leboncoin refuses the publish. The kind of thing no LLM suggests on first ask because the official docs don't cover it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prospect Watch — the private API everyone pretends not to see
&lt;/h2&gt;

&lt;p&gt;The watch part is what made me actually write the extension. The bumper is useful but boring. Prospect watch is what turns the tool into an opportunity generator.&lt;/p&gt;

&lt;p&gt;Leboncoin has a private JSON API behind its frontend: &lt;code&gt;POST /finder/search&lt;/code&gt;. The same one the frontend calls when you type a search. Clean JSON, stable schema, pagination via &lt;code&gt;limit&lt;/code&gt;/&lt;code&gt;page&lt;/code&gt;. I call it by reusing the browser's session, from an active Leboncoin tab to avoid triggering DataDome:&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;searchAds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filters&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;body&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;keywords&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="s1"&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="na"&gt;category&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;                    &lt;span class="c1"&gt;// Services&lt;/span&gt;
      &lt;span class="na"&gt;location&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;departments&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;depts&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;ranges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;price&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;min&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceMin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;max&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;priceMax&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="na"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;listing_source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;direct-search&lt;/span&gt;&lt;span class="dl"&gt;'&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;chrome&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;scripting&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executeScript&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;tabId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;leboncoinTab&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;func&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/finder/search&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="na"&gt;credentials&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;include&lt;/span&gt;&lt;span class="dl"&gt;'&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;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="na"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(([{&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="p"&gt;}])&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ads&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Once the ads are pulled in, scoring. Three simple rules that work surprisingly well:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; &lt;code&gt;+2 × weight&lt;/code&gt; if the keyword matches in the title&lt;/li&gt;
&lt;li&gt; &lt;code&gt;+1 × weight&lt;/code&gt; if the keyword matches in the description&lt;/li&gt;
&lt;li&gt; &lt;code&gt;+1&lt;/code&gt; if we detect a demand intent: &lt;em&gt;looking for&lt;/em&gt;, &lt;em&gt;seeking&lt;/em&gt;, &lt;em&gt;need&lt;/em&gt;, &lt;em&gt;help&lt;/em&gt;, &lt;em&gt;advice&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Weights come from a readable syntax in the keywords themselves: &lt;code&gt;wordpress:3 prestashop symfony:2&lt;/code&gt;. WordPress weighs 3, PrestaShop weighs 1 by default, Symfony weighs 2. Hovering the star in the popup shows the score breakdown — &lt;em&gt;"wordpress (title, ×3): +6 | demand detected: +1"&lt;/em&gt;. No black box. When a false positive slips through, I see why in a second.&lt;/p&gt;

&lt;p&gt;Demand-vs-offer detection is regex over the first 200 chars. Imperfect but cheap. &lt;em&gt;"Looking for WordPress dev"&lt;/em&gt; matches. &lt;em&gt;"Offering WordPress services"&lt;/em&gt; doesn't. Across 800 ads scanned over three weeks, precision sits around 90 %.&lt;/p&gt;

&lt;h2&gt;
  
  
  The anti-scam filter — nine patterns covering 95 % of scams
&lt;/h2&gt;

&lt;p&gt;Not planned originally. I added it after getting three messages in the same week with the exact same Western Union phrasing. The Leboncoin inbox is a scammer magnet, and the native filter is very low-level.&lt;/p&gt;

&lt;p&gt;I listed every scam I'd received over two years, pulled out nine regex patterns:&lt;/p&gt;

&lt;p&gt;Pattern&lt;/p&gt;

&lt;p&gt;Detected example&lt;/p&gt;

&lt;p&gt;Money order / Western Union&lt;/p&gt;

&lt;p&gt;"I'll pay by money order, give me your full name"&lt;/p&gt;

&lt;p&gt;QR-code payment&lt;/p&gt;

&lt;p&gt;"scan this QR to release the payment"&lt;/p&gt;

&lt;p&gt;Fake carrier&lt;/p&gt;

&lt;p&gt;"my driver will come tomorrow, plan for €35 shipping"&lt;/p&gt;

&lt;p&gt;Off-platform&lt;/p&gt;

&lt;p&gt;"contact me on WhatsApp +33 6..."&lt;/p&gt;

&lt;p&gt;Foreign number&lt;/p&gt;

&lt;p&gt;+44, +234, +1 in the body&lt;/p&gt;

&lt;p&gt;Short external link&lt;/p&gt;

&lt;p&gt;bit.ly, tinyurl, t.co&lt;/p&gt;

&lt;p&gt;PayPal Friends &amp;amp; Family&lt;/p&gt;

&lt;p&gt;"send via friends and family option"&lt;/p&gt;

&lt;p&gt;SMS code requested&lt;/p&gt;

&lt;p&gt;"I'll send a confirmation code, forward it to me"&lt;/p&gt;

&lt;p&gt;Urgency + travel&lt;/p&gt;

&lt;p&gt;"I'm traveling, urgent, my husband/wife will pick up"&lt;/p&gt;

&lt;p&gt;Every incoming message gets classified: 🚨 Scam · 💬 Lead · ❓ Question · 🗑 Spam. The inbox shows real leads first and hides scams under a filter. Three weeks after enabling it, I haven't read a single Western Union message — even though two arrive every week.&lt;/p&gt;

&lt;h2&gt;
  
  
  Native Node tests — zero deps, 120 ms, 35 tests
&lt;/h2&gt;

&lt;p&gt;An extension that does DOM scraping looks untestable. And it's true for the scraping layer: &lt;code&gt;chrome.scripting.executeScript&lt;/code&gt; doesn't mock. But &lt;strong&gt;all the useful logic is plain JS&lt;/strong&gt;, separated from chrome.* — and that part I test with the native Node test runner. No Jest, no Vitest, no Mocha.&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;// tests/scoring.test.js&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:test&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;strict&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;assert&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:assert&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;scoreAd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parseKeywords&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;../lib/scoring.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;keyword in title beats keyword in description&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="o"&gt;=&amp;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;ad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;looking for wordpress dev&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&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;kw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseKeywords&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wordpress&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;scoreAd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2 (title) + 1 (looking for)&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;weighted keyword multiplies score&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="o"&gt;=&amp;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;kw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;parseKeywords&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wordpress:3&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;ad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;wordpress pro&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;description&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="nx"&gt;assert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;equal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;scoreAd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;kw&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// 2 × 3&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;npm test&lt;/code&gt; runs in 120 ms. 35 tests covering keyword regexes (accented characters, &lt;code&gt;C++&lt;/code&gt;, &lt;code&gt;.NET&lt;/code&gt;, parens), v2 scoring, weight parsing, display sorting, post-filters, profile serialization.&lt;/p&gt;

&lt;p&gt;No framework. No transpiler. No config. &lt;code&gt;node --test tests/&lt;/code&gt;. That's the comfort I wanted from a side-project: if I come back to the repo six months from now, I shouldn't have to reinstall anything to run the tests. &lt;em&gt;git pull &amp;amp;&amp;amp; npm test&lt;/em&gt;, done.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why it's not on the Chrome Web Store
&lt;/h2&gt;

&lt;p&gt;Leboncoin's ToS explicitly forbid automation (article 8: &lt;em&gt;"use of any robot, script, or device allowing automated access to the site"&lt;/em&gt;). Submitting a public extension that does exactly that means getting rejected in review, then reported by Leboncoin with the risk of permanent developer account closure.&lt;/p&gt;

&lt;p&gt;So: manual install, public repo on GitHub, MIT, at your own risk. It's stated upfront in the README and it's a condition I'm sticking to — a dev who clones and installs knows what they're doing. A regular user downloading from the Web Store doesn't. The friction is part of the accountability.&lt;/p&gt;

&lt;p&gt;Risk-wise for my account: three months of daily use, zero flag. I stay at human frequencies (one bump per day max, one scan per hour), I use a real tab, I don't go under suspicious thresholds. DataDome seems to watch the &lt;em&gt;pattern&lt;/em&gt; more than the &lt;em&gt;fact&lt;/em&gt; of automation — if you behave like a hurried human rather than a bot, you pass.&lt;/p&gt;

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

&lt;p&gt;What's surprising looking back at the repo after three months is what I &lt;em&gt;didn't&lt;/em&gt; write. No framework. No TypeScript. No build step. No bundler. Vanilla JS, vanilla CSS, MV3, native Node tests. The whole thing fits in ~2,000 lines of code. When I need to add a feature, I re-read the relevant file in under a minute and code straight in.&lt;/p&gt;

&lt;p&gt;The real productivity of a side-project is the absence of tooling. Every dependency I'd have added on day one would have become a maintenance chore the week after. Instead, the extension runs, I forget about it, it pings me when a WordPress prospect posts in my area on a Tuesday at 2 PM. ROI for a tool I built over a weekend: roughly one client a month on average. Largely paid off.&lt;/p&gt;

</description>
      <category>chromeextension</category>
      <category>manifestv3</category>
      <category>leboncoin</category>
      <category>scraping</category>
    </item>
  </channel>
</rss>
