<?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: Chichebe John</title>
    <description>The latest articles on DEV Community by Chichebe John (@chichebe_john_5b060931c73).</description>
    <link>https://dev.to/chichebe_john_5b060931c73</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%2F4028587%2F410bd0cc-72dd-44db-9bc0-5a700b91415f.jpg</url>
      <title>DEV Community: Chichebe John</title>
      <link>https://dev.to/chichebe_john_5b060931c73</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chichebe_john_5b060931c73"/>
    <language>en</language>
    <item>
      <title>Scam Listings In, Trustworthy Price Out: The Guardrail Stack Behind an AI Pricing Engine</title>
      <dc:creator>Chichebe John</dc:creator>
      <pubDate>Thu, 30 Jul 2026 14:19:16 +0000</pubDate>
      <link>https://dev.to/chichebe_john_5b060931c73/scam-listings-in-trustworthy-price-out-the-guardrail-stack-behind-an-ai-pricing-engine-2in8</link>
      <guid>https://dev.to/chichebe_john_5b060931c73/scam-listings-in-trustworthy-price-out-the-guardrail-stack-behind-an-ai-pricing-engine-2in8</guid>
      <description>&lt;p&gt;&lt;em&gt;Someone asked how I handle noise in search results when there's no clean dataset to fall back on. Here's the full answer: six layers, none of which trust the model.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;After I wrote about &lt;a href="https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj"&gt;building a car pricing engine for a market with no data&lt;/a&gt;, &lt;a class="mentioned-user" href="https://dev.to/topstar_ai"&gt;@topstar_ai&lt;/a&gt; &lt;a href="https://dev.to/topstar_ai/comment/3b92f"&gt;asked something in the comments&lt;/a&gt; that deserved more than a reply:&lt;/p&gt;


&lt;div class="ltag__comment crayons-card my-2 p-0 overflow-hidden"&gt;
    &lt;a href="https://dev.to/chichebe_john_5b060931c73/the-next-billion-users-dont-come-with-datasets-and-thats-the-opportunity-406o" class="flex items-center gap-2 p-3 fs-s color-base-60 hover:color-base-90"&gt;
      

      &lt;span&gt;Comment on &lt;strong class="fw-medium color-base-90"&gt;The Next Billion Users Don't Come With Datasets, and That's the Opportunity&lt;/strong&gt;&lt;/span&gt;
    &lt;/a&gt;
  &lt;div class="p-4"&gt;
    &lt;div class="flex items-center gap-2 mb-3"&gt;
      &lt;a href="/topstar_ai" class="crayons-avatar crayons-avatar--l"&gt;
        &lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3917922%2Fcccf9eab-7f08-4807-b13a-0e70a306a79d.png" alt="topstar_ai" class="crayons-avatar__image"&gt;
      &lt;/a&gt;
      &lt;div&gt;
        &lt;a href="/topstar_ai" class="crayons-link fw-medium"&gt;Luis Cruz&lt;/a&gt;
        &lt;span class="fs-xs color-base-60 ml-1"&gt;Jul 15&lt;/span&gt;
      &lt;/div&gt;
    &lt;/div&gt;
    &lt;div class="text-styles"&gt;
      &lt;p&gt;I was struck by the idea that LLMs can synthesize a price at request time from live search results, which flips the traditional supervised learning approach on its head. The fact that the dataset is no longer a prerequisite, but rather the exhaust, is a game-changer for building AI products in data-poor markets. I've worked on similar projects where we had to rely on proxy data sources and clever feature engineering to overcome data scarcity, but the LLM approach seems to offer a more scalable and flexible solution. How do you handle the issue of data quality and noise in the search results, and what kinds of domain guardrails have you implemented to ensure the model's output is reliable and trustworthy?&lt;/p&gt;


    &lt;/div&gt;
  &lt;/div&gt;
&lt;/div&gt;


&lt;p&gt;Two questions in there, and they're the right two. If your ground truth is live search results instead of a curated dataset, then your data quality problem doesn't go away. It moves. It becomes a runtime problem you have to solve on every single request.&lt;/p&gt;

&lt;p&gt;Here's what I actually do at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;AutoValue&lt;/a&gt;, layer by layer.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, what the noise actually looks like
&lt;/h2&gt;

&lt;p&gt;Generic advice about "filtering low-quality sources" is useless until you know the specific failure modes. Mine, when I search for Nigerian used car prices:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Asking prices, not transaction prices.&lt;/strong&gt; Every listed price on Jiji is an opening move. Nigerian buyers negotiate 10 to 20% below the ask as a matter of routine. A model that reports the listed number is systematically overpricing the market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pre-devaluation listings that never got taken down.&lt;/strong&gt; The naira went from around ₦460 to the dollar to around ₦1,650. Listings from 2022 are still indexed and still show 2022 prices. They aren't wrong, exactly. They're archaeology.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trim contamination.&lt;/strong&gt; Search "Toyota Land Cruiser Prado price Nigeria" and most of what comes back is the VX full option, because that's what dealers advertise. If you take that as the base price for a standard trim, every Prado prices high forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Currency mixing.&lt;/strong&gt; Export and auction sites quote USD. Nigerian listing sites quote naira. Both land in the same result set.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scam bait.&lt;/strong&gt; A ₦2.5 million Range Rover is not a data point. It's a lure. But it's a perfectly well-formed number that any naive parser will happily average in.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice that only the last one is "bad data" in the usual sense. The rest are all &lt;em&gt;correct&lt;/em&gt; data that's wrong for my purpose. That distinction drives the whole design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: Constrain the search, not just the parsing
&lt;/h2&gt;

&lt;p&gt;The cheapest filter is the one that stops noise from being retrieved at all.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;make&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; price Nigeria site:jiji.ng OR site:cars45.com`&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;searchRes&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="s2"&gt;https://google.serper.dev/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="s2"&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="s2"&gt;X-API-KEY&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;serperKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&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="s2"&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="na"&gt;q&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;gl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ng&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;hl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;num&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&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;&lt;code&gt;site:&lt;/code&gt; scoping to the two marketplaces that actually matter, and &lt;code&gt;gl: "ng"&lt;/code&gt; to geo-locate the search in Nigeria. That last one matters more than it looks. Without it you get US results for the same car, in dollars, for a market with entirely different import economics.&lt;/p&gt;

&lt;p&gt;I also prioritise Google's answer box and knowledge panel above organic results when they're present, because Google has already done a round of synthesis across sources:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;priorityLines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;answerBox&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;answerBox&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;answer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;priorityLines&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`GOOGLE SUMMARY: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;answerBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;answerBox&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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;organicLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;organic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&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;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;\n&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;snippet&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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;allLines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;priorityLines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;organicLines&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Layer 2: Separate extraction from reasoning
&lt;/h2&gt;

&lt;p&gt;This is the structural decision everything else depends on, and it's the one I'd most want someone to take away.&lt;/p&gt;

&lt;p&gt;There are two completely different jobs here. One is &lt;em&gt;read these messy snippets and tell me what this car currently sells for&lt;/em&gt;. The other is &lt;em&gt;given a base price, adjust it for this specific car's mileage, condition, trim and location&lt;/em&gt;. The tempting move is one prompt that does both. Don't.&lt;/p&gt;

&lt;p&gt;They're two separate Claude calls in my pipeline, and the reason is auditability. When a price comes out wrong in a single-call design, you cannot tell whether the model misread a snippet or misapplied an adjustment rule. Split them and every bad output localises to one stage. My debugging time dropped more from this change than from any prompt improvement.&lt;/p&gt;

&lt;p&gt;Extraction runs on Haiku with a procedure, not a request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Extract the REALISTIC TRANSACTION PRICE RANGE, what this car actually sells
for, not what sellers hope to get.

- Naira devalued sharply: ₦460/$ in 2022 to ₦1,650/$ in 2026. Car prices are
  now 3 to 4x higher than pre-2023.
- Jiji and Cars45 show ASKING prices. Nigerian buyers negotiate 10 to 20%
  below the listed ask.
- STEP 1: any price more than 60% below the highest-priced cluster member is a
  pre-devaluation or junk listing. SKIP IT.
- STEP 2: SKIP listings marked "5+ years on Jiji". ENTERPRISE is a dealer
  badge, do NOT skip those, dealers are valid sellers.
- STEP 3: prices in $ multiply by ₦1,650.
- STEP 4: from remaining valid prices take the MIDDLE cluster. Ignore the
  bottom 15% and anything above 2x the cluster median.
- price_low = 25th percentile. price_high = 75th percentile.
- If only 1 to 2 valid prices remain: low = price × 0.88, high = price × 1.08.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of those rules is a scar. STEP 2's carve-out for &lt;code&gt;ENTERPRISE&lt;/code&gt; exists because my first version of "skip listings with badges" quietly deleted all the dealer inventory, which is the most reliable pricing signal on the site.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: Percentiles, never averages
&lt;/h2&gt;

&lt;p&gt;Worth calling out on its own because it's a one-line change with outsized effect.&lt;/p&gt;

&lt;p&gt;The output is the 25th and 75th percentile of the surviving cluster. Not the mean, not min and max. A single scam listing at ₦2.5M destroys a mean and it destroys a min. It barely moves a 25th percentile. Robustness to outliers is a property you get for free from the right statistic, and no amount of prompt engineering substitutes for picking it.&lt;/p&gt;

&lt;p&gt;The narrow-data case gets handled explicitly too. With only one or two valid prices left, percentiles are meaningless, so the rule degrades to a fixed spread around the single observation. Say what should happen when the data is thin, or the model will invent something.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 4: Make the model report, let code decide
&lt;/h2&gt;

&lt;p&gt;Trim contamination is the subtlest failure in the list, and the fix generalises well beyond pricing.&lt;/p&gt;

&lt;p&gt;I could ask the extraction model to "adjust the price down if the listings look like a high trim." That's asking for a silent, unauditable, unrepeatable adjustment buried inside a number. Instead the model &lt;em&gt;reports what it saw&lt;/em&gt; as a separate field, and the arithmetic happens in code where I can read it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;TRIM_FACTOR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;full_option&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;limited&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;1.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sport&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="mf"&gt;1.08&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;standard&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;unknown&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;     &lt;span class="mf"&gt;1.00&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;trimFactor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;TRIM_FACTOR&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;searched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detected_trim&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mf"&gt;1.00&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;normalizedLow&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_low&lt;/span&gt;  &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;trimFactor&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&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;normalizedHigh&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;round&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searched&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_high&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;trimFactor&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every stored anchor is a base-trim price, the trim premium gets re-added later as a visible line item the seller can see, and I can change the multipliers without touching a prompt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ask the model for observations. Keep the calculations in code.&lt;/strong&gt; If a model's judgment is going to move a number, make it move the number through a mechanism you can print.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 5: A deterministic invariant after the model responds
&lt;/h2&gt;

&lt;p&gt;The adjustment model is given explicit percentage rules: excellent interior is +2%, mileage below expected earns +0.3% per 10,000km for a 4 to 7 year old car, and so on.&lt;/p&gt;

&lt;p&gt;It does not always follow its own rules. Verified live: on one car it applied a +7.8% mileage bonus where its own stated rule produced 0.3%. That's not hallucination in the dramatic sense. It's a model doing approximate arithmetic and landing somewhere plausible-looking.&lt;/p&gt;

&lt;p&gt;So there's a clamp that runs in code after the response comes back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;CONDITION_UPLIFT_KEYS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;condition&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;interior&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;engine&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;mileage&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;applyAgeConditionCeiling&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;carAge&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// Younger car, smaller allowed uplift: "excellent" is the expected norm for a&lt;/span&gt;
  &lt;span class="c1"&gt;// 2-year-old car and already priced into the anchor, but a genuine rarity on&lt;/span&gt;
  &lt;span class="c1"&gt;// a 10-year-old one.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;capPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;carAge&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.03&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;carAge&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="mf"&gt;0.06&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&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;baseMedian&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_low&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;price_high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upliftNGN&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;CONDITION_UPLIFT_KEYS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;key&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;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;adjustments&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upliftPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;upliftNGN&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;baseMedian&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;upliftPct&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="nx"&gt;capPct&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="nx"&gt;estimate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cappedPct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

  &lt;span class="c1"&gt;// Subtract only the excess above the ceiling, proportionally.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;excessPct&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;upliftPct&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;capPct&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;The domain reasoning matters as much as the code. A search anchor for a two-year-old car &lt;em&gt;already assumes&lt;/em&gt; near-excellent condition, because that's what two-year-old cars on the market are. Stacking excellent body, excellent interior, excellent engine and a low-mileage bonus on top of that anchor prices a clean used car like a new one. The failure was worst on thin-data models, where the only search results are new-car dealer prices and the anchor is already the ceiling.&lt;/p&gt;

&lt;p&gt;Trim and location uplifts are deliberately exempt from the cap, because they're structural rather than condition-based. Trim was divided out in layer 4 and is legitimately being re-added.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Never trust a model to guarantee an arithmetic invariant.&lt;/strong&gt; If a relationship must hold, enforce it in code after the fact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 6: Fail closed
&lt;/h2&gt;

&lt;p&gt;This is the one I'd argue about with most people building LLM products.&lt;/p&gt;

&lt;p&gt;When the search returns nothing usable, the endpoint does not fall back to the model's own knowledge. It returns a 503:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;searched&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="nx"&gt;NextResponse&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="na"&gt;success&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="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pricing_unavailable&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;We couldn't find live market data for this car right now. Please try again in a few minutes.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;503&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;Most LLM products fail open. Retrieval comes back empty, and the model answers anyway from training data, and the user cannot tell the difference between a grounded answer and a guess. For a chatbot that's mildly bad. For a product whose entire value proposition is "this number is trustworthy," it's fatal, because the failure is invisible exactly when it's most damaging.&lt;/p&gt;

&lt;p&gt;Related, and covered in more depth in the &lt;a href="https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj"&gt;previous article&lt;/a&gt;: model-generated prices were once cached as anchors, then re-injected on the next request as if they were market data. The fix has since hardened from a runtime bypass into a type. The stored anchor's source can only be &lt;code&gt;manual&lt;/code&gt;, &lt;code&gt;serper_search&lt;/code&gt;, or &lt;code&gt;google_cse&lt;/code&gt;, and the read query filters on those three explicitly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;source&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;serper_search&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;google_cse&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A model's own output is no longer something the system is capable of treating as evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd still fix
&lt;/h2&gt;

&lt;p&gt;Being honest about the holes, since that's the part these posts usually skip.&lt;/p&gt;

&lt;p&gt;The exchange rate is a hardcoded constant, &lt;code&gt;USD_TO_NGN = 1650&lt;/code&gt;. I wrote an entire article about how stale currency assumptions destroyed my prices, and then I put a stale currency assumption in my own code. It needs to be a live lookup, and it will be.&lt;/p&gt;

&lt;p&gt;The percentile cutoffs, the 60% staleness threshold, the trim multipliers: those are calibrated from my own market knowledge, not fitted to outcome data. Once enough listings sell through the platform, actual transaction prices become the training signal that replaces my guesses. That's the point where the exhaust finally becomes the dataset.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Know your specific noise.&lt;/strong&gt; Most of my bad data isn't false, it's correct data that's wrong for the question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate extraction from reasoning.&lt;/strong&gt; Two calls, two failure domains, debuggable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use robust statistics.&lt;/strong&gt; Percentiles over means. One scam listing shouldn't move your answer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Models report, code computes.&lt;/strong&gt; Any adjustment that moves a number should be visible and versioned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce invariants after the model, in code.&lt;/strong&gt; Stated rules are not guaranteed rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fail closed.&lt;/strong&gt; No data is a better answer than a confident guess, when trust is the product.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I build AutoValue in the open at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;autovalue.tech&lt;/a&gt;. If you're doing retrieval-grounded work in a market where the sources are this messy, I'd like to hear which layer you'd add.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>typescript</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Next Billion Users Don't Come With Datasets, and That's the Opportunity</title>
      <dc:creator>Chichebe John</dc:creator>
      <pubDate>Wed, 15 Jul 2026 12:52:33 +0000</pubDate>
      <link>https://dev.to/chichebe_john_5b060931c73/the-next-billion-users-dont-come-with-datasets-and-thats-the-opportunity-406o</link>
      <guid>https://dev.to/chichebe_john_5b060931c73/the-next-billion-users-dont-come-with-datasets-and-thats-the-opportunity-406o</guid>
      <description>&lt;p&gt;&lt;em&gt;An opinion from someone building AI where the training data runs out.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Every AI product tutorial makes the same silent assumption: that your problem domain is already measured. Want to build a price estimator? There's a Kaggle dataset. A recommendation engine? Scrape the reviews. A valuation model? Kelley Blue Book publishes the ground truth and your model just has to approximate it.&lt;/p&gt;

&lt;p&gt;Now try building the same product in Lagos.&lt;/p&gt;

&lt;p&gt;I build AutoValue, an AI pricing platform for used cars in Nigeria, and I can tell you exactly what the data landscape looks like: there is no Blue Book. There is no clean listings API. The largest marketplaces are full of asking prices that everyone understands to be opening bids, posted by sellers who expect to be haggled down 20%. Two identical cars sit ₦8 million apart on the same page. The "dataset" for my domain is a negotiation culture, a WhatsApp group, and a dealer's gut feeling.&lt;/p&gt;

&lt;p&gt;The standard takeaway is that markets like this are too hard for AI products. I think that conclusion is exactly backwards, and I want to make the case for why data-poor markets are the most interesting place to build right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where there's no dataset, there's no incumbent
&lt;/h2&gt;

&lt;p&gt;In the US, a startup doing AI car valuation is competing against Kelley Blue Book, Carvana's pricing engine, CarGurus' deal ratings, and forty years of accumulated market data. The AI is a marginal improvement on an already-solved problem. The moat belongs to whoever collected the data decades ago.&lt;/p&gt;

&lt;p&gt;In Nigeria, the same product is not an improvement on the incumbent. It &lt;em&gt;creates the category&lt;/em&gt;. Whoever produces the first trusted price reference doesn't compete with the Blue Book. They become it. And this generalizes far beyond cars: think rents in Nairobi, used electronics in Dhaka, farm equipment in rural Brazil. Enormous, active markets that trade daily without a single reliable price signal. Every one of them is waiting for its reference source, and the reference source is now buildable by a small team with an LLM, a search API, and domain stubbornness.&lt;/p&gt;

&lt;h2&gt;
  
  
  LLMs changed what "no dataset" means
&lt;/h2&gt;

&lt;p&gt;Here's the technical shift that makes this possible. The classical approach to a pricing model is supervised learning: collect a hundred thousand labeled transactions, train, deploy. No transactions, no model. Data-poor markets were locked out by definition.&lt;/p&gt;

&lt;p&gt;LLMs flipped the order of operations. My pricing engine doesn't learn from a historical dataset; it &lt;em&gt;synthesizes&lt;/em&gt; a price at request time from live search results, structural knowledge (depreciation curves, trim hierarchies, mileage effects), and hard domain guardrails. The model contributes judgment, the search contributes freshness, and the database of resolved prices grows as a byproduct of usage. The dataset isn't a prerequisite anymore. It's the exhaust.&lt;/p&gt;

&lt;p&gt;That inversion is the entire unlock. You can now bootstrap a data business in a market that has no data, because the first version of the product doesn't need the data the product will eventually accumulate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The catch: the model thinks your market doesn't exist
&lt;/h2&gt;

&lt;p&gt;I won't romanticize this. Building AI for a market the training data forgot means the model's priors actively work against you. Mine believed a dollar was still ₦450 and priced every car in the country at a third of its value, because its memorized world predated a currency collapse. Data that does surface in search is polluted with scam listings and aspirational prices. Payment rails that every Stripe tutorial assumes simply don't apply.&lt;/p&gt;

&lt;p&gt;So you build differently. Retrieval isn't an optimization here; it's load-bearing. Domain guardrails aren't nice-to-haves; they're the only thing standing between you and confidently wrong output. Every architectural decision that's optional in a data-rich market is mandatory in a data-poor one. The upside is that these constraints force you to build the robust version of the system on day one, the version that data-rich builders only discover they need after their first embarrassing failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who should be building here
&lt;/h2&gt;

&lt;p&gt;If you're a developer in Lagos, Nairobi, Karachi, or Jakarta, you have an unfair advantage that no one in San Francisco can replicate: you can tell when the model is wrong. You know the street price, the negotiation dance, the scam patterns, the real exchange rate. Ground truth lives in your head, and in markets without datasets, ground truth in someone's head is the scarcest resource in the entire stack.&lt;/p&gt;

&lt;p&gt;The standard advice says to copy proven Western products for your local market. I'd argue the opposite is now more valuable: build the measurement layer your market never had. Not another marketplace on top of chaos, but the price signal, the quality score, the trust index that makes the chaos legible. Marketplaces come and go. Reference data compounds.&lt;/p&gt;

&lt;p&gt;The next billion users aren't waiting for translated versions of solved products. They're living inside unsolved measurement problems, and for the first time, the tools to solve them don't require a data moat you don't have. They require judgment about a market you already understand better than the training data does.&lt;/p&gt;

&lt;p&gt;That's not a handicap. That's the entire edge.&lt;/p&gt;

&lt;p&gt;I build AutoValue in the open at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;autovalue.tech&lt;/a&gt;, and I write about the engineering behind it, including &lt;a href="https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj"&gt;the pricing engine and the currency bug that nearly sank it&lt;/a&gt;. If you're building for a market the datasets forgot, I'd genuinely like to hear what you're measuring.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>data</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
    <item>
      <title>Build an AI Agent With Tool Calling in a Next.js Route Handler (Claude API)</title>
      <dc:creator>Chichebe John</dc:creator>
      <pubDate>Tue, 14 Jul 2026 16:16:53 +0000</pubDate>
      <link>https://dev.to/chichebe_john_5b060931c73/build-an-ai-agent-with-tool-calling-in-a-nextjs-route-handler-claude-api-2gc3</link>
      <guid>https://dev.to/chichebe_john_5b060931c73/build-an-ai-agent-with-tool-calling-in-a-nextjs-route-handler-claude-api-2gc3</guid>
      <description>&lt;p&gt;&lt;em&gt;A step-by-step guide to the pattern behind AutoValue's listing agent: a conversational AI that calls real functions, keeps state across turns, and lives entirely in one Next.js route.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most "build a chatbot" tutorials stop at text in, text out. Production agents are different: they call your functions, wait for real data, and decide what to do next based on the results. I run one in production at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;AutoValue&lt;/a&gt;, where a conversational agent walks Nigerian car sellers through a full listing flow: identifying the car from a photo, reading the odometer, pulling live market prices, and publishing the finished listing to the marketplace. This tutorial builds the core of that pattern from scratch: a tool-calling agent inside a single Next.js route handler.&lt;/p&gt;

&lt;p&gt;You'll need Node 18+, a Next.js App Router project, and an Anthropic API key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the SDK and set your key
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; @anthropic-ai/sdk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Put your key in &lt;code&gt;.env.local&lt;/code&gt; so Next.js keeps it server-side:&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="nv"&gt;ANTHROPIC_API_KEY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;sk-ant-...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The SDK reads that variable automatically, so the client needs no arguments.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Define a tool
&lt;/h2&gt;

&lt;p&gt;A tool is a JSON Schema description of a function the model may call. The model never executes anything itself; it returns a structured request, and your code runs the function. Here's a simplified version of AutoValue's pricing tool:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&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;tools&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Tool&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_market_price&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="s2"&gt;Get the current Nigerian market price range for a specific car. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Call this whenever the user asks what a car is worth, or before &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
      &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;suggesting a listing price. Do not estimate prices from memory.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;input_schema&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="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;make&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="s2"&gt;string&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="s2"&gt;e.g. Toyota&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;model&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="s2"&gt;string&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="s2"&gt;e.g. Camry&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;year&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="s2"&gt;integer&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="s2"&gt;e.g. 2015&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="na"&gt;mileage_km&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="s2"&gt;integer&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="s2"&gt;Odometer reading in kilometres, if known&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;required&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="s2"&gt;make&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;model&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;year&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two lessons from production hiding in that description. First, say &lt;strong&gt;when&lt;/strong&gt; to call the tool, not just what it does. Models decide from the description, and "call this whenever the user asks what a car is worth" measurably beats a bare "gets car prices." Second, the line "do not estimate prices from memory" exists because a model's memorized prices can be years stale. In a volatile economy that's a 3x error. Make the tool the only allowed source of truth for anything that changes fast.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The route handler with the agent loop
&lt;/h2&gt;

&lt;p&gt;The Claude API is stateless: you send the full conversation every time. The agent loop is simple in shape: call the model, and if it stops because it wants a tool (&lt;code&gt;stop_reason: "tool_use"&lt;/code&gt;), run the tool, append the result, and call again. When it stops for any other reason, you have your answer.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;app/api/agent/route.ts&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@anthropic-ai/sdk&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;NextResponse&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="s2"&gt;next/server&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;client&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;Anthropic&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;SYSTEM_PROMPT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You are AutoValue's listing assistant. You help Nigerian car sellers &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;price and list their cars. Always base prices on the get_market_price &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool, never on memory. Be concise and friendly.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&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;unknown&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&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;name&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;get_market_price&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="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;make&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;year&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;input&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="na"&gt;make&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="nl"&gt;year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="c1"&gt;// In production this hits our search pipeline (Serper → Google CSE)&lt;/span&gt;
    &lt;span class="c1"&gt;// and a Supabase anchor table. Stubbed here:&lt;/span&gt;
    &lt;span class="k"&gt;return&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="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;NGN&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;low&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="nx"&gt;_500_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;high&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;_200_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;live market listings&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;car&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;year&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;make&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Unknown tool: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageParam&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;history&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MessageParam&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

  &lt;span class="k"&gt;while &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;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&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;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;claude-opus-4-8&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;max_tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;thinking&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="s2"&gt;adaptive&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="na"&gt;system&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;SYSTEM_PROMPT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="nx"&gt;tools&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;

    &lt;span class="c1"&gt;// Append the FULL content array, not just the text.&lt;/span&gt;
    &lt;span class="c1"&gt;// It contains tool_use and thinking blocks the API needs back.&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stop_reason&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_use&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&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="nx"&gt;b&lt;/span&gt; &lt;span class="k"&gt;is&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TextBlock&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;text&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;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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&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="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;history&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="c1"&gt;// The model may request several tools in one turn. Run them all,&lt;/span&gt;
    &lt;span class="c1"&gt;// then return ALL results in a single user message.&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toolResults&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ToolResultBlockParam&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;block&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;tool_use&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&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;runTool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;block&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;toolResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="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="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&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;content&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="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;toolResults&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
          &lt;span class="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="s2"&gt;tool_result&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
          &lt;span class="na"&gt;tool_use_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;block&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;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="k"&gt;instanceof&lt;/span&gt; &lt;span class="nb"&gt;Error&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
          &lt;span class="na"&gt;is_error&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;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;toolResults&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;The frontend just POSTs &lt;code&gt;{ messages }&lt;/code&gt; and stores the returned &lt;code&gt;messages&lt;/code&gt; array for the next turn. That's the whole state model: the client holds the transcript, the server holds nothing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The four bugs you will hit (because I hit them)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Appending only the text.&lt;/strong&gt; If you push &lt;code&gt;{ role: "assistant", content: replyText }&lt;/code&gt; instead of the full &lt;code&gt;response.content&lt;/code&gt;, you drop the &lt;code&gt;tool_use&lt;/code&gt; blocks, and the API rejects the next request because your &lt;code&gt;tool_result&lt;/code&gt; references an ID that no longer exists in history. Always append the whole content array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Splitting parallel tool results.&lt;/strong&gt; The model can request three tools in one turn. If you send each result as its own user message, the request may fail on role alternation, and worse, you quietly teach the model to stop parallelizing. All results go in one user message.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Swallowing tool errors.&lt;/strong&gt; If a tool throws and you skip its result, the API errors on the dangling &lt;code&gt;tool_use&lt;/code&gt;. Return a &lt;code&gt;tool_result&lt;/code&gt; with &lt;code&gt;is_error: true&lt;/code&gt; instead; the model reads the error and adapts, often by asking the user a clarifying question, which is exactly what you want.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Trusting the model to carry state.&lt;/strong&gt; In AutoValue's six-phase flow we learned that data the model collected in phase one (like body type) doesn't reliably survive to a tool call in phase three if you expect the model to re-emit it as a tool argument. Accumulate authoritative state in your own code and inject it into tool executions yourself. The model drives the conversation; your code owns the facts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where to go from here
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The SDK can run the loop for you.&lt;/strong&gt; &lt;code&gt;client.beta.messages.toolRunner()&lt;/code&gt; with &lt;code&gt;betaZodTool&lt;/code&gt; handles the call-execute-append cycle automatically. I've shown the manual loop because you should understand what the runner does before you let it do it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Streaming&lt;/strong&gt;: swap &lt;code&gt;create&lt;/code&gt; for &lt;code&gt;client.messages.stream()&lt;/code&gt; and forward text deltas to the client for a typing effect; &lt;code&gt;stream.finalMessage()&lt;/code&gt; gives you the same object the loop needs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: for high-traffic routes, a smaller model like &lt;code&gt;claude-haiku-4-5&lt;/code&gt; handles narrow, well-prompted tool flows at a fraction of the cost. AutoValue runs different models for different jobs: a big model for the conversational agent, a small one for the pricing calculator.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The full production version of this pattern (vision-based car identification, odometer OCR, photo phase management, publish flow) runs at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;autovalue.tech&lt;/a&gt;. I wrote about the pricing engine behind the &lt;code&gt;get_market_price&lt;/code&gt; tool, and the currency bug that nearly sank it, in &lt;a href="https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj"&gt;my previous article&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>The LLM Thought a Dollar Was Still ₦450: Building a Car Pricing Engine for a Market With No Data</title>
      <dc:creator>Chichebe John</dc:creator>
      <pubDate>Tue, 14 Jul 2026 10:56:09 +0000</pubDate>
      <link>https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj</link>
      <guid>https://dev.to/chichebe_john_5b060931c73/the-llm-thought-a-dollar-was-still-n450-building-a-car-pricing-engine-for-a-market-with-no-data-1lmj</guid>
      <description>&lt;p&gt;&lt;em&gt;How I built an AI valuation engine for Nigerian used cars, and what it taught me about why you should never let a language model price anything on its own.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: a market where nobody knows the price
&lt;/h2&gt;

&lt;p&gt;In the US, if you want to know what a 2018 Toyota Camry is worth, you check Kelley Blue Book. There is no Kelley Blue Book for Nigeria. Used cars here trade through an informal economy: dealer clusters, WhatsApp groups, and listing sites where the asking price is an opening move in a negotiation, not a market value. Two identical cars can be listed ₦8 million apart on the same site, on the same day.&lt;/p&gt;

&lt;p&gt;I'm building &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;AutoValue&lt;/a&gt;, a platform that gives Nigerian sellers a data-backed valuation from photos of their car. The AI parts you'd expect worked early: a vision model identifies the car from a photo, reads the odometer, and scores the condition. The part that nearly sank the product was the one that looked easiest. Asking an LLM: "what is this car worth in Nigeria?"&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug: every car costs ₦22 million
&lt;/h2&gt;

&lt;p&gt;The first version did the obvious thing. Collect the car's details, hand them to a fast model (Claude Haiku), and ask for a Nigerian market price. In testing it seemed plausible. Then a pattern emerged that no prompt tweak would kill:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Almost every car priced out between ₦20 and 25 million, regardless of what it was.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A 2020 Land Cruiser Prado, a ₦70 to 95 million car in today's market, came back at ₦21 million. Budget sedans and luxury SUVs drifted toward the same band. The model wasn't hallucinating randomly. It was being wrong in a suspiciously consistent way, and consistent wrongness always has a cause.&lt;/p&gt;

&lt;h2&gt;
  
  
  The root cause: LLMs freeze the economy at training time
&lt;/h2&gt;

&lt;p&gt;Here's the cause, and it's the most useful thing in this article if you're building LLM products for any volatile economy.&lt;/p&gt;

&lt;p&gt;The model's training data was full of Nigerian car prices from when the naira traded around ₦450 to the dollar. By the time I was building, the street rate was around ₦1,500. The naira had lost roughly 70% of its dollar value, and nearly every used car in Nigeria is an import whose price tracks dollars. So the model wasn't guessing badly. It was recalling accurately from a world that no longer existed, and quoting it into one where every number had tripled.&lt;/p&gt;

&lt;p&gt;No prompt fixes this. You can tell the model "the exchange rate is now ₦1,500/$" and it will acknowledge the fact and then keep leaning on thousands of memorized price examples denominated in the old world. Training data isn't a fact the model can bracket off. It's the water the model swims in.&lt;/p&gt;

&lt;p&gt;This generalizes beyond Nigeria and beyond cars. If your product asks an LLM for prices, salaries, rents, or costs in any economy with meaningful inflation or currency movement, the model is answering from the past, with total confidence, in fluent and convincing prose.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix: demote the model from oracle to calculator
&lt;/h2&gt;

&lt;p&gt;The fix was not better prompting. It was changing the model's job description.&lt;/p&gt;

&lt;p&gt;The current architecture works like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Get a real number first.&lt;/strong&gt; Before the LLM sees anything, the system searches the live web for the actual asking prices of that make, model, and year in Nigeria right now. I use Serper (a Google Search API) as the primary engine, with Google Custom Search as fallback, scoped toward Nigerian listing sites. The result becomes the &lt;strong&gt;base price anchor&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Make the anchor mandatory, not advisory.&lt;/strong&gt; The search result is injected into the prompt as a hard constraint: this is the base price, from live data, for a base-model car in good condition at average mileage. Not "for context." The base.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The LLM only applies adjustments.&lt;/strong&gt; The system prompt no longer asks "what is this car worth?" It says, in effect: you are a pricing calculator. Here is the base price from live market data. Apply percentage adjustments for this car's mileage, condition, verified faults, and location, and show your work.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The model went from source of truth to arithmetic-with-judgment, which is what it's actually good at. Depreciation for high mileage, a discount for a rough interior, a premium for a clean one: those relative adjustments are stable knowledge that doesn't expire the way absolute prices do.&lt;/p&gt;

&lt;p&gt;Accuracy went from unusable to defensible in one architectural change.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second bug: the cache that poisoned itself
&lt;/h2&gt;

&lt;p&gt;Live search costs time and API calls, so every resolved price gets saved to a &lt;code&gt;car_price_anchors&lt;/code&gt; table in Supabase. Next time anyone prices a 2018 Lexus RX 350, the anchor is already there. The table is effectively a self-training price database: the more cars users price, the less searching the system needs.&lt;/p&gt;

&lt;p&gt;Then prices went wrong again, and this bug was self-inflicted.&lt;/p&gt;

&lt;p&gt;Early on, when search returned nothing, I let the model's own estimate be saved as an anchor, labeled &lt;code&gt;claude_estimate&lt;/code&gt;. It felt harmless, a placeholder until real data arrived. But on the next request for that car, the pipeline found an existing anchor, skipped the live search, and injected the model's old guess back into the prompt as if it were market data. The model was seasoning its own hallucination and being told it was ground truth. Wrong prices weren't just persisting. They were compounding.&lt;/p&gt;

&lt;p&gt;The fix was one rule with a big lesson inside it: &lt;strong&gt;provenance beats existence.&lt;/strong&gt; Every anchor carries its source, and model-generated anchors are now bypassed at read time, which forces a fresh live search to overwrite them. Trust ranking: manually verified &amp;gt; live search &amp;gt; nothing. A model's own output never re-enters its context labeled as data.&lt;/p&gt;

&lt;p&gt;If you're building anything where LLM outputs get cached and might be fed back in, label the provenance of every stored value and decide explicitly what the model is allowed to see again. Feedback loops in LLM systems don't announce themselves. They just slowly replace your data with the model's opinion of itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  The last line of defense: domain floors
&lt;/h2&gt;

&lt;p&gt;Search results have their own failure modes: a scam listing, a clickbait price, a snippet quoting the wrong trim. So the final guard is dumb and effective: a table of price floors by brand and year. If an anchor claims a 2018 Range Rover Velar costs less than ₦35 million, the anchor is rejected as implausible, because no real Velar in Nigeria does. It's the kind of rule a human dealer applies without thinking, encoded as the system's sanity check.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell anyone building LLM pricing features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LLM knowledge has an expiry date, and prices expire fastest.&lt;/strong&gt; In a stable economy the staleness hides. In a volatile one it's a 3-4x error delivered confidently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retrieval must be mandatory, not decorative.&lt;/strong&gt; If live data is "context," the model can ignore it. Make it the base the model isn't allowed to override.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give the model relative judgments, not absolute recall.&lt;/strong&gt; "Adjust this verified base for mileage and condition" survives inflation. "What does this cost?" does not.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track provenance on everything you cache.&lt;/strong&gt; The moment a model's output can re-enter its own context as "data," you have a poisoning loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep one layer of dumb domain rules.&lt;/strong&gt; Floors and plausibility checks catch the failures your clever layers create.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The next challenge is pricing cars with almost no local listing data at all: Chinese brands like Changan and Chery are entering Nigeria faster than resale data about them exists, so the anchor itself has to be synthesized from new-price and import-cost signals. Thin-data pricing is its own problem, and probably its own article.&lt;/p&gt;

&lt;p&gt;I'm building AutoValue in the open at &lt;a href="https://autovalue.tech" rel="noopener noreferrer"&gt;autovalue.tech&lt;/a&gt;. If you're working on LLM products for markets the training data forgot, I'd genuinely like to compare notes.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>llm</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
