<?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: Pangolinfo</title>
    <description>The latest articles on DEV Community by Pangolinfo (@pangolinfo).</description>
    <link>https://dev.to/pangolinfo</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%2F3256991%2Ffb3f5bf5-283a-4195-8c6f-61a7e7e1174c.png</url>
      <title>DEV Community: Pangolinfo</title>
      <link>https://dev.to/pangolinfo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pangolinfo"/>
    <language>en</language>
    <item>
      <title>I Compared Amazon Data APIs by Field. The Checkboxes Were Lying.</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Fri, 04 Sep 2026 02:51:13 +0000</pubDate>
      <link>https://dev.to/pangolinfo/i-compared-amazon-data-apis-by-field-the-checkboxes-were-lying-2n62</link>
      <guid>https://dev.to/pangolinfo/i-compared-amazon-data-apis-by-field-the-checkboxes-were-lying-2n62</guid>
      <description>&lt;p&gt;Every "best Amazon data API" article has the same table.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vendor | Product Data | Reviews | Search | Price Monitoring
-------|--------------|---------|--------|-----------------
A      | ✓            | ✓       | ✓      | ✓
B      | ✓            | ✓       | ✓      | ✓
C      | ✓            | ✗       | ✓      | ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A and B look identical. You pick based on price, or whoever replied to your email fastest.&lt;/p&gt;

&lt;p&gt;Here is the thing I learned after actually running the comparison: &lt;strong&gt;one of those vendors returned 12 fields. The other returned 58.&lt;/strong&gt; Same checkmark.&lt;/p&gt;

&lt;p&gt;This post is the method I used to find that out. It takes two days, it needs no cooperation from any vendor, and the script is about 90 lines. I am publishing it because the alternative — reading vendor docs and hoping — does not work.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Related: if you have not already, &lt;a href="https://dev.to/pangolinfo/i-stopped-trusting-real-time-claims-after-one-bad-quarter-heres-the-100-call-test-i-run-instead-3223"&gt;I stopped trusting "real-time" claims after one bad quarter&lt;/a&gt; covers the reliability side of this. This post goes one level deeper: not &lt;em&gt;is it fast&lt;/em&gt;, but &lt;em&gt;is it complete&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;One scope note, because it removes an entire category of pointless comparison: everything below is about vendors competing for the same job. The official SP-API is not one of them — its authorisation model binds to a seller account, not to the marketplace, so competitor data and market-level rank were never in scope. That is a design boundary, not a missing feature. If your requirements sit near that line, read &lt;a href="https://dev.to/pangolinfo/amazon-api-vs-web-scraping-stop-choosing-build-both-routes-3p9d"&gt;Amazon API vs Web Scraping: Stop Choosing, Build Both Routes&lt;/a&gt; first; it will tell you whether you need this post at all.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why "supported" is an information black hole
&lt;/h2&gt;

&lt;p&gt;A feature grid uses a binary. Real delivery is continuous, and it has at least three layers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Layer 1: does the schema have the field?        → field coverage
Layer 2: when it has it, does it have a value?  → non-null fill rate
Layer 3: how many records pass both?            → usable record rate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only layer 3 has anything to do with your business. Layers 1 and 2 are diagnostics.&lt;/p&gt;

&lt;p&gt;Let me show you why layer 2 matters more than most people assume.&lt;/p&gt;

&lt;h3&gt;
  
  
  The null field problem
&lt;/h3&gt;

&lt;p&gt;Here is a real response shape, with values removed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coupon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deliveryEstimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Schema validation passes. JSON parsing succeeds. Row count is correct. HTTP status is 200. Your monitoring is green.&lt;/p&gt;

&lt;p&gt;And your coupon dimension is permanently empty.&lt;/p&gt;

&lt;p&gt;I know a team that signed an annual contract after seeing 60+ fields and 92% coverage. Three months in, the coupon column was empty in most reports and delivery estimates were empty in most reports. Both fields existed in the schema. Both were populated less than 15% of the time. On an annual deal, switching mid-term was expensive.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Ten minutes with the script below would have caught it.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  A counterintuitive result
&lt;/h3&gt;

&lt;p&gt;A vendor with 95% coverage and 60% fill rate is usually worth &lt;em&gt;less&lt;/em&gt; to you than one with 70% coverage and 98% fill rate.&lt;/p&gt;

&lt;p&gt;The first hands you a pile of nulls. You write defensive &lt;code&gt;if (x != null)&lt;/code&gt; branches in every downstream consumer, and you still cannot tell "this product has no coupon" from "we failed to fetch the coupon."&lt;/p&gt;

&lt;p&gt;The second hands you records that are solid end to end. Fewer dimensions, every one of them trustworthy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Certain absence beats false completeness.&lt;/strong&gt; Low coverage means "I do not have this dimension," which you can plan around. Low fill rate means "I think I have this dimension and I do not," which produces bad decisions.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three response shapes
&lt;/h2&gt;

&lt;p&gt;Feed the same ASINs to different vendors and responses cluster into three shapes. This taxonomy is more useful than any ranking.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape A — thin, 12–18 fields
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"brand"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"reviewCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1284&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mainImage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bsr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for: price monitoring, basic sourcing. Useless for anything involving variants, promotions, ad placement or seller dimensions.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape B — mid, 30–40 fields, variants but no ad flags
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"parentAsin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V90001"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;69.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"coupon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"amount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;5.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"percentage"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"deliveryEstimate"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1284&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
             &lt;/span&gt;&lt;span class="nl"&gt;"histogram"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"5"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;62&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"4"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;21&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;}},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bsr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Electronics"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1842&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
          &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Portable Audio"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;97&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"256GB"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XR"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"White"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"512GB"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;79.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"seller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"isFulfilledByAmazon"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"images"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"variant"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MAIN"&lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"badges"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"Amazon's Choice"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for: sourcing, variant analysis, rating distribution. Useless for rank attribution, because ads and organic results are indistinguishable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Shape C — complete
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;everything&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;shape&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;B&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;plus:&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"variants"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0C7V9K2XQ"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"attributes"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"color"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Black"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"size"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"256GB"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"original"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;69.99&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"value"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;812&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
     &lt;/span&gt;&lt;span class="nl"&gt;"isBuyBoxWinner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"offerCount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSponsored"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"organicPosition"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"searchContext"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"keyword"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"positionOnPage"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"sponsoredCountAhead"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
                    &lt;/span&gt;&lt;span class="nl"&gt;"adTypesAhead"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"SB"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"SP"&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"priceHistory"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"min90d"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;44.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"max90d"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;69.99&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"subscribeAndSave"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"discount"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"buyBoxWinner"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"seller"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;49.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"shipping"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"FREE"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Good for: rank attribution, ad-competitive analysis, variant-level insight, historical price ranges.&lt;/p&gt;

&lt;p&gt;On a feature grid these three are the same checkmark. In your reporting, you are not buying data — &lt;strong&gt;you are buying which questions you are allowed to ask.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The method
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: write the field contract
&lt;/h3&gt;

&lt;p&gt;Not vendor docs. Your own list, with priorities.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;P0&lt;/strong&gt; — the record is dead without it. Goes to the dead-letter queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P1&lt;/strong&gt; — analysis gets shallower, still works.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;P2&lt;/strong&gt; — nice to have.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;CONTRACT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price.current&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;availability.status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating.value&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating.count&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bsr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;variants&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;parentAsin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price.original&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price.currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coupon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;seller.name&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;offerCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;images&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
           &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;badges&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;isSponsored&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;categoryPath&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Eight to fifteen P0 fields is the sweet spot. The test is one question: &lt;em&gt;if this field were missing, would you dead-letter the record?&lt;/em&gt; Yes means P0.&lt;/p&gt;

&lt;p&gt;Per object, the critical fields differ:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Critical fields&lt;/th&gt;
&lt;th&gt;Why&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;price.current&lt;/code&gt;, &lt;code&gt;bsr&lt;/code&gt;, &lt;code&gt;variants&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;pricing and sourcing fundamentals&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;page&lt;/code&gt;, &lt;code&gt;items[].position&lt;/code&gt;, &lt;code&gt;items[].isSponsored&lt;/code&gt;, &lt;code&gt;items[].adType&lt;/code&gt;, &lt;code&gt;items[].organicRank&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;without the last three, your rank tracker is sorting ads and organic together&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Reviews&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;reviewId&lt;/code&gt;, &lt;code&gt;asin&lt;/code&gt; (child), &lt;code&gt;parentAsin&lt;/code&gt;, &lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;rating&lt;/code&gt;, &lt;code&gt;date&lt;/code&gt;, &lt;code&gt;verifiedPurchase&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;variant&lt;/code&gt; decides whether insight can be pinned to a configuration&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  Step 2: the flatten function (this is the whole trick)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Flatten to a.b.c paths; sample list items; empty values are dropped.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&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="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;[]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;obj&lt;/span&gt;          &lt;span class="c1"&gt;# &amp;lt;-- the entire trick
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last &lt;code&gt;elif&lt;/code&gt; is the whole method. &lt;strong&gt;Empty values never enter the result.&lt;/strong&gt; So when you later ask "which contract fields came back," you are asking "which contract fields came back &lt;em&gt;with a value&lt;/em&gt;."&lt;/p&gt;

&lt;p&gt;One function, two metrics. Coverage comes from the count. Fill rate is handled implicitly — the &lt;code&gt;coupon: null&lt;/code&gt; case from earlier simply never lands in &lt;code&gt;out&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: evaluate and aggregate
&lt;/h3&gt;



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

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cov_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;flat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONTRACT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;CONTRACT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;present&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;contract&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flat&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;contract&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;p0_ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flat&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CONTRACT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# P0 must be 100% populated
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;coverage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p0_ok&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;coverage&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;cov_threshold&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;spend_usd&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nf"&gt;evaluate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;records&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;usable&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usable&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usable_rate&lt;/span&gt;&lt;span class="sh"&gt;"&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="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;avg_coverage&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;coverage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ev&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;cost_per_1k_usable&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;spend_usd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;usable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                              &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;usable&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&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;p0_ok&lt;/code&gt; uses &lt;code&gt;all()&lt;/code&gt;, not a ratio. P0 is a hard constraint — half present is not usable.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 4: classify failures
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;classify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;_http_status&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;hard&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# visible, retry it
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;soft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;        &lt;span class="c1"&gt;# 200 but empty body
&lt;/span&gt;    &lt;span class="n"&gt;flat&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;flat&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;CONTRACT&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;P0&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;partial&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# SILENT — the expensive one
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ok&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&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;Type&lt;/th&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Visibility&lt;/th&gt;
&lt;th&gt;Cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Hard&lt;/td&gt;
&lt;td&gt;non-200, timeout&lt;/td&gt;
&lt;td&gt;Fully visible&lt;/td&gt;
&lt;td&gt;Low&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Soft&lt;/td&gt;
&lt;td&gt;200, empty body&lt;/td&gt;
&lt;td&gt;Half visible&lt;/td&gt;
&lt;td&gt;Medium&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Partial&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;200, valid structure, P0 null&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Silent&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Highest&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stale&lt;/td&gt;
&lt;td&gt;values present but old&lt;/td&gt;
&lt;td&gt;Silent&lt;/td&gt;
&lt;td&gt;High&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Row three is why this post exists. It raises no error, trips no alert, and generates no billing dispute. It just quietly degrades your reporting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 5: what the output looks like
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Vendor A ===
  samples            : 300
  avg coverage       : 0.94
  usable rate        : 61.33%
  cost per 1k usable : $12.40
  failures           : {'ok': 184, 'partial': 97, 'soft': 14, 'hard': 5}

=== Vendor B ===
  samples            : 300
  avg coverage       : 0.71
  usable rate        : 92.67%
  cost per 1k usable : $4.85
  failures           : {'ok': 278, 'partial': 12, 'soft': 8, 'hard': 2}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;A has much better coverage and two-thirds the usable rate, at 2.5x the cost per usable record.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you only look at coverage — or at a feature grid — you pick A. Look at usable rate and the conclusion inverts.&lt;/p&gt;

&lt;p&gt;Note vendor A's 97 &lt;code&gt;partial&lt;/code&gt; records. Measured the traditional way, A's success rate is (184 + 97 + 14) / 300 = 98.3%. Looks excellent. With a field contract applied, real usable rate is 61%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;That gap is exactly where conventional monitoring fails. It confuses "returned" with "usable."&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Sample design: why your test showed no differences
&lt;/h2&gt;

&lt;p&gt;When someone tells me "I tested four vendors and they were all the same," the sample set is almost always the problem.&lt;/p&gt;

&lt;p&gt;The most common mistake: testing fifteen bestseller ASINs. Those products have the richest data on the platform, so every vendor returns everything and all four score 100%.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You measured Amazon's data completeness for bestsellers, not the vendors' capability.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A discriminating sample has five kinds of record:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Across categories&lt;/strong&gt; — four or more top-level categories. Page templates differ, field availability follows.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Across marketplaces&lt;/strong&gt; — US plus at least two non-US. This is the direct test for the "US-only field" surprise.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Heavy variants&lt;/strong&gt; — 20+ variants, to probe variant depth.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Weak-data products&lt;/strong&gt; — new listings with no reviews, long-term out of stock, no Buy Box. &lt;strong&gt;This is the litmus test for fill rate.&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep pages&lt;/strong&gt; — for search, go through page five.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two hundred to five hundred records per vendor is enough. Below that, noise dominates.&lt;/p&gt;

&lt;p&gt;Four execution disciplines — break any one and the numbers stop being comparable:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;same ASINs
same time window    &amp;lt;- compress into hours; prices and stock move
same concurrency
same retry policy   &amp;lt;- easiest one to get wrong
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That last one: give vendor A three retries and vendor B one, and A's usable rate is inflated while its real cost is triple. &lt;strong&gt;You are not measuring the vendor, you are measuring your retry policy.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Five places where vendors actually diverge
&lt;/h2&gt;

&lt;p&gt;After running this repeatedly, differentiation concentrates in five spots. If you are short on time, look here first.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Ad placement flags and organic rank
&lt;/h3&gt;

&lt;p&gt;The most discriminating item. Vendors carrying &lt;code&gt;isSponsored&lt;/code&gt; and &lt;code&gt;organicRank&lt;/code&gt; are a minority; most return only page position.&lt;/p&gt;

&lt;p&gt;Without them, the rank metric is systematically distorted. A team tracked a core keyword at a steady position three for four months, concluded organic had plateaued, and prepared to raise ad spend. Recomputed with &lt;code&gt;isSponsored&lt;/code&gt; applied, the truth was organic position one with two competitors' Sponsored Brands units above it. Organic had been improving the whole time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One boolean field put a quarter of budget decisions on an inverted premise.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Variant-level depth
&lt;/h3&gt;

&lt;p&gt;Many vendors ship a &lt;code&gt;variants&lt;/code&gt; array containing nothing but ASINs — no per-variant price, stock or rating. That tells you five variants exist without telling you which is selling, which is out of stock, or which is losing rating.&lt;/p&gt;

&lt;p&gt;A review analysis once produced a high-confidence finding that users complain about battery life. It could not be actioned. Adding &lt;code&gt;variant&lt;/code&gt; localised it to one capacity tier — concentrated in the low-spec model, near zero in the high-spec one. The response changed from "redesign the battery across the range" to "fix spec copy on the low tier." Cost fell by an order of magnitude.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Variant attribution is the field that takes an insight from plausible to executable.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Deep search pages
&lt;/h3&gt;

&lt;p&gt;Little difference across the first two pages. Sharp divergence from page three: some vendors return duplicates, some return empty.&lt;/p&gt;

&lt;p&gt;Consequence: competitive density is decided in the deep pages. Seeing only two pages makes you systematically underestimate competition. And when you can only see two pages, "this market is not competitive" describes your &lt;em&gt;observation range&lt;/em&gt;, not the market.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Promotions and coupons
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;coupon&lt;/code&gt;, &lt;code&gt;promotions&lt;/code&gt; and Subscribe &amp;amp; Save discounts are usually thin. But they matter enormously for pricing — list price alone systematically overstates what competitors transact at.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Seller and Buy Box detail
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;offerCount&lt;/code&gt;, &lt;code&gt;buyBoxWinner&lt;/code&gt; and seller ratings are core metrics in heavily resold categories, and absent from many responses entirely.&lt;/p&gt;




&lt;h2&gt;
  
  
  Billing: why entry price tells you nothing
&lt;/h2&gt;

&lt;p&gt;Four billing models, not comparable to each other:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Headline&lt;/th&gt;
&lt;th&gt;Reality&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Per request&lt;/td&gt;
&lt;td&gt;Cheapest, most seductive&lt;/td&gt;
&lt;td&gt;Failed requests bill too; deep pages and retries multiply&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per successful result&lt;/td&gt;
&lt;td&gt;Middle&lt;/td&gt;
&lt;td&gt;200 with missing fields still bills — you pay full price for a broken record&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Per record&lt;/td&gt;
&lt;td&gt;Impossible to rank by eye&lt;/td&gt;
&lt;td&gt;Only comparable as cost per thousand usable records&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monthly quota&lt;/td&gt;
&lt;td&gt;Priciest, least friction&lt;/td&gt;
&lt;td&gt;Overage rates are steep&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A vendor billing per request can quote a third of one billing per record and cost more in practice, because deep pages fail, retries are frequent, and failures bill anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The only comparable number is cost per thousand usable records.&lt;/strong&gt; Do not do cost accounting on the vendor's billing basis — do it on your own usable record count. The multiple between the two is precisely the cost of silent failures and retries.&lt;/p&gt;




&lt;h2&gt;
  
  
  Make regression routine
&lt;/h2&gt;

&lt;p&gt;A one-off acceptance test proves it works now. Vendor coverage drifts: page redesigns, policy changes, capacity shifts.&lt;/p&gt;

&lt;p&gt;Run a fixed sample weekly — fifty to a hundred records is plenty and cheap — and trend four numbers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;median latency / p95 latency / usable record rate / average coverage
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Chart them, and chart the failure classification as a stacked area. A rising &lt;code&gt;partial&lt;/code&gt; share is the most valuable early warning signal you can have.&lt;/p&gt;




&lt;h2&gt;
  
  
  The checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Field contract written down&lt;/strong&gt;, P0/P1/P2 agreed with the business side — not invented by an engineer.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sample covers all five record types.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Four or more vendors, identical terms.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All four numbers in hand&lt;/strong&gt; — coverage, fill rate, usable rate, cost per 1k usable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failures classified&lt;/strong&gt;, &lt;code&gt;partial&lt;/code&gt; share confirmed. This is the only class needing dedicated code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Quotas modelled&lt;/strong&gt; per operation type, with backoff tested against real limits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Regression scheduled&lt;/strong&gt; weekly, four numbers trended.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Item seven is the easiest to skip and the most valuable.&lt;/p&gt;




&lt;h2&gt;
  
  
  Two questions that end the sales conversation
&lt;/h2&gt;

&lt;p&gt;Once the contract is written, there are two questions worth asking every vendor before signing anything. They are not gotcha questions. They are diagnostic, and the shape of the answer tells you more than the content does.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Can you send me the field-level schema for the product object, not the object list?"&lt;/strong&gt; Most vendors will send a page saying "product data, search data, review data." Push once. If they can send an actual field dictionary, you just saved two days of testing. If they cannot — or if what comes back is clearly a marketing page with field names sprinkled on it — you have learned something about how they think about delivery.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"If a P0 field comes back null for a week, what happens?"&lt;/strong&gt; Listen for whether they have a category for that. Vendors who monitor fill rate answer immediately, because they already have the dashboard. Vendors who only monitor HTTP status pause. That pause is the answer.&lt;/p&gt;

&lt;p&gt;Neither question is hostile. Both are what a serious buyer asks, and a vendor who handles them well is showing you they have been through this before.&lt;/p&gt;

&lt;h2&gt;
  
  
  The retry multiplier nobody puts in the pricing calculator
&lt;/h2&gt;

&lt;p&gt;Here is an arithmetic that reorders rankings more often than any feature comparison does.&lt;/p&gt;

&lt;p&gt;Say vendor A charges \$0.001 per request and vendor B charges \$0.0025 per record. On paper A looks 2.5x cheaper. Now add behaviour. A has an 82% usable rate on your sample, so you retry failures twice, and failed attempts still bill because the billing unit is the request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;effective_cost = unit_price × attempts_per_usable_record / usable_rate

A: 0.0010 × (1 + 2 × 0.18) / 0.82 = 0.00166
B: 0.0025 × 1.00            / 0.97 = 0.00258
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A still wins, but by 1.55x rather than 2.5x. Now change one assumption. Suppose A's deep-page coverage is weak, so the pages you actually care about need three retries and the usable rate on page 3+ drops to 60%:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;A (pages 3+): 0.0010 × (1 + 3 × 0.40) / 0.60 = 0.00367
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now A costs more than B, on precisely the task you bought it for. Nothing on the pricing page changed. Only the denominator did.&lt;/p&gt;

&lt;p&gt;This is why I stopped normalising list prices and started computing cost per thousand usable records on my own sample. It takes an afternoon, and it is the only number that survives contact with production.&lt;/p&gt;

&lt;h2&gt;
  
  
  Catching staleness before it reaches your dashboard
&lt;/h2&gt;

&lt;p&gt;Silent failure is the expensive class and staleness is its quietest variant. A field returning a plausible but frozen value passes every schema check you can write. Here is the smallest useful detector — it watches for values that stop moving:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timedelta&lt;/span&gt;

&lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;# field_path -&amp;gt; [(ts, value)]
&lt;/span&gt;&lt;span class="n"&gt;STALE_AFTER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;timedelta&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;days&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;14&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;observe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;flatten&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;stale_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;STALE_AFTER&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;utcnow&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;recent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;obs&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;window&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;                                  &lt;span class="c1"&gt;# not enough signal yet
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;recent&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run it weekly over a fixed sample. A price field that has not moved in two weeks across fifty ASINs is not a stable market — it is a frozen parser. The distinction matters, and no status code will tell you which one you are looking at.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two ways to accidentally measure the wrong thing
&lt;/h2&gt;

&lt;p&gt;Two production realities will distort every number in this article if you ignore them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Concurrency changes your success rate.&lt;/strong&gt; If you run the evaluation at 50 parallel requests and the vendor throttles at 20, the failures you observe are yours, not theirs. You will record a low usable rate that has nothing to do with field coverage, and you will carry that error into the vendor comparison. Run the test at the concurrency you actually plan to use in production, and record that number alongside the results. A measurement you cannot repeat is an anecdote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caches hide staleness.&lt;/strong&gt; If responses are cached, a field can look fresh because you are being served last week's copy. A weekly regression run against a cached endpoint will report perfect stability right up until the cache expires and the parser turns out to have been broken for a month. Either bypass the cache for the measurement run, or record the cache TTL as part of the result and compare like with like — one vendor's five-minute TTL and another's twenty-four-hour TTL are not the same product, whatever the fill rates say.&lt;/p&gt;

&lt;p&gt;Both are easy to miss for the same reason: both make the numbers look &lt;em&gt;better&lt;/em&gt; than reality. Be suspicious of measurements that come out clean.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The core idea is one sentence: &lt;strong&gt;write down what you need first, then see who can deliver it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Most people do the reverse — they read the vendor docs and reverse-engineer requirements from them. The docs lead, you buy a pile of fields nobody uses, and the one field you needed is missing.&lt;/p&gt;

&lt;p&gt;Ninety lines of Python, two days, four vendors. That is a much better deal than two weeks of reading marketing pages and emailing sales teams for a field dictionary.&lt;/p&gt;

&lt;p&gt;If you want to run this against real responses: &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; covers product and search objects, &lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt; covers reviews, and &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; is the agent-facing path. &lt;a href="https://tool.pangolinfo.com/?referrer=devto" rel="noopener noreferrer"&gt;Grab a key from the console&lt;/a&gt; and run the test; docs are at the &lt;a href="https://docs.pangolinfo.com/en-help-center/mcp/amazon-insight/overview?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One last thing, and I mean it: &lt;strong&gt;run this on us too.&lt;/strong&gt; Completeness numbers a vendor reports about itself — ours included — deserve to be verified.&lt;/p&gt;

&lt;p&gt;If you have run something like this, I would be curious which failure class dominated for you. In my experience &lt;code&gt;partial&lt;/code&gt; is the one nobody budgets for.&lt;/p&gt;

</description>
      <category>api</category>
      <category>data</category>
    </item>
    <item>
      <title>Amazon API vs Web Scraping: Stop Choosing. Build Both Routes.</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Wed, 02 Sep 2026 06:28:55 +0000</pubDate>
      <link>https://dev.to/pangolinfo/amazon-api-vs-web-scraping-stop-choosing-build-both-routes-3p9d</link>
      <guid>https://dev.to/pangolinfo/amazon-api-vs-web-scraping-stop-choosing-build-both-routes-3p9d</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27sdyflh3hq2f7srbcuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F27sdyflh3hq2f7srbcuz.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every team integrating Amazon data eventually hits the same debate: use the official SP-API, or scrape?&lt;/p&gt;

&lt;p&gt;The debate never resolves cleanly, and it never will, because the question is wrong. It assumes the two are alternative implementations of the same thing, so the comparison collapses into which is more compliant, cheaper or more stable. That framing guarantees a bad decision.&lt;/p&gt;

&lt;p&gt;Here is the actual situation: &lt;strong&gt;they retrieve two different classes of data.&lt;/strong&gt; The official SP-API gives you your account. Public pages give you the market. Once you internalise that, the route debate disappears and what remains is a concrete architectural division of labour.&lt;/p&gt;

&lt;p&gt;This post covers the authorization boundary, the public-versus-private line, what SP-API throttling actually costs in engineering time, and a hybrid architecture with runnable code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The two domains
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Category&lt;/th&gt;
&lt;th&gt;Nature&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product facts (title, price, rating, variants, BSR)&lt;/td&gt;
&lt;td&gt;Publicly visible&lt;/td&gt;
&lt;td&gt;Public collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search and ads (keyword, rank, ad type, creative)&lt;/td&gt;
&lt;td&gt;Publicly visible&lt;/td&gt;
&lt;td&gt;Public collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review content (rating, body, date, variant)&lt;/td&gt;
&lt;td&gt;Publicly visible&lt;/td&gt;
&lt;td&gt;Public collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ranks and categories&lt;/td&gt;
&lt;td&gt;Publicly visible&lt;/td&gt;
&lt;td&gt;Public collection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Orders and fulfillment&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API (authorized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buyer information&lt;/td&gt;
&lt;td&gt;Account-private + personal&lt;/td&gt;
&lt;td&gt;SP-API (restricted PII role)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Inventory and settlement&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API (authorized)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Own ad performance&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API (authorized)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The route follows from the nature of the data, not from preference. This is also why "can we just use the official API" is answered no in most teams — you need market data, and it sits outside the official interface's authorization scope.&lt;/p&gt;

&lt;p&gt;You would not use the SP-API to check a competitor's price, because it is not designed to expose that. You would not scrape your own orders, because they sit behind login and are account-private.&lt;/p&gt;

&lt;p&gt;So the useful question is not "which one". It is: &lt;strong&gt;which of my requirements are account-domain and which are market-domain?&lt;/strong&gt; For most teams the answer is both.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayv1ea5cmi7qjo7ibst3.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fayv1ea5cmi7qjo7ibst3.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Where each route gets its legitimacy
&lt;/h2&gt;

&lt;p&gt;Compliance discussions slide into an unanswerable "which is more legal". The two routes draw legitimacy from entirely different places, and what determines compliance is what you collect, not which method you use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The SP-API is legitimate through an agreement chain&lt;/strong&gt;: developer registration, seller authorization via Login with Amazon, role-based access granting minimum necessary scope. Operations touching personally identifiable information require a separate restricted role and review. What you can violate is the developer agreement and data protection policy, and the consequence is revoked authorization. The boundary is clear but narrow — only what a seller has actively authorized your application to access.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public page collection has no contract.&lt;/strong&gt; It sits under the platform's conditions of use, robots directives and rate limits. That is not the same as prohibition; publicly visible, non-personal page information can generally be collected, which is long-standing practice.&lt;/p&gt;

&lt;p&gt;Risk rises with three specific things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Login-gated content&lt;/strong&gt; — anything visible only after signing in has clearly crossed the public line.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Personal data&lt;/strong&gt; — buyer names, addresses and contact details are governed by data protection law.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Aggressive request rates&lt;/strong&gt; — traffic heavy enough to affect site operation shifts the activity from reading public information to interfering with a service.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The one-line test: &lt;strong&gt;is the data publicly visible and non-personal?&lt;/strong&gt; The method is irrelevant. Misusing PII through the official interface is still a violation; collecting a fully public page can still be compliant.&lt;/p&gt;

&lt;p&gt;Here is what structured public data looks like. Every field is public and non-personal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"asin"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"B0CXYZ1234"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"amazon.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stainless Steel Insulated Water Bottle, 32 oz"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"price"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"current"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;34.99&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"currency"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"USD"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"listPrice"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;44.99&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rating"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"average"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;12847&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"bsr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"category"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sports &amp;amp; Outdoors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"rank"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"availability"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"In Stock"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sponsored"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"fetchedAt"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-08-30T10:22:41Z"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No buyer information, nothing requiring seller authorization. That is the compliance basis. By contrast, orders, buyer addresses and settlement detail never appear on public pages — they are only reachable through the SP-API after explicit authorization.&lt;/p&gt;

&lt;h2&gt;
  
  
  What SP-API throttling actually costs
&lt;/h2&gt;

&lt;p&gt;This is the part teams consistently underestimate: &lt;strong&gt;even though the official interface is free, throttling converts into engineering complexity.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The SP-API uses a token bucket model. Each operation has its own request rate and quota, and quota refills continuously at that rate. For some operations, initial quota and refill rate also scale with seller business volume. Three consequences:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Quotas are independent per operation.&lt;/strong&gt; Estimating on "total call volume" is always wrong. &lt;code&gt;getOrders&lt;/code&gt; and &lt;code&gt;getInventorySummaries&lt;/code&gt; are separate buckets and need separate models. This is the single most common design mistake.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Being throttled is normal, not exceptional.&lt;/strong&gt; At high polling frequency it is close to inevitable. The correct response is queueing with backoff, not letting it throw and kill the job. Treating 429 as an error also destroys your alerting signal — the team learns to ignore it, and then the real failure goes unnoticed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Values change.&lt;/strong&gt; Amazon adjusts rates and quotas. Verify current figures in the official documentation rather than reusing numbers from older docs. I have watched a team design quotas from a year-old document and get throttled into the ground on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The code
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A per-operation token bucket
&lt;/h3&gt;



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


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Per-operation token bucket.

    rate:      tokens refilled per second (SP-API restore rate)
    capacity:  bucket size (SP-API maximum quota / burst)
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;capacity&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Lock&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Block until tokens are available. Returns seconds waited.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;waited&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;capacity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_updated&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_updated&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt;
                    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;waited&lt;/span&gt;
                &lt;span class="n"&gt;deficit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tokens&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tokens&lt;/span&gt;
                &lt;span class="n"&gt;sleep_for&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;deficit&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt;
            &lt;span class="c1"&gt;# sleep outside the lock so other threads are not blocked
&lt;/span&gt;            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sleep_for&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;waited&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;sleep_for&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;OperationLimiter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Maintains an independent bucket per SP-API operation.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;TokenBucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;capacity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;op&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cfg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_buckets&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Calls with backoff
&lt;/h3&gt;



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

&lt;span class="n"&gt;RETRYABLE_STATUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;429&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;502&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="mi"&gt;504&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_sp_api&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_retries&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;SP-API call with token bucket queueing and exponential backoff.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_retries&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;limiter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;acquire&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# queue for a token first
&lt;/span&gt;        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RequestException&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;network: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRYABLE_STATUS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Retry-After&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retry_after&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;retry_after&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;attempt&lt;/span&gt;
            &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;delay&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;uniform&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="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;last_error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="c1"&gt;# 4xx other than 429: parameter or auth problem, retrying is pointless
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;http &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; (not retryable): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;exhausted retries: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;last_error&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two details that matter. &lt;strong&gt;Prefer the server's &lt;code&gt;Retry-After&lt;/code&gt; header&lt;/strong&gt; over your own backoff — it is more accurate. And &lt;strong&gt;only retry 429 and 5xx&lt;/strong&gt;; a 400, 401 or 403 is a parameter or authorization problem that will fail identically a hundred times while burning quota and hiding the real error.&lt;/p&gt;

&lt;h3&gt;
  
  
  The unified ingestion layer
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;IngestResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;failure_type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;   &lt;span class="c1"&gt;# throttled | blocked | incomplete | request_error
&lt;/span&gt;    &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UnifiedIngestion&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Converges SP-API and public collection into one internal model.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field_map&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required_fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field_map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field_map&lt;/span&gt;      &lt;span class="c1"&gt;# external field -&amp;gt; internal field
&lt;/span&gt;        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;required_fields&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Allowlist normalization: keep only declared internal fields.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst_key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;field_map&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;src_key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{}):&lt;/span&gt;
                &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dst_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;src_key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ingest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Callable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;IngestResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;fetcher returns (payload, error); non-empty error means request failure.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;IngestResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;fetcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;throttled&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;429&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;request_error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

        &lt;span class="n"&gt;record&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;normalize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# parsed fine but fields absent: a coverage problem, not a request problem
&lt;/span&gt;            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;failure_type&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;incomplete&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The critical design choice is &lt;strong&gt;three separate failure classes&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;throttled&lt;/code&gt; / &lt;code&gt;request_error&lt;/code&gt; — request layer; look at quota and backoff&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;blocked&lt;/code&gt; — a 200 that is not a real page (public collection side)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;incomplete&lt;/code&gt; — parsed fine, required field missing (a coverage problem)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These have completely different fixes. Merge them into one error log and you cannot tell whether to adjust quotas, lower concurrency or chase coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Detecting blocked responses at the content layer
&lt;/h3&gt;

&lt;p&gt;This is the most expensive gap on the public collection side. &lt;strong&gt;A blocked page returns HTTP 200.&lt;/strong&gt; Captcha interstitials, bot-detection pages and pages that never finished rendering all come back green. If your check is the status code, that bad data enters your warehouse wearing a success badge.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;BLOCK_MARKERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;captcha&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;robot check&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;enter the characters you see&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;automated access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;to discuss automated access&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;looks_blocked&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Content-layer block detection. Status code alone is not enough.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;lowered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;20000&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;BLOCK_MARKERS&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lowered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# markers present, or page implausibly short for a real product page
&lt;/span&gt;    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hits&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wire this into &lt;code&gt;UnifiedIngestion&lt;/code&gt; and blocked responses get classified as &lt;code&gt;blocked&lt;/code&gt; instead of silently becoming data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compliance as code, not as a wiki page
&lt;/h2&gt;

&lt;p&gt;Most teams keep compliance requirements in a document and enforce nothing in code. The result is "the doc says don't collect that, the code has been collecting it for months" — which is the worst position to be in during an audit.&lt;/p&gt;

&lt;p&gt;Four constraints worth hardcoding:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A field allowlist.&lt;/strong&gt; Keep only fields your internal model declares; drop everything else. Note that &lt;code&gt;normalize()&lt;/code&gt; above is already allowlist-based rather than denylist-based, so even if an upstream response starts carrying personal data, it never lands in your store.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Path constraints.&lt;/strong&gt; Collection jobs may only request public paths. Account pages and login redirects should be rejected and alerted on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hard rate ceiling&lt;/strong&gt; at the job layer, so one runaway job cannot push overall frequency into "interfering with a service" territory.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Audit logging&lt;/strong&gt; of source, time and field list per collection, plus a retained &lt;code&gt;fetchedAt&lt;/code&gt; on every record. Without a capture timestamp a record cannot enter a time series and cannot be audited later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freshness is a budget decision
&lt;/h2&gt;

&lt;p&gt;Having argued the official API is not free, the opposite error deserves equal attention: running every collection job at maximum frequency.&lt;/p&gt;

&lt;p&gt;Field change rates vary enormously. Brand, category and variant dimensions barely move. Price, stock, Buy Box and sponsored placements move constantly. Treating them identically is the most common form of budget waste in this category.&lt;/p&gt;

&lt;p&gt;Three tiers work in practice:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High frequency (minutes to hours)&lt;/strong&gt; — price, stock, Buy Box holder, sponsored placements, BSR. For these, a stale value does not weaken the insight, it invalidates it. A competitor's price from yesterday cannot support a pricing decision today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily&lt;/strong&gt; — new reviews, rating changes, seller lists, best seller ranks. Trend signals that matter over weeks, not minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly or on demand&lt;/strong&gt; — brand, category, variant dimensions, images, A+ content.&lt;/p&gt;

&lt;p&gt;The top 20% of ASINs drive roughly 80% of decisions. Run those hot and the long tail cold and cost typically halves with almost no business impact. No vendor negotiation required — it is a scheduling decision in your own ingestion layer.&lt;/p&gt;

&lt;p&gt;Implement it there rather than hoping a provider does it, and revisit it periodically. A tiering decision made eighteen months ago is usually wrong today.&lt;/p&gt;

&lt;p&gt;When comparing prices on the collection side, use &lt;strong&gt;cost per thousand usable records&lt;/strong&gt;, not per thousand requests. Failed, blocked and field-incomplete responses all bill. A plan that is 25% cheaper on the rate card is often only about 10% cheaper in reality.&lt;/p&gt;

&lt;p&gt;And for the build decision, convert engineering hours to money first. A maintenance load of 0.3 engineer-days a month sounds trivial; priced at real internal cost over a year it frequently exceeds the API bill for a mid-sized volume — and unlike the bill, it recurs every quarter and grows with every page redesign.&lt;/p&gt;

&lt;h2&gt;
  
  
  What good looks like
&lt;/h2&gt;

&lt;p&gt;Median latency under five seconds for on-demand fetches. p95 no more than roughly twice the median. Success rate above 98% measured at the content layer, not the status code. Field completeness above 95% against your own list.&lt;/p&gt;

&lt;p&gt;Those are not laws of nature. They are the range where teams stop maintaining the data layer and start building on it. Below that range, the data layer becomes the most expensive dependency you have — not because of the invoice, but because of everything your engineers stop building while they babysit it.&lt;/p&gt;

&lt;p&gt;Two fields worth treating as non-negotiable:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetchedAt&lt;/code&gt;.&lt;/strong&gt; Without a capture timestamp, a record cannot enter a time series and cannot be audited. Three months later, when a price looks wrong, you cannot tell whether it was wrong then or got mangled in processing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;isSponsored&lt;/code&gt;.&lt;/strong&gt; Without it, organic rank and ad placement are indistinguishable. You think you rank third; the top two slots are ads; your true organic position is first. Every decision built on that number optimises for the wrong target.&lt;/p&gt;

&lt;p&gt;Sponsored is also the hardest object to collect reliably, which is precisely why it needs its own evaluation line rather than an assumption that every provider has it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision table
&lt;/h2&gt;

&lt;p&gt;Skip the route debate and read your task off this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your task&lt;/th&gt;
&lt;th&gt;Domain&lt;/th&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Sync your own orders and fulfillment&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Manage inventory and settlements&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Monitor competitor price and stock&lt;/td&gt;
&lt;td&gt;Public market&lt;/td&gt;
&lt;td&gt;Public collection / specialized API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Track keyword organic rank&lt;/td&gt;
&lt;td&gt;Public market&lt;/td&gt;
&lt;td&gt;Public collection / specialized API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Analyse competitor ad placements&lt;/td&gt;
&lt;td&gt;Public market&lt;/td&gt;
&lt;td&gt;Public collection / specialized API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Run review insights&lt;/td&gt;
&lt;td&gt;Public market&lt;/td&gt;
&lt;td&gt;Public collection / specialized API&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Need both orders and competitor monitoring&lt;/td&gt;
&lt;td&gt;Both&lt;/td&gt;
&lt;td&gt;Hybrid architecture&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Only your own operating data&lt;/td&gt;
&lt;td&gt;Account-private&lt;/td&gt;
&lt;td&gt;SP-API only&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That last row deserves emphasis: &lt;strong&gt;if your requirements stop at your own operations, do not add public collection.&lt;/strong&gt; Architectural complexity should match problem complexity. Converging two channels costs real money, and it is only worth paying when requirements genuinely span both domains.&lt;/p&gt;

&lt;h2&gt;
  
  
  One operational trap
&lt;/h2&gt;

&lt;p&gt;A specific misdiagnosis shows up constantly in hybrid pipelines.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Symptom:&lt;/strong&gt; volume for a field drops, or a window of data has holes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First instinct:&lt;/strong&gt; data quality degraded. So the team goes to the vendor, checks parsers, digs through logs. Frequently the real cause is quota — the token bucket saturated, jobs queued, and some timed out and were dropped.&lt;/p&gt;

&lt;p&gt;Distinguishing them is easy if you classified failures: if &lt;code&gt;throttled&lt;/code&gt; is climbing while &lt;code&gt;blocked&lt;/code&gt; and &lt;code&gt;incomplete&lt;/code&gt; are flat, it is a quota problem, not a data problem. The fixes are entirely different — tiering and scheduling for the former, coverage and parsing for the latter.&lt;/p&gt;

&lt;p&gt;And do not alert on 429 directly. Record it as a &lt;code&gt;throttled&lt;/code&gt; counter and watch the trend; alert only when the share crosses a sustained threshold. Otherwise the team learns to ignore the alert, and then the real failure goes unseen.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to build nothing
&lt;/h2&gt;

&lt;p&gt;Worth stating the inverse, since architecture posts have a bias toward building.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Requirements stop at your own operations&lt;/strong&gt; — orders, inventory, your listings, your ads. Use the SP-API and stop there. Public collection buys coverage you will never use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You need a one-off answer&lt;/strong&gt; — a category survey, a competitor list for a deck. Do not build a pipeline. Pull the data and move on. Infrastructure built for a single question tends to outlive the question.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Volume is tiny and fields are narrow&lt;/strong&gt; — thirty ASINs, four fields, weekly. Write a script. The break-even arithmetic only crosses once maintenance becomes recurring work, and below that line simpler is genuinely better.&lt;/p&gt;

&lt;p&gt;Hybrid is correct when requirements span both domains. Verify that condition before committing engineering time. A decision framework is only half useful if it cannot also tell you not to build.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;The ordering matters more than any individual decision:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Split requirements into account-domain and market-domain.&lt;/li&gt;
&lt;li&gt;Build the unified ingestion layer before either pipeline.&lt;/li&gt;
&lt;li&gt;Model SP-API throttling per operation; retry only 429 and 5xx.&lt;/li&gt;
&lt;li&gt;Detect &lt;code&gt;blocked&lt;/code&gt; at the content layer, never on status code alone.&lt;/li&gt;
&lt;li&gt;Classify failures three ways and put the ratios on a dashboard.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For the market side, &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; and &lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt; cover the public objects; &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; is the path when an agent calls data directly. Keys come from the &lt;a href="https://tool.pangolinfo.com/?referrer=devto" rel="noopener noreferrer"&gt;console&lt;/a&gt;, and the &lt;a href="https://docs.pangolinfo.com/en-help-center/mcp/amazon-insight/overview?referrer=devto" rel="noopener noreferrer"&gt;MCP docs&lt;/a&gt; cover the agent path.&lt;/p&gt;

&lt;p&gt;The full comparison, including the authorization boundary and decision table: &lt;a href="https://www.pangolinfo.com/amazon-api-vs-web-scraping/?referrer=devto" rel="noopener noreferrer"&gt;Amazon API vs Web Scraping&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;One more note on why the classification approach is worth adopting: it ends arguments that otherwise recur. Framed as compliance versus pragmatism, this debate never resolves, because nobody changes values in a meeting. Framed as "is this data public and non-personal", it resolves in half an hour.&lt;/p&gt;

&lt;p&gt;If you are running a hybrid pipeline already, I would be curious which of the three failure classes dominates for you. In my experience &lt;code&gt;incomplete&lt;/code&gt; is the one nobody budgets for.&lt;/p&gt;

</description>
      <category>api</category>
      <category>scraper</category>
      <category>amazondata</category>
    </item>
    <item>
      <title>I Stopped Trusting "Real-Time" Claims After One Bad Quarter. Here's the 100-Call Test I Run Instead.</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Tue, 01 Sep 2026 03:32:20 +0000</pubDate>
      <link>https://dev.to/pangolinfo/i-stopped-trusting-real-time-claims-after-one-bad-quarter-heres-the-100-call-test-i-run-instead-3223</link>
      <guid>https://dev.to/pangolinfo/i-stopped-trusting-real-time-claims-after-one-bad-quarter-heres-the-100-call-test-i-run-instead-3223</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0smwh94nzz8stdnk8owq.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F0smwh94nzz8stdnk8owq.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A few years ago I integrated an Amazon data provider into a pricing service. The sales page said "real-time data". The docs said "high success rate". Both were technically true and both were useless.&lt;/p&gt;

&lt;p&gt;What I eventually learned: the pricing service was reading from a cache refreshed once a day, and "high success rate" counted HTTP 200 responses, which included a meaningful number of captcha pages. We shipped two weeks of pricing recommendations built partly on blocked page responses, because our client only checked the status code.&lt;/p&gt;

&lt;p&gt;This post is the test I wish I had run first. It takes an afternoon, it is about a hundred lines of Python, and it turns every vague vendor claim into four numbers you can put on a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four numbers that actually matter
&lt;/h2&gt;

&lt;p&gt;Before the code, the definitions. These four are the difference between a provider you can build on and a provider you have to babysit.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Definition&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Median latency&lt;/td&gt;
&lt;td&gt;50th percentile response time&lt;/td&gt;
&lt;td&gt;Your everyday experience&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;p95 latency&lt;/td&gt;
&lt;td&gt;95th percentile response time&lt;/td&gt;
&lt;td&gt;Sets your timeout and retry policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Success rate&lt;/td&gt;
&lt;td&gt;Parsed successfully ÷ total requests&lt;/td&gt;
&lt;td&gt;Not HTTP 200. Determines your real unit price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Field completeness&lt;/td&gt;
&lt;td&gt;Records with all required fields ÷ successful records&lt;/td&gt;
&lt;td&gt;Whether the data is usable at all&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The success rate definition deserves emphasis, because it is where most home-grown checks go wrong. An HTTP 200 that returns a bot-detection page is a failure, not a success. If your client only looks at the status code, that bad record flows straight into your database and you find out two weeks later when someone asks why the dashboard looks strange.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5svcq5g9nnw1kph1ut0h.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5svcq5g9nnw1kph1ut0h.png" alt=" " width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
On p95: it determines your timeout. If p95 is five seconds and you set a three second timeout, you are throwing away a slice of requests that would have succeeded. Set it to ten seconds and you slow the whole pipeline. A reasonable starting point is 1.5× p95, with a retry cap of two. More retries than that usually means you are being throttled, and retrying into a throttle makes it worse.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why per-request pricing is the wrong unit
&lt;/h2&gt;

&lt;p&gt;Here is the part that changes how you read a pricing page.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A failed request still bills. A blocked page still bills. A response missing the field you need still bills.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So the unit worth comparing is cost per thousand usable records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cost per 1k usable records = (monthly spend ÷ records parsed successfully with all required fields) × 1000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worked example, because the gap is counterintuitive:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Plan A&lt;/strong&gt;: $1.20 per 1k requests, 92% success, 88% field completeness. Usable share is roughly 81%, so the real cost is about &lt;strong&gt;$1.48 per 1k usable records&lt;/strong&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Plan B&lt;/strong&gt;: $1.60 per 1k requests, 99% success, 98% field completeness. Usable share is about 97%, so the real cost is about &lt;strong&gt;$1.65 per 1k usable records&lt;/strong&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Plan A looks 25% cheaper on the rate card. The real gap is about 10%. Add the debugging hours Plan A will generate and the ranking usually flips.&lt;/p&gt;

&lt;p&gt;So the question to ask a vendor is never "how much per thousand requests". It is: how do you define success rate, what is your field completeness, and do failures bill?&lt;/p&gt;

&lt;h2&gt;
  
  
  The four-layer pipeline
&lt;/h2&gt;

&lt;p&gt;Whichever route you take, the chain has the same shape. What differs is which layers you own.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Amazon public pages (product / search / review / best sellers / sponsored)
        ↓
[Collection]   anti-bot · browser rendering · proxies and geo · retries
        ↓
[Structuring]  parsing · field normalization · type contract · failure semantics
        ↓
[Delivery]     REST API   ── or ──   MCP tools
        ↓
Your application / data pipeline / AI agent
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four routes, and how they split ownership:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Route&lt;/th&gt;
&lt;th&gt;Collection&lt;/th&gt;
&lt;th&gt;Structuring&lt;/th&gt;
&lt;th&gt;Delivery&lt;/th&gt;
&lt;th&gt;You maintain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Self-built scraper&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Everything&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;General scraping API&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;You&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Parsing, field drift&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amazon-native data API&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Business logic only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Official SP-API&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Vendor&lt;/td&gt;
&lt;td&gt;Quota and auth&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Choosing a route is really deciding which layers you want to own. More layers means more control and more responsibility.&lt;/p&gt;

&lt;p&gt;One thing worth stating plainly: &lt;strong&gt;if you only need data tied to your own seller account, use the official SP-API and skip third parties entirely.&lt;/strong&gt; Orders, inventory, your own listings. It is the most compliant and cheapest path, and recommending anything paid for that job would be dishonest.&lt;/p&gt;

&lt;p&gt;The real fork is whether you need public marketplace facts — competitors, categories, search results, sponsored placements. That is where the other three routes become relevant.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the test
&lt;/h2&gt;

&lt;p&gt;Enough theory. Here is the script. It samples across marketplaces and object types, then reports all four numbers plus a breakdown of which fields went missing.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;#!/usr/bin/env python3
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
Amazon Data API acceptance test.
Samples N calls across marketplaces and object types, then reports
median latency, p95 latency, success rate and field completeness.
&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;statistics&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;concurrent.futures&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;as_completed&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;

&lt;span class="n"&gt;API_BASE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pangolinfo.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;  &lt;span class="c1"&gt;# confirm against current docs
&lt;/span&gt;
&lt;span class="c1"&gt;# Your business-critical fields. Change these to match what you actually store.
&lt;/span&gt;&lt;span class="n"&gt;REQUIRED_PRODUCT_FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rating&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bsr&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetchedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;REQUIRED_SEARCH_FIELDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sponsoredCount&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;fetchedAt&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;MARKETPLACES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.co.uk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;amazon.de&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SAMPLE_ASINS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B0CXYZ1234&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B0ABCDEFGH&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B012345678&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B0TEST0001&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B0TEST0002&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B0TEST0003&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;SAMPLE_KEYWORDS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;insulated water bottle&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;standing desk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;air purifier&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;CallResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;latency_ms&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;fields_ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;check_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;{})]&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;missing&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CallResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CallResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketplace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;

        &lt;span class="c1"&gt;# HTTP 200 is not success. A blocked page also returns 200.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;

        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REQUIRED_PRODUCT_FIELDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: BLE001
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;call_search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;CallResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;CallResult&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;started&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;API_BASE&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketplace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;HTTP &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
        &lt;span class="n"&gt;payload&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields_ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;check_fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;REQUIRED_SEARCH_FIELDS&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# noqa: BLE001
&lt;/span&gt;        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;started&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;build_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;Alternate product and search calls; rotate marketplace and page depth.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;marketplace&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MARKETPLACES&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MARKETPLACES&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&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="n"&gt;asin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SAMPLE_ASINS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SAMPLE_ASINS&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;product&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_product&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;asin&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SAMPLE_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SAMPLE_KEYWORDS&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
            &lt;span class="n"&gt;page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# 1..3, also probes deep-page availability
&lt;/span&gt;            &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;search&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;call_search&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;page&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;
    &lt;span class="n"&gt;ordered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;pct&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;ordered&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;argparse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;ArgumentParser&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--samples&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_argument&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--concurrency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parser&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse_args&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;PANGOLINFO_API_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Set PANGOLINFO_API_KEY first&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_tasks&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;samples&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concurrency&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;tasks&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;latencies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;latency_ms&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;succeeded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;complete&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields_ok&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;success_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;completeness&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;usable_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;complete&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;=== Amazon Data API acceptance report ===&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;samples            : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;median latency     : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;statistics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;median&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;p95 latency        : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;percentile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;max latency        : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;latencies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; ms&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success rate       : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;success_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;field completeness : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;completeness&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;usable share       : &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usable_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;%&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;missing_counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;succeeded&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;missing_counter&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;missing_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;field_name&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;missing_counter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;missing fields (by occurrence):&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing_counter&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;field_name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;error distribution:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;count&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
            &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;count&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;Feed the usable share into the formula to get your real unit price:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  cost per 1k usable records = monthly spend / usable records * 1000&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;  at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;usable_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;% usable, the list price is effectively &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;usable_rate&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;x higher&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Reading the output
&lt;/h2&gt;

&lt;p&gt;A healthy run looks something like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;=== Amazon Data API acceptance report ===
samples            : 100
median latency     : 2980 ms
p95 latency        : 5240 ms
max latency        : 8120 ms
success rate       : 99.0%
field completeness : 98.0%
usable share       : 97.0%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to look at beyond the headline numbers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The missing-fields breakdown.&lt;/strong&gt; If gaps cluster on one field, that field has weak coverage in your target categories. That is a specific conversation with the vendor, not a reason to reject the whole provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The error distribution.&lt;/strong&gt; If you see a lot of timeouts, you have a latency problem. If you see parse failures on otherwise healthy responses, you have a structuring problem. These two have completely different fixes, and lumping them into one error log is how teams spend a week debugging the wrong thing.&lt;/p&gt;

&lt;p&gt;Then re-run it weekly. &lt;strong&gt;A single run proves it works today. Weekly runs prove it still works.&lt;/strong&gt; Quality degradation is almost always gradual — drifting from 99% to 95% takes weeks, and humans notice about two weeks after it matters.&lt;/p&gt;

&lt;h2&gt;
  
  
  The field matrix behind the checklist
&lt;/h2&gt;

&lt;p&gt;The &lt;code&gt;REQUIRED_PRODUCT_FIELDS&lt;/code&gt; list should not be invented. It should come from a matrix of what public Amazon pages can actually be structured into.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Object&lt;/th&gt;
&lt;th&gt;Key fields&lt;/th&gt;
&lt;th&gt;Common gaps&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Product&lt;/td&gt;
&lt;td&gt;ASIN, title, brand, price, rating, BSR, availability, variants&lt;/td&gt;
&lt;td&gt;Incomplete variant dimensions; lost parent-child ASIN links; sale price mixed with list price&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search&lt;/td&gt;
&lt;td&gt;Keyword, page, organic rank, ad rank, isSponsored&lt;/td&gt;
&lt;td&gt;Sponsored mixed with organic; deep pages (7+) truncated&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review&lt;/td&gt;
&lt;td&gt;Rating, title, body, date, verified purchase, variant, helpful votes&lt;/td&gt;
&lt;td&gt;Truncated bodies from summary pages; no variant attribution&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Offers&lt;/td&gt;
&lt;td&gt;Seller, price, shipping, Buy Box holder, stock&lt;/td&gt;
&lt;td&gt;Inconsistent Buy Box logic; only first offer returned&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Best Sellers&lt;/td&gt;
&lt;td&gt;Category, rank, ASIN, rank movement&lt;/td&gt;
&lt;td&gt;Unlabeled category-tree changes; non-continuous history&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Seller&lt;/td&gt;
&lt;td&gt;Seller ID, name, rating, listing count&lt;/td&gt;
&lt;td&gt;Weak seller-to-brand mapping; IDs differ by marketplace&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;Category tree, node ID, filters, item count&lt;/td&gt;
&lt;td&gt;Node IDs vary by site; incomplete filter enums&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Sponsored&lt;/td&gt;
&lt;td&gt;Ad type (SP/SB/SD), placement, rank, creative&lt;/td&gt;
&lt;td&gt;Ads not separated from organic; missing creative fields&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two fields I would put on every checklist.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;fetchedAt&lt;/code&gt;.&lt;/strong&gt; A capture timestamp. Without it the record cannot enter a time series and cannot be audited. Three months later, when a price looks wrong, you cannot tell whether it was wrong then or got mangled later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;isSponsored&lt;/code&gt; on search results.&lt;/strong&gt; Without it, organic rank and ad placement are indistinguishable. You think you rank third; the top two slots are ads; your true organic position is first. Every optimization decision built on the contaminated number points the wrong way.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not everything needs to be real time
&lt;/h2&gt;

&lt;p&gt;Having argued that "real time" is widely abused, it is worth being equally clear about the opposite mistake: refreshing everything at maximum frequency.&lt;/p&gt;

&lt;p&gt;Field change rates vary by orders of magnitude. Brand names, category assignments and variant dimensions barely move. Price, stock, Buy Box ownership and sponsored placements move constantly. Treating them identically is the most common form of budget waste in this category.&lt;/p&gt;

&lt;p&gt;A three-tier split is usually right:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High frequency, minutes to hours&lt;/strong&gt; — price, stock, Buy Box holder, sponsored placements, BSR. For these, a stale value does not weaken the insight, it invalidates it. A competitor's price from yesterday cannot support a pricing decision today.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Daily&lt;/strong&gt; — new reviews, rating changes, seller lists, best seller ranks. Trend signals that matter over weeks rather than minutes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Weekly or on demand&lt;/strong&gt; — brand, category, variant dimensions, images, A+ content. Refreshing these hourly is pure waste with no analytical benefit.&lt;/p&gt;

&lt;p&gt;The practical observation from many deployments: the top 20% of ASINs drive roughly 80% of the decisions. Running that 20% at high frequency and the long tail weekly typically cuts cost by half with almost no measurable business impact. It is the highest-leverage cost optimization available, and it requires no vendor negotiation — it is purely a scheduling decision in your own ingestion layer.&lt;/p&gt;

&lt;p&gt;Two implementation notes.&lt;/p&gt;

&lt;p&gt;First, implement the tiering in your own ingestion layer rather than hoping the provider does it. You know which ASINs matter to your business; they do not.&lt;/p&gt;

&lt;p&gt;Second, revisit it periodically. Business priorities shift, a category that was long tail becomes strategic, and a tiering decision made eighteen months ago is usually wrong today. Put a recurring reminder on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  REST or MCP
&lt;/h2&gt;

&lt;p&gt;Worth separating, because they get conflated constantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;REST fits deterministic, scheduled batch work.&lt;/strong&gt; You know which ASINs to pull, how often, and which table the results land in. Data pipelines and scale collection.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;MCP fits exploratory work that needs reasoning.&lt;/strong&gt; A user or agent states a business question and the agent decides which tools to call, how many pages to check, and how to cross-validate. Research, diagnostics, one-off analysis.&lt;/p&gt;

&lt;p&gt;One line: with REST you write a program that fetches data; with MCP you ask an agent to fetch and explain. Backend service consumer, use REST. AI agent consumer, MCP removes a lot of glue code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision tree
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Step 1. Is the data only about your own seller account?&lt;/strong&gt; Yes, use the official SP-API. No, continue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 2. Do you need public marketplace facts — competitors, categories, search, sponsored placements?&lt;/strong&gt; No, revisit the requirement. Yes, continue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Step 3. Do you have engineers who will own anti-bot, rendering and parsing maintenance long term?&lt;/strong&gt; No, pick an Amazon-native data API. Yes, convert engineering hours to money and compare against purchase price on a per-thousand-usable-records basis.&lt;/p&gt;

&lt;p&gt;Step 3 is where teams go wrong by comparing cash only. Price the engineering time at real internal cost. Plenty of "building is cheaper" instincts reverse once that number is on the page.&lt;/p&gt;

&lt;p&gt;For reference, Pangolinfo publishes a median latency around 3 seconds, a 99% success rate, and more than 30 million calls per day; on sponsored placements, the published collection rate across 13 marketplaces is 91.4%. The point of those numbers is not that they are large. It is that you can reproduce or refute them with the script above.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;If the numbers come back bad, here is the triage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Low success rate with high latency&lt;/strong&gt; is usually capacity. Lower your concurrency, or ask about quotas and whether you are being throttled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;High success rate with low field completeness&lt;/strong&gt; is a coverage problem. Identify exactly which fields are missing and ask about those specific fields in your target categories. This is often fixable, or at least scoped to particular categories you can route around.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Good numbers on page one that collapse by page three&lt;/strong&gt; is a deep-page limitation. This one is usually a hard boundary rather than a transient issue, and it matters enormously if your business depends on tracking keywords where you rank deep.&lt;/p&gt;

&lt;p&gt;Knowing which of the three you have determines whether the fix is configuration, a conversation, or a different provider. Collapsing them into "the data is bad" is how teams waste a week.&lt;/p&gt;

&lt;p&gt;If you take one thing from this post, take the ordering:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Write the field checklist first (15–30 fields).&lt;/li&gt;
&lt;li&gt;Score candidates field by field, not "do you support products?".&lt;/li&gt;
&lt;li&gt;Convert list price to cost per thousand usable records.&lt;/li&gt;
&lt;li&gt;Run the 100-call test and record all four numbers.&lt;/li&gt;
&lt;li&gt;Look at the price sheet last.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want to run the script against real data, grab a key from the &lt;a href="https://tool.pangolinfo.com/?referrer=devto" rel="noopener noreferrer"&gt;console&lt;/a&gt; or read the &lt;a href="https://docs.pangolinfo.com/en-help-center/mcp/amazon-insight/overview?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP docs&lt;/a&gt;. The underlying products are &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; for product, search, best seller, category and sponsored objects, &lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt; for reviews and customer voice, &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; when an agent needs to call data directly, and &lt;a href="https://www.pangolinfo.com/amazon-scraper-skill/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper Skill&lt;/a&gt; for conversational workflows.&lt;/p&gt;

&lt;p&gt;The full buyer's guide with the field matrix, three real JSON samples and the acceptance method is here: &lt;a href="https://www.pangolinfo.com/amazon-data-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data API: The Complete Buyer's Guide&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;If you run the test, I would be curious what numbers you get — especially p95. That is the one where published specs and reality tend to diverge most.&lt;/p&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>mcp</category>
      <category>ai</category>
    </item>
    <item>
      <title>Build an Amazon Review Monitor: ratings, complaint signals, and review media</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Tue, 01 Sep 2026 02:43:34 +0000</pubDate>
      <link>https://dev.to/pangolinfo/build-an-amazon-review-monitor-ratings-complaint-signals-and-review-media-3mb7</link>
      <guid>https://dev.to/pangolinfo/build-an-amazon-review-monitor-ratings-complaint-signals-and-review-media-3mb7</guid>
      <description>&lt;p&gt;Amazon review data is useful only when it can answer a decision: has a new quality issue emerged, what do verified buyers say, and is there visual evidence behind it? This guide shows a small, production-oriented pattern for collecting reviews, flagging complaint risk, and storing the fields needed for follow-up analysis.&lt;/p&gt;

&lt;p&gt;The example uses the &lt;a href="https://www.pangolinfo.com/amazon-review-api/" rel="noopener noreferrer"&gt;Pangolinfo Amazon Review API&lt;/a&gt;. It returns structured review records rather than page HTML, including star rating, title, text, verified-purchase status, helpful votes, product variant data, review images, and review video URLs.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Decide what to monitor
&lt;/h2&gt;

&lt;p&gt;Start with a target ASIN and a clear observation rule. A useful baseline is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;retrieve the most recent page of reviews every day;&lt;/li&gt;
&lt;li&gt;retrieve one-star reviews separately to surface issues early;&lt;/li&gt;
&lt;li&gt;preserve image and video URLs for review validation;&lt;/li&gt;
&lt;li&gt;keep the review ID and date so repeated runs can be deduplicated.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This keeps the workflow focused on new customer feedback instead of reprocessing the entire review history.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Make a structured review request
&lt;/h2&gt;

&lt;p&gt;Create a Pangolinfo API Key in the &lt;a href="https://tool.pangolinfo.com/" rel="noopener noreferrer"&gt;Console&lt;/a&gt;, then make the request below. Do not put a production key into shared workflows, browser code, or public collections.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://scrapeapi.pangolinfo.com/api/v1/scrape &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer YOUR_PANGOLINFO_API_KEY'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "url": "https://www.amazon.com/dp/B076CLQDR4",
    "site": "",
    "bizContext": {
      "bizKey": "review",
      "pageCount": 1,
      "asin": "B076CLQDR4",
      "filterByStar": "one_star",
      "sortBy": "recent"
    },
    "format": "json",
    "formatType": "all_formats",
    "mediaType": "all_contents",
    "parserName": "amzReviewV2"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same endpoint supports &lt;code&gt;all_stars&lt;/code&gt; when you need a broad feedback sample. Full request and response fields are documented in the &lt;a href="https://docs.pangolinfo.com/en-api-reference/amazonReviewAPI/amazonReviewAPI" rel="noopener noreferrer"&gt;Amazon Review API reference&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Persist only the fields that drive decisions
&lt;/h2&gt;

&lt;p&gt;For each record in &lt;code&gt;data.json[].data.results[]&lt;/code&gt;, store at least:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Field&lt;/th&gt;
&lt;th&gt;Why it matters&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;reviewId&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Deduplicate repeated scheduled runs.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;star&lt;/code&gt;, &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;content&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Detect rating shifts and complaint themes.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;date&lt;/code&gt;, &lt;code&gt;country&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Attribute changes to a time period and marketplace.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;purchased&lt;/code&gt;, &lt;code&gt;vineVoice&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Separate verified-buyer feedback from other sources.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;helpful&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Prioritize issues readers find useful.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;imgs&lt;/code&gt;, &lt;code&gt;videos&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Preserve evidence for quality and listing reviews.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;attributes&lt;/code&gt;, &lt;code&gt;asin&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Identify a problematic size, color, or variant.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  4. Turn raw reviews into alerts
&lt;/h2&gt;

&lt;p&gt;A simple alert rule is more robust than a generic sentiment score:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Alert when a new verified one-star review has an image or video.&lt;/li&gt;
&lt;li&gt;Alert when two or more reviews in the same run mention the same failure mode.&lt;/li&gt;
&lt;li&gt;Include ASIN, review ID, rating, title, date, and evidence URLs in the alert.&lt;/li&gt;
&lt;li&gt;Add a cooldown key such as &lt;code&gt;asin:reviewId&lt;/code&gt; so the same review is not sent twice.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;For no-code automation, the &lt;a href="https://www.npmjs.com/package/n8n-nodes-pangolinfo" rel="noopener noreferrer"&gt;Pangolinfo n8n node&lt;/a&gt; and public Postman &lt;a href="https://www.postman.com/grey-escape-778224/pangolinfo-public-apis/collection/0ambh2g/pangolinfo-amazon-review-api" rel="noopener noreferrer"&gt;Amazon Review API collection&lt;/a&gt; provide starting points.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Use the MCP route for agent workflows
&lt;/h2&gt;

&lt;p&gt;If an agent needs to combine reviews with product, keyword, niche, and AI-search data, use &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; instead of maintaining separate parsers. The MCP server exposes the review capability alongside the broader Amazon and AI-search toolset, while your application keeps control of its own API key and usage limits.&lt;/p&gt;

&lt;h2&gt;
  
  
  Practical takeaway
&lt;/h2&gt;

&lt;p&gt;An effective review monitor is not an endless scrape. It is a small, repeatable process: collect recent structured feedback, retain identifiers and evidence, detect new recurring issues, and send a deduplicated alert to the team that can act on it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;/blockquote&gt;

</description>
      <category>api</category>
      <category>automation</category>
      <category>tutorial</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>We Built an Amazon Brand Operations Cockpit for a Global Electronics Brand. Here's the Architecture.</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Mon, 24 Aug 2026 09:27:10 +0000</pubDate>
      <link>https://dev.to/pangolinfo/we-built-an-amazon-brand-operations-cockpit-for-a-global-electronics-brand-heres-the-architecture-13jd</link>
      <guid>https://dev.to/pangolinfo/we-built-an-amazon-brand-operations-cockpit-for-a-global-electronics-brand-heres-the-architecture-13jd</guid>
      <description>&lt;p&gt;Most "data-driven Amazon" setups stop at exporting backend numbers into a spreadsheet. Exporting is not operating. This post walks through a real, anonymized project — a global consumer-electronics brand in the action-camera and pocket-gimbal category, across several major overseas markets — where we rebuilt their growth decisioning from a weekly lag into an hour-level response system: an &lt;strong&gt;Amazon Brand Operations Cockpit&lt;/strong&gt;. The brand name and specific numbers are redacted; the architecture, dimensions, and SOPs are reusable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem: why monthly reports can't save your growth
&lt;/h2&gt;

&lt;p&gt;A monthly report is post-hoc description. Its collection grain is weekly, its presentation grain is monthly, while Amazon's competitive rhythm is hourly: deal badges, coupons, Buy Box rotation, keyword-rank jumps all happen within a single day. By the time the monthly report ships, the window is closed.&lt;/p&gt;

&lt;p&gt;Our goal for this brand was never "a prettier report." It was: anomalies surface themselves the same day, with an attached "what to do." That requires splitting signals into three layers and bolting the third directly onto the data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssoadq82rufd6fx4ozy5.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fssoadq82rufd6fx4ozy5.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 1: real-time Amazon sales signals (8 dimensions)
&lt;/h2&gt;

&lt;p&gt;We locked in eight concrete dimensions, each with a data source, refresh cadence, and trigger threshold. This table is lifted from the actual collection schema:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Dimension&lt;/th&gt;
&lt;th&gt;What it reveals&lt;/th&gt;
&lt;th&gt;Trigger threshold (example)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;BSR daily trajectory &amp;amp; velocity&lt;/td&gt;
&lt;td&gt;Demand shifting before sales move&lt;/td&gt;
&lt;td&gt;Same ASIN moving one direction for days, or a single-day drop beyond category norm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Price vs. category median&lt;/td&gt;
&lt;td&gt;Competitor pre-emptive cuts, own squeeze&lt;/td&gt;
&lt;td&gt;Effective price drops below median, or multiple rivals cut together&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Buy Box win rate&lt;/td&gt;
&lt;td&gt;Losing the box (reseller, logistics, price war)&lt;/td&gt;
&lt;td&gt;Win rate in a market or time window below own baseline&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Core keyword rank volatility&lt;/td&gt;
&lt;td&gt;Rank erosion before it hits BSR&lt;/td&gt;
&lt;td&gt;Core term falls off the first search screen, or a new term breaks into the top&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Review velocity &amp;amp; rating drift&lt;/td&gt;
&lt;td&gt;Quality / reputation inflection&lt;/td&gt;
&lt;td&gt;Rating trends down, or negatives cluster in a short window&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Search placement &amp;amp; share-of-shelf&lt;/td&gt;
&lt;td&gt;New entrants, ad slots taken&lt;/td&gt;
&lt;td&gt;Organic position drops, sponsored slot captured by rival&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Variant / ASIN cannibalization&lt;/td&gt;
&lt;td&gt;New variant stealing main variant traffic&lt;/td&gt;
&lt;td&gt;New variant rises while main variant falls in sync&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cross-market divergence&lt;/td&gt;
&lt;td&gt;Same ASIN behaving differently by market&lt;/td&gt;
&lt;td&gt;One market anomalous while others are flat&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first five (BSR, price, Buy Box, keywords, reviews) are "lead" signals — they usually move before sales actually drop.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Implementation note:&lt;/strong&gt; the underlying data uses the &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; for hourly collection covering BSR, price, coupons, inventory, and ad slots, with timestamp alignment — that alignment is what makes "velocity" and "divergence" calculations possible. Review and rating anomalies are pulled separately via the &lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt; so they aren't drowned out by the main-signal noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 2: external social-demand signals (6 dimensions)
&lt;/h2&gt;

&lt;p&gt;This is the off-site calibration. We plugged cross-platform social listening and broke it into six dimensions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Share of voice by platform&lt;/strong&gt; (TikTok / Instagram / YouTube / Reddit / X): where the conversation actually lives. The "home platform" differs completely by category and market.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment split&lt;/strong&gt; (positive / neutral / negative): whether the narrative is helping or planting a landmine. A negative cluster often precedes a rating drop on Amazon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement by content format&lt;/strong&gt;: short video, image, long-form, live — which format actually drives interaction. It decides where content budget goes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creator-tier mix&lt;/strong&gt; (macro / micro / nano): was voice driven by top creators or spread by mid/small creators — this determines whether the channel strategy is repeatable or lucky.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbranded demand intensity&lt;/strong&gt;: are users searching a brand name, or "how to shoot / which to buy" generic demand. This is the key signal for whether the category is pulling new buyers or the brand is catching them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor co-mention&lt;/strong&gt;: who users compare together. Shifts in co-mention are the earliest substitution-threat signal.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Social is often the "cause" while Amazon on-site signals are the "result" — social heat typically leads search by 1–2 weeks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Layer 3: decision rules (trigger → hypothesis → action)
&lt;/h2&gt;

&lt;p&gt;The first two layers are only "seeing." The third is "driving." We attached every anomaly to a loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;when dimension crosses threshold:
    hypothesis = generate_falsifiable_guess(dimension, context)
    action = map_action(hypothesis)
    owner = route_owner(dimension)
    notify(owner, anomaly + hypothesis + action)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We wired the &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; so an Agent reads the data and issues alerts directly — operators can ask in plain language "which ASINs are anomalous today" without writing SQL.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three signals the monthly report missed
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Signal 1 (cross-market · price):&lt;/strong&gt; In one major overseas market, several same-category rivals pushed effective price below the category median within days, while the brand's pricing stayed put. In the first week, that market's BSR began slipping and Buy Box win rate fell with it — but all of it would only appear in the month-end report. The cockpit set "price vs. category median" to daily monitoring and triggered the same day, letting the operator judge, before the window closed, whether the rival was running a short promo or a long-term strategic cut.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal 2 (cross-platform · social):&lt;/strong&gt; Social listening showed category-level "how to choose / which to buy" discussion contributed the vast majority of reach on short-video platforms, with engagement well above norm and sentiment mostly positive. But crossing social high-frequency terms with Amazon search terms exposed a glaring gap: a high-search-volume generic category term had &lt;strong&gt;zero Amazon ranking in the first two pages&lt;/strong&gt; for the brand, while competitors already ranked organically near the top and ran sponsored placements. The category was acquiring new buyers on short video; the brand failed to catch the narrative into search.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Signal 3 (head-to-head · share of voice):&lt;/strong&gt; In the launch of a new-generation pocket gimbal camera, the rival looked far stronger on the surface. The teardown revealed two things the monthly report ignored: first, the rival leaned on first-mover advantage and aggressive pricing to spike early, while the brand's own product had a steeper post-launch month-over-month acceleration — slower to heat, stronger in the back half; second, and most critically, &lt;strong&gt;the two products had almost zero keyword overlap&lt;/strong&gt; in search. Zero overlap meant the generic and scenario long-tail terms the brand had never bid on were likely low-cost incremental pockets. At the same time, the rival lifted share of voice on a single platform via mid-tier creators while the brand's owned content underperformed on engagement.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four playbooks that turn signals into action
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Price-band defense SOP:&lt;/strong&gt; threshold = effective price vs. category median ±X%. Short promo → defend keywords with ads, don't follow price; multi-day strategic cut → evaluate adjusting the price band or launching a secondary SKU. Owner: operations, daily.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keyword rescue SOP:&lt;/strong&gt; core term falls off the first screen → three-step: buy impression back with PPC, reinforce the related term in content and bullets, then check whether reviews/rating are dragging. Conversely, when a term first breaks into the top, increase investment immediately (most teams miss this step).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reputation &amp;amp; review recovery SOP:&lt;/strong&gt; review anomaly → cluster by theme (logistics / quality / expectation gap) → route action. Treat reviews as free user research.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Launch cadence &amp;amp; social reallocation SOP:&lt;/strong&gt; use "unbranded demand intensity × platform" as the pre-launch heat map; move content budget to high-demand platforms; on Amazon, pre-stock and pre-schedule PPC by heat.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Cross-market and cross-platform attribution methodology
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lead-lag first:&lt;/strong&gt; did the signal appear on Amazon first, or on social first? If social leads, use it as leading intel.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Horizontal matrix:&lt;/strong&gt; read divergence with a "market × dimension" matrix to avoid single-market bias.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Allocate by elasticity:&lt;/strong&gt; in markets where BSR is price-sensitive, prioritize price defense; in markets where it's content-sensitive, prioritize social reallocation.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build a minimum-viable cockpit in three phases
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Phase 1 (weeks 1–2):&lt;/strong&gt; watch only daily price, BSR and Buy Box on core ASINs, with threshold alerts pushing anomalies the same day. Data via Amazon Scraper API hourly collection.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 2 (weeks 3–4):&lt;/strong&gt; add keyword rank and review anomalies (Review API), wire Amazon Data MCP for Agent auto-alerts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Phase 3 (from month 2):&lt;/strong&gt; connect social listening for external calibration, close the trigger→hypothesis→action loop, and write the four playbooks into the operations SOP.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  A concrete "cockpit workday"
&lt;/h2&gt;

&lt;p&gt;To keep the dimensions grounded, here is an anonymized real morning. Monday 9 a.m., the cockpit pushed three reds:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;One major overseas market: a flagship's effective price was pushed below the category median by rivals, BSR slipped day-over-day, and Buy Box win rate dipped from its high.&lt;/li&gt;
&lt;li&gt;Social: "how to choose / which to buy" discussion on short-video platforms rose week-over-week, while our Amazon rank on that scenario term was still blank.&lt;/li&gt;
&lt;li&gt;Reviews: after a new variant launched, the main variant's rating dipped slightly and negatives clustered on "expectation gap."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The operator spent 15 minutes on three moves. For Market A, they first judged it a short promo — checking the rival's promo history confirmed a weekly pattern, not a strategic cut — so they defended core keywords with ads and did not follow price. They dropped the trending social scenario term into the content calendar and PPC candidates. For the main variant's "expectation gap" negatives, they published a video for expectation management. By Friday's review, Market A's BSR had recovered, the scenario term had earned organic plus sponsored placement, and the main variant's rating had stopped falling. That is the gap between "same-day response" and "monthly lag" — and all three moves would have waited until the month-end review under the old flow.&lt;/p&gt;

&lt;h2&gt;
  
  
  Threshold configuration: how to set each of the 8 Amazon thresholds
&lt;/h2&gt;

&lt;p&gt;A cockpit lives or dies by its thresholds. Guesswork either false-alarms daily or never fires. Here is how we set each of the eight:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BSR trajectory &amp;amp; velocity:&lt;/strong&gt; compute a 14-day rolling baseline per ASIN; alert when the day-over-day delta exceeds two standard deviations, or when the slope turns negative for three consecutive days. Velocity (the slope) matters more than the level.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price vs. category median:&lt;/strong&gt; pull the category median daily; alert when own or any tracked rival's effective price crosses ±X% of median. The median — not a fixed price — is the right reference because categories drift.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy Box win rate:&lt;/strong&gt; track win rate per market per day; alert when a market's 7-day average drops below its own historical baseline by more than a band. A dip in one market while others are stable is the classic "something is wrong here" signal.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core keyword rank:&lt;/strong&gt; alert when a core term falls off the first search screen, or when a tracked term enters the top for the first time (the latter is an opportunity, not a threat).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review velocity &amp;amp; rating:&lt;/strong&gt; alert when daily review count spikes above baseline (possible spike of incentivized or fake reviews) or when rolling rating drops by a threshold. Cluster negatives by theme automatically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search placement &amp;amp; share-of-shelf:&lt;/strong&gt; alert when organic position for a core term drops, or when a rival's sponsored slot appears above your organic result.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variant / ASIN cannibalization:&lt;/strong&gt; alert when a new variant's BSR rises while the main variant's falls in sync — a sign the launch is stealing internal traffic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-market divergence:&lt;/strong&gt; alert when one market diverges from the median of all markets for the same ASIN.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The pattern: every threshold is "relative to a baseline or a median," never an absolute number pulled from thin air.&lt;/p&gt;

&lt;h2&gt;
  
  
  Second worked example: the head-to-head launch week
&lt;/h2&gt;

&lt;p&gt;Signal 3 deserves its own walkthrough because it is the one most teams misread. When the rival's new pocket-gimbal launched, surface metrics said they were winning: early sales were multiples of ours. The cockpit's teardown changed the call. First, the rival's velocity came from first-mover advantage and aggressive pricing; our product's post-launch month-over-month acceleration was actually steeper — slower to heat, stronger in the back half. Second, and decisively, the two products had almost zero keyword overlap: they owned brand terms and a few generic words; we owned a different brand-term family. They were not colliding; they were in different traffic pools.&lt;/p&gt;

&lt;p&gt;The action that followed was counter-intuitive: instead of panic-bidding on the terms where the rival was strong, we mapped the generic and scenario long-tail terms neither of us ranked for, confirmed real search demand, and quietly built content and PPC there. Those became low-cost incremental pockets. Meanwhile we flagged that the rival was lifting share of voice on a single platform via mid-tier creators while our owned content underperformed — a narrative-share problem the monthly report would never surface, addressed by reallocating content budget toward that platform's high-demand format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five most common mistakes
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treating a dashboard as a cockpit:&lt;/strong&gt; seeing isn't driving; without a trigger→action loop, a dashboard is just a pricier report.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watching only yourself, not competitors:&lt;/strong&gt; share is taken by others; rival price, rank, launches, and lost Buy Box are the real alert sources.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Siloing social and Amazon:&lt;/strong&gt; social heat leads search by 1–2 weeks; siloing throws away the lead.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Guesswork thresholds:&lt;/strong&gt; without "vs. category median" or "vs. own baseline" references, thresholds either false-alarm daily or never fire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No owner:&lt;/strong&gt; routing an anomaly to "the team" means routing it to no one. Every anomaly must map to a specific owner.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How to measure whether the cockpit actually works
&lt;/h2&gt;

&lt;p&gt;Five health metrics, themselves part of the cockpit: anomaly response time (target within a day), price-defense hit rate (correct "defend vs. follow" calls), keyword-rescue success rate (terms that return to the first screen), social→search conversion (trending social terms that earn Amazon rank), and share-recovery cycle (average time for an affected ASIN's BSR/share to return to baseline). The cockpit must both drive and self-check.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tooling and data pipeline (no detours)
&lt;/h2&gt;

&lt;p&gt;Real-time collection via the &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; (hourly, timestamp-aligned); review and rating anomalies via the &lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt;; Agent natural-language alerts via the &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt;; the social layer via cross-platform listening for external calibration. We deliberately did not build a black-box SaaS — the data pipeline and integration are yours to wire into your own BI, Agent, and alert logic.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why each of the 8 Amazon dimensions matters (with mini-examples)
&lt;/h2&gt;

&lt;p&gt;Most teams ask for "more data" without clarifying what each dimension actually defends. Here is the per-dimension logic, because skipping any one of them leaves a blind spot.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;BSR daily trajectory &amp;amp; velocity&lt;/strong&gt; defends against "we only find out when sales drop." BSR reacts to demand before sales do, and velocity (consecutive same-direction moves) beats the single-day level. In the anonymized case, a flagship's BSR fell three days straight before sales moved on day four — watching sales alone costs you four days of response time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Price vs. category median&lt;/strong&gt; defends against "being quietly undercut." Using "relative to median" rather than an absolute price matters because categories drift; when rivals push effective price below median, your original price band effectively becomes expensive even if you didn't move.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Buy Box win rate&lt;/strong&gt; defends against "losing the box without knowing." Resellers, logistics delays, and price wars all drop win rate; reading it per market localizes exactly where you lost.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Core keyword rank&lt;/strong&gt; defends against "rank dropped before it hit BSR." Rank is upstream of BSR; rescue it while it's still off the first screen, not after BSR already slid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review velocity &amp;amp; rating&lt;/strong&gt; defends against "reputation inflection drowned out." A rating decline or negative cluster often precedes a sales problem by weeks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Search placement &amp;amp; share-of-shelf&lt;/strong&gt; defends against "new entrants and ad slots taken." An organic position drop or a rival's sponsored slot above you are both direct share-loss signals.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Variant / ASIN cannibalization&lt;/strong&gt; defends against "fighting yourself." A new variant stealing the main variant's traffic is frequently mistaken for competitor action.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-market divergence&lt;/strong&gt; defends against "global averages hiding local problems." One market anomalous while others are flat signals a local issue, not a category-wide one.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How the 6 social dimensions actually get used
&lt;/h2&gt;

&lt;p&gt;Social is not "post content and watch views"; it is Amazon's leading indicator. How each dimension is operationalized:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Share of voice by platform&lt;/strong&gt; tells you where the conversation lives. This category lives on short video, that one on long-form; being on the wrong platform wastes the entire budget.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment split&lt;/strong&gt; gives a 1–2 week early warning of an Amazon rating risk the moment negatives cluster — long before the star rating moves.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Engagement by format&lt;/strong&gt; decides budget allocation: if short video drives interaction, stop pouring money into image posts that quietly underperform.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Creator-tier mix&lt;/strong&gt; tells you whether voice built from mid-tier creators is more repeatable than a single top creator's one-off spike.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Unbranded demand intensity&lt;/strong&gt; is the switch between "category acquiring" and "brand catching." High unbranded demand plus a blank brand rank is the single biggest gap we found.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Competitor co-mention&lt;/strong&gt; flags substitution threat the moment users start comparing you with someone new.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Writing "trigger → hypothesis → action" into your ops manual
&lt;/h2&gt;

&lt;p&gt;This layer is the most important and the easiest to skip. What we built was not a "dashboard" but a "dashboard + rules": when any dimension crosses its threshold, the system first proposes a falsifiable hypothesis (short promo or long-term cut), then a standard action and an owner. The operator receives not a pile of numbers but a prioritized to-do with a recommendation attached. Without this layer, the first two are just a pricier weekly report. The hypothesis step matters because it prevents over-reaction: a price dip that is a week-long promo does not deserve the same response as a strategic repricing, and the system forces that distinction to be made explicitly rather than in someone's gut.&lt;/p&gt;

&lt;h2&gt;
  
  
  What this looks like as a weekly operating rhythm
&lt;/h2&gt;

&lt;p&gt;Monday the cockpit surfaces three reds; the operator spends 15 minutes triaging each against its hypothesis. Tuesday through Thursday, the same loop runs daily but lighter — mostly confirming yesterday's actions are holding. Friday is the review: not "what happened last month," but "did our same-day responses hold share, and which thresholds need recalibration." That Friday recalibration is where the system learns — thresholds that false-alarmed get widened, blind spots get new dimensions added. Over a quarter, the cockpit stops being a project and becomes the team's default operating rhythm.&lt;/p&gt;

&lt;h2&gt;
  
  
  A note on team size
&lt;/h2&gt;

&lt;p&gt;The most common objection is "we're too small for this." You are not. The minimum viable version is three dimensions (price, BSR, Buy Box) on your core ASINs, with threshold alerts pushed to one person's phone. That alone closes the "monthly lag" gap. The social layer and the full eight dimensions are phase-two upgrades, not prerequisites. The point is to start driving with a few instruments, not to wait until you can afford the whole cockpit.&lt;/p&gt;

&lt;h2&gt;
  
  
  A deeper look at cross-market attribution (worked)
&lt;/h2&gt;

&lt;p&gt;Attribution is where most multi-market teams quietly fail, because they reverse cause and effect. The discipline we built has three moves. First, lead-lag: determine whether a signal appeared on Amazon first or on social first. In the anonymized case, the "how to choose" social spike preceded the Amazon search lift by roughly two weeks — so social was the leading indicator, and the team learned to pre-stage PPC before the search spike, rather than explaining it after. Second, the horizontal matrix: read divergence with a "market × dimension" grid. When Market A's BSR dropped, the matrix showed Markets B and C were flat — so this was a local competitive event in A (a rival promo), not a category-wide demand dip. Treating it as category-wide would have triggered a wasteful global price cut. Third, allocate by elasticity: in markets where BSR is price-sensitive, priority goes to price defense; in markets where it is content-sensitive, priority goes to social reallocation. A one-size playbook fails across markets because the lever that moves the needle differs by market.&lt;/p&gt;

&lt;h2&gt;
  
  
  A second anonymized mini-case: the review-velocity early warning
&lt;/h2&gt;

&lt;p&gt;The first case was about price and share of voice; the second was quieter and arguably more valuable. A new variant launched, and within days the main variant's review velocity doubled while its rolling rating dipped by a hair — not enough to trip a "rating dropped" alert, but the velocity spike was anomalous versus baseline. The cockpit auto-clustered the new negatives and found them concentrated on "expectation gap": the listing promised one thing, the unboxing video implied another. The team published a short expectation-management video and tightened the bullet copy. Two weeks later, when a competitor launched a similar variant and their rating actually fell, our main variant's rating had already stabilized. The lesson: velocity — the rate of change — is a better early signal than the level, and clustering negatives by theme turns a raw rating into an actionable fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  What to put on the cockpit home screen (a template)
&lt;/h2&gt;

&lt;p&gt;If you build this, the home screen should be six to eight cards, not a wall of charts. Today's reds (the prioritized action queue). A price-vs-median heatmap across markets. Buy Box win rate by market. Keyword movers (terms that entered or left the first screen). Social share of voice by platform with sentiment. Review health (velocity + rating + top negative theme). An action queue with owner and SLA. And a response-time gauge showing the average hours from signal to action. That last card is the whole point: the cockpit is only as good as how fast the team closes the loop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;An Amazon Brand Operations Cockpit is not a report; it's the operating system for growth. It doesn't replace strategy, but it lets strategy be executed correctly at the right moment. The full deep-dive is here: &lt;a href="https://www.pangolinfo.com/amazon-brand-operations-cockpit/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Brand Operations Cockpit: Why Monthly Reports Can't Save Your Growth&lt;/a&gt;. More similar practice: &lt;a href="https://www.pangolinfo.com/scrape-api-data-scraping-cases/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo customer cases&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>agents</category>
    </item>
    <item>
      <title>Customer Service Agent End-to-End Integration: Defining the Delivery Boundary, Not Wiring More APIs</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Fri, 14 Aug 2026 07:34:50 +0000</pubDate>
      <link>https://dev.to/pangolinfo/customer-service-agent-end-to-end-integration-defining-the-delivery-boundary-not-wiring-more-apis-gh8</link>
      <guid>https://dev.to/pangolinfo/customer-service-agent-end-to-end-integration-defining-the-delivery-boundary-not-wiring-more-apis-gh8</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmf5duztva8qho3bd01z.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ffmf5duztva8qho3bd01z.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;If you have read more than three "build a customer support agent" tutorials in the last year, you have seen the same script: pick a model, chunk your FAQs, drop them into a vector store, add a retriever, wrap it in a chat UI, and declare victory. I have sat in on too many post-mortems where a team shipped exactly that and then watched the agent fail the first time a customer asked "where is my refund?" instead of "what is your return policy?" The gap between a demo and a system that actually closes tickets is almost never about the model. It is about &lt;strong&gt;Customer Service Agent End-to-End Integration&lt;/strong&gt; — and most teams define that phrase wrong.&lt;/p&gt;

&lt;p&gt;The mistake is treating "end-to-end" as a plumbing problem. More connectors, more APIs, more middleware. In my experience, that framing is the single biggest reason support agents stall at the proof-of-concept stage. End-to-end is not the count of integrations you have connected. It is the &lt;em&gt;delivery boundary&lt;/em&gt;: the exact line where the agent stops talking and starts changing something, what it is allowed to change, who confirms it, and how you undo it if it was wrong. If you cannot draw that boundary on a whiteboard in one sentence, you do not have an end-to-end agent. You have a chatbot with a longer résumé.&lt;/p&gt;

&lt;p&gt;This article is a field guide to drawing that boundary. It is written for engineers and technical leads who are past the "hello world" stage and are now being asked to make a support agent actually &lt;em&gt;do&lt;/em&gt; things: look up orders, draft refunds, write tickets, and survive the day a policy changes mid-incident. I am not going to teach you how to embed text. I am going to give you a framework you can implement this quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The counter-consensus: Customer Service Agent End-to-End Integration is a delivery boundary, not API count
&lt;/h2&gt;

&lt;p&gt;RAG tutorials quietly assume that 80% of support is "answer the question from the knowledge base." That is true only for a narrow slice of contacts: pre-sales curiosity, policy explanation, how-to. The contacts that cost you money and attrition are the ones that require &lt;em&gt;state change&lt;/em&gt; — a refund issued, an address edited, a replacement shipped, a ticket escalated. Those are the calls that pile up in queues, and they are exactly the ones a pure RAG bot cannot touch.&lt;/p&gt;

&lt;p&gt;So when I say Customer Service Agent End-to-End Integration, I mean the agent's ability to carry a request from "customer asked" to "real-world state changed, logged, and reversible." That journey crosses four kinds of capability that most tutorials never separate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reading the knowledge base (the part everyone builds).&lt;/li&gt;
&lt;li&gt;Looking up live operational truth (order status in an ERP, inventory, shipping).&lt;/li&gt;
&lt;li&gt;Writing an action with authority and bounds (refund, ticket, flag).&lt;/li&gt;
&lt;li&gt;Assigning responsibility and a rollback path for every step.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Notice the last two. They are the unglamorous half. They are also, in every failure review I have run, the half that was missing. A team will happily spend six weeks tuning retrieval and then hand the agent refund authority with no approver and no undo. The integration that matters is the one that says: &lt;em&gt;here is the boundary, here is who crosses it, here is the receipt.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me give you the mental model I use to design that boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four tables framework behind Customer Service Agent End-to-End Integration
&lt;/h2&gt;

&lt;p&gt;Every production support agent I have helped ship runs on what I call the four tables. They are not literally four database tables in every case, but they are four distinct sources of truth the agent must consult on every transactional turn. If one is missing, the agent is either unsafe or useless for real work.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table 1 — The Fact table (real-time operational truth)
&lt;/h3&gt;

&lt;p&gt;This is your live order/ERP/inventory state. It is the &lt;em&gt;only&lt;/em&gt; truth for "what is happening right now." The knowledge base is not truth; it is guidance. The Fact table answers "is the order actually shipped?" not "what does our policy say about shipping?"&lt;/p&gt;

&lt;p&gt;Common mistakes: teams let the agent answer order-status questions from the KB ("usually ships in 3–5 days") instead of the Fact table. That is how you tell a customer their package is en route when it was returned to sender two days ago. The Fact table must be the system of record, queried live, with a timestamp the customer can see.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table 2 — The Rule table (versioned policy)
&lt;/h3&gt;

&lt;p&gt;Policy is not a paragraph you paste into the prompt. Policy is a versioned table with effective dates, priorities, and conflict-resolution logic. "Effective 2026-06-01, electronics returns within 30 days, priority over the generic 14-day rule." When two rules collide, the table decides, not the model's vibes.&lt;/p&gt;

&lt;p&gt;I have seen a launch slip because marketing changed the return window in a blog post but never updated the agent's policy source. The agent kept quoting the old window for three weeks. A versioned Rule table with an effective-date column and a published changelog would have caught it on day one. If you cannot answer "which policy version answered this customer?" you do not have a Rule table; you have a prompt someone edited at 2 a.m.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table 3 — The Action table (writes + authority bounds)
&lt;/h3&gt;

&lt;p&gt;This table declares which writes the agent may trigger and the authority boundaries around each. "May draft a refund up to $50; may not issue it. May open a ticket in queue P2; may not ban an account." This is where most RAG-only teams freeze, because it forces a real decision about authority.&lt;/p&gt;

&lt;p&gt;The key insight: the Action table is not about &lt;em&gt;capability&lt;/em&gt; (can the API do it?) but about &lt;em&gt;mandate&lt;/em&gt; (are we allowed to let the agent do it unsupervised?). Decoupling those two questions is what lets you ship incrementally instead of not at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  Table 4 — The Responsibility table (approver, owner, rollback, audit)
&lt;/h3&gt;

&lt;p&gt;This is the one nobody builds and everyone wishes they had. For every action step, it records: who approves, who owns the outcome, how to roll back, and what the audit record looks like. Missing the Responsibility table is the #1 cause of failed deployments I have reviewed. Teams build read + lookup + draft, then discover they have no defined approver when the agent wants to issue a $200 refund at 11 p.m., no owner when a write goes wrong, and no undo when a duplicate ticket is created.&lt;/p&gt;

&lt;p&gt;Here is the skeleton I hand teams:&lt;/p&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;Fact source&lt;/th&gt;
&lt;th&gt;Rule applied&lt;/th&gt;
&lt;th&gt;Action (R/W)&lt;/th&gt;
&lt;th&gt;Authority&lt;/th&gt;
&lt;th&gt;Approver&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Order lookup&lt;/td&gt;
&lt;td&gt;ERP read replica&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Eligibility&lt;/td&gt;
&lt;td&gt;Rule table v2026.06&lt;/td&gt;
&lt;td&gt;Return window&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Auto&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund draft&lt;/td&gt;
&lt;td&gt;Fact + Rule&lt;/td&gt;
&lt;td&gt;Amount cap&lt;/td&gt;
&lt;td&gt;Write (draft)&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;Human (tier 2)&lt;/td&gt;
&lt;td&gt;Void draft by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ticket open&lt;/td&gt;
&lt;td&gt;CRM&lt;/td&gt;
&lt;td&gt;Category&lt;/td&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;Auto (queue)&lt;/td&gt;
&lt;td&gt;Close by ticket ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Refund issue&lt;/td&gt;
&lt;td&gt;Payment API&lt;/td&gt;
&lt;td&gt;&amp;gt;$50&lt;/td&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;Human only&lt;/td&gt;
&lt;td&gt;Tier 2 agent&lt;/td&gt;
&lt;td&gt;Reverse by txn ID&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If your design fits in that table, you can build it. If it doesn't, the blank cells are your risks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Q&amp;amp;A bot vs transactional agent: the line that changes everything
&lt;/h2&gt;

&lt;p&gt;I want to be precise about a distinction the market blurs on purpose. A &lt;strong&gt;Q&amp;amp;A bot&lt;/strong&gt; only reads the knowledge base and outputs text. It is stateless with respect to your business. A &lt;strong&gt;transactional agent&lt;/strong&gt; reads, looks up live facts, writes a state change, and audits it. The second one delivers a real outcome; the first one delivers a sentence.&lt;/p&gt;

&lt;p&gt;This is not a moral hierarchy. Most contacts should be handled by the Q&amp;amp;A bot, and that is fine. The error is pretending your Q&amp;amp;A bot is "end-to-end" because it can also call one read API. A customer whose package is lost does not care that your bot "integrates" with the carrier's tracking page. They care that someone changes their situation. The transactional agent is the only one who can do that, and it is the only one that needs the four tables.&lt;/p&gt;

&lt;p&gt;A practical way to decide where to invest: tally your last 1,000 tickets by &lt;em&gt;required capability&lt;/em&gt;, not by topic. How many needed only a text answer? How many needed a lookup? How many needed a write? You will usually find that 20–30% of volume is transactional, and that 20–30% is responsible for most of your cost and most of your escalations. That is your integration target. Do not spend your budget making the FAQ bot marginally smoother.&lt;/p&gt;

&lt;h2&gt;
  
  
  No ERP API? Start with tiered integration (three tiers)
&lt;/h2&gt;

&lt;p&gt;The most common objection I hear: "our ERP has no API, so we can't do this." That is false, and it is the excuse that keeps support teams on manual queues for another year. You do not need a clean API to start. You need a &lt;em&gt;tier&lt;/em&gt; you can ship today and a path to the next tier.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 1 — Read replica for lookup.&lt;/strong&gt; If the ERP stores data in a database, stand up a read-only replica or a scheduled export. The agent queries order status from there. No writes, no risk to production, near-zero blast radius. This alone deflects the "where is my order?" calls that dominate repeat contact.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 2 — Agent drafts, human approves.&lt;/strong&gt; The agent prepares the ticket, the refund suggestion, the response — and a human clicks confirm. The agent does the cognitive work; the human carries the mandate. This is where you capture most of the efficiency without handing over authority.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Tier 3 — Low-amount, low-risk actions auto-execute and log.&lt;/strong&gt; Only after Tier 1 and 2 are stable do you let the agent auto-issue, say, refunds under a small cap or auto-close certain ticket types. Every auto-action is logged with an audit ID and is reversible.&lt;/p&gt;

&lt;p&gt;I worked with a retailer running a ten-year-old ERP that had no API and no budget to expose one. We pointed a read replica at the order database, gave the agent order-status lookup against it, and kept all tickets and refunds human-approved. Result: it deflected 60% of repeat "status" calls within the first quarter, with zero writes to the legacy system. Nobody touched the ERP. The agent just read a copy. That is Customer Service Agent End-to-End Integration at the tier that fits your reality — not the tier in the vendor demo.&lt;/p&gt;

&lt;p&gt;The trap is skipping Tier 1 to chase Tier 3. Teams that jump straight to auto-writes on a fragile connection ship incidents, then retreat to "the bot is off." Tiers exist so you can show value this quarter and earn authority next quarter.&lt;/p&gt;

&lt;h2&gt;
  
  
  Human approval is a first-class node, not a fallback
&lt;/h2&gt;

&lt;p&gt;When teams finally add a human, they bolt it on as an afterthought: "if confidence is low, escalate." That is a fallback, not a node. A first-class approval node has three things a fallback lacks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An &lt;strong&gt;SLA&lt;/strong&gt;: the agent's draft waits in a queue with a target response time, and the customer sees "pending review, ~20 min." No silent hanging.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;escalation path&lt;/strong&gt;: if the approver does not act in time, it routes up, not sideways into the void.&lt;/li&gt;
&lt;li&gt;An &lt;strong&gt;evidence UI&lt;/strong&gt;: the approver sees exactly what the agent saw — the Fact row, the Rule version, the proposed action — so they approve in seconds, not by re-investigating.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Risk tiers govern what auto-runs versus what waits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Read-only and ticket-draft&lt;/strong&gt;: can be automatic. Looking up an order and opening a structured ticket carries little irreversible risk.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Low-value compensation&lt;/strong&gt;: capped and sampled. Auto-issue a small goodwill credit, but sample 5% for human review so the pattern stays honest.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Refund, address change, account ban&lt;/strong&gt;: always human. These change money, identity, or relationship. No exception, no "just this once" override in the prompt.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Treating approval as a node changes your architecture. You build an approval service, not a branch in the prompt. The agent's job ends at "draft submitted, ID returned." The human's job starts there. That boundary is what makes the whole thing auditable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Failure paths: three lines, not one happy path
&lt;/h2&gt;

&lt;p&gt;A support agent that only handles the happy path is a liability, because support is where things go wrong on schedule. I design three lines:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Happy line&lt;/strong&gt; — everything resolves, state changes, confirmation sent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Degraded line&lt;/strong&gt; — a dependency is down or uncertain. Route to a human, and the agent must &lt;em&gt;state what is missing&lt;/em&gt; ("I can't confirm shipping because the carrier feed is delayed; routing you to an agent who can see the backup system"). Degraded is not failure; it is honest handoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback line&lt;/strong&gt; — a write was wrong or duplicate. Undo it by audit ID. If you cannot roll back a write, you do not ship that write. That is the rule. No rollback, no auto-execute.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The logs are what make the three lines operable. Every transactional turn records six things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Trigger&lt;/strong&gt; — what the customer said / what event fired.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — the Fact rows and Rule version the agent used.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;System called&lt;/strong&gt; — which downstream service, with request ID.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read/write result&lt;/strong&gt; — success, partial, timeout, with payload summary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Approver&lt;/strong&gt; — who confirmed (or "auto" with the cap that permitted it).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rollback time&lt;/strong&gt; — when it was undone, if applicable, with the audit ID.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If a post-mortem can't be reconstructed from those six fields, the integration was not end-to-end. It was a guess with a chat box.&lt;/p&gt;

&lt;h2&gt;
  
  
  Returns and exchanges capability map (eight steps)
&lt;/h2&gt;

&lt;p&gt;Here is the capability map I use to pressure-test a returns/exchanges flow. Walk each step and fill the columns. If a column is blank, that is your gap.&lt;/p&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;Input&lt;/th&gt;
&lt;th&gt;Source&lt;/th&gt;
&lt;th&gt;R/W&lt;/th&gt;
&lt;th&gt;Failure mode&lt;/th&gt;
&lt;th&gt;Human owner&lt;/th&gt;
&lt;th&gt;Rollback&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. User question&lt;/td&gt;
&lt;td&gt;"I want to return X"&lt;/td&gt;
&lt;td&gt;Chat&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Unclear item&lt;/td&gt;
&lt;td&gt;Agent (clarify)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Product identify&lt;/td&gt;
&lt;td&gt;SKU / order line&lt;/td&gt;
&lt;td&gt;Fact (order)&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Multiple matches&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Order lookup&lt;/td&gt;
&lt;td&gt;Order ID&lt;/td&gt;
&lt;td&gt;Fact (ERP replica)&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Order not found&lt;/td&gt;
&lt;td&gt;Agent&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Policy match&lt;/td&gt;
&lt;td&gt;Return window, category&lt;/td&gt;
&lt;td&gt;Rule table&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Conflicting rules&lt;/td&gt;
&lt;td&gt;Policy owner&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Eligibility&lt;/td&gt;
&lt;td&gt;Pass/fail + reason&lt;/td&gt;
&lt;td&gt;Rule + Fact&lt;/td&gt;
&lt;td&gt;Read&lt;/td&gt;
&lt;td&gt;Edge case&lt;/td&gt;
&lt;td&gt;Tier 2&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Refund/ticket action&lt;/td&gt;
&lt;td&gt;Amount, queue&lt;/td&gt;
&lt;td&gt;Action table&lt;/td&gt;
&lt;td&gt;Write (draft)&lt;/td&gt;
&lt;td&gt;Over cap&lt;/td&gt;
&lt;td&gt;Human approver&lt;/td&gt;
&lt;td&gt;Void by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Notify&lt;/td&gt;
&lt;td&gt;Email/SMS&lt;/td&gt;
&lt;td&gt;Notify API&lt;/td&gt;
&lt;td&gt;Write (notify)&lt;/td&gt;
&lt;td&gt;Bounce&lt;/td&gt;
&lt;td&gt;System retry&lt;/td&gt;
&lt;td&gt;Resend by ID&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Audit&lt;/td&gt;
&lt;td&gt;Record&lt;/td&gt;
&lt;td&gt;Log store&lt;/td&gt;
&lt;td&gt;Write&lt;/td&gt;
&lt;td&gt;Loss&lt;/td&gt;
&lt;td&gt;Eng on-call&lt;/td&gt;
&lt;td&gt;Rebuild from source&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two things to notice. Only steps 1–5 (reads) and step 7 (notify) are truly automatic. Every &lt;em&gt;write&lt;/em&gt; that changes state — step 6 especially — is gated. And step 8, the audit, is not optional polish; it is the thing that makes steps 6 and 7 safe to exist at all. If you ship steps 1–5 and 7 but skip 8, you have a bot that sometimes does things you can't explain. That is how trust dies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Pangolinfo fits: the external Amazon data layer
&lt;/h2&gt;

&lt;p&gt;Let me be clear about scope, because vendors love to imply they do everything. Your internal systems — ERP, CRM, payment, warehouse — are yours to wire. That is the work above. What an external specialist can add is the &lt;em&gt;Amazon-side fact layer&lt;/em&gt;: real-time product data, review signals, and ad-placement facts that live outside your walls.&lt;/p&gt;

&lt;p&gt;For teams selling on Amazon, the customer's question often depends on data Amazon holds, not you. "Is this review pattern new?" "Did our ad placement change this week?" "What does the live listing say versus what we think we published?" Pangolinfo fills that external Amazon data layer — the product, review, and ad-placement real-time facts — so your agent reasons from current marketplace truth instead of a stale export. The &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; is the integration point we use to pipe those facts into the agent's Fact table without the team building scrapers themselves.&lt;/p&gt;

&lt;p&gt;That is the only slice we own. Everything that writes to your customer's account is your Responsibility table, your approver, your rollback. Keep that separation and the architecture stays honest: external facts in, internal authority out.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it to the rest of your program
&lt;/h2&gt;

&lt;p&gt;This article is sub-article #2 in our Enterprise AI Transformation series. It assumes the trust foundation laid in sub-article #1, &lt;a href="https://www.pangolinfo.com/customer-service-agent-refusal/?referrer=devto" rel="noopener noreferrer"&gt;Customer Service Agent Refusal&lt;/a&gt;, where we argue the agent must be able to &lt;em&gt;refuse&lt;/em&gt; unsafe or unclear requests as the precondition for being allowed to act. An agent that can't say no should never be given write authority. Read those together, then read the pillar, &lt;a href="https://www.pangolinfo.com/ecommerce-ai-transformation/?referrer=devto" rel="noopener noreferrer"&gt;Enterprise AI Transformation&lt;/a&gt;, for the full map.&lt;/p&gt;

&lt;p&gt;The throughline: refusal is the brake, integration is the engine. You do not ship the engine without the brake.&lt;/p&gt;

&lt;h2&gt;
  
  
  A build order you can actually follow
&lt;/h2&gt;

&lt;p&gt;If you take one thing from this piece, take a sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Draw the delivery boundary in one sentence. If you can't, stop and think.&lt;/li&gt;
&lt;li&gt;Stand up the Fact table (even a read replica counts).&lt;/li&gt;
&lt;li&gt;Version your Rule table with effective dates.&lt;/li&gt;
&lt;li&gt;Define the Action table: list every write, assign a mandate tier.&lt;/li&gt;
&lt;li&gt;Build the Responsibility table: approver, owner, rollback, audit per step.&lt;/li&gt;
&lt;li&gt;Ship Tier 1 (lookup) and Tier 2 (draft + human approve). Measure deflection.&lt;/li&gt;
&lt;li&gt;Only then promote low-risk writes to Tier 3, capped and logged.&lt;/li&gt;
&lt;li&gt;Wire the six-field log on every transactional turn.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Most teams invert this: they wire APIs first and define authority last, usually after an incident. The four tables put authority first, where it belongs.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part nobody tells you
&lt;/h2&gt;

&lt;p&gt;The hardest conversation in this whole project is not technical. It is the one where you tell the business: "We can make the agent answer anything, but it may only &lt;em&gt;change&lt;/em&gt; these three things, and only with a human on two of them." That conversation feels like you are under-delivering. You are not. You are the only person in the room protecting the company from an agent that quietly issues refunds it shouldn't.&lt;/p&gt;

&lt;p&gt;I have watched teams lose six months because they promised "full automation" and then couldn't get sign-off on a single write. The tiered model lets you ship value now and earn the rest. The four tables let you have the authority conversation with a diagram instead of a prayer. That is what Customer Service Agent End-to-End Integration actually is. Not more APIs. A boundary, drawn well.&lt;/p&gt;




&lt;p&gt;If you are mapping this onto a live Amazon support operation and want the external marketplace facts handled for you, start with the deep dive on &lt;a href="https://www.pangolinfo.com/customer-service-agent-integration/?referrer=devto" rel="noopener noreferrer"&gt;Customer Service Agent End-to-End Integration&lt;/a&gt;. It pairs with the refusal foundation and the transformation pillar above, and it is where we keep the current version of the four-tables template.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Teach Your Customer Service Agent to Say "I Don't Know" — A Refusal-First Engineering Guide</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Thu, 13 Aug 2026 01:47:13 +0000</pubDate>
      <link>https://dev.to/pangolinfo/teach-your-customer-service-agent-to-say-i-dont-know-a-refusal-first-engineering-guide-45hm</link>
      <guid>https://dev.to/pangolinfo/teach-your-customer-service-agent-to-say-i-dont-know-a-refusal-first-engineering-guide-45hm</guid>
      <description>&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcab7vdjh1cyt9q6zdq4b.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fcab7vdjh1cyt9q6zdq4b.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;By Leo, AI &amp;amp; ecommerce data solutions lead at &lt;a href="https://www.pangolinfo.com/customer-service-agent-refusal/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Most teams building a customer service agent measure success wrong from day one. They wire up a retrieval pipeline, point it at a vector store, and celebrate when the bot answers 92% of conversations without a human in the loop. That number is a trap. The bot you just shipped is probably confident about things it has no business being confident about — and the first time it invents a refund window or a coupon code, you lose a customer you spent months acquiring.&lt;/p&gt;

&lt;p&gt;This article is the engineering take on a core thesis from our pillar on &lt;a href="https://www.pangolinfo.com/ecommerce-ai-transformation/?referrer=devto" rel="noopener noreferrer"&gt;enterprise AI transformation&lt;/a&gt;: &lt;strong&gt;the most overlooked capability of a customer service agent is not answering more questions. It is refusing to answer when the evidence isn't there.&lt;/strong&gt; I'll show you &lt;em&gt;why&lt;/em&gt; this is a data problem and not a model problem, then give you a concrete, runnable design: a confidence-thresholded decision function, a policy-version resolver, an escalation-logging schema, and the integration pattern that actually keeps a bot honest.&lt;/p&gt;

&lt;p&gt;If you just want code, skip to the Python section. But read the diagnosis first — most of your hallucination problems are not where you think they are.&lt;/p&gt;




&lt;h2&gt;
  
  
  The surface complaints are a smokescreen
&lt;/h2&gt;

&lt;p&gt;When users dodge AI customer service, the feedback sounds like tone problems: "it has no human feel," "it doesn't understand me," "it makes things up." I've sat in too many post-mortems where a team responds to those by swapping the LLM or rewriting the prompt's personality. Stop. The root cause is almost never the model. It's the &lt;strong&gt;foundation under the model&lt;/strong&gt; — an incomplete knowledge base and incomplete product data — and a missing safety behavior: &lt;em&gt;controlled refusal.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Let me reframe each complaint with what's actually happening underneath.&lt;/p&gt;

&lt;h3&gt;
  
  
  Truth 1: "No human feel" is a recognition problem, not a tone problem
&lt;/h3&gt;

&lt;p&gt;People assume human feel comes from phrasing — warmer greetings, more empathy tokens, a friendlier persona. Wrong. What a user actually wants is: &lt;em&gt;you remember what I bought, where I'm stuck, and what we already discussed.&lt;/em&gt; When a bot greets every customer as a blank text box and opens with a policy recital, that isn't stiff tone. It's that the bot was never connected to the user's orders, logistics, and history. &lt;strong&gt;Polite without context is more annoying than impolite without context.&lt;/strong&gt; The fix is a data integration, not a prompt.&lt;/p&gt;

&lt;p&gt;Here is a concrete example from a brand I worked with. A customer who had already opened a replacement ticket for a damaged blender came back the next day and typed "any update?" The bot answered as if it were the first contact in human history: "Hello! I'd be happy to help with your blender. Could you tell me your order number and what's wrong?" The customer had to re-explain the damage, re-paste the order ID, and re-state the problem. Nothing about the bot's phrasing was unfriendly — but the bot had zero memory of the ticket it had helped create twelve hours earlier. The customer's "no human feel" complaint was really "this thing doesn't even know we were just talking." You cannot prompt your way out of that. You have to connect the agent to the ticketing system so the opening line reads "Hi Sam, I see your replacement ticket for order ORD-44190 is still in transit — want me to check the latest tracking?" That is recognition, not tone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Truth 2: "Doesn't understand me" is a context-assembly failure
&lt;/h3&gt;

&lt;p&gt;A user says &lt;em&gt;"the cup arrived broken, I want a return."&lt;/em&gt; Behind that one sentence sit at least three system facts the agent must assemble: &lt;strong&gt;identify the SKU&lt;/strong&gt;, &lt;strong&gt;check the return window&lt;/strong&gt;, and &lt;strong&gt;decide refund versus reship&lt;/strong&gt;. Many agents collapse the whole thing into a generic "return policy FAQ" dump. The failure isn't NLP quality — it's the inability to map one sentence onto a stack of system facts: product facts, order state, and policy version. All three have to be present and consistent, or the answer is wrong by construction.&lt;/p&gt;

&lt;p&gt;A real failure story makes this concrete. A kitchenware seller rolled out a bot and a customer wrote: "the lid of my 8-cup French press shattered in the box, I want a replacement." The bot found the keyword "replacement," retrieved the generic returns FAQ, and replied with a link to print a return label and a note that refunds process in 5–7 days. What it failed to do was assemble the facts: the product had a known manufacturing defect on one variant (so the right move was a free reship, not a return-label-and-wait), the order was 9 days old (inside the window, so a refund was also possible but not required), and the policy version for defective-goods handling had a priority flag that beat the generic returns FAQ. The bot never pulled the variant, never checked order age, never loaded the defective-goods policy. It "understood" the word replacement and then guessed. The customer printed a label, shipped the whole set back, waited a week, and posted a one-star review about a "broken returns process." The model was fine. The context assembly was missing.&lt;/p&gt;

&lt;h3&gt;
  
  
  Truth 3: "Makes things up" is the most trust-destroying of all
&lt;/h3&gt;

&lt;p&gt;This is the one that makes users give up for good. When evidence is thin, a model will produce a plausible, fully fabricated answer: a wrong refund window, a coupon that doesn't exist, a delivery time pulled from nowhere. &lt;strong&gt;One confident lie destroys more goodwill than a hundred correct answers earn.&lt;/strong&gt; The bug isn't that the model "can hallucinate" — every LLM can. The bug is that the system never told it to stay quiet when it should.&lt;/p&gt;

&lt;p&gt;Here is the independent take I keep repeating to teams: &lt;em&gt;to judge whether a support agent is trustworthy, don't ask "how much did it get right." Ask first: "when it didn't know, did it honestly say so?"&lt;/em&gt; The first measures demo quality; the second measures production reliability.&lt;/p&gt;

&lt;p&gt;A hallucination war story worth memorizing: a team seeded their bot with a discount FAQ that mentioned a "WELCOME10" code for new customers. Six months later the promotion ended and the FAQ was deleted from the source folder — but a stale copy lingered in the vector store because nobody re-indexed. A returning customer asked "what coupon can I use today?" The bot, finding only the expired snippet, cheerfully told her to apply WELCOME10 at checkout. She tried it, the code failed at the cart, and she came back angry: "your own bot told me this code works." The bot had manufactured a fact from a document that was no longer true. No model swap fixes that. A retrieval pipeline with effective dates and an expiry rule would have made the stale snippet invisible.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why the root cause is data, not the model
&lt;/h2&gt;

&lt;p&gt;Eight out of ten bad AI support deployments fail not because someone picked the wrong model, but because they confused &lt;strong&gt;"documents" with "knowledge"&lt;/strong&gt; and &lt;strong&gt;"fields" with "data."&lt;/strong&gt; Let's pull each layer apart, because the fix for each is different and concrete.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 1 — Incomplete knowledge base: documents ≠ knowledge
&lt;/h3&gt;

&lt;p&gt;The default move is to dump PDFs, spreadsheets, and old chat logs into a vector store and declare the knowledge base done. But policies are not static. A return rule differs during a sale versus normal days, on the US site versus the CN site, across product categories. Without &lt;strong&gt;effective dates, scope, priority, and a conflict-resolution rule&lt;/strong&gt;, the agent can retrieve a clause uploaded last year and long expired. The more it "knows," the more confidently it errs. You didn't build a knowledge base; you built a confidence amplifier for stale facts.&lt;/p&gt;

&lt;p&gt;What "knowledge" actually requires, as structured metadata on every policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;effective_date&lt;/code&gt; and &lt;code&gt;expiry_date&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;marketplace&lt;/code&gt; scope (US / CN / EU / JP)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;product_scope&lt;/code&gt; (category, brand, or specific ASIN list)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;owner&lt;/code&gt; (who is accountable for the version)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;priority&lt;/code&gt; (which version wins when two match)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;conflict_rule&lt;/code&gt; (how to adjudicate overlapping matches)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A second, sharper failure story for this layer: a beauty brand ran a "holiday 45-day return" promotion scoped to the US marketplace only. The promo clause carried no &lt;code&gt;marketplace&lt;/code&gt; tag and no &lt;code&gt;expiry_date&lt;/code&gt;, so it sat in the store permanently. Two months after the promo ended, a UK customer asked about returning a holiday gift. The bot retrieved the US promo clause (higher lexical similarity than the plain UK 30-day rule), and told her she had 45 days. She returned the item past the real UK window, the warehouse rejected it, and the brand ate a chargeback plus a complaint to the payment processor. The document existed; the &lt;em&gt;knowledge&lt;/em&gt; did not, because the document was not scoped or dated. Tagging the promo with &lt;code&gt;marketplace: US&lt;/code&gt; and &lt;code&gt;expiry_date: 2025-01-15&lt;/code&gt; would have made the conflict rule pick the correct, narrower, in-scope policy.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 2 — Incomplete product data: it can't even answer "can this SKU ship to the US?"
&lt;/h3&gt;

&lt;p&gt;A huge share of support questions are about specific products: is it in stock, which variant still has the size, can this ZIP receive it, is the price shown on the ad placement current. These need not documents but &lt;strong&gt;real-time, structured, traceable product facts&lt;/strong&gt; — &lt;code&gt;ASIN&lt;/code&gt;, &lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;inventory&lt;/code&gt;, &lt;code&gt;ZIP pricing&lt;/code&gt;, &lt;code&gt;ad placement status&lt;/code&gt;. If product data is hand-moved, scraped ad hoc, or lags by hours, the agent can only offer "probably" and "maybe." Users want certainty, and certainty requires a live, structured data layer behind the answer.&lt;/p&gt;

&lt;p&gt;The classic failure here is the "size sold out but the bot didn't know" case. A customer asked a fashion brand's bot: "do you still have the medium in the blue hoodie?" The bot answered "yes, it's in stock" because the catalog snapshot it was pointed at had been refreshed that morning and still showed inventory. But the snapshot was six hours stale; the medium had sold out in a lunchtime drop an hour before the question. The customer ordered, got a backorder notice three days later, and felt misled. The field &lt;code&gt;inventory&lt;/code&gt; existed, but it wasn't &lt;em&gt;live data&lt;/em&gt; — it was a copy that lagged. Similarly, a seller running a ZIP-based pricing rule (lower price in ZIP 90210 promo zone) will burn trust if the bot quotes the national price because the &lt;code&gt;ZIP pricing&lt;/code&gt; field was never wired in. The fix is not a better sentence. It's a live, structured product-facts layer the agent can read at query time.&lt;/p&gt;

&lt;h3&gt;
  
  
  Layer 3 — No live access to orders and ticketing
&lt;/h3&gt;

&lt;p&gt;The deepest gap: many support agents can only &lt;em&gt;read&lt;/em&gt;, not &lt;em&gt;look up&lt;/em&gt; or &lt;em&gt;act&lt;/em&gt;. They can read policy but can't check this user's real order state; they can describe the flow but can't open a ticket or initiate a refund. So in a half-informed state, they fill the blanks with guesses — &lt;strong&gt;which is the main source of hallucination.&lt;/strong&gt; To kill hallucination, first let the agent check the system when it should. And when it can't (read-only phase, or the system is down), it must refuse the conclusion rather than fabricate it.&lt;/p&gt;

&lt;p&gt;A vivid example: a customer asked "where is my package, it's been a week?" The bot had no order-lookup tool, so instead of checking, it synthesized a plausible answer: "your package is currently at the regional carrier facility and will arrive in 2 days." It sounded specific and reassuring. In reality the order had been cancelled by the warehouse for a stock error the day before, and there was no package at all. The customer waited two more days, then wrote a furious message. The bot could have said "I can't see your order status right now — let me connect you with someone who can" and been both honest and useful. Instead, with no system access and no refusal, it guessed. This is the precise mechanism by which missing order access becomes hallucination.&lt;/p&gt;




&lt;h2&gt;
  
  
  The overlooked capability: refusal as a first-class behavior
&lt;/h2&gt;

&lt;p&gt;Mainstream demos treat a high answer rate as proof of intelligence — every question gets a full reply, looks great on stage. But what a production system actually needs is the opposite: &lt;strong&gt;refuse, escalate, and log when evidence is insufficient.&lt;/strong&gt; A weak refusal is a cold "I don't know, contact an agent." A &lt;em&gt;good&lt;/em&gt; refusal has three parts:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;State what evidence is missing&lt;/strong&gt; — &lt;em&gt;"I can't yet verify the shipping status of this order."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Give a clear next step&lt;/strong&gt; — &lt;em&gt;"I've routed you to a human, ~2 min wait."&lt;/em&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Record the gap&lt;/strong&gt; — &lt;em&gt;"logged missing field: tracking trail, queued for knowledge update."&lt;/em&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Users don't need an all-knowing bot. They need one that is honest, useful, and not making things worse.&lt;/p&gt;

&lt;p&gt;And here's the part that changes how you budget: &lt;strong&gt;refusal is not failure — it's a demand sampler for the next round of knowledge-base work.&lt;/strong&gt; Every refusal tells you "this knowledge or data isn't ready yet." Aggregate those refusals and you don't get a pile of failures; you get a &lt;strong&gt;priority list for the next round of knowledge and data work.&lt;/strong&gt; An agent that refuses, records, and follows up is worth far more than one that guesses forever. It turns "I don't know" into "we'll know next week."&lt;/p&gt;

&lt;p&gt;Think about what this does to your roadmap. Without refusal logging, knowledge-base work is driven by whoever complains loudest in the weekly meeting. With refusal logging, it's driven by evidence: the &lt;code&gt;missing_fields&lt;/code&gt; keys that show up most often &lt;em&gt;are&lt;/em&gt; the backlog. If &lt;code&gt;order_state:unverified&lt;/code&gt; appears in 40% of refusals, you stop debating and you connect the order API. If &lt;code&gt;evidence:insufficient_or_conflicting&lt;/code&gt; dominates, you stop blaming the model and you fix policy metadata. The refusal log converts a political argument into a measured one.&lt;/p&gt;




&lt;h2&gt;
  
  
  The three-piece design set: confidence + evidence + escalation
&lt;/h2&gt;

&lt;p&gt;Refusal should not rely on the model's "judgment." It should be engineered with a &lt;strong&gt;confidence threshold + an evidence threshold + explicit escalation rules.&lt;/strong&gt; Here is the trigger table we use, expanded with a scenario for each row so the behavior is unambiguous in code review:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Trigger&lt;/th&gt;
&lt;th&gt;Agent behavior&lt;/th&gt;
&lt;th&gt;Backend action&lt;/th&gt;
&lt;th&gt;Real scenario&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Evidence found, confidence ≥ threshold&lt;/td&gt;
&lt;td&gt;Draft reply with cited sources&lt;/td&gt;
&lt;td&gt;Log trace for spot checks&lt;/td&gt;
&lt;td&gt;A customer asks "what's the return window for ASIN:B0XYZ123 in the US?" The agent retrieves a current, in-scope policy (&lt;code&gt;effective_date&lt;/code&gt; valid, &lt;code&gt;marketplace: US&lt;/code&gt;), a reranker scores confidence 0.91, sources ≥ 1. It drafts a reply quoting the 30-day window and logs the trace. Green path.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;No evidence / conflicting sources&lt;/td&gt;
&lt;td&gt;State insufficiency, escalate&lt;/td&gt;
&lt;td&gt;Write to "missing field" list&lt;/td&gt;
&lt;td&gt;A customer asks "can I return an open food item?" Two policies match with opposite answers and no &lt;code&gt;priority&lt;/code&gt; to break the tie. The agent has a conflict, sources are conflicting, so it escalates with "I can't give a definitive answer on opened food returns yet" and writes &lt;code&gt;evidence:insufficient_or_conflicting&lt;/code&gt; to the gap log.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Order state incomplete / unverifiable&lt;/td&gt;
&lt;td&gt;Refuse the conclusion, give process only&lt;/td&gt;
&lt;td&gt;Retry read-only order lookup&lt;/td&gt;
&lt;td&gt;A customer asks "did my refund for ORD-99821 post yet?" The order API returns 503 / no &lt;code&gt;order_state_verified&lt;/code&gt;. The agent refuses to state a refund status, hands the customer the self-service tracking link, and triggers a retry. It never says "yes it posted" from a guess.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Action exceeds permission (refund, ticket)&lt;/td&gt;
&lt;td&gt;No execution, draft for approval&lt;/td&gt;
&lt;td&gt;Push human approval, keep audit&lt;/td&gt;
&lt;td&gt;A customer says "just refund me." Refunds are not in &lt;code&gt;allowed_write_actions&lt;/code&gt; in phase one. The agent drafts a refund request for a human approver, executes nothing, and keeps a full audit row. No money moves on a guess.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only principle that matters: &lt;strong&gt;when it answers, it must show evidence; when it can't, it must say what's missing and to whom it handed off.&lt;/strong&gt; Make both "answer" and "refuse" explainable and auditable, and the system earns trust. Notice that the first and last rows are about &lt;em&gt;acting safely&lt;/em&gt; (cite or don't execute), while the middle two are about &lt;em&gt;not lying&lt;/em&gt; (escalate or refuse). The table is really two promises: I will show my work when I answer, and I will not invent work when I can't.&lt;/p&gt;

&lt;p&gt;A practical note on tuning the thresholds: don't set &lt;code&gt;confidence_threshold&lt;/code&gt; to 0.9 on day one and wonder why everything escalates. Calibrate it against your evaluation set from the pilot (below). Start at 0.7, then look at the cases that passed at 0.7–0.8 and were later judged wrong by humans — those tell you whether to raise the floor. The threshold is a knob you turn with data, not a number you copy from a blog post. Likewise, &lt;code&gt;min_sources&lt;/code&gt; of 1 is intentionally permissive for a greenfield agent; once you have conflicting-source detection, consider requiring 2 non-conflicting sources for high-liability answers like refunds and shipping promises.&lt;/p&gt;




&lt;h2&gt;
  
  
  A runnable refusal decision function
&lt;/h2&gt;

&lt;p&gt;Below is a working Python sketch. It is intentionally framework-agnostic — no LangChain, no vendor lock-in — so you can drop it into whatever orchestration you run. It takes the retrieved evidence, a confidence score, an order-state check, and a permission check, then returns one of four decisions plus an escalation record.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;enum&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Enum&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Enum&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ANSWER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;answer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;                  &lt;span class="c1"&gt;# confident, evidence-backed -&amp;gt; draft with citations
&lt;/span&gt;    &lt;span class="n"&gt;ESCALATE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;escalate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;              &lt;span class="c1"&gt;# no/conflicting evidence -&amp;gt; human handoff + log gap
&lt;/span&gt;    &lt;span class="n"&gt;REFUSE_CONCLUSION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refuse&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="c1"&gt;# order state incomplete -&amp;gt; process only, retry lookup
&lt;/span&gt;    &lt;span class="n"&gt;DRAFT_FOR_APPROVAL&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;draft&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;       &lt;span class="c1"&gt;# action exceeds permission -&amp;gt; no execution
&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;                 &lt;span class="c1"&gt;# e.g. ["policy:pricing:us:2026-08-01", "order:ORD-99821"]
&lt;/span&gt;    &lt;span class="n"&gt;has_conflict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;order_state_verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0&lt;/span&gt;            &lt;span class="c1"&gt;# 0.0 - 1.0 from your reranker / grader
&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;confidence_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;
    &lt;span class="c1"&gt;# evidence_threshold: minimum number of distinct, non-conflicting sources required
&lt;/span&gt;    &lt;span class="n"&gt;min_sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="n"&gt;allowed_write_actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# e.g. {"refund", "open_ticket"}
&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Decision&lt;/span&gt;
    &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ticket_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;decide_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;user_query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Returns (decision, draft_or_message, escalation_record).
    The escalation_record is ALWAYS produced so refusals feed the knowledge pipeline.
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&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="c1"&gt;# 1) Action permission gate — checked FIRST, before anything executes.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;requested_action&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;requested_action&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_write_actions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DRAFT_FOR_APPROVAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;permission:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Generated a draft for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;. Routed to human approver; no action taken.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;DRAFT_FOR_APPROVAL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ve prepared a draft for &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; and sent it for human approval. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Nothing has been changed yet.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 2) Order-state gate — if a concrete conclusion needs verified order state.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;order_state_verified&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order_state:unverified&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REFUSE_CONCLUSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Refused to state a conclusion. Gave self-service process; retrying read-only order lookup.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;REFUSE_CONCLUSION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I can&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t yet confirm the current state of your order, so I won&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t guess. &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Here&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s the self-service process to check it, and I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ve triggered a retry on our side.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 3) Evidence + confidence gates.
&lt;/span&gt;    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has_conflict&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;min_sources&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence:insufficient_or_conflicting&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Stated insufficiency, escalated to human, wrote gap to missing-field list.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I don&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;t have reliable evidence for this yet, so I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m connecting you with a specialist &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;(~2 min wait) rather than guessing. I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;ve logged what&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s missing so we can answer faster next time.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence_threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;confidence:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence_threshold&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;missing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Confidence below threshold, escalated to human, wrote gap to missing-field list.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ESCALATE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m not confident enough to give you a definitive answer, so I&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;m routing you to a human &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;instead of risking a wrong one.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# 4) Passed all gates -&amp;gt; answer with citations.
&lt;/span&gt;    &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EscalationRecord&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ANSWER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
        &lt;span class="n"&gt;next_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Drafted cited reply from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt; sources; trace logged for QA.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ANSWER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[Cited draft reply based on &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;, &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;evidence&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# --- Example usage ---
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;policy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Policy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;confidence_threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_sources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed_write_actions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Case A: user asks for a refund, but refunds are not yet permitted in phase 1.
&lt;/span&gt;    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;please refund my broken cup&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;order:ORD-99821&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;order_state_verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.9&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;refund&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ESCALATION:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="c1"&gt;# Case B: thin evidence -&amp;gt; escalate.
&lt;/span&gt;    &lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;decide_reply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;what&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s my return window for this item?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="nc"&gt;Evidence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sources&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="n"&gt;has_conflict&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_state_verified&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;requested_action&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ESCALATION:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rec&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;to_json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The escalation record is the quiet hero here. Every branch produces one, including the "answer" branch (for QA sampling). That record is what turns refusal into a pipeline: you stream &lt;code&gt;missing_fields&lt;/code&gt; into a "missing field" list, and that list &lt;em&gt;is&lt;/em&gt; your next sprint's knowledge-base backlog.&lt;/p&gt;




&lt;h2&gt;
  
  
  A policy-version resolver (so stale clauses never win)
&lt;/h2&gt;

&lt;p&gt;The decision function above assumes the right policy is already in &lt;code&gt;evidence.sources&lt;/code&gt;. But &lt;em&gt;which&lt;/em&gt; policy is right depends on resolving the version conflicts that Layer 1 produces. Here is a small, standalone resolver you run at retrieval time so the agent never sees two contradictory clauses at once. It picks the single best policy for a (marketplace, product, date) tuple using the metadata we defined earlier.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;


&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PolicyDoc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;policy_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;effective_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;
    &lt;span class="n"&gt;expiry_date&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;          &lt;span class="c1"&gt;# None == still active
&lt;/span&gt;    &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                     &lt;span class="c1"&gt;# "US" / "CN" / "EU" / "JP"
&lt;/span&gt;    &lt;span class="n"&gt;product_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;                   &lt;span class="c1"&gt;# "category:kitchen" / "ASIN:B0XXXXXX"
&lt;/span&gt;    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;                        &lt;span class="c1"&gt;# higher wins on tie
&lt;/span&gt;    &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PolicyResolver&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
    Given a query context, return the single best-matching active policy,
    or None if no active policy covers the case (=&amp;gt; agent must escalate).
    &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PolicyDoc&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;policies&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;product_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;date&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;PolicyDoc&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;candidates&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketplace&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;marketplace&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;_scope_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;product_scope&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;product_scope&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_date&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;on&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expiry_date&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expiry_date&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;on&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="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# nothing active -&amp;gt; escalate, do not guess
&lt;/span&gt;        &lt;span class="c1"&gt;# highest priority wins; tie-break by most recent effective_date
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;effective_date&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="nd"&gt;@staticmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;_scope_matches&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rule_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# "category:kitchen" matches "ASIN:B0XXXXXX" if the ASIN belongs to kitchen;
&lt;/span&gt;        &lt;span class="c1"&gt;# for the resolver we keep a simple rule: exact match or category covers ASIN.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule_scope&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;query_scope&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rule_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;query_scope&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASIN:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# in production, look up the ASIN's category from the product facts layer
&lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;  &lt;span class="c1"&gt;# placeholder for catalog join
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;


&lt;span class="c1"&gt;# --- Example ---
&lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;policies&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="nc"&gt;PolicyDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;returns_base_us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category:kitchen&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Standard 30-day return for kitchen items.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="nc"&gt;PolicyDoc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;holiday_promo_us&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;category:kitchen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                  &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Holiday 45-day return for kitchen items.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;resolver&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PolicyResolver&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;policies&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# On Dec 20 2025 the higher-priority holiday promo is active and wins.
&lt;/span&gt;    &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASIN:B0XXXXXX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2025&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# On Feb 1 2026 the promo has expired; resolver falls back to the base rule.
&lt;/span&gt;    &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;resolver&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ASIN:B0XXXXXX&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2026&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;policy_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The key property: when nothing is active (or the join fails), &lt;code&gt;resolve()&lt;/code&gt; returns &lt;code&gt;None&lt;/code&gt;, and your decision function should treat that as &lt;code&gt;evidence:insufficient_or_conflicting&lt;/code&gt; and escalate. This is how you stop the bot from quoting a dead promo. The resolver is deliberately dumb and explicit — no LLM deciding "which policy feels right." Version selection is a data lookup, not a language task.&lt;/p&gt;




&lt;h2&gt;
  
  
  A schema for the "missing field" list, plus an escalation-log query
&lt;/h2&gt;

&lt;p&gt;To make the sampler real, structure the gap log. Here's a JSON schema you can store in any queue or table:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"$schema"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://json-schema.org/draft/2020-12/schema"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MissingFieldLog"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"object"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"required"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"conversation_id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"missing_fields"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"logged_at"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"properties"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"conversation_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"decision"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"escalate"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"refuse"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"draft"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"missing_fields"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"array"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Stable field keys, e.g. order_state:unverified, evidence:insufficient_or_conflicting, permission:refund, confidence:0.42&amp;lt;0.70"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"detected_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"date-time"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"marketplace"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"examples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"US"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"CN"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"EU"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"JP"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"product_scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"examples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ASIN:B0XXXXXX"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"category:kitchen"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"enum"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"in_progress"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"resolved"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"wont_fix"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"default"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"open"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"resolved_by"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"reused_in_kb"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"boolean"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Did the human rewrite become a reusable knowledge item?"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Aggregate &lt;code&gt;missing_fields&lt;/code&gt; by key weekly. The top keys &lt;em&gt;are&lt;/em&gt; your knowledge debt. If &lt;code&gt;order_state:unverified&lt;/code&gt; dominates, your problem is the order API, not the LLM. If &lt;code&gt;evidence:insufficient_or_conflicting&lt;/code&gt; dominates, your problem is policy metadata (Layer 1). The schema makes the diagnosis measurable instead of political.&lt;/p&gt;

&lt;p&gt;Once those records pile up, you want to query them. Here is a small SQL-style query (works in Postgres / BigQuery / SQLite with minor syntax tweaks) that produces the weekly knowledge-debt report you actually read in the steering meeting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Weekly knowledge-debt report: which gaps hurt most, and are we closing them?&lt;/span&gt;
&lt;span class="k"&gt;SELECT&lt;/span&gt;
    &lt;span class="n"&gt;mf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing_field&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&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;AS&lt;/span&gt; &lt;span class="n"&gt;occurrences&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'resolved'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;ROUND&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;COUNT&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="n"&gt;FILTER&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'resolved'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="k"&gt;NULLIF&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;COUNT&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;                                                   &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;close_rate_pct&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;COUNT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;DISTINCT&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;marketplace&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                       &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;markets_affected&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;missing_field_log&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
     &lt;span class="k"&gt;UNNEST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing_fields&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;AS&lt;/span&gt; &lt;span class="n"&gt;mf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;missing_field&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;detected_at&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;DATE_TRUNC&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'week'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;CURRENT_DATE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;INTERVAL&lt;/span&gt; &lt;span class="s1"&gt;'7 days'&lt;/span&gt;
&lt;span class="k"&gt;GROUP&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;mf&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;missing_field&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;occurrences&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That single query turns your refusal log into a backlog you can argue about with numbers. When &lt;code&gt;order_state:unverified&lt;/code&gt; is at the top for the third week running, the argument "the model is the problem" collapses, because the data says the order API is the problem. This is the loop that makes refusal pay for itself: refuse → log → measure → fix → answer more, safely.&lt;/p&gt;




&lt;h2&gt;
  
  
  From 200 conversations to a first evaluation set
&lt;/h2&gt;

&lt;p&gt;Don't aim for full automation on day one. A realistic pilot starts with 200 real conversations. Here's the five-step shape we recommend, with what to watch for at each step and the common mistake that sinks pilots:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Label 200 recent conversations&lt;/strong&gt; (last 30 days) as &lt;code&gt;directly-answerable&lt;/code&gt;, &lt;code&gt;system-dependent&lt;/code&gt;, or &lt;code&gt;human-judgment&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What to watch for:&lt;/em&gt; your labelers will disagree on the boundary between &lt;code&gt;system-dependent&lt;/code&gt; and &lt;code&gt;human-judgment&lt;/code&gt;. Get two labelers per conversation and measure inter-rater agreement; below 0.7, your labeling rubric is too vague and the eval set is noise.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Common mistake:&lt;/em&gt; labeling by &lt;em&gt;topic&lt;/em&gt; ("returns," "shipping") instead of by &lt;em&gt;dependency&lt;/em&gt; (can the bot verify the fact itself, or does it need a system lookup?). Topic labels don't tell you what to build.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Add &lt;code&gt;effective_date&lt;/code&gt; + &lt;code&gt;marketplace&lt;/code&gt; + &lt;code&gt;product_scope&lt;/code&gt; + &lt;code&gt;owner&lt;/code&gt;&lt;/strong&gt; to every policy so version conflicts can be adjudicated.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What to watch for:&lt;/em&gt; legacy policies with no owner. If nobody is accountable, the tag is decorative. Assign a real person per policy before go-live.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Common mistake:&lt;/em&gt; back-dating &lt;code&gt;effective_date&lt;/code&gt; to "make the numbers look clean." The resolver depends on these dates being true; fake dates turn the resolver into a liar.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Suggested replies only, zero write permissions&lt;/strong&gt; in phase one — no refunds, no ticket changes.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What to watch for:&lt;/em&gt; the bot "helpfully" describing an action it can't take ("I've started your refund"). That wording is how Northbrook-style incidents happen. Ban any verb that implies execution.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Common mistake:&lt;/em&gt; giving write access "just for one safe action" in week two. Permission creep is how pilots blow up. Keep the gate closed until the eval set proves the green path is safe.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Save low-confidence cases, final human handling, and human rewrites&lt;/strong&gt; as an evaluation set (question, evidence, model answer, human edit, outcome).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What to watch for:&lt;/em&gt; only saving the &lt;em&gt;wrong&lt;/em&gt; cases. You also need the &lt;em&gt;correct-but-close&lt;/em&gt; cases to tune the confidence threshold. A balanced set beats a pile of failures.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Common mistake:&lt;/em&gt; treating the eval set as fixed. It should grow every week as new refusal types appear, or your threshold tuning goes stale.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Weekly review&lt;/strong&gt; of refusal rate, human adoption rate, error escalation rate, and new knowledge items; use the data to decide whether to open order-lookup tools.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;em&gt;What to watch for:&lt;/em&gt; a refusal rate that spikes after a product catalog change — that's a data-layer signal, not a model regression. Read refusals as telemetry.&lt;/li&gt;
&lt;li&gt;
&lt;em&gt;Common mistake:&lt;/em&gt; declaring victory when auto-resolution climbs. Auto-resolution climbing while error-escalation also climbs is the danger zone. Watch the bundle, not the one number.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The point isn't "replace people fast." It's to first carry the low-risk, verifiable part, free human agents for real exceptions, and turn the judgment living in senior agents' heads into a business asset the company can inspect and regression-test. Only after the evaluation set is solid and adoption stable do you open transactional tools.&lt;/p&gt;




&lt;h2&gt;
  
  
  The counter-intuitive metrics to watch
&lt;/h2&gt;

&lt;p&gt;Most teams stare at one number: &lt;strong&gt;auto-resolution rate.&lt;/strong&gt; That is the most misleading metric — it tells you how many the agent caught, not whether what it caught was right, nor whether it quietly amplified errors. Here is &lt;em&gt;why&lt;/em&gt; it misleads, concretely:&lt;/p&gt;

&lt;p&gt;Auto-resolution rate is &lt;code&gt;resolved_by_bot / total&lt;/code&gt;. It is blind to &lt;em&gt;correctness&lt;/em&gt; and &lt;em&gt;harm&lt;/em&gt;. Suppose your bot resolves 80% of chats. If 15 of those 80 were confident lies (wrong refund windows, fake coupon codes, invented shipping dates), you didn't resolve 80% — you manufactured 15 incidents at machine speed. The metric &lt;em&gt;rewards&lt;/em&gt; the behavior that destroys trust, because a fabricated answer still counts as "resolved" until a human happens to catch it. Worse, auto-resolution gives managers a reason to &lt;em&gt;turn off&lt;/em&gt; the human safety net ("we're at 80%, let's cut the queue"), which means the 15 lies never get caught. The number points up and to the right while the brand burns. That is why we never report it alone.&lt;/p&gt;

&lt;p&gt;Track &lt;strong&gt;"errors not amplified by automation"&lt;/strong&gt; instead, as a bundle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;High-quality refusal rate&lt;/strong&gt; — did it refuse the right things? Direct signal of production reliability. &lt;em&gt;Example:&lt;/em&gt; of 100 chats where the bot lacked verified order state, it correctly refused 97 and guessed on 3. The 97 is your reliability number; the 3 are your incidents. A high-quality refusal rate near 100% means the bot is honest even when it can't help — exactly what earns trust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Error escalation rate&lt;/strong&gt; — of wrong answers, how many were caught by humans? Size of the risk from guessing. &lt;em&gt;Example:&lt;/em&gt; the bot gave 20 wrong answers this week; humans caught 18 before they reached the customer. Escalation rate 90% means your safety net works; 40% means lies are leaking to customers and you must tighten the gates.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Post-handoff handling time&lt;/strong&gt; — did handoff actually save time? Whether the agent lightens human load. &lt;em&gt;Example:&lt;/em&gt; if handing off takes a human 6 minutes to re-orient (because the bot logged nothing), the "agent" added work. If the handoff carries the escalation record and prior context, the human resolves in 2 minutes. This metric tells you if refusal is helping or just relabeling the problem.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reusable knowledge share&lt;/strong&gt; — how much of human edits reflowed as knowledge? Whether the system self-improves. &lt;em&gt;Example:&lt;/em&gt; a human rewrote 30 bot answers this week; 22 of those became tagged policies or product-fact fixes. A rising share means the bot gets better every week; a flat share means you're paying humans to patch the same gaps forever.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Recontact rate&lt;/strong&gt; — did users come back after a refusal? The ultimate trust gauge. &lt;em&gt;Example:&lt;/em&gt; 8% of users who got a clean "I can't verify that, here's a human" recontacted with the same issue. If that number is low, your refusals are landing as honest and useful. If it's high, your "next step" is failing and users are bouncing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Read together, these show the system is building trust rather than manufacturing polite incidents. The auto-resolution rate is allowed on the dashboard only as context next to these five — never as the headline.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where the external Amazon data layer fits (and where it doesn't)
&lt;/h2&gt;

&lt;p&gt;Back to the root cause: incomplete product data is a major source of support-agent hallucination. Your order/refund/ERP/ticketing systems are your own to integrate — no vendor can substitute for that. But the &lt;strong&gt;external Amazon data layer&lt;/strong&gt; — the structured facts about products, search, rankings, categories, and ad placements that live outside your walls — is exactly where a specialized provider earns its place.&lt;/p&gt;

&lt;p&gt;At &lt;a href="https://www.pangolinfo.com/customer-service-agent-refusal/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo&lt;/a&gt; we supply that layer rather than packaging all internal systems into a prebuilt agent. The difference shows up in the &lt;em&gt;specific fields&lt;/em&gt; the agent can finally cite instead of guess:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.pangolinfo.com/amazon-review-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Review API&lt;/a&gt;&lt;/strong&gt; — real customer feedback including the structured &lt;strong&gt;&lt;code&gt;Customer Says&lt;/code&gt;&lt;/strong&gt; field, so when a customer asks "is this blender reliable?" the agent can ground its answer in what buyers actually wrote ("blades dull after a month" appears in 40 &lt;code&gt;Customer Says&lt;/code&gt; snippets) instead of emitting a generic reassurance. That single field turns a guess into a citation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt;&lt;/strong&gt; — structured facts keyed by &lt;strong&gt;&lt;code&gt;ASIN&lt;/code&gt;&lt;/strong&gt; and &lt;strong&gt;&lt;code&gt;variant&lt;/code&gt;&lt;/strong&gt;, so "which color of B0XYZ123 is still in stock?" resolves to a concrete &lt;code&gt;variant&lt;/code&gt; row; &lt;strong&gt;&lt;code&gt;ZIP pricing&lt;/code&gt;&lt;/strong&gt; so a location-specific price quote is real, not national-average; and &lt;strong&gt;&lt;code&gt;ad placement status&lt;/code&gt;&lt;/strong&gt; so "is my Sponsored Products placement live?" returns &lt;code&gt;active&lt;/code&gt;/&lt;code&gt;paused&lt;/code&gt; rather than a hopeful "it should be." These are the exact fields Layer 2 says you need live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt;&lt;/strong&gt; — an agent-facing tool layer, so you don't rewrite scraping and parsing logic inside every agent; the structured fields above are exposed as callable tools with consistent schemas.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;a href="https://www.pangolinfo.com/amazon-scraper-skill/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper Skill&lt;/a&gt;&lt;/strong&gt; — packages common Amazon data tasks into conversational workflows your agents can call.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Shorten and stabilize the external Amazon data path, and the support agent earns the right to speak when it knows and stay quiet when it doesn't. When &lt;code&gt;ASIN&lt;/code&gt;, &lt;code&gt;variant&lt;/code&gt;, &lt;code&gt;ZIP pricing&lt;/code&gt;, &lt;code&gt;ad placement status&lt;/code&gt;, and &lt;code&gt;Customer Says&lt;/code&gt; are live and structured, the agent stops saying "probably" and starts saying "here is the fact, here is its source" — or, when the fact isn't there yet, "I don't know, and here's who does."&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion: make the agent honest before you make it smart
&lt;/h2&gt;

&lt;p&gt;Users dodge AI support because they're dodging a conversation that pretends to understand. The fix isn't a bigger model. It's to first lay the foundation of knowledge base and product data, then teach the agent the most critical and most overlooked lesson: &lt;strong&gt;when evidence is insufficient, say "I don't know," and turn that into a better answer next time.&lt;/strong&gt; When refusal becomes an explainable, logged, follow-up-able action, a support agent moves from a demo prop to a production system you can actually trust.&lt;/p&gt;

&lt;p&gt;If you're planning support or data agents for an Amazon business, start from the pillar on &lt;a href="https://www.pangolinfo.com/ecommerce-ai-transformation/?referrer=devto" rel="noopener noreferrer"&gt;why ecommerce AI transformation shouldn't start by buying agents&lt;/a&gt;, then come back to this refusal design to execute.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Want the full refusal framework, trigger table, and pilot plan?&lt;/strong&gt; Read the deep-dive on &lt;a href="https://www.pangolinfo.com/customer-service-agent-refusal/?referrer=devto" rel="noopener noreferrer"&gt;Customer Service Agent Refusal at Pangolinfo&lt;/a&gt;, and grab the &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; or &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; to give your agent real product facts to stand on.&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>How to Get Amazon Data for Free Without Fooling Yourself</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Fri, 31 Jul 2026 01:53:13 +0000</pubDate>
      <link>https://dev.to/pangolinfo/how-to-get-amazon-data-for-free-without-fooling-yourself-16d3</link>
      <guid>https://dev.to/pangolinfo/how-to-get-amazon-data-for-free-without-fooling-yourself-16d3</guid>
      <description>&lt;p&gt;You can get Amazon data for free, but “free” usually means zero subscription fee, not zero cost. The real bill shows up as maintenance time, missing ad slots, broken selectors, blocked IPs, and stale datasets.&lt;/p&gt;

&lt;p&gt;Most free-Amazon-data guides list the same options: Keepa, CamelCamelCamel, Helium 10 free tier, Jungle Scout free tools, Octoparse, Google Sheets, and a quick Python scraper. Useful, yes. Complete, no.&lt;/p&gt;

&lt;h2&gt;
  
  
  The four free sources
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lvf31ebqmp4is36h842.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F5lvf31ebqmp4is36h842.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Official sources.&lt;/strong&gt; Brand Analytics, Selling Partner API, Product Advertising API, and public Amazon pages. They are the cleanest starting point, but access is not universal. SP-API requires a Selling Partner setup; PA-API usually depends on affiliate performance; Brand Analytics requires brand eligibility.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Free tool layers.&lt;/strong&gt; Keepa free charts, CamelCamelCamel alerts, limited Helium 10/Jungle Scout/AMZScout features. Great for validation, weak for raw data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;DIY scraping.&lt;/strong&gt; Python + BeautifulSoup for static pages, Playwright/Selenium for dynamic pages, Google Sheets for tiny one-off extraction. Flexible but fragile.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Public datasets.&lt;/strong&gt; Kaggle and GitHub datasets are useful for research, not for real-time commercial decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  A minimal Python scraper
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;bs4&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;BeautifulSoup&lt;/span&gt;

&lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;User-Agent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Mozilla/5.0 Chrome/124.0 Safari/537.36&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Accept-Language&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;en-US,en;q=0.9&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://www.amazon.com/s?k=wireless+earbuds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;html&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;
&lt;span class="n"&gt;soup&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;BeautifulSoup&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;html&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;html.parser&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;soup&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;div[data-component-type=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;s-search-result&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[:&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;h2 span&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;select_one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;span.a-offscreen&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;card&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;data-asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;title&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;price&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This script is good for learning. It is not a production pipeline. It has no retry logic, no raw HTML archive, no missing-field check, no sponsored-slot detection, and no alerting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What most free guides miss
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhl0krf9ihtejlm3d1lm.webp" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fwhl0krf9ihtejlm3d1lm.webp" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;br&gt;
Free tools deliver summaries, not raw facts. A price chart is not a replayable dataset. A sales estimate is not an order record.&lt;/p&gt;

&lt;p&gt;DIY scrapers break silently. Amazon’s DOM changes, JS-loaded content moves, and anti-bot systems evolve. A script that runs today may return empty data next month.&lt;/p&gt;

&lt;p&gt;Ad slots are the hidden trap. Sponsored placements are often dynamic and interleaved. If your scraper only captures visible organic product cards, your competitor map is structurally incomplete.&lt;/p&gt;

&lt;p&gt;Time is the cost nobody records. Maintaining a small Amazon scraper across 1-2 marketplaces can easily take 8-20 engineer-hours a month. At $40-80/hour, “free” becomes expensive quickly.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decision rule
&lt;/h2&gt;

&lt;p&gt;Use free sources for learning, occasional product research, and small validation samples. Move to a paid data layer when you need batch volume, fresh raw data, ad-slot coverage, and predictable uptime.&lt;/p&gt;

&lt;p&gt;At that point, &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto_amz" rel="noopener noreferrer"&gt;Pangolinfo Amazon Scraper API&lt;/a&gt; is usually more economical than maintaining scrapers yourself. For agent workflows, use &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto_amz" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt;. For conversational data retrieval, use &lt;a href="https://www.pangolinfo.com/amazon-scraper-skill/?referrer=devto_amz" rel="noopener noreferrer"&gt;Amazon Scraper Skill&lt;/a&gt;. Commercial projects should optimize for the lowest real cost, not the lowest invoice line.&lt;/p&gt;

</description>
      <category>python</category>
      <category>data</category>
      <category>scraper</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Amazon Keyword Search Results Scraping: Six Signal Blind Spots Mainstream Tools Miss</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Tue, 28 Jul 2026 07:12:46 +0000</pubDate>
      <link>https://dev.to/pangolinfo/amazon-keyword-search-results-scraping-six-signal-blind-spots-mainstream-tools-miss-32i1</link>
      <guid>https://dev.to/pangolinfo/amazon-keyword-search-results-scraping-six-signal-blind-spots-mainstream-tools-miss-32i1</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;By Leo · Head of Engineering, Pangolinfo&lt;br&gt;
For: Amazon advertisers, competitor-intelligence teams, SaaS builders, and Agent developers&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Every "Amazon data" post opens the same way: list SerpApi, BrightData, Oxylabs, then teach you to parse JSON for ASINs. True — but it flattens a high-dimensional intelligence job into "list fetching." This piece flips it: treat the SERP as a continuously shifting competitive signal canvas, name the six signal blind spots mainstream tools miss, and give the SLA-backed standard for 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the mainstream actually sells
&lt;/h2&gt;

&lt;p&gt;Spread the mainstream Amazon keyword search results scraping solutions out and you get three camps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Generic SERP / e-commerce scraping APIs.&lt;/strong&gt; SerpApi's Amazon Search API (&lt;code&gt;engine=amazon&lt;/code&gt;, &lt;code&gt;k=keyword&lt;/code&gt;), BrightData, Oxylabs, Zyte, ScraperAPI, ScrapingBee, SOAX, Nimbleway. Their core deliverable is a structured JSON: &lt;code&gt;organic_results&lt;/code&gt; (ASIN, title, price, rating) + &lt;code&gt;product_ads&lt;/code&gt; (ads) + pagination.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Seller-facing data tools.&lt;/strong&gt; SellerSprite, Helium 10, Jungle Scout. They layer keyword databases, reverse-ASIN, opportunity scores on top of raw data, but the underlying scrape is still "list-ified."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;In-house crawler teams.&lt;/strong&gt; A 3–5 person scraping squad maintaining against anti-bot, CAPTCHAs, and DOM drift.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Different product languages, identical assumption: &lt;strong&gt;the SERP is a commodity list waiting to be serialized.&lt;/strong&gt; The engineering value sits in "anti-bot + parse" — turning Amazon's HTML into JSON. True, but it reduces an intelligence job to "data fetching."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "a list of ASINs" is only the cheapest half
&lt;/h2&gt;

&lt;p&gt;The SERP was never a list; it's a canvas. One keyword's results carry at least six interlocked signal classes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Organic rank&lt;/strong&gt; — the A9-relevance product sequence.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SP ad insertions&lt;/strong&gt; — "Sponsored" paid slots interleaved with organic.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Traffic badges&lt;/strong&gt; — Amazon's Choice, Best Seller, coupon, deal, Prime.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Editorial &amp;amp; brand zones&lt;/strong&gt; — Editorial Recommendations, Sponsored Brands.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Demand-shaping modules&lt;/strong&gt; — Customers also bought, Related searches, Frequently bought together.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Geo &amp;amp; device context&lt;/strong&gt; — the same query in different ZIPs or devices can differ by screens.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The &lt;strong&gt;relative position, co-occurrence, and tempo&lt;/strong&gt; of these elements is the real competitive intelligence. A "list of ASINs + price + rating" throws away almost all six classes — what you get is a context-stripped skeleton.&lt;/p&gt;

&lt;h2&gt;
  
  
  Six signal blind spots everyone misses
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Blind spot 1: position ≠ rank
&lt;/h3&gt;

&lt;p&gt;Most tools report "rank #3" without saying whether that's a Top-of-Search ad slot or an organic position sandwiched between three ads. Three SP ads can sit between organic #1 and #2, so "organic #1" actually appears on screen four — where users never scroll. Rank without position context is misleading. You should record: is this ASIN above the fold or collapsed? How many ads sit above it? Where does it sit relative to Amazon's Choice?&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 2: snapshot ≠ trajectory
&lt;/h3&gt;

&lt;p&gt;A single scrape is one point on a timeline. Amazon reshuffles results hourly and daily: bid swings rearrange ad slots, dayparting makes daytime and pre-dawn SERPs completely different, A/B layout tests shift the organic/ad ratio. Without a time series, your "rank" is likely the most anomalous sample of the day, mistaken for the norm. What you should store is the &lt;strong&gt;trajectory&lt;/strong&gt; — consecutive snapshots of the same keyword revealing who is holding, who is grabbing, who is quietly exiting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 3: captured ≠ complete — the costliest leak
&lt;/h3&gt;

&lt;p&gt;The most hidden and most expensive blind spot. Most tools return only 30–50% of Sponsored slots; the rest simply "don't exist" in the response. The problem: an ad slot you can't see doesn't exist in your mind, so your competitor ad map is broken at the root. We've argued specifically that &lt;strong&gt;Pangolinfo has the highest SP ad collection rate of all solutions — none equal&lt;/strong&gt;: monitored across 13 marketplaces, overall daily ad coverage is 91.4%, and a Feishu bot pushes the coverage report to on-call ops, fixing anomalies the same day. An uncaptured ad slot equals non-existence; the coverage gap directly decides your intelligence completeness.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 4: products only, missing demand-shaping signals
&lt;/h3&gt;

&lt;p&gt;Customers also bought, Related searches, Editorial Recommendations, Amazon's Choice, coupon/deal badges — these aren't "extra products," they're demand-shaping signals showing how Amazon teaches users to think. Related searches reveal where intent goes next; Customers also bought exposes real substitute/supplement relations; Editorial Recommendations is Amazon's editor making the buy decision for the user. These signals are worth more than a single ASIN yet are almost never captured, because they "don't fit a product table."&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 5: ignoring geo drift
&lt;/h3&gt;

&lt;p&gt;The same keyword in New York 10041, LA 90001, Chicago 60601 can return SERPs differing by screens: different Buy Box prices, fulfillment options, ad density, even different ASINs. National-level proxies flatten this entirely. ZIP-level collection reveals the real local competitive landscape — critical for pricing, fulfillment, and geo-targeted bidding. Treating the SERP as "one national map" is like using average temperature to decide what to wear today.&lt;/p&gt;

&lt;h3&gt;
  
  
  Blind spot 6: scraping ≠ monitoring
&lt;/h3&gt;

&lt;p&gt;"Can scrape" ≠ "scrapes completely and reliably." Many solutions have no coverage monitoring, no SLA, no anomaly alert. When a market's parse rule breaks, you get silent, incomplete data — often discovered weeks later when a report doesn't reconcile. Unmonitored scraping is a machine with no one watching the dashboard: it may have stopped exactly when you needed it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advanced answer: a monitored signal layer with an SLA
&lt;/h2&gt;

&lt;p&gt;Flip the six blind spots and you get the 2026-grade standard. Four layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Structured signal map, not bare ASINs.&lt;/strong&gt; Each record carries position index, result type (organic / SP ad / Sponsored Brands / editorial / badge), owning module, and neighbors. Make position context a first-class citizen.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Time-series first.&lt;/strong&gt; Multi-timepoint collection, continuous storage — snapshots become trajectories enabling volatility, grab, and exit analysis.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Coverage SLA.&lt;/strong&gt; Explicitly declare and continuously monitor "what % of ad slots and result modules do I actually cover," turning that number into a measurable KPI — exactly what Pangolinfo publishes daily and leads on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full-component capture.&lt;/strong&gt; Products + ads + recommendations + badges + geo, especially the neglected demand-shaping signals.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How Pangolinfo closes these gaps
&lt;/h2&gt;

&lt;p&gt;Pangolinfo's difference isn't "we can also scrape" — it's "we scrape completely, stably, and monitorably":&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Highest SP ad collection rate, none equal.&lt;/strong&gt; Monitored across 13 marketplaces, overall daily coverage 91.4%; a Feishu bot pushes the report to ops, alerting/diagnosing/recovering the same day. This "continuous monitoring + real-time push + same-day fix" loop is something no other vendor does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZIP-level geo collection.&lt;/strong&gt; Collect by ZIP to see real local SERP differences, not a national-proxy average.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Full result composition.&lt;/strong&gt; Organic, SP ads, Sponsored Brands, editorial, badges, and demand-shaping modules returned together — a signal map, not a bare list.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real-time / raw backbone.&lt;/strong&gt; From Amazon Scraper API (real-time raw) to Amazon Data MCP (an agent calls it directly) to the universal collection API docs — the data flow from scrape to analysis is one pipeline.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Build it: from bare list to signal map (engineer view)
&lt;/h2&gt;

&lt;p&gt;If you build the pipeline yourself, the hard part isn't "can you parse" — it's tagging every result with position, result type, and neighbors, then storing trajectories over time and geo by ZIP. The stabler path: hand anti-bot and 13-marketplace DOM adaptation to a managed service. Use &lt;a href="https://dev.to__SCRAPER_EN__"&gt;Amazon Scraper API&lt;/a&gt; for real-time raw results and &lt;a href="https://dev.to__MCP_EN__"&gt;Amazon Data MCP&lt;/a&gt; so an agent calls it directly — let "complete, stable, monitored" live with the side that publishes a coverage SLA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;The quality of Amazon keyword search results scraping isn't whether you "can turn the page into JSON" — it's how much you actually see on this signal canvas, especially the ad slots, position context, and geo drift others miss. Upgrading search scraping from "list fetching" to competitive intelligence is the 2026-grade foundation.&lt;/p&gt;

&lt;p&gt;Read the full argument: &lt;a href="https://dev.to__POST_EN__"&gt;Amazon keyword search results scraping: six signal blind spots&lt;/a&gt;. For the API side, see &lt;a href="https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo's complete guide to Amazon data scraping&lt;/a&gt; and &lt;a href="https://www.pangolinfo.com/amazon-sponsored-ads-placement-data/?referrer=devto" rel="noopener noreferrer"&gt;why SP ad coverage is the watershed&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Scraper API (highest SP ad coverage in the industry): &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Data MCP (agent-native): &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AMZ Data Tracker (zero-code monitoring): &lt;a href="https://www.pangolinfo.com/amz-data-tracker/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amz-data-tracker/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Universal collection API docs: &lt;a href="https://docs.pangolinfo.com/en-api-reference/universalApi/universalApi?referrer=devto" rel="noopener noreferrer"&gt;https://docs.pangolinfo.com/en-api-reference/universalApi/universalApi?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Complete guide to Amazon data scraping: &lt;a href="https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol: &lt;a href="https://modelcontextprotocol.io/" rel="nofollow noopener noreferrer"&gt;Model Context Protocol&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
    </item>
    <item>
      <title>Amazon Sponsored Ads Placement Data: Why Collection Rate — Not "Can You Scrape" — Is the Only Metric That Matters</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Mon, 27 Jul 2026 07:49:20 +0000</pubDate>
      <link>https://dev.to/pangolinfo/amazon-sponsored-ads-placement-data-why-collection-rate-not-can-you-scrape-is-the-only-1j0c</link>
      <guid>https://dev.to/pangolinfo/amazon-sponsored-ads-placement-data-why-collection-rate-not-can-you-scrape-is-the-only-1j0c</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;By Leo · Head of Engineering, Pangolinfo&lt;br&gt;
For: Amazon advertisers, competitor-intelligence teams, SaaS builders, and Agent developers&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The wrong question everyone asks
&lt;/h2&gt;

&lt;p&gt;Every "Amazon ad intelligence" article opens the same way: list Helium 10, Jungle Scout, SellerSprite, then teach you to read the backend placement report for Top of Search. All true — and all of them dodge a more fundamental question. When your tool only covers 60% of ad slots, the competitor ad map you see is broken at the root.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the mainstream actually sells
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon-native:&lt;/strong&gt; Placement Report (own account only, T+2 lag, TOS/ROS aggregated), Search Term Report, Brand Analytics, AMC, and the Ads API — which returns &lt;em&gt;only the advertiser's own data&lt;/em&gt;. Amazon has no public ad library.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Third-party PPC tools:&lt;/strong&gt; Helium 10 Adtomic, Jungle Scout, SellerSprite, SmartScout Ad Spy (competitor paid keywords, but estimates), DataHawk, Semrush, Sif.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;They all anchor to one wrong premise: that collection means "can you scrape a few ads." Hence five blind spots.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five blind spots everyone misses
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;"Can scrape" instead of coverage rate.&lt;/strong&gt; A vendor covering 60% in en-us and zero in de-de gives a distorted map. Publish per-marketplace coverage: pt-br 100%, en-au 99.0%, en-us 91.2%, ja-jp 90.2%…&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;T+2 aggregated reports vs real-time per-query.&lt;/strong&gt; The backend report can't answer "right now, on mobile US, is the rival ASIN on TOS?"&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Desktop-only bias.&lt;/strong&gt; Mobile is a huge share of Amazon traffic; sampling desktop only abandons half the slots.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;English-marketplace bias.&lt;/strong&gt; Vendors that only do en-us leave most of 13 marketplaces blank.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No monitoring, no SLA.&lt;/strong&gt; A silent drop from 91% to 70% often goes unnoticed for weeks.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Why SP placement is hard (six layers)
&lt;/h2&gt;

&lt;p&gt;Extreme dynamism / async loading and deferred rendering / cross-marketplace DOM differences (German "Anzeige", French "Sponsorisé") / cumulative anti-bot blacklisting / the real-time bidding black box / self-built crawlers that realistically hit only ~62%.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advanced answer: placement as a coverage metric with an SLA
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Primary metric = per-marketplace ad coverage rate, not "how many ads today."&lt;/li&gt;
&lt;li&gt;Real-time, per-query (keyword + device + marketplace + hour).&lt;/li&gt;
&lt;li&gt;Device-aware (mobile + desktop).&lt;/li&gt;
&lt;li&gt;Across 13 marketplaces, each reporting coverage.&lt;/li&gt;
&lt;li&gt;Continuous monitoring + alerting + SLA, fixed the same day.&lt;/li&gt;
&lt;li&gt;Consume managed data: &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; instead of a proxy fleet; let an agent call it via &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pangolinfo's SP coverage: highest, none equal
&lt;/h2&gt;

&lt;p&gt;We monitor SP scraping across 13 marketplaces daily. Overall coverage is &lt;strong&gt;91.4%&lt;/strong&gt;: pt-br 100%, en-au 99.0%, es-mx 99.6%, en-us 91.2%, de-de 91.6%, ja-jp 90.2%…&lt;/p&gt;

&lt;p&gt;The report is generated daily and pushed to on-call ops via a &lt;strong&gt;Feishu bot&lt;/strong&gt;; any dip is alerted, diagnosed, and restored the same day. That "continuous monitoring + real-time push + same-day fix" loop is &lt;strong&gt;something no other vendor does&lt;/strong&gt; — which is why Pangolinfo has the highest SP ad collection rate of all solutions, none equal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build it two ways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Yourself:&lt;/strong&gt; pull with a managed SERP API, extract SP placements, count ads_captured / sampled across 13 marketplaces → that's coverage.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consume managed data:&lt;/strong&gt; hand "how not to get blocked, how to adapt 13 marketplaces' DOMs" to &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt;; use &lt;a href="https://www.pangolinfo.com/amz-data-tracker/?referrer=devto" rel="noopener noreferrer"&gt;AMZ Data Tracker&lt;/a&gt; for zero-code visual monitoring, recording coverage as a time series.&lt;/p&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;Ad placement data is the other half of Amazon visibility. What separates vendors isn't "caught a few ads" but "how much you cover across every marketplace, device, and hour" — exactly the metric Pangolinfo publishes daily and leads (91.4% overall).&lt;/p&gt;

&lt;p&gt;Full breakdown and the six-difficulty teardown: &lt;a href="https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo's complete guide to Amazon data scraping&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Amazon Scraper API (highest SP ad coverage in the industry): &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AMZ Data Tracker (zero-code monitoring): &lt;a href="https://www.pangolinfo.com/amz-data-tracker/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amz-data-tracker/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Amazon Data MCP (agent-native): &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Complete guide to Amazon data scraping: &lt;a href="https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto" rel="noopener noreferrer"&gt;https://www.pangolinfo.com/pangolinfo-api-amazon-data-scraper-guide/?referrer=devto&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Universal collection API docs: &lt;a href="https://docs.pangolinfo.com/en-api-reference/universalApi/universalApi" rel="noopener noreferrer"&gt;https://docs.pangolinfo.com/en-api-reference/universalApi/universalApi&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Model Context Protocol: &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;https://modelcontextprotocol.io/&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>python</category>
      <category>api</category>
      <category>mcp</category>
    </item>
    <item>
      <title>Amazon Keyword Organic Ranking Tracking: Why "What Position" Is an Outdated Metric</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Fri, 24 Jul 2026 09:30:53 +0000</pubDate>
      <link>https://dev.to/pangolinfo/amazon-keyword-organic-ranking-tracking-why-what-position-is-an-outdated-metric-5b3l</link>
      <guid>https://dev.to/pangolinfo/amazon-keyword-organic-ranking-tracking-why-what-position-is-an-outdated-metric-5b3l</guid>
      <description>&lt;p&gt;&lt;em&gt;Leo · Head of Engineering, Pangolinfo&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Nearly every rank tracker sells the same promise: tell you "what position your product holds for keyword X." That metric is breaking down — not because the tools are inaccurate, but because "what position" answers the wrong question. Amazon's results swing by geo, device, Prime status, and personalization, so a single normalized "true rank" is a fiction. This post repeats the mainstream consensus, names five blind spots it leaves, and finishes with the alternative built for 2026.&lt;/p&gt;

&lt;h2&gt;
  
  
  The mainstream consensus (and the premise it gets wrong)
&lt;/h2&gt;

&lt;p&gt;The industry's eight-point playbook: separate organic from sponsored; daily is enough, ignore hourly noise (A10 "shuffle" self-heals in 72–96h); rank is an estimate, watch trends not screenshots; verify manually with incognito + VPN; pair with Brand Analytics / Search Query Performance; use proxies to avoid blocks; view rank alongside conversion, inventory, price; pick 10–30 core keywords and commit to a cadence. Each point is correct — but they all anchor to one premise: that the goal is the most accurate possible "what position." Accept that, and the five blind spots below become inevitable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The five blind spots
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. A normalized rank instead of segment SoV + traffic proxy.&lt;/strong&gt; A "#7" hides the completely different Top 10 a New York mobile Prime member sees versus a Los Angeles desktop non-Prime user. What matters is segment share-of-voice (how often your ASIN appears in the head of a keyword cluster) plus a traffic proxy from the position CTR curve — position 1 can capture 10x the clicks of position 7.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Filtering noise throws away the only real-time dividend.&lt;/strong&gt; "Ignore hourly swings" averages away competitor ad gaps, inventory / coupon windows, and temporal arbitrage. A cat-litter-box seller at $200/day and 35%–40% ACoS switched to real-time monitoring, found competitors paused ads 2–4 PM and weekends were weaker, moved 30% of budget into the afternoon gap — CTR +25%, CVR +18%, ACoS to 24%. Filter the noise and you throw away the most valuable signal.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. Tracking only "known keywords," never a ranking gap map.&lt;/strong&gt; Dashboards assume you already know which keywords to track. You never discover "I'm completely unranked for this term, yet it converts brutally well" because nobody continuously diffs your indexed terms against the category's query graph.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. Rank decoupled from its levers.&lt;/strong&gt; Mainstream advice stops at side-by-side observation. What's missing is closed-loop attribution: automatically pull price, coupon, inventory, competitor ad intensity, and your PPC as covariates and tell you "72% of this drop is competitor X's bid, 18% your stockout."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;5. Fighting anti-bot with proxies, still human-stares-at-dashboard, not agent-native.&lt;/strong&gt; The category teaches you how not to get blocked and ships a dashboard for a human. In 2026 rank data should be a primitive an agent calls directly — "what's my US mobile rank, is there a competitor window, adjust my bid" — with no human watching arrows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The advanced answer: ranking as data infrastructure
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Primary metric = segment SoV + traffic proxy, not a single normalized rank.&lt;/li&gt;
&lt;li&gt;Keep the volatility distribution; mine it for windows.&lt;/li&gt;
&lt;li&gt;Continuously draw the ranking gap map; proactively surface "should-rank-but-doesn't" terms.&lt;/li&gt;
&lt;li&gt;Treat price / coupon / inventory / competitor ad intensity / PPC as rank covariates, auto-attributed.&lt;/li&gt;
&lt;li&gt;Consume managed SERP data via &lt;a href="https://www.pangolinfo.com/amazon-scraper-api/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Scraper API&lt;/a&gt; instead of self-hosted proxy scrapers.&lt;/li&gt;
&lt;li&gt;Expose rank through &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt; so an agent calls and acts on it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Implementation: two ways
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Option A, build it yourself.&lt;/strong&gt; Pull from a managed SERP API, store, schedule, alert. Minimal Python skeleton:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;
&lt;span class="n"&gt;api_key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://api.pangolinfo.com/serp/amazon&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;keyword&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wireless earbuds&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;marketplace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;US&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;page&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;include_sponsored&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Authorization&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Bearer &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;organic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;organic_results&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;my_rank&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;organic&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;asin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;YOUR_ASIN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;my_rank&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;my_rank&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wireless earbuds organic rank fell out of Top10, now &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;my_rank&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Contract in the &lt;a href="https://docs.pangolinfo.com/en-api-reference/universalApi/universalApi" rel="noopener noreferrer"&gt;Universal Scrape API docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B, zero code.&lt;/strong&gt; Use &lt;a href="https://www.pangolinfo.com/amz-data-tracker/?referrer=devto" rel="noopener noreferrer"&gt;AMZ Data Tracker&lt;/a&gt;: configure rules in the UI, the system calls the API, stores, charts, and notifies.&lt;/p&gt;

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

&lt;ul&gt;
&lt;li&gt;Rank tracking isn't "make the number more accurate" — it's "turn rank into intelligence that triggers action."&lt;/li&gt;
&lt;li&gt;Six things mainstream tools don't do: segment SoV, real-time windows, ranking gap maps, closed-loop attribution, managed data, agent-native.&lt;/li&gt;
&lt;li&gt;For visual monitoring use &lt;a href="https://www.pangolinfo.com/amz-data-tracker/?referrer=devto" rel="noopener noreferrer"&gt;AMZ Data Tracker&lt;/a&gt;; for agent-native callable rank data, use &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Amazon Data MCP&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Full case study: &lt;a href="https://www.pangolinfo.com/amazon-real-time-keyword-data/?referrer=devto" rel="noopener noreferrer"&gt;why real-time SERP data is the only way to analyze competitors&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Leo runs engineering at &lt;a href="https://www.pangolinfo.com/amazon-data-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo&lt;/a&gt;, building real-time Amazon data infrastructure for developers and AI agents.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>data</category>
      <category>ai</category>
    </item>
    <item>
      <title>What Is Voice of Customer Analysis, and How Do You Build It in 2026</title>
      <dc:creator>Pangolinfo</dc:creator>
      <pubDate>Tue, 21 Jul 2026 08:34:22 +0000</pubDate>
      <link>https://dev.to/pangolinfo/what-is-voice-of-customer-analysis-and-how-do-you-build-it-in-2026-2me9</link>
      <guid>https://dev.to/pangolinfo/what-is-voice-of-customer-analysis-and-how-do-you-build-it-in-2026-2me9</guid>
      <description>&lt;p&gt;&lt;em&gt;Leo · Head of Engineering, Pangolinfo&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you're a developer building anything customer-aware — a brand monitor, a churn early-warning system, an agent that briefs your PM — the trap isn't your model. It's whether you can actually hear what customers are saying, across the platforms where they talk. I run the real-time consumer-insight systems at Pangolinfo (30M+ calls/day, 99% success), and I keep watching teams ship features on data they never collected.&lt;/p&gt;

&lt;p&gt;This post is practical: what VOC is, why 2026 changed it, and two runnable patterns — an MCP setup that puts multi-platform customer voice directly into your agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Voice of Customer analysis actually is
&lt;/h2&gt;

&lt;p&gt;Voice of Customer (VOC) analysis is the practice of turning raw customer feedback from social, reviews, and support channels into measurable share of voice, sentiment, and actions a brand can actually take. The key difference from surveys: it &lt;em&gt;listens&lt;/em&gt; rather than &lt;em&gt;asks&lt;/em&gt;, so there's no question-design bias, it's always-on, and it drills down to a single post.&lt;/p&gt;

&lt;p&gt;What counts as VOC is broader than most think: comments and ratings on TikTok, Instagram, YouTube, X, Facebook, Pinterest, Trustpilot; e-commerce reviews; support transcripts; deep forum discussion on Reddit; plus surveys/NPS as a supplement. (Compliance note: we only use public data outside mainland China — not Xiaohongshu, Weibo, or in-region LinkedIn.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The five-step method
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Collect&lt;/strong&gt; — pull raw expressions from multiple platforms.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Volume / SoV&lt;/strong&gt; — compute your share of voice in the category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentiment&lt;/strong&gt; — label positive / negative / neutral, and net sentiment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Drivers&lt;/strong&gt; — attribute the &lt;em&gt;reason&lt;/em&gt; behind the emotion (late shipping, weak packaging).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Act&lt;/strong&gt; — route the list to product / ops / support / PR.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Worked example: a coffee-gear brand ran the loop for a month, pulled 12,000 posts, found its SoV at 18% vs a 34% leader, saw a Friday sentiment dip, traced it to mid-week shipping delays, shifted cutoff times — and the dip flattened. The loop paid for itself.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into an AI agent with MCP (3-minute setup)
&lt;/h2&gt;

&lt;p&gt;Before the MCP (Model Context Protocol) standard, getting this data into a model meant a copy-paste loop. We replaced that with &lt;a href="https://www.pangolinfo.com/voc-insight-mcp/?referrer=devto" rel="noopener noreferrer"&gt;VOC Insight MCP&lt;/a&gt;. One config, and the agent gains &lt;strong&gt;26&lt;/strong&gt; VOC tools across 7 default platforms (TikTok, Instagram, YouTube, X, Facebook, Pinterest, Trustpilot) plus optional Threads and Reddit.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;.mcp.json&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;—&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;drop&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;into&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;any&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;MCP-capable&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;client&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;(Claude&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Code,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cursor,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Windsurf,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;Cline)&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;exact&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;install&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;command&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;per&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;docs:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://docs.pangolinfo.com/en-help-center/mcp/voc-insight/overview&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"pangolinfo-voc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npx"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"-y"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"@pangolinfo/voc-insight-mcp"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"env"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"PANGOLIN_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"YOUR_API_KEY"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After that you just say: &lt;em&gt;"This week on TikTok, how do our share of voice and sentiment compare to competitor A?"&lt;/em&gt; — the agent discovers the right tools, calls them, and returns a report. No glue code. Enterprise environments can set &lt;code&gt;PANGOLIN_READ_ONLY=true&lt;/code&gt; for a read-only agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Processing the payload (example)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;summarize_voc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;mentions&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;total&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&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="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;share_of_voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{}}&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;positive&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;neg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;negative&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;sov&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;brand&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;unknown&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;sov&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sov&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&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="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;total&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;net_sentiment&lt;/span&gt;&lt;span class="sh"&gt;"&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="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;share_of_voice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;b&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="n"&gt;c&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;total&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sov&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;()},&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="c1"&gt;# Agent returns mentions[]; the function above turns it straight into insight.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



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

&lt;ul&gt;
&lt;li&gt;VOC = listening, not asking. No survey bias, always-on, drill-down to a single post.&lt;/li&gt;
&lt;li&gt;2026 made it mandatory: social = storefront, generative search rewired discovery, agents quote real voice.&lt;/li&gt;
&lt;li&gt;Self-built scrapers cost more than the bill suggests — anti-scraping, coverage gaps, compliance risk.&lt;/li&gt;
&lt;li&gt;MCP turns a VOC pipe into a 3-minute, 26-tool upgrade for any model you already use.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're evaluating a customer-insight layer for an agent, audit coverage and compliance &lt;em&gt;before&lt;/em&gt; you touch the prompt. The model was never the bottleneck — the signal was. Full guide: &lt;a href="https://www.pangolinfo.com/what-is-voice-of-customer-analysis/?referrer=devto" rel="noopener noreferrer"&gt;What Is Voice of Customer Analysis&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Leo runs engineering at &lt;a href="https://www.pangolinfo.com/voc-insight-mcp/?referrer=devto" rel="noopener noreferrer"&gt;Pangolinfo&lt;/a&gt;, building real-time consumer-insight infrastructure for developers and AI agents.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>voc</category>
      <category>data</category>
      <category>chatgpt</category>
    </item>
  </channel>
</rss>
