<?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: Vivian Oliveres</title>
    <description>The latest articles on DEV Community by Vivian Oliveres (@vivianoliveres).</description>
    <link>https://dev.to/vivianoliveres</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%2F4137283%2Fa2404a58-4ffc-4d6d-a4e9-da206080fe60.jpg</url>
      <title>DEV Community: Vivian Oliveres</title>
      <link>https://dev.to/vivianoliveres</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vivianoliveres"/>
    <language>en</language>
    <item>
      <title>I built a lead-intent pipeline on 1.75M posts/day for €10/day. It got 1 trial</title>
      <dc:creator>Vivian Oliveres</dc:creator>
      <pubDate>Tue, 22 Sep 2026 09:18:23 +0000</pubDate>
      <link>https://dev.to/vivianoliveres/i-built-a-lead-intent-pipeline-on-175m-postsday-for-eu10day-it-got-1-trial-4p5m</link>
      <guid>https://dev.to/vivianoliveres/i-built-a-lead-intent-pipeline-on-175m-postsday-for-eu10day-it-got-1-trial-4p5m</guid>
      <description>&lt;p&gt;On September 15, I stopped Dwelverson.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Seven months solo on &lt;strong&gt;Dwelverson&lt;/strong&gt;, a B2B lead-intent product: read ~1.75M social posts a day, find the ones where someone is looking to buy what a client sells, deliver them by email and Slack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The pipeline works.&lt;/strong&gt; 14 steps: keyword/embeddings filters → DeBERTa gates → a self-hosted Qwen3.5-35B → SPLADE retrieval → a chain-of-thought judge on GLM-5.2. Recall and precision measured every night against Claude Opus labels.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;~€10 a day&lt;/strong&gt;, where sending every post to Opus would have cost ~€100k a month.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The business doesn't work.&lt;/strong&gt; Reddit is 75% of the leads, and a new Reddit account can't reply to anything (hidden quality score). ~800 cold emails produced 1 trial, and Microsoft sent 100% of them to spam.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All the code was written by AI&lt;/strong&gt;: ~339,000 lines, ~6,800 tests, and not one line read by me.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Seven months of work, from February to September 2026. 1,883 commits. A pipeline reading ~1.75M social posts a day and finding a few hundred real buying signals every night, for about €10 a day. A web app with billing, email and Slack delivery. And a sales result that fits in one line: ~800 cold emails, 1 trial, 0 paying clients.&lt;/p&gt;

&lt;p&gt;This post is the full picture. The technical part first, because that's what worked. Then the business part, because that's what didn't.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Dwelverson Did
&lt;/h1&gt;

&lt;p&gt;Dwelverson was a B2B &lt;a href="https://en.wikipedia.org/wiki/Lead_generation" rel="noopener noreferrer"&gt;lead generation&lt;/a&gt; product based on intent. A client, typically a small software company, described what it sells. Dwelverson watched Bluesky, X/Twitter, Hacker News, Reddit and a set of Discourse/Khoros community forums, and found the posts where someone was actively looking for that kind of product. If a client sells a helpdesk tool, a Reddit post saying "we're 12 people drowning in support emails, what do you all use?" is a lead. The client received those posts by email and Slack, with a link to go and answer.&lt;/p&gt;

&lt;p&gt;The whole product came down to one definition of a &lt;strong&gt;True Lead&lt;/strong&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A post where the author is &lt;strong&gt;seeking&lt;/strong&gt;, AND the need &lt;strong&gt;matches&lt;/strong&gt; what the client sells.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Both halves are required. Someone promoting their own CRM writes a post that is 100% relevant to a CRM vendor, and it's not a lead. Neither is a thought-leadership thread on "why CRMs fail", a job seeker, a student asking which certification to take, or a founder building a competitor. &lt;strong&gt;Relevance is necessary but not sufficient.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two halves became the two phases of the pipeline. Phase 1 reads each post alone and asks "is the author seeking something?". Phase 2 takes the posts that pass and asks, for each client, "is this what they sell?".&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; Bluesky firehose ─┐
 X/Twitter API ────┤
 Hacker News API ──┼─► collectors ──► Redpanda ──► MinIO (S3)
 Reddit RSS ───────┤    (Go/Rust)      (queue)        │
 Discourse/Khoros ─┘                                  ▼
                                        preprocessing (Polars)
                                           ~475K posts/day
                                                      │
        ┌─────────────────────────────────────────────┘
        ▼
  PHASE 1  is this post a lead?          shared, once per post
        │        ~700 seeking posts/day
        ▼
  PHASE 2  does it match this client?    once per client
        │        leads
        ▼
  digest ──► email + Slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h1&gt;
  
  
  Ingestion: ~1.75M Posts a Day
&lt;/h1&gt;

&lt;p&gt;Ingestion runs 24/7 as five microservices, one per source. Bluesky is a live stream (the firehose). The others poll their source continuously. Volumes are averages over 8 nights (August 31 to September 7):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;How&lt;/th&gt;
&lt;th&gt;Volume/day&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td&gt;firehose, Go collector&lt;/td&gt;
&lt;td&gt;~1.58M&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X/Twitter&lt;/td&gt;
&lt;td&gt;keyword queries on TwitterAPI.io, paid per tweet, hourly spend cap, Rust collector&lt;/td&gt;
&lt;td&gt;~14K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hacker News&lt;/td&gt;
&lt;td&gt;official API, Rust collector&lt;/td&gt;
&lt;td&gt;~10.5K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reddit&lt;/td&gt;
&lt;td&gt;~240 subreddits via RSS, Rust collector&lt;/td&gt;
&lt;td&gt;~9.4K&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discourse/Khoros&lt;/td&gt;
&lt;td&gt;29 forum sections, Rust collector&lt;/td&gt;
&lt;td&gt;~560&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every collector writes to Redpanda (a Kafka-compatible message queue). Kafka Connect lands the posts in &lt;a href="https://en.wikipedia.org/wiki/MinIO" rel="noopener noreferrer"&gt;MinIO&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/Amazon_S3" rel="noopener noreferrer"&gt;S3&lt;/a&gt;-compatible storage) as &lt;a href="https://en.wikipedia.org/wiki/Apache_Parquet" rel="noopener noreferrer"&gt;Parquet&lt;/a&gt; files. A &lt;a href="https://en.wikipedia.org/wiki/Polars_(software)" rel="noopener noreferrer"&gt;Polars&lt;/a&gt; preprocessing step then keeps English, top-level posts that pass a basic quality filter: &lt;strong&gt;~475K posts a day&lt;/strong&gt; enter the lead pipeline. Reddit and the forums re-collect some posts already seen on earlier days; preprocessing drops those duplicates.&lt;/p&gt;

&lt;p&gt;Reddit deserves a note. The official API is closed to newcomers, the &lt;code&gt;.json&lt;/code&gt; endpoints return 403, and even RSS started blocking me on the &lt;a href="https://en.wikipedia.org/wiki/Transport_Layer_Security" rel="noopener noreferrer"&gt;TLS&lt;/a&gt; fingerprint (JA3). The collector now impersonates Firefox with the &lt;code&gt;wreq&lt;/code&gt; crate. It works, but you're always one Reddit change away from a silent zero.&lt;/p&gt;

&lt;p&gt;X/Twitter was the only source where volume could grow with money, so it got its own calibration work: which queries, which cost per True Lead, which cap.&lt;/p&gt;

&lt;p&gt;LinkedIn, the obvious B2B network, was never collected. It offers no public API for posts, and scraping it breaks its terms of service. That is a legal risk I wasn't willing to take as a solo founder.&lt;/p&gt;

&lt;h1&gt;
  
  
  Phase 1: Is This Post a Lead?
&lt;/h1&gt;

&lt;p&gt;Phase 1 looks at the post alone, with no client in the picture. Real numbers from the night of September 7:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; 460,106 posts   one night, five sources
     │   Step 1     keywords + logistic regression
     ▼
  24,109
     │   Steps 1.4-1.6   DeBERTa gates
     ▼                   persona / self-promoter / buyer stance
   4,273
     │   Step 2     per-source classifier
     ▼
   3,496
     │   Step 2.5   "is the author seeking?"
     ▼              Qwen3.5-35B, self-hosted
     689 seeking posts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Sources&lt;/th&gt;
&lt;th&gt;Posts in → out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;keyword filter&lt;/td&gt;
&lt;td&gt;regex + FeatureClf (&lt;a href="https://en.wikipedia.org/wiki/Logistic_regression" rel="noopener noreferrer"&gt;logistic regression&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;460,106 → 24,109&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.4&lt;/td&gt;
&lt;td&gt;persona detector&lt;/td&gt;
&lt;td&gt;&lt;a href="https://en.wikipedia.org/wiki/BERT_(language_model)" rel="noopener noreferrer"&gt;DeBERTa-v3&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Bluesky, HN&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.5&lt;/td&gt;
&lt;td&gt;self-promoter filter&lt;/td&gt;
&lt;td&gt;DeBERTa student&lt;/td&gt;
&lt;td&gt;Reddit, X/Twitter&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1.6&lt;/td&gt;
&lt;td&gt;buyer-stance gate&lt;/td&gt;
&lt;td&gt;DeBERTa student&lt;/td&gt;
&lt;td&gt;Reddit, forums, X/Twitter&lt;/td&gt;
&lt;td&gt;24,109 → 4,273 (1.4 to 1.6)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;per-source classifier&lt;/td&gt;
&lt;td&gt;DeBERTa-v3-large / LR on &lt;a href="https://en.wikipedia.org/wiki/Word_embedding" rel="noopener noreferrer"&gt;embeddings&lt;/a&gt;
&lt;/td&gt;
&lt;td&gt;Reddit, X/Twitter&lt;/td&gt;
&lt;td&gt;4,273 → 3,496&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2.5&lt;/td&gt;
&lt;td&gt;"is the author seeking?" judge&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://en.wikipedia.org/wiki/Qwen" rel="noopener noreferrer"&gt;Qwen3.5-35B&lt;/a&gt;, local&lt;/td&gt;
&lt;td&gt;all&lt;/td&gt;
&lt;td&gt;3,496 → 689&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The steps are ordered by cost: the cheapest models run first on the most posts, and the LLM runs last on the fewest (a regex on 460K posts, Qwen3.5-35B on 3.5K). A cheap gate's job is to remove obvious noise, not to decide. So each one is tuned for &lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Precision_and_recall" rel="noopener noreferrer"&gt;recall&lt;/a&gt;&lt;/strong&gt;, not precision: it only ships if it keeps ≥95% of the True Leads the next stage would find.&lt;/p&gt;

&lt;p&gt;Step 2 is where "one model per source" meets the data. Each source writes differently, so each deserves its own classifier, but only where I had enough labelled posts to train one. Reddit, the biggest lead source, got a DeBERTa-v3-large trained on Claude Opus (Anthropic's model) labels. X/Twitter had ~1,700 labelled posts with only 106 positives: too few for a DeBERTa, enough for a logistic regression on Snowflake Arctic embeddings, which cuts ~75% of the tweets while keeping ~92% of the leads. Bluesky, HN and the forums skip Step 2: their older models broke when the posts drifted away from the training data, and I didn't have enough labels to retrain them. Step 2.5 handles them.&lt;/p&gt;

&lt;p&gt;The only LLM in phase 1 is self-hosted: &lt;strong&gt;Qwen3.5-35B-A3B&lt;/strong&gt;, served by &lt;a href="https://en.wikipedia.org/wiki/VLLM" rel="noopener noreferrer"&gt;vLLM&lt;/a&gt; on a single RTX 5090 (32 GB). It wasn't my first choice. I started with a dense Qwen3.5-9B, which fits on the card without quantization. Qwen3.5-35B is a &lt;a href="https://en.wikipedia.org/wiki/Mixture_of_experts" rel="noopener noreferrer"&gt;mixture-of-experts&lt;/a&gt;: 35B parameters, but only 3B active per token, so it runs at a fraction of its size once quantized to GPTQ Int4 (with an fp8 KV cache). In May, a head-to-head on 4,535 hard pairs labelled by Opus settled it: at the same recall, Qwen3.5-35B doubled Qwen3.5-9B's precision (+10 points, 95% CI +8.2 to +11.8), for 2.8× the runtime. A later bake-off against online models confirmed it on the seeking task: none came close (PR-AUC 0.447 for Qwen3.5-35B against 0.255 for the best candidate, a 397B Qwen). Qwen3.5-35B keeps 3.7% of the Bluesky candidates. Most "I need..." posts on Bluesky are not somebody buying anything.&lt;/p&gt;

&lt;p&gt;There is no Step 3. It used to enrich each post with whatever context could be found: the author, their company, the product they mentioned. But most of the volume comes from Reddit, where authors are almost 100% anonymous, so it added nothing and I removed it.&lt;/p&gt;

&lt;p&gt;One rule shaped all of this: &lt;strong&gt;every step must be computable on a single post.&lt;/strong&gt; The long-term plan was full streaming: judge each post minutes after it's published, and alert the client while the thread is still fresh. A streaming pipeline never sees "the day", so no step was allowed to depend on it: no corpus-wide statistics, no "top 50 of the day", no day-level normalization. I deleted &lt;a href="https://en.wikipedia.org/wiki/Okapi_BM25" rel="noopener noreferrer"&gt;BM25&lt;/a&gt; because of that rule (&lt;a href="https://en.wikipedia.org/wiki/Tf%E2%80%93idf" rel="noopener noreferrer"&gt;IDF&lt;/a&gt; is a corpus statistic). Unfortunately, streaming never shipped: running the GPU pipeline around the clock only made sense once clients paid for it, so launch used a batch three times a day (08:00, 12:00, 17:00). The rule still paid off. Each run processes only the hours not yet processed, and that took no rewrite of any step.&lt;/p&gt;

&lt;p&gt;Where do the leads actually come from? Average over 8 nights (August 31 to September 7), per source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;Posts/day after preprocessing&lt;/th&gt;
&lt;th&gt;Seeking posts/day&lt;/th&gt;
&lt;th&gt;Share of seeking posts&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reddit&lt;/td&gt;
&lt;td&gt;~4,500&lt;/td&gt;
&lt;td&gt;~740&lt;/td&gt;
&lt;td&gt;74%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;X/Twitter&lt;/td&gt;
&lt;td&gt;~13,500&lt;/td&gt;
&lt;td&gt;~117&lt;/td&gt;
&lt;td&gt;12%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Bluesky&lt;/td&gt;
&lt;td&gt;~457,000&lt;/td&gt;
&lt;td&gt;~115&lt;/td&gt;
&lt;td&gt;11%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Discourse/Khoros&lt;/td&gt;
&lt;td&gt;~200&lt;/td&gt;
&lt;td&gt;~21&lt;/td&gt;
&lt;td&gt;2%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hacker News&lt;/td&gt;
&lt;td&gt;~440&lt;/td&gt;
&lt;td&gt;~7&lt;/td&gt;
&lt;td&gt;1%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Reddit alone is three quarters of the seeking posts, and ~80% of the True Leads once precision is audited (July audit): Bluesky's seeking posts rarely match a client. Bluesky is 96% of the posts and 11% of the seeking posts. Hacker News and the forums are good but tiny. So without Reddit, Dwelverson would have lost three quarters of its seeking posts and ~80% of its True Leads.&lt;/p&gt;

&lt;h1&gt;
  
  
  Phase 2: Does It Match This Client?
&lt;/h1&gt;

&lt;p&gt;Phase 2 takes the posts where someone is seeking and pairs each one with every client, then keeps only the pairs where the need matches what the client sells. Same night, run for ~1,200 prospects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     689 seeking posts  ×  ~1,200 prospects
     │   Step 4        SPLADE retrieval, per-source threshold
     ▼
  20,500 (post, prospect) pairs
     │   Steps 4.5-5   noise filter + cross-encoder cascade
     ▼
   8,780 pairs
     │   Step 6        chain-of-thought judge, GLM-5.2
     ▼                 (Qwen3.5-35B screens X/Twitter first)
     646 leads  ──►  Steps 7-9: enrichment, digest, email + Slack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Step&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Volume in → out&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;retrieval, absolute threshold per source&lt;/td&gt;
&lt;td&gt;SPLADE (&lt;a href="https://en.wikipedia.org/wiki/Learned_sparse_retrieval" rel="noopener noreferrer"&gt;learned sparse retrieval&lt;/a&gt;)&lt;/td&gt;
&lt;td&gt;689 posts → 20,500 pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4.5&lt;/td&gt;
&lt;td&gt;noise filter&lt;/td&gt;
&lt;td&gt;DeBERTa&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;cross-encoder &lt;a href="https://en.wikipedia.org/wiki/Cascading_classifiers" rel="noopener noreferrer"&gt;cascade&lt;/a&gt;, one per source&lt;/td&gt;
&lt;td&gt;DeBERTa-v3&lt;/td&gt;
&lt;td&gt;→ 8,780 pairs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;routed &lt;a href="https://en.wikipedia.org/wiki/Prompt_engineering#Chain-of-thought" rel="noopener noreferrer"&gt;chain-of-thought&lt;/a&gt; relevance judge&lt;/td&gt;
&lt;td&gt;GLM-5.2 (+ Qwen3.5-35B for X/Twitter)&lt;/td&gt;
&lt;td&gt;→ 646 leads&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6.6&lt;/td&gt;
&lt;td&gt;second opinion on near-miss rejects (clients only)&lt;/td&gt;
&lt;td&gt;Sonnet 5&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7 to 9&lt;/td&gt;
&lt;td&gt;enrichment, digest, delivery&lt;/td&gt;
&lt;td&gt;Qwen3.5-35B, Resend email, Slack&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Step 4 is a retrieval problem, not a classification one: score every post against every prospect and keep the plausible pairs. It uses &lt;strong&gt;SPLADE&lt;/strong&gt;, a learned sparse retrieval model. Like BM25, it turns a text into a list of weighted words, so a precise product word ("helpdesk", "SOC 2") still counts as a match. Unlike BM25, it learns the weights, adds related words the text doesn't contain, and needs no corpus statistics: a pair's score depends only on that post and that prospect. Until May, Step 4 was BM25 plus the top results of an embedding search. A zero-shot SPLADE, not even trained on my data, found 72% of the true pairs that retrieval missed. Fine-tuned on Opus labels (12 minutes on the 5090), it replaced both. Each prospect's ideal customer profile, or ICP (brand, buyer signal, keywords) is encoded once. Every post is scored against every prospect, and every pair above a fixed per-source threshold is kept. No top-K, again because of the single-post rule. Bluesky adds a dense-embedding union to catch what the sparse model misses.&lt;/p&gt;

&lt;p&gt;Step 6 is the only place where a paid API does bulk work: &lt;strong&gt;GLM-5.2 on DeepInfra&lt;/strong&gt;, with a chain-of-thought prompt and &lt;a href="https://en.wikipedia.org/wiki/Few-shot_learning" rel="noopener noreferrer"&gt;few-shot&lt;/a&gt; examples. X/Twitter is the exception. It produces ~6K pairs a night, and sending all of them to GLM would have cost ~€870 a month. So X/Twitter pairs go through Qwen3.5-35B first, on my GPU, and only its survivors reach GLM. Results: ~€30 a month, for the same X/Twitter recall (0.70) on the frozen benchmark. Same recall for ~30× less money.&lt;/p&gt;

&lt;p&gt;GLM won a bake-off. In June, I ran four online models (Sonnet, GLM-5.2, DeepSeek, a 397B Qwen) and Qwen3.5-35B on the same frozen set of pairs labelled by Opus. Sonnet and GLM tied for first, both clearly ahead of Qwen3.5-35B on relevance. GLM kept more of the true leads and was cheaper per token, so it got the job.&lt;/p&gt;

&lt;p&gt;The Step 4 threshold turned out to be &lt;strong&gt;the volume knob&lt;/strong&gt;: every Reddit pair above it goes to the paid judge. By August, the prospect pool had grown to ~1,200 companies, and the nightly prospects run cost too much for companies that weren't paying. So I swept the Reddit threshold from 1.35 to 1.80, measuring at each value the DeepInfra cost and the True Lead recall against Opus labels. The pairs between 1.35 and 1.80 were 40% of the DeepInfra bill. Cutting them kept 83% of the Reddit True Leads, and only 11 prospect companies out of ~1,200 were left with no lead at all. So prospects run at 1.80, for cost, and paying clients keep 1.35, for full recall.&lt;/p&gt;

&lt;p&gt;The pipeline ran on three targets that share phase 1 and split at phase 2:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;prospects&lt;/strong&gt;: ~1,200 companies I wanted to sell to, nightly. Their leads became the sales pitch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;clients&lt;/strong&gt;: paying accounts, three times a day. Pairs the judge narrowly rejected get a second opinion from Sonnet 5 (Step 6.6), then leads are delivered by email and Slack.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;autopromo&lt;/strong&gt;: a cohort I'll explain below, replayed only to count leads.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  From ~€100k a Month to ~€10 a Day
&lt;/h1&gt;

&lt;p&gt;The naive version of this product is one Opus call per post: "here's a post, here's the list of prospects, is it a lead for any of them?" That is ~€100k a month for a single pass (at 1.75M posts/day), before you even multiply by prospects.&lt;/p&gt;

&lt;p&gt;What the pipeline actually cost in September: €3 to €12 a night of DeepInfra, €2 to €3 a day of TwitterAPI.io. &lt;strong&gt;~€10 a day&lt;/strong&gt; for ~1,200 prospects. The bill scales with the number of prospects and clients, not with the number of posts. The GPU was already on my desk (electricity isn't counted).&lt;/p&gt;

&lt;p&gt;Three things made that possible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cascade.&lt;/strong&gt; The expensive model sees less than 2% of the posts. Cheap models buy recall, LLMs buy precision.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Self-hosting where the volume is.&lt;/strong&gt; Phase 1, the X/Twitter screen and the enrichment run on Qwen3.5-35B, on my own GPU. The paid API only sees the ~9K pairs that survive the cascade.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://en.wikipedia.org/wiki/Knowledge_distillation" rel="noopener noreferrer"&gt;Distillation&lt;/a&gt;.&lt;/strong&gt; Opus 5 is the gold annotator (small, expensive, audited sets). Sonnet 5 produces bulk labels. DeBERTa students learn from them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Step 1.5: Qwen3.5-9B teacher → DeBERTa student. Shipped.&lt;/li&gt;
&lt;li&gt;Step 1.6: Sonnet teacher (validated against Opus) → DeBERTa student. Shipped.&lt;/li&gt;
&lt;li&gt;Step 2 and the Reddit cascade: trained on Opus gold. Shipped.&lt;/li&gt;
&lt;li&gt;Pair relevance, Opus → cross-encoder: I retrained the Step 5 cross-encoder on ~28.5K Opus labels instead of Qwen labels. &lt;a href="https://en.wikipedia.org/wiki/F-score" rel="noopener noreferrer"&gt;F1&lt;/a&gt; 0.275, against 0.266 for the production model: a tie. &lt;strong&gt;Failed.&lt;/strong&gt; Small encoders hit a ceiling around F1 0.27 on "does this need match this product?". That question needs reasoning, so it stayed with an LLM.&lt;/li&gt;
&lt;li&gt;Opus chain-of-thought → Qwen3.5-9B &lt;a href="https://en.wikipedia.org/wiki/Fine-tuning_(deep_learning)#Low-rank_adaptation" rel="noopener noreferrer"&gt;QLoRA&lt;/a&gt;: the fine-tuned 9B reached F1 0.277, against 0.253 for Qwen3.5-35B, the judge in production then. Not conclusive, parked.&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Measuring It
&lt;/h1&gt;

&lt;p&gt;You can't tune a 14-step pipeline by reading its output. You need numbers, and numbers can lie in a subtle way: if the judge and the audit measure the same wrong thing, everything looks green. That happened to me. Until June, both the Step 6 judge and the nightly audit asked a single question: "does this post match what the company sells?". Relevance, not seeking. A founder's thread on CRM trends, matched to a CRM vendor, passed the judge, reached the client as a lead, and the audit counted it as correct. The fix was to measure each half of the definition where it is decided: Step 2.5 owns "is the author seeking?", Step 6 only judges relevance, and the audit checks both halves separately. Removing the seeking question from the Step 6 prompt alone roughly doubled True Lead precision.&lt;/p&gt;

&lt;p&gt;So a large part of the work was the measuring instrument itself:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;A frozen recall benchmark.&lt;/strong&gt; 477 real True Leads, labelled by Opus, set aside once. Every night they're replayed through the current pipeline, and each lost lead is traced to the step that dropped it. The set never changes, so if the number moves, the pipeline changed, not the data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conditional recall, per step.&lt;/strong&gt; Raw per-step recall blames a step for every True Lead it drops, even when a later step would have dropped it anyway. So each dropped lead is replayed through the remaining steps: would it have been delivered? The step only counts a loss if the answer is yes. On three nights in June, raw recall read 0.22 to 0.32 and conditional recall 0.52 to 0.70. Most "losses" were leads the pipeline would never have delivered anyway.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A nightly precision audit.&lt;/strong&gt; Every night, Opus re-judged a sample of the delivered leads: how many were real True Leads? Weighted by volume, so a big source counts more than a small one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error bars.&lt;/strong&gt; An audit on a few hundred leads is noisy. Every number came with a &lt;a href="https://en.wikipedia.org/wiki/Confidence_interval" rel="noopener noreferrer"&gt;confidence interval&lt;/a&gt; (&lt;a href="https://en.wikipedia.org/wiki/Binomial_proportion_confidence_interval#Wilson_score_interval" rel="noopener noreferrer"&gt;Wilson&lt;/a&gt; for proportions, &lt;a href="https://en.wikipedia.org/wiki/Bootstrapping_(statistics)" rel="noopener noreferrer"&gt;bootstrap&lt;/a&gt; for the rest). A change shipped only if its gain stood clear of the noise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A golden cohort.&lt;/strong&gt; 1,000 pairs labelled once by Opus, to compare a new judge with the old one on identical data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consistent labels.&lt;/strong&gt; Opus 5 produced the gold labels, Sonnet 5 the cheaper bulk labels. Two models don't label the same way, so their agreement was checked (&lt;a href="https://en.wikipedia.org/wiki/Cohen%27s_kappa" rel="noopener noreferrer"&gt;κ&lt;/a&gt;, stop below 0.80) before mixing them. Training and test sets were always split by date, never at random: a random split lets the model see tomorrow's posts during training (&lt;a href="https://en.wikipedia.org/wiki/Leakage_(machine_learning)" rel="noopener noreferrer"&gt;leakage&lt;/a&gt;).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every experiment gets a permanent ID (&lt;code&gt;S6-OpusCoTDistill&lt;/code&gt;, &lt;code&gt;S4-Reddit-CostReprice&lt;/code&gt;...) and an entry in a log, including the failures. There are ~95 of them.&lt;/p&gt;

&lt;p&gt;At the end, recall went from 0.48 in June to 0.58. Precision went from 0.24 in June to 0.5. Yet my targets were 0.75 recall and 0.5 precision so I reached the second one, never the first.&lt;/p&gt;

&lt;h1&gt;
  
  
  A 5090 on a Desk
&lt;/h1&gt;

&lt;p&gt;The model worked. The machine under it took months to become stable.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;April&lt;/strong&gt;: full freezes with GPU errors (Xid 109) when vLLM was killed and relaunched. Driver downgrade, &lt;a href="https://en.wikipedia.org/wiki/PCI_Express" rel="noopener noreferrer"&gt;PCIe&lt;/a&gt; Gen4 forced in the BIOS, &lt;a href="https://en.wikipedia.org/wiki/Active_State_Power_Management" rel="noopener noreferrer"&gt;ASPM&lt;/a&gt; disabled, BIOS update, power cap at 450W, clock lock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;May&lt;/strong&gt;: random &lt;code&gt;libcuda&lt;/code&gt; segfaults from a bug in the NVIDIA driver, hitting any CUDA job (cascade, embeddings, training), with no traceback. Forcing synchronous CUDA calls (&lt;code&gt;CUDA_LAUNCH_BLOCKING=1&lt;/code&gt;) helped once, then failed. What worked was retrying: free the GPU memory, including stray vLLM workers still holding it, then re-run the step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;June&lt;/strong&gt;: the crashes started to be more frequent. Since then, a step that detects a dead engine restarts vLLM and resumes where it stopped.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;July&lt;/strong&gt;: segfaults became machine-fatal. Driver swap to &lt;code&gt;nvidia-open&lt;/code&gt; 610 and that fixed almost everything. I also throttled the embedding load (smaller batches, 30s pause between chunks).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;August&lt;/strong&gt;: a total freeze at idle. &lt;a href="https://en.wikipedia.org/wiki/Memory_paging" rel="noopener noreferrer"&gt;Swap&lt;/a&gt; was 99.97% full with 23 GB of RAM free, a reclaim &lt;a href="https://en.wikipedia.org/wiki/Deadlock_(computer_science)#Livelock" rel="noopener noreferrer"&gt;livelock&lt;/a&gt; after 12 days of uptime. Lower swappiness and an hourly &lt;code&gt;swapoff&lt;/code&gt;/&lt;code&gt;swapon&lt;/code&gt; recycle.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The nightly run is a chain of 14 steps launched by cron, and one crash at 3 a.m. used to kill the whole night. Now each step has a retry rule that depends on how it can fail:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Kind of step&lt;/th&gt;
&lt;th&gt;On failure&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPU steps (DeBERTa, SPLADE, embeddings)&lt;/td&gt;
&lt;td&gt;free the GPU memory, re-run (up to 2 times)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;LLM steps on vLLM (Steps 2.5, 6, 7)&lt;/td&gt;
&lt;td&gt;if the engine died: restart vLLM, resume where it stopped (up to 3 times)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CPU and network steps (filters, digest, storage)&lt;/td&gt;
&lt;td&gt;wait 30 s, retry once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;anything else&lt;/td&gt;
&lt;td&gt;no retry: a real bug must fail loudly, not hide behind retries&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Retrying is safe because every step is &lt;a href="https://en.wikipedia.org/wiki/Idempotence" rel="noopener noreferrer"&gt;idempotent&lt;/a&gt; (it deletes its previous output before writing) and the long ones are resumable (they skip the work already done).&lt;/p&gt;

&lt;p&gt;After the 610 driver, the embedding throttle and the swap recycle, there were no more crashes. The pipeline was solid from then. 💪&lt;/p&gt;

&lt;h1&gt;
  
  
  Watching It Run
&lt;/h1&gt;

&lt;p&gt;A pipeline that runs at 1 a.m. has to tell me when it breaks. Monitoring ran on &lt;a href="https://en.wikipedia.org/wiki/Prometheus_(software)" rel="noopener noreferrer"&gt;Prometheus&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Grafana" rel="noopener noreferrer"&gt;Grafana&lt;/a&gt;, in the same Docker Compose stack as everything else:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Metrics&lt;/strong&gt;: every collector exposes its own (posts fetched, errors, silence per source), plus exporters for the host, Postgres and MinIO, and a probe on the web backend.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pipeline health&lt;/strong&gt;: every phase of every run writes a row to Postgres (start, end, status), so I can see which night failed at which step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Costs&lt;/strong&gt;: daily spend per provider (DeepInfra, TwitterAPI.io, Anthropic, Resend), with a monthly budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;6 dashboards&lt;/strong&gt;: collectors and ingestion, pipeline runs, platform, costs, client app, cold email.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The alerting rule was strict: &lt;strong&gt;an alert exists only if a human must act, now or today&lt;/strong&gt;. Everything else is a dashboard panel, and there are no "warnings". Each of the 26 rules had to pass the five questions &lt;a href="https://en.wikipedia.org/wiki/Site_reliability_engineering" rel="noopener noreferrer"&gt;SRE&lt;/a&gt; practice asks before paging someone. Then alerts went from Grafana to a small webhook receiver on my machine, which turned them into desktop notifications.&lt;/p&gt;

&lt;h1&gt;
  
  
  The Reddit Wall
&lt;/h1&gt;

&lt;p&gt;The product was live on August 6: €99/month, 14-day trial, Paddle billing, a React app, a Rust (Axum) backend on Scaleway, deploys gated by 126 &lt;a href="https://en.wikipedia.org/wiki/System_testing" rel="noopener noreferrer"&gt;end-to-end&lt;/a&gt; scenarios.&lt;/p&gt;

&lt;p&gt;On day 1, I &lt;a href="https://en.wikipedia.org/wiki/Eating_your_own_dog_food" rel="noopener noreferrer"&gt;dogfooded&lt;/a&gt; it. I took real Dwelverson leads and answered them from my 6-month-old, 0-&lt;a href="https://en.wikipedia.org/wiki/Karma_(Reddit)" rel="noopener noreferrer"&gt;karma&lt;/a&gt; Reddit account. Every reply was invisible to logged-out users (&lt;a href="https://en.wikipedia.org/wiki/Shadow_banning" rel="noopener noreferrer"&gt;shadow banned&lt;/a&gt;, in practice). A bot on r/ShadowBan gave me the reason: &lt;code&gt;CQS: Low&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;CQS (Contributor Quality Score) is a hidden Reddit score. Many subreddits filter comments from low-CQS accounts silently. Removed comments lower your CQS a bit more. After ~20 days of honest effort, my account had 4 karma.&lt;/p&gt;

&lt;p&gt;Reddit was ~80% of my lead volume. Unlocking a new account takes 30 to 90 days. My trial lasted 14. A client without an established Reddit account would receive great leads they couldn't answer, and churn at the end of the trial.&lt;/p&gt;

&lt;p&gt;That was a huge problem. The product worked, but only for clients who already had a Reddit account good enough to answer. Everyone else would get great leads and no way to use them. I had two options: find the people who already had such an account and sell to them, or stop the project.&lt;/p&gt;

&lt;h1&gt;
  
  
  Cold Email: 6 Weeks of Warm-Up, 800 Emails, 1 Trial
&lt;/h1&gt;

&lt;p&gt;I ran the campaign anyway. Six weeks of warm-up were sunk, and I wanted to know whether anyone would reply at all. The main acquisition channel was cold email, built as a pipeline too:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Apollo (companies &amp;lt;50 employees, filtered by category)
  → Bouncer (verified twice: at import and before export)
  → the company joins the nightly prospects run for 14 days
  → gate: ≥7 leads in 14 days
  → an email built on its strongest real lead
  → CSV → Saleshandy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Apollo is a B2B database of names, job titles and work emails. Bouncer is an email verification service, so emails don't bounce. Saleshandy sends the email sequences.&lt;/p&gt;

&lt;p&gt;The idea: don't pitch, show. The email quoted a real post from someone asking for what the prospect builds, with the link, then "14 days free, then €99/month".&lt;/p&gt;

&lt;p&gt;The infrastructure: 10 mailboxes on 3 secondary domains through Zapmail, sequenced by Saleshandy, warmed up with TrulyInbox (sold by Saleshandy). Warm-up started July 1. The first real send was August 11. That's &lt;strong&gt;~6 weeks of warm-up&lt;/strong&gt; before a single cold email.&lt;/p&gt;

&lt;p&gt;The results:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~800 emails delivered to ~500 contacts&lt;/li&gt;
&lt;li&gt;3 real replies (the other replies were people asking to be removed from the campaign)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;1 trial&lt;/strong&gt;, which ended 14 days later without converting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And the placement tests:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Microsoft/Outlook: 100% spam.&lt;/strong&gt; Before the campaign and after it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Google/Workspace: ~50% spam&lt;/strong&gt; after the campaign.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The list was fine: a Bouncer audit found 99.4% of the addresses still valid. The setup (SPF, DKIM, DMARC, reverse DNS, one-click unsubscribe, no blacklist) was fine. And the email text was fine too: SpamAssassin scored it 0.4 against a threshold of 5, and on one domain the same text went from 12% to 64% spam without a word changing. My diagnosis is engagement starvation (&lt;a href="https://en.wikipedia.org/wiki/Cold_email#Email_deliverability" rel="noopener noreferrer"&gt;deliverability&lt;/a&gt;): new domains, few replies, and the filters learn that nobody wants these emails. TrulyInbox reported a Microsoft score of 83/100 while Microsoft was sending 25 out of 25 test emails to spam. Microsoft was closed to me for months. For some segments (&lt;a href="https://en.wikipedia.org/wiki/Managed_service_company" rel="noopener noreferrer"&gt;MSPs&lt;/a&gt;), 62% of contacts were on Microsoft.&lt;/p&gt;

&lt;h1&gt;
  
  
  Auto-Promote: Finding People Who Can Climb the Wall
&lt;/h1&gt;

&lt;p&gt;Back to the Reddit wall. I flipped the question: who already gets past it?&lt;/p&gt;

&lt;p&gt;People who promote their own product on Reddit. My Step 1.5 was throwing them away every night (a self-promoter is a seller, not a lead). But a company whose founder posts on Reddit has an account that works, and it needs exactly what Dwelverson sells.&lt;/p&gt;

&lt;p&gt;The auto-promote pipeline:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes the Step 1.5 drops (self-promoters).&lt;/li&gt;
&lt;li&gt;Resolves the company behind the author (LLM + web search).&lt;/li&gt;
&lt;li&gt;Checks the author's karma history in Arctic Shift, a public archive of Reddit posts and comments searchable by author. "Can reply" means 100+ comment karma in the last 30 days.&lt;/li&gt;
&lt;li&gt;Finds people to contact with Apollo and checks their addresses with Bouncer.&lt;/li&gt;
&lt;li&gt;Replays phase 2 for that company as a prospect, to count the leads it would actually get.&lt;/li&gt;
&lt;li&gt;Keeps the company only if it got ≥4 leads in 7 days, then renders a personalized email built on those leads.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Over 33 days, it found 407 companies, 209 of them with a reachable contacts: ~6 a day. I sent ~60 emails (the first plan was LinkedIn, in the end everything went by direct email). None of them turned into a trial.&lt;/p&gt;

&lt;h1&gt;
  
  
  What I Learned
&lt;/h1&gt;

&lt;p&gt;&lt;strong&gt;Test the market before building the product.&lt;/strong&gt; A lead you can't act on is worth nothing, and the Reddit wall decides whether a client can use the product at all. If I had tried to answer a single lead early on, I would have hit that wall in the first weeks and stopped building. I'm not a Reddit user, and that's probably the root of the problem: I didn't know the wall existed. Competitors at the same price point survive because their buyers already have a Reddit presence. Six months building a product its clients couldn't use is a mistake. I should have found a way to test the market much earlier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Building on a platform you don't control is a risk.&lt;/strong&gt; Reddit was three quarters of my seeking posts, and Reddit is closing its doors: no more API for newcomers, blocked endpoints, fingerprinting, hidden quality scores. Some of it for good reasons, like fighting &lt;a href="https://en.wikipedia.org/wiki/Astroturfing" rel="noopener noreferrer"&gt;astroturfing&lt;/a&gt; and protecting its content from &lt;a href="https://en.wikipedia.org/wiki/Generative_engine_optimization" rel="noopener noreferrer"&gt;GEO/AEO&lt;/a&gt; manipulation. Some of it for business reasons, like selling Reddit Ads. Either way, anything built on top of it can become obsolete overnight, and every month invested there is exposed to that.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cold emails are filtered by sender, not by content.&lt;/strong&gt; I hate spam like everyone else, and I did the opposite of spamming. Before contacting a company, I checked that its ICP actually got leads and that it fit what Dwelverson does. Then each email was built on a real lead found for that company: templated, but personal. None of it mattered to the filters. SpamAssassin rated the content 0.4, and Microsoft still sent every email to spam. A new domain with no history and few replies lands in spam, whatever it says. I would have loved to understand how successful cold email campaigns actually work, beyond buying thousands of domains and burning them one after another. Out of pure curiosity: the problem is genuinely interesting.&lt;/p&gt;

&lt;h1&gt;
  
  
  Building It With AI
&lt;/h1&gt;

&lt;p&gt;I made one unusual choice: AI wrote all the code. I didn't write a single line, and I didn't read one either. It was a solo project and I wanted to move fast. On a team project, I would have chosen differently.&lt;/p&gt;

&lt;p&gt;It worked: I moved much faster than I could have alone. And it wasn't a small project:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~339,000 lines of code (78% Python, 9% Rust, 5% TypeScript)&lt;/li&gt;
&lt;li&gt;~47,000 lines of Markdown documentation, plus ~22,000 lines of Claude Code configuration&lt;/li&gt;
&lt;li&gt;1,883 commits&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built safety nets instead of code review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~6,800 unit tests&lt;/li&gt;
&lt;li&gt;126 end-to-end tests for the UI&lt;/li&gt;
&lt;li&gt;pre-commit checks&lt;/li&gt;
&lt;li&gt;deploys gated on the end-to-end suite&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Still, it was harder than expected. The specs are what matter, and writing them takes real engineering and ML knowledge: left unguided, the AI makes choices you wouldn't, and you always pay the price at some point (I did). The easy part was the configuration for each domain agent (ML, backend, frontend, data engineering, DevOps) and the best practices per language and technology, mostly because of my tech background. The hard part was fixing drift in its behaviour. For that, I built a &lt;code&gt;.claude/decisions/&lt;/code&gt; folder: a log of every decision, by topic, with what I rejected and why, so the AI stops recommending what I already said no to and remembers what we already tried.&lt;/p&gt;

&lt;p&gt;Every time the AI drifted, I assumed a context problem: some part of the Claude Code configuration was wrong or out of date. That proved right every time. But fixing it took a lot of time. At some point I built a &lt;code&gt;sync-docs&lt;/code&gt; command that reads the latest commits and flags what the documentation and configuration no longer match. Even with it, I still had to review the configuration by hand from time to time. In the end, I traded code-review time for config-review time. But I was still very fast/productive.&lt;/p&gt;

&lt;p&gt;I don't believe AI will replace developers. Without a technical background, building all this would have been much harder and much slower, with far more mistakes. What I got instead was the feeling of being an augmented developer.&lt;/p&gt;

&lt;h1&gt;
  
  
  What Came Next
&lt;/h1&gt;

&lt;p&gt;On September 15, I commented out every cron, tore down the Scaleway stack and the Cloudflare records, and moved Postgres back to my machine. The code stays: collectors, pipelines, web app, tests. &lt;br&gt;
What comes next is open. I'm exploring other ideas.&lt;/p&gt;

&lt;p&gt;Let's see what the future will bring 🚀&lt;/p&gt;

</description>
      <category>ai</category>
      <category>machinelearning</category>
      <category>saas</category>
      <category>indiehacker</category>
    </item>
  </channel>
</rss>
