<?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: Kun Shen</title>
    <description>The latest articles on DEV Community by Kun Shen (@kun_shen_eedb57cc827955f5).</description>
    <link>https://dev.to/kun_shen_eedb57cc827955f5</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%2F3985949%2F1b0c3bf8-9aab-4dcf-9892-13308c130654.png</url>
      <title>DEV Community: Kun Shen</title>
      <link>https://dev.to/kun_shen_eedb57cc827955f5</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kun_shen_eedb57cc827955f5"/>
    <language>en</language>
    <item>
      <title>Deriving Break-Even ROAS from Contribution Margin (Instead of Guessing a Target)</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Mon, 17 Aug 2026 14:04:39 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/deriving-break-even-roas-from-contribution-margin-instead-of-guessing-a-target-1k5l</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/deriving-break-even-roas-from-contribution-margin-instead-of-guessing-a-target-1k5l</guid>
      <description>&lt;p&gt;&lt;strong&gt;Direct answer:&lt;/strong&gt; Break-even ROAS is attributable sales divided by the maximum ad spend the order can absorb before profit reaches zero. Compute that ad budget only after VAT, platform fees, product cost, fulfilment, creator commission and expected return loss are represented on the correct bases.&lt;/p&gt;

&lt;p&gt;Teams often inherit a rule such as “never run below 3× ROAS.” That threshold may be sensible for one SKU and disastrous for another. The correct threshold is an output of the unit economics.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the variables
&lt;/h2&gt;

&lt;p&gt;Let:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;G&lt;/code&gt; = attributable gross sales shown by the ad measurement system;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;V&lt;/code&gt; = VAT amount embedded in &lt;code&gt;G&lt;/code&gt;, when applicable;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;P&lt;/code&gt; = platform commission;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;C&lt;/code&gt; = product and inbound cost;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;F&lt;/code&gt; = fulfilment, packaging and delivery cost;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;A&lt;/code&gt; = affiliate/creator cost;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;R&lt;/code&gt; = expected return loss;&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;T&lt;/code&gt; = target profit per attributed order.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Maximum ad spend = G - V - P - C - F - A - R - T&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;For pure break-even, set &lt;code&gt;T = 0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Break-even ROAS = G / maximum ad spend&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This ratio is valid only if &lt;code&gt;G&lt;/code&gt; uses the same sales definition as the observed ad-platform ROAS. If the platform reports tax-inclusive gross sales, the numerator should match that definition while VAT remains a cost bridge below it.&lt;/p&gt;

&lt;h2&gt;
  
  
  A hypothetical test case
&lt;/h2&gt;

&lt;p&gt;Suppose the ad platform attributes EUR 119 of tax-inclusive sales to one order. Assume:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;VAT estimate: EUR 19;&lt;/li&gt;
&lt;li&gt;platform commission: EUR 10.71;&lt;/li&gt;
&lt;li&gt;product and inbound cost: EUR 35;&lt;/li&gt;
&lt;li&gt;fulfilment and packaging: EUR 8;&lt;/li&gt;
&lt;li&gt;affiliate cost: EUR 5;&lt;/li&gt;
&lt;li&gt;expected return loss: EUR 3.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The maximum break-even ad spend is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;119 - 19 - 10.71 - 35 - 8 - 5 - 3 = EUR 38.29&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The gross-sales break-even ROAS is:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;119 / 38.29 = 3.11&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This is an illustrative scenario, not a benchmark. Change any input and the threshold changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implementation sketch
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Economics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;attributedGrossSales&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;vatAmount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;platformCommission&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;productAndInbound&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;fulfilment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;affiliate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;expectedReturnLoss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;targetProfit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;breakEvenRoas&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Economics&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;maxAdSpend&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributedGrossSales&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;vatAmount&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;platformCommission&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;productAndInbound&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fulfilment&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;affiliate&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;expectedReturnLoss&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;targetProfit&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;maxAdSpend&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;attributedGrossSales&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;maxAdSpend&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;Returning &lt;code&gt;null&lt;/code&gt; when pre-ad contribution is zero or negative is intentional. The product cannot buy its way to break-even with more advertising. Showing “0” would hide the failure state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tests worth keeping
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;No headroom:&lt;/strong&gt; pre-ad contribution is zero; output is not a finite ROAS.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher target profit:&lt;/strong&gt; break-even threshold rises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Higher expected returns:&lt;/strong&gt; break-even threshold rises.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Different VAT rate:&lt;/strong&gt; sales and tax bases remain internally consistent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No affiliate attribution:&lt;/strong&gt; creator cost is zero only when evidence supports it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rounding:&lt;/strong&gt; monetary lines round at the documented stage, not opportunistically.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Connect campaign and finance data carefully
&lt;/h2&gt;

&lt;p&gt;Ad dashboards, order exports and settlement reports may use different time zones and attribution windows. A useful diagnostic keeps both the marketing definition and the finance definition. Do not force them into false precision; label late refunds and attribution uncertainty.&lt;/p&gt;

&lt;h2&gt;
  
  
  Add a target-profit ROAS, not just break-even
&lt;/h2&gt;

&lt;p&gt;Break-even is a boundary, not necessarily an operating target. If a product needs EUR 10 contribution after advertising to fund overhead or inventory risk, include &lt;code&gt;targetProfit: 10&lt;/code&gt; in the same function. The available ad budget shrinks and the required ROAS rises. Reporting both values is useful:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;break-even ROAS answers, “At what measured efficiency does contribution reach zero?”&lt;/li&gt;
&lt;li&gt;target ROAS answers, “At what measured efficiency does the order preserve the chosen contribution?”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The difference creates a safety buffer for delayed refunds and attribution noise. Teams can alert before the mathematical loss line instead of after it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Do not average away SKU economics
&lt;/h2&gt;

&lt;p&gt;Campaign-level ROAS can hide a profitable hero SKU subsidising a loss-making product. Calculate the threshold per SKU or economically equivalent group, then compare observed performance on the same attribution definition. A blended number is useful for portfolio reporting only after the underlying thresholds are visible.&lt;/p&gt;

&lt;p&gt;An &lt;a href="https://tokmargin.com/en/tools/break-even-roas" rel="noopener noreferrer"&gt;interactive break-even ROAS model&lt;/a&gt; can help test the variables, but its defaults are planning assumptions. Account-specific rates, product VAT treatment and actual settlement data remain authoritative.&lt;/p&gt;

&lt;p&gt;Sources verified 17 August 2026:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;European Commission, &lt;a href="https://europa.eu/youreurope/business/finance-and-tax/vat/vat-rules-rates/indexamp_en.htm" rel="noopener noreferrer"&gt;VAT rules and rates&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;TikTok Shop Seller University, &lt;a href="https://seller-pt.tiktok.com/university/essay?knowledge_id=309517108905745&amp;amp;lang=en-GB" rel="noopener noreferrer"&gt;Platform Commission Fee&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Disclosure: I work on TokMargin. This article was prepared with AI-assisted editing and manually checked against the cited primary sources.&lt;/p&gt;

</description>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why adi-registration.properties Fails After a Correct Copy-Paste</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Sat, 15 Aug 2026 06:39:53 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/why-adi-registrationproperties-fails-after-a-correct-copy-paste-m7g</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/why-adi-registrationproperties-fails-after-a-correct-copy-paste-m7g</guid>
      <description>&lt;p&gt;&lt;code&gt;adi-registration.properties&lt;/code&gt; is an ownership-challenge asset used during certain manual Android package-registration flows. The important rule is simple: use the exact snippet generated by the official console, place the file in the APK assets directory, then sign and submit the intended challenge artifact. Do not invent the keys, reuse another package's file, or “clean up” the generated value.&lt;/p&gt;

&lt;p&gt;When a console rejects the challenge, the visible text is often not the problem. The file may have been packaged into the wrong variant, renamed by an editor, omitted by a build rule, or added before a later step produced a different artifact.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat the snippet as opaque input
&lt;/h2&gt;

&lt;p&gt;The official console is the source of the challenge value. Keep it exact:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;preserve the filename &lt;code&gt;adi-registration.properties&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;preserve the generated property names and values;&lt;/li&gt;
&lt;li&gt;avoid smart quotes, added comments, and line wrapping;&lt;/li&gt;
&lt;li&gt;do not copy a sample token from a tutorial;&lt;/li&gt;
&lt;li&gt;do not publish the live snippet in a repository or article.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The task is packaging, not reverse-engineering the challenge format.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put it in the APK assets root
&lt;/h2&gt;

&lt;p&gt;For a conventional Android application module, the source path is commonly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;app/src/main/assets/adi-registration.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Product flavors can change that source location. A flavor-specific asset may live under a path such as &lt;code&gt;app/src/enterprise/assets/&lt;/code&gt;, and Gradle merges source sets according to the selected variant. The only decisive evidence is the built APK.&lt;/p&gt;

&lt;p&gt;After building, inspect the archive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;unzip &lt;span class="nt"&gt;-l&lt;/span&gt; app-release.apk | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="s1"&gt;'assets/adi-registration.properties'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The expected packaged path is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assets/adi-registration.properties
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the archive listing does not show that exact entry, the console cannot read what never reached the submitted APK.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pkgready.com/guides/adi-registration-properties" rel="noopener noreferrer"&gt;adi-registration.properties packaging checks&lt;/a&gt; collect the common source-set, filename, and re-signing mistakes in one checklist. They supplement, rather than replace, the instructions shown for your package in the official console.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verify the artifact after the final signing step
&lt;/h2&gt;

&lt;p&gt;Do not inspect an intermediate APK and upload a later one. Release pipelines can rebuild, shrink, align, bundle, split, or re-sign artifacts after the file was first added.&lt;/p&gt;

&lt;p&gt;Use this sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Add the console-generated file to the intended variant's assets.&lt;/li&gt;
&lt;li&gt;Build the challenge APK.&lt;/li&gt;
&lt;li&gt;Sign it with the key required by the registration flow.&lt;/li&gt;
&lt;li&gt;Inspect that signed APK for the asset entry.&lt;/li&gt;
&lt;li&gt;Extract its public signing-certificate fingerprint.&lt;/li&gt;
&lt;li&gt;Confirm the package name and certificate are the pair you intend to prove.&lt;/li&gt;
&lt;li&gt;Upload that exact file through the official console.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Record a cryptographic hash of the uploaded APK for internal provenance. The APK file hash is not the signing-certificate fingerprint, but it lets the team confirm that the inspected and uploaded files were identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  Diagnose failures without exposing secrets
&lt;/h2&gt;

&lt;p&gt;A useful support record includes the package name, variant, expected asset path, archive listing, public certificate fingerprint, artifact file hash, build ID, and console error text. It should not include the private signing key, keystore password, Google credentials, or unpublished challenge value.&lt;/p&gt;

&lt;p&gt;Common failures map to concrete checks:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Symptom&lt;/th&gt;
&lt;th&gt;Check first&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Asset missing from APK&lt;/td&gt;
&lt;td&gt;Source-set and packaging rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wrong package in console&lt;/td&gt;
&lt;td&gt;Final &lt;code&gt;applicationId&lt;/code&gt; and flavor suffix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key not eligible&lt;/td&gt;
&lt;td&gt;Actual signer and official console guidance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;File looks correct but fails&lt;/td&gt;
&lt;td&gt;Uploaded artifact identity and exact generated snippet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Works locally but not in CI&lt;/td&gt;
&lt;td&gt;Workspace path, generated files and clean-build behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Remove or retain it deliberately
&lt;/h2&gt;

&lt;p&gt;Follow the current console instructions on whether the challenge asset must remain in later production releases. Do not assume a one-time challenge file is a permanent runtime configuration, and do not remove it before the official flow confirms completion. Record the decision in the release repository so a future cleanup does not happen by accident.&lt;/p&gt;

&lt;p&gt;Primary references: Play Console Help's &lt;a href="https://support.google.com/googleplay/android-developer/answer/16984799" rel="noopener noreferrer"&gt;package-name registration instructions&lt;/a&gt; and the Android Developer Console &lt;a href="https://support.google.com/android-developer-console/answer/16640821" rel="noopener noreferrer"&gt;package registration guide&lt;/a&gt;, rechecked August 15, 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on PkgReady, an independent readiness tool. It is not affiliated with or endorsed by Google or Android. This article was prepared with AI assistance and manually reviewed against the linked primary sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>gradle</category>
      <category>debugging</category>
      <category>mobile</category>
    </item>
    <item>
      <title>Stop Comparing the APK File Hash to the Signing Certificate</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Sat, 15 Aug 2026 06:39:11 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/stop-comparing-the-apk-file-hash-to-the-signing-certificate-4ipo</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/stop-comparing-the-apk-file-hash-to-the-signing-certificate-4ipo</guid>
      <description>&lt;p&gt;An APK can have at least two SHA-256 values that developers casually call a “fingerprint.” The SHA-256 hash of the APK file identifies the exact bytes of that file. The SHA-256 digest of the signing certificate identifies the public certificate used to sign it. Android package registration checks use the certificate fingerprint, not the file hash.&lt;/p&gt;

&lt;p&gt;Mixing them creates a frustrating failure mode: the value is valid hexadecimal, the length looks right, and the status check still cannot match the registered signing identity.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extract the certificate from the final APK
&lt;/h2&gt;

&lt;p&gt;Android's &lt;code&gt;apksigner&lt;/code&gt; is the preferred artifact-aware tool because it understands modern APK signature schemes. Run it against the signed APK that will actually be distributed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;apksigner verify &lt;span class="nt"&gt;--print-certs&lt;/span&gt; app-release.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Find the line labeled 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;Signer #1 certificate SHA-256 digest: a32a43cd...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That digest is the public certificate fingerprint. A file checksum command such as &lt;code&gt;sha256sum app-release.apk&lt;/code&gt; answers a different question: whether two APK files contain identical bytes.&lt;/p&gt;

&lt;p&gt;Google's current Play Console Help also documents a &lt;code&gt;keytool&lt;/code&gt; fallback:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;keytool &lt;span class="nt"&gt;-printcert&lt;/span&gt; &lt;span class="nt"&gt;-jarfile&lt;/span&gt; app-release.apk
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Use an up-to-date Android SDK Build Tools version when possible. An old signing tool may not interpret every modern signature scheme the way your release pipeline does.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the correct artifact
&lt;/h2&gt;

&lt;p&gt;The extraction command is easy; selecting the artifact is the hard part. Check these variables before trusting the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;release versus debug build;&lt;/li&gt;
&lt;li&gt;product flavor and &lt;code&gt;applicationIdSuffix&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;local APK versus store-delivered APK;&lt;/li&gt;
&lt;li&gt;upload key versus store app-signing key;&lt;/li&gt;
&lt;li&gt;universal APK versus split or channel-specific output;&lt;/li&gt;
&lt;li&gt;key rotation or signing migration state.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If Google Play re-signs the app through Play App Signing, the certificate on the Play-distributed artifact can differ from the certificate used to upload the bundle. For a registration check about the installed Play app, use the app-signing identity shown by Play or extracted from the relevant distributed artifact—not an upload certificate selected merely because it is nearby.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pkgready.com/guides/find-sha256-fingerprint" rel="noopener noreferrer"&gt;APK signing-certificate fingerprint guide&lt;/a&gt; summarizes extraction and normalization across these sources. It is an independent workflow aid; the artifact and official console are authoritative.&lt;/p&gt;

&lt;h2&gt;
  
  
  Normalize only formatting
&lt;/h2&gt;

&lt;p&gt;Tools may display a fingerprint with uppercase letters and colons while an API example uses lowercase text without separators. Normalizing case and removing colons is safe because it changes presentation, not bytes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'%s'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$FINGERPRINT&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;':'&lt;/span&gt; | &lt;span class="nb"&gt;tr&lt;/span&gt; &lt;span class="s1"&gt;'[:upper:]'&lt;/span&gt; &lt;span class="s1"&gt;'[:lower:]'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After normalization, a SHA-256 certificate digest should contain exactly 64 hexadecimal characters. Do not truncate it, hash the displayed string again, or convert it to SHA-1.&lt;/p&gt;

&lt;h2&gt;
  
  
  Handle multiple signers explicitly
&lt;/h2&gt;

&lt;p&gt;If &lt;code&gt;apksigner&lt;/code&gt; reports more than one signer, preserve each signer index and digest. Do not silently take the first line and discard the rest. Multiple signers may be legitimate for a specific signing history, but the registration workflow needs a deliberate mapping between the certificate being checked and the artifact's signing configuration.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the build prove its own identity
&lt;/h2&gt;

&lt;p&gt;A robust release pipeline extracts and records the public certificate immediately after signing. Store these non-secret fields with the build metadata:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;application ID;&lt;/li&gt;
&lt;li&gt;artifact digest for byte-level provenance;&lt;/li&gt;
&lt;li&gt;signing-certificate SHA-256 digest for identity;&lt;/li&gt;
&lt;li&gt;signer index if multiple signers exist;&lt;/li&gt;
&lt;li&gt;build and source revision;&lt;/li&gt;
&lt;li&gt;channel and signing path;&lt;/li&gt;
&lt;li&gt;extraction tool version.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This distinction improves more than registration checks. The APK file hash proves you have a particular binary; the certificate digest helps prove that binary belongs to the expected signing lineage. They complement each other but are not interchangeable.&lt;/p&gt;

&lt;p&gt;Primary sources: Google's &lt;a href="https://support.google.com/googleplay/android-developer/answer/16641489" rel="noopener noreferrer"&gt;SHA-256 certificate fingerprint guide&lt;/a&gt; and Android Developers' &lt;a href="https://developer.android.com/tools/apksigner" rel="noopener noreferrer"&gt;&lt;code&gt;apksigner&lt;/code&gt; reference&lt;/a&gt;, both rechecked August 15, 2026.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on PkgReady, an independent Android release-readiness tool. It is not affiliated with or endorsed by Google or Android. This article was prepared with AI assistance and manually reviewed against the linked sources.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>security</category>
      <category>mobile</category>
      <category>beginners</category>
    </item>
    <item>
      <title>Build a Read-Only Android Package Registration Gate</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Sat, 15 Aug 2026 06:38:48 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/build-a-read-only-android-package-registration-gate-58da</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/build-a-read-only-android-package-registration-gate-58da</guid>
      <description>&lt;p&gt;An Android registration gate should answer two different questions in sequence: “Is this package name registered to any verified developer?” and “Is this package name registered with the public certificate that signed this release?” Combining those questions into one vague green check hides the exact failure a developer needs to fix.&lt;/p&gt;

&lt;p&gt;The Android Developer ID Status API is suitable for this job because it is a read-only status service. It does not register a package or mutate keys. The official documentation describes a package-only request and an optional &lt;code&gt;certificateFingerprint&lt;/code&gt; query parameter for the certificate-aware request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 1: package-only signal
&lt;/h2&gt;

&lt;p&gt;The package-only call tests whether the application ID is registered. It is useful early in a build, when the final signed artifact may not exist yet.&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;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Goog-Api-Key: &lt;/span&gt;&lt;span class="nv"&gt;$ANDROID_STATUS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://androiddeveloperidstatus.googleapis.com/v1/packages/com.example.app/packageRegistrationStatus:check"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A valid response state is &lt;code&gt;REGISTERED&lt;/code&gt; or &lt;code&gt;NOT_REGISTERED&lt;/code&gt;. The result does not tell you whether your next release certificate matches. It also does not prove that your organization owns the package. It reports registration state for the supplied identifier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Level 2: artifact-aware signal
&lt;/h2&gt;

&lt;p&gt;After signing the release artifact, extract the signing certificate's public SHA-256 fingerprint and make the certificate-aware check.&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;-sS&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Goog-Api-Key: &lt;/span&gt;&lt;span class="nv"&gt;$ANDROID_STATUS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="s2"&gt;"https://androiddeveloperidstatus.googleapis.com/v1/packages/com.example.app/packageRegistrationStatus:check?certificateFingerprint=PUBLIC_SHA256_HEX"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That check can return:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;REGISTERED&lt;/code&gt;: the package-and-fingerprint pair is registered.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;REGISTERED_WITH_ANOTHER_CERTIFICATE_FINGERPRINT&lt;/code&gt;: the package is registered, but not with the fingerprint supplied in this request.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NOT_REGISTERED&lt;/code&gt;: the supplied lookup is not registered according to the service response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The distinction is the reason to retain both levels. A package-only green result and an artifact-aware mismatch should produce a signing investigation, not a generic “registration passed” message.&lt;/p&gt;

&lt;p&gt;For a compact state and remediation map, see this &lt;a href="https://pkgready.com/guides/check-app-registration-status" rel="noopener noreferrer"&gt;package-registration status workflow&lt;/a&gt;. Treat it as an operational explanation; the API response and official consoles are the source of truth.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail the right thing
&lt;/h2&gt;

&lt;p&gt;Status, authentication, quota, and service availability are separate dimensions. The official API guide uses standard Google Cloud error objects and recommends building logic around the canonical &lt;code&gt;status&lt;/code&gt; field rather than parsing English error text.&lt;/p&gt;

&lt;p&gt;A reasonable gate policy is:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Condition&lt;/th&gt;
&lt;th&gt;Release-gate action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;REGISTERED&lt;/code&gt; for final package and certificate&lt;/td&gt;
&lt;td&gt;Pass the registration check&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT_REGISTERED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Block protected-channel promotion and assign registration work&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different certificate&lt;/td&gt;
&lt;td&gt;Block and start signing-identity diagnosis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;INVALID_ARGUMENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fail the job configuration without retry&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;PERMISSION_DENIED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fail configuration; verify API enablement and credential scope&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;RESOURCE_EXHAUSTED&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Mark the check inconclusive; retry later with backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;INTERNAL&lt;/code&gt; or &lt;code&gt;UNAVAILABLE&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Retry with bounded exponential backoff&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Do not translate every non-200 response into &lt;code&gt;NOT_REGISTERED&lt;/code&gt;. That produces false negatives precisely when quota or networking is unhealthy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the API key on the server side
&lt;/h2&gt;

&lt;p&gt;The API supports key credentials, but that does not make a key safe to embed in a browser bundle, APK, build log, or public workflow file. Store it in the CI platform's secret store, send it only from a trusted runner, and mask derived command output that might echo headers.&lt;/p&gt;

&lt;p&gt;Restrict the key to the required API where Google Cloud supports that restriction. Rotate it if it appears in logs. Also budget calls: the official page currently publishes a default limit of 1,000 &lt;code&gt;CheckPackageRegistrationStatus&lt;/code&gt; requests per project per day. Cache a result only for a deliberately short period, because registration state can change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Log decisions, not credentials
&lt;/h2&gt;

&lt;p&gt;A useful audit record contains the final package name, a normalized public fingerprint, response state, HTTP status, canonical error status when present, timestamp, workflow run, and source commit. It must not contain the API key, a private signing key, keystore password, or whole APK.&lt;/p&gt;

&lt;p&gt;This turns the gate into reproducible evidence. When a later build changes state, the team can compare the exact artifact identity and status response instead of debating which screenshot was current.&lt;/p&gt;

&lt;p&gt;Primary reference: &lt;a href="https://developer.android.com/developer-verification/guides/check-registration-status" rel="noopener noreferrer"&gt;Check app registration status with the Android Developer ID Status API&lt;/a&gt;, last updated by Android Developers on July 30, 2026 when checked for this article.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I work on PkgReady, an independent implementation aid. It is not affiliated with or endorsed by Google or Android. This article was prepared with AI assistance and manually checked against the linked primary source on August 15, 2026.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>android</category>
      <category>devops</category>
      <category>api</category>
      <category>security</category>
    </item>
    <item>
      <title>Modeling a Phone Upgrade Contract as a State Machine</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Wed, 12 Aug 2026 10:09:48 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/modeling-a-phone-upgrade-contract-as-a-state-machine-4gi2</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/modeling-a-phone-upgrade-contract-as-a-state-machine-4gi2</guid>
      <description>&lt;p&gt;An upgrade-cost calculator should not begin with a price field. It should begin with a state.&lt;/p&gt;

&lt;p&gt;When a user asks what an iPhone upgrade will cost, the application must know whether the user intends to return the phone, replace it with a new lease, leave early, or keep it. Those are different contract paths, not variations of one total.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the terminal states
&lt;/h2&gt;

&lt;p&gt;A minimal model can use four terminal states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;RETURN_AT_TERM
REPEAT_UPGRADE
EARLY_RETURN
PURCHASE_DEVICE
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each state activates a different group of inputs. &lt;code&gt;PURCHASE_DEVICE&lt;/code&gt; requires a purchase-option amount and purchase tax. &lt;code&gt;RETURN_AT_TERM&lt;/code&gt; does not. &lt;code&gt;EARLY_RETURN&lt;/code&gt; may require remaining scheduled payments and a condition assessment. &lt;code&gt;REPEAT_UPGRADE&lt;/code&gt; closes one contract and begins a separate application; it should not inherit the new payment as though it were an extension of the old schedule.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a schedule, not an average
&lt;/h2&gt;

&lt;p&gt;Do not store only &lt;code&gt;monthlyPayment&lt;/code&gt; and &lt;code&gt;termMonths&lt;/code&gt;. A schedule is safer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Money&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;cents&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;USD&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ScheduledPayment&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;sequence&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;dueOn&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This structure can represent payment changes, credits that expire, and an individual quote that does not match a public example. Integer cents prevent binary floating-point errors from leaking into consumer totals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model unknown as a real state
&lt;/h2&gt;

&lt;p&gt;Public information cannot determine a customer’s local tax, final trade-in value, approval, condition fee, or exact Klarna purchase quote. Those values should be nullable or represented by a tagged union:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;T&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;known&lt;/span&gt;&lt;span class="dl"&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="nx"&gt;T&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nl"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Never convert unknown tax to zero. Zero is a verified value; unknown means the result is incomplete. A calculation can still return the supported subtotal plus an explicit list of unresolved fields.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep cost buckets separate
&lt;/h2&gt;

&lt;p&gt;The calculation contract should distinguish:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;UpgradeCosts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;scheduledLease&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;leaseTax&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;appleCare&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;earlyExitObligation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;conditionCharge&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;purchaseOption&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;carrierCharges&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Known&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The separation is more important than the final addition. AppleCare is optional and separate from the current Apple Upgrade lease. Carrier charges come from a wireless-service relationship, not the Klarna lease. A purchase option belongs only to the ownership path.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://phoneupgradecalc.com/methodology/" rel="noopener noreferrer"&gt;published calculation methodology&lt;/a&gt; uses the same principle: show the inputs and boundaries instead of inventing a universal residual percentage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attach provenance to volatile fields
&lt;/h2&gt;

&lt;p&gt;Every price or rule that can change should carry:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;source URL;&lt;/li&gt;
&lt;li&gt;date verified;&lt;/li&gt;
&lt;li&gt;device and configuration;&lt;/li&gt;
&lt;li&gt;whether it is a public example or an individual quote;&lt;/li&gt;
&lt;li&gt;source snapshot or content hash; and&lt;/li&gt;
&lt;li&gt;review status.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A value without provenance should not silently replace the last known-good snapshot. Freshness is useful only when the new record passes validation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Assert exclusions in tests
&lt;/h2&gt;

&lt;p&gt;State-machine tests should assert both what is included and what is excluded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;returnAtTerm&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;purchaseOption&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeExcluded&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;purchaseDevice&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;purchaseOption&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toBeKnown&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;earlyReturn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;remainingPayments&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeAssumedZero&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;allPaths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;carrierCharges&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;apple-lease&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Useful cases include an ordinary end-of-term return, three payments remaining on an early exit, ownership after partial payment credit, and a schedule whose promotional credit expires.&lt;/p&gt;

&lt;h2&gt;
  
  
  Return a scenario result
&lt;/h2&gt;

&lt;p&gt;The result should identify its boundary:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;ScenarioResult&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;state&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;TerminalState&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;knownTotal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Money&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;includedFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;excludedFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;unresolvedFields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="nl"&gt;verifiedAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This lets the UI say “known return-path total before local tax” instead of presenting an unsupported universal cost.&lt;/p&gt;

&lt;p&gt;The architecture rule is simple: choose the ending, activate the fields that belong to that ending, preserve unknowns, and show the evidence attached to every volatile input.&lt;/p&gt;

&lt;p&gt;Primary references: &lt;a href="https://www.apple.com/shop/apple-upgrade" rel="noopener noreferrer"&gt;Apple Upgrade&lt;/a&gt; and &lt;a href="https://www.apple.com/shop/apple-upgrade/how-to" rel="noopener noreferrer"&gt;Apple’s upgrade and purchase-option guidance&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Disclosure: I maintain PhoneUpgradeCalc. This article was prepared with AI assistance and manually reviewed against the cited primary sources on August 12, 2026.``&lt;/p&gt;

</description>
      <category>programming</category>
      <category>webdev</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
    <item>
      <title>How to Build a Crawl Budget That Keeps AI Agents Fast and Predictable</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Sun, 19 Jul 2026 02:52:52 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/how-to-build-a-crawl-budget-that-keeps-ai-agents-fast-and-predictable-3ge7</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/how-to-build-a-crawl-budget-that-keeps-ai-agents-fast-and-predictable-3ge7</guid>
      <description>&lt;p&gt;AI agents often begin with a deceptively simple web-access loop: take a URL, fetch it, extract text, and pass the result to a model. That loop works in a demo. In production, it can become a source of latency spikes, runaway costs, repeated requests, and inconsistent evidence.&lt;/p&gt;

&lt;p&gt;A crawl budget is the control system that keeps this work predictable. It is more than a request limit. A useful budget decides which pages deserve attention, how much effort each page may consume, and when the agent should stop.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the job, not the crawler
&lt;/h2&gt;

&lt;p&gt;The correct budget depends on the agent's task. A monitoring agent may revisit a small set of pages on a schedule. A research agent may explore many domains once. A shopping agent may need current prices but can ignore most navigation pages.&lt;/p&gt;

&lt;p&gt;Write the task as a small contract before choosing limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;what evidence must be collected;&lt;/li&gt;
&lt;li&gt;how fresh the evidence needs to be;&lt;/li&gt;
&lt;li&gt;how many independent sources are required;&lt;/li&gt;
&lt;li&gt;the maximum acceptable latency;&lt;/li&gt;
&lt;li&gt;the maximum cost per completed task.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This contract prevents the crawler from treating every discovered URL as equally valuable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every request an expected value
&lt;/h2&gt;

&lt;p&gt;A URL should enter the queue with a reason. Useful signals include its relationship to the query, the authority of its host, its distance from a known source, its content type, and the chance that it contains new information.&lt;/p&gt;

&lt;p&gt;A simple priority score can combine those signals: priority equals relevance times freshness need times source value, divided by expected cost.&lt;/p&gt;

&lt;p&gt;The formula does not need to be mathematically perfect. Its purpose is to make tradeoffs visible. A product specification linked from a manufacturer's page should normally outrank a tag archive discovered five clicks away.&lt;/p&gt;

&lt;p&gt;Expected cost should include more than bandwidth. JavaScript rendering consumes more time and compute than a direct HTML fetch. A screenshot adds storage and downstream vision cost. Retries also consume the budget, even when they produce no content.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use a staged access strategy
&lt;/h2&gt;

&lt;p&gt;The cheapest successful method should win. Start with a normal fetch and examine the result. Escalate to rendering only when the response lacks the content that should be present, relies on client-side navigation, or contains an application shell instead of the requested data.&lt;/p&gt;

&lt;p&gt;Search is often a better first step than blind crawling. A targeted search can identify a few relevant pages before the agent spends budget extracting them. For focused discovery, &lt;a href="https://anycrawler.com/crawler/search/page/" rel="noopener noreferrer"&gt;AnyCrawler's search-page endpoint&lt;/a&gt; is one example of a workflow that combines result discovery with page-level access.&lt;/p&gt;

&lt;p&gt;Screenshots should be deliberate. They are valuable when layout, charts, canvas elements, or visual state are evidence. They should not be the default representation of a text article.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate task, host, and page budgets
&lt;/h2&gt;

&lt;p&gt;One global limit is too coarse. Use three layers.&lt;/p&gt;

&lt;p&gt;A task budget limits total requests, rendered pages, bytes, elapsed time, and retries for one user goal. A host budget prevents one domain from consuming the entire task. A page budget caps the work spent on a single stubborn URL.&lt;/p&gt;

&lt;p&gt;Host-level controls also improve politeness. Limit concurrency per host, respect crawl directives, and add delays when a server returns rate-limit or overload responses. Backoff should consume elapsed-time budget so that the agent cannot wait forever.&lt;/p&gt;

&lt;p&gt;Page budgets should define a clear escalation ceiling. For example, allow one fetch, one render attempt if justified, and one retry for a transient failure. Authentication walls, persistent access denials, and repeated empty responses should become explicit outcomes rather than infinite loops.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deduplicate before spending
&lt;/h2&gt;

&lt;p&gt;Agents frequently encounter the same content through tracking parameters, alternate paths, print views, and redirects. Normalize URLs before enqueueing them. Remove known tracking parameters, resolve relative links, and store the final URL after redirects.&lt;/p&gt;

&lt;p&gt;Content fingerprints catch duplicates that URL rules miss. A lightweight hash of normalized main text can prevent the same syndicated article from being processed repeatedly. Keep the source URLs even when content is duplicated; provenance still matters.&lt;/p&gt;

&lt;p&gt;Caching should reflect freshness requirements. Stable documentation can be reused longer than a live price or breaking-news page. Record the retrieval time and cache policy next to the extracted evidence so the agent can decide whether reuse is acceptable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make stopping a first-class decision
&lt;/h2&gt;

&lt;p&gt;A good agent stops because it has enough evidence, not merely because it has exhausted the web. Define completion signals such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the required facts are supported by two independent sources;&lt;/li&gt;
&lt;li&gt;new pages have stopped adding unique claims;&lt;/li&gt;
&lt;li&gt;remaining queue items fall below a value threshold;&lt;/li&gt;
&lt;li&gt;the time or cost reserve is needed for synthesis and verification.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Reserve part of the total budget for verification. Discovering ten pages is not useful if no capacity remains to check their claims, dates, and canonical sources.&lt;/p&gt;

&lt;h2&gt;
  
  
  Measure outcomes, not just requests
&lt;/h2&gt;

&lt;p&gt;Request counts alone cannot reveal whether a budget works. Track useful pages per task, unique evidence items, duplicate rate, render escalation rate, median and tail latency, bytes transferred, and cost per accepted source.&lt;/p&gt;

&lt;p&gt;Also log why pages were skipped or stopped. Reasons such as low relevance, duplicate content, access denied, budget exhausted, and stale cache make later tuning possible.&lt;/p&gt;

&lt;p&gt;Review failures by task type. If research tasks often run out of render budget, the discovery stage may be selecting too many application pages. If monitoring tasks repeatedly fetch unchanged documents, caching or conditional requests need improvement.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical default policy
&lt;/h2&gt;

&lt;p&gt;A reasonable starting policy is conservative: search first, fetch selected pages, render only on evidence of client-side content, and capture screenshots only for visual claims. Cap per-host concurrency, normalize and deduplicate URLs, reserve verification capacity, and stop when evidence coverage is sufficient.&lt;/p&gt;

&lt;p&gt;The exact numbers will change with the product and workload. The structure should remain stable. A crawl budget turns web access from an open-ended exploration into an accountable resource allocation process. That makes agents faster, cheaper, easier to debug, and more respectful of the sites they depend on.&lt;/p&gt;

</description>
      <category>agents</category>
      <category>ai</category>
      <category>performance</category>
      <category>webscraping</category>
    </item>
    <item>
      <title>Model an Evidence Chain, Not a Bag of Citations</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Sat, 18 Jul 2026 04:26:39 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/model-an-evidence-chain-not-a-bag-of-citations-3igh</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/model-an-evidence-chain-not-a-bag-of-citations-3igh</guid>
      <description>&lt;p&gt;AI research products often display citations, but a row of links at the bottom of an answer does not tell you how the answer was built. A citation can be relevant to the topic without supporting the sentence beside it. Several links can repeat the same underlying source. A model can also drop important uncertainty while turning notes into fluent prose.&lt;/p&gt;

&lt;p&gt;The data model has to preserve more than URLs. It should preserve the path from the original question to research queries, retrieved material, evidence grouped by claim, and the final report. We call that path an evidence chain.&lt;/p&gt;

&lt;p&gt;This article focuses on the database and pipeline boundaries that make such a chain reconstructable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The common shortcut: one giant JSON result
&lt;/h2&gt;

&lt;p&gt;The fastest implementation is usually a job table with an input question and one JSON column containing everything else. It works until the product needs to answer operational questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which research query found this source?&lt;/li&gt;
&lt;li&gt;Which source supported this claim?&lt;/li&gt;
&lt;li&gt;Did two citations come from the same domain?&lt;/li&gt;
&lt;li&gt;Was this page reused from an earlier crawl?&lt;/li&gt;
&lt;li&gt;Which model stage changed the wording?&lt;/li&gt;
&lt;li&gt;Can we regenerate the summary without repeating the crawl?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A single blob makes these questions expensive and fragile. Every query becomes a custom JSON traversal, and relationships that should be enforced by keys exist only by convention.&lt;/p&gt;

&lt;p&gt;The better approach is to keep the original run as the parent while giving important artifacts their own records.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Start with a search run
&lt;/h2&gt;

&lt;p&gt;A search run represents one research question and its lifecycle. It owns the status, timestamps, final report, content metadata, and publication decision.&lt;/p&gt;

&lt;p&gt;The run should not pretend that generation is atomic. A real research workflow may expand keywords, search multiple providers, crawl pages, extract page-level facts, aggregate evidence, synthesize a report, classify the content, and calculate an indexability result. Those stages should all reference the same run ID.&lt;/p&gt;

&lt;p&gt;This parent key is what lets an operator reconstruct one execution without correlating timestamps across unrelated log streams.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Store research queries in order
&lt;/h2&gt;

&lt;p&gt;Generated research queries deserve their own ordered records. Order matters because it shows the strategy the system attempted, and stable indices make retries deterministic.&lt;/p&gt;

&lt;p&gt;A minimal query record needs the run ID, a query index, and the query text. You may also want the prompt version that produced it, the provider used, and whether the query was executed or skipped.&lt;/p&gt;

&lt;p&gt;Keeping queries separate makes it possible to evaluate query quality independently from answer quality. If a report misses an important perspective, you can tell whether the gap originated in query generation or later retrieval.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Treat crawled pages as artifacts
&lt;/h2&gt;

&lt;p&gt;Search results and crawled pages are inputs, not yet evidence. A page artifact should record the run, artifact type, query or keyword, URL, retrieval metadata, and the raw or normalized payload needed by later stages.&lt;/p&gt;

&lt;p&gt;That distinction prevents a dangerous shortcut: assuming that every retrieved page supports the answer. Most search results are candidates. Some are duplicates, some only mention the topic, and some contradict the emerging conclusion.&lt;/p&gt;

&lt;p&gt;Page artifacts also create a clean caching boundary. A system can reuse a recent crawl for the same URL while still running fresh evidence extraction for a new question. Retrieval freshness and claim relevance are different concerns and should not share one cache key.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Model evidence around claims
&lt;/h2&gt;

&lt;p&gt;Evidence becomes useful when it is connected to a claim. An evidence record can contain:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the parent run ID&lt;/li&gt;
&lt;li&gt;a stable evidence index&lt;/li&gt;
&lt;li&gt;the claim it supports or challenges&lt;/li&gt;
&lt;li&gt;the research keyword or query&lt;/li&gt;
&lt;li&gt;source title, summary, and URL&lt;/li&gt;
&lt;li&gt;normalized domain&lt;/li&gt;
&lt;li&gt;an explanation of relevance&lt;/li&gt;
&lt;li&gt;nested source details when several passages support one item&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The claim field is the critical part. It gives reviewers a unit they can inspect. Instead of asking whether a source is “about the topic,” they can ask whether the source supports the specific statement that will appear in the report.&lt;/p&gt;

&lt;p&gt;This structure also enables domain-diversity checks and source counts without parsing rendered Markdown. Quality rules should operate on evidence records before the final page exists.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Keep model-call provenance beside content provenance
&lt;/h2&gt;

&lt;p&gt;Content provenance explains where facts came from. Model-call provenance explains how the system transformed those facts. Both are required for reproducibility.&lt;/p&gt;

&lt;p&gt;For each stage, retain request messages, structured inputs, the raw provider response, parsed output, model and reasoning configuration, token usage, latency, prompt version, and errors. Link every call to the same run.&lt;/p&gt;

&lt;p&gt;With that relationship, a reviewer can move in both directions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;from a sentence in the report to its claim and sources&lt;/li&gt;
&lt;li&gt;from a suspicious source to the extraction and synthesis calls that consumed it&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is much more actionable than a generic “generated by AI” label.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Save the final report, but do not make it the source of truth
&lt;/h2&gt;

&lt;p&gt;The final report is a presentation artifact. It may be Markdown or HTML, and it may include inline links for readers. But source counts, domain counts, confidence, and evidence sufficiency should come from structured data, not from scraping the report after generation.&lt;/p&gt;

&lt;p&gt;That separation has a practical benefit: presentation can change without destroying provenance. You can redesign citation components, generate mobile summaries, or expose an API while keeping the same underlying evidence relationships.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Make insufficiency a valid outcome
&lt;/h2&gt;

&lt;p&gt;An evidence chain should be able to end without a publishable answer. If retrieval produces too few sources, domains are not independent, or the evidence conflicts, the run can complete with an insufficiency flag and clear reasons.&lt;/p&gt;

&lt;p&gt;This is not a pipeline failure. It is a research result. Treating insufficiency as data prevents the system from filling gaps with more confident prose simply to satisfy a success state.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Expose enough of the chain to readers
&lt;/h2&gt;

&lt;p&gt;Internal logs can contain sensitive or operational details, so they should not be dumped into a public page. Readers still benefit from a carefully selected public surface:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;visible source links&lt;/li&gt;
&lt;li&gt;source titles and domains&lt;/li&gt;
&lt;li&gt;claim-oriented evidence groups&lt;/li&gt;
&lt;li&gt;correction and attribution channels&lt;/li&gt;
&lt;li&gt;methodology and editorial policy pages&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The public &lt;a href="https://omniracle.com/methodology" rel="noopener noreferrer"&gt;Omniracle methodology&lt;/a&gt; describes how signal discovery, research, evidence, model provenance, and publication gates fit together. The &lt;a href="https://omniracle.com/recommended" rel="noopener noreferrer"&gt;recommended reports&lt;/a&gt; show the reader-facing side of that architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  A useful mental model
&lt;/h2&gt;

&lt;p&gt;Think of the pipeline as a directed chain:&lt;/p&gt;

&lt;p&gt;question → research queries → page artifacts → claim evidence → synthesis calls → final report → publication decision&lt;/p&gt;

&lt;p&gt;Each arrow should be represented by a durable relationship, not inferred later from similar text. When a result is challenged, the system can then answer the most important engineering question: not merely “Which links were shown?” but “How did this claim travel from source material into the published answer?”&lt;/p&gt;

&lt;p&gt;That is the difference between citation decoration and an auditable research system.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>database</category>
      <category>dataengineering</category>
    </item>
    <item>
      <title>Building Reliable Web Access for AI Agents: Search, Crawl, Markdown, and Screenshots</title>
      <dc:creator>Kun Shen</dc:creator>
      <pubDate>Mon, 15 Jun 2026 16:54:41 +0000</pubDate>
      <link>https://dev.to/kun_shen_eedb57cc827955f5/building-reliable-web-access-for-ai-agents-search-crawl-markdown-and-screenshots-e9e</link>
      <guid>https://dev.to/kun_shen_eedb57cc827955f5/building-reliable-web-access-for-ai-agents-search-crawl-markdown-and-screenshots-e9e</guid>
      <description>&lt;p&gt;AI agents are only as useful as the context they can reach. For many product, research, support, and competitive-intelligence workflows, that context lives on public websites: documentation pages, changelogs, pricing pages, articles, search results, screenshots, and long-tail reference content.&lt;/p&gt;

&lt;p&gt;The hard part is not simply "scraping a page." The hard part is giving an agent a repeatable web access layer that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;search for candidate sources,&lt;/li&gt;
&lt;li&gt;fetch static pages cheaply,&lt;/li&gt;
&lt;li&gt;render JavaScript-heavy pages when needed,&lt;/li&gt;
&lt;li&gt;convert pages into clean markdown,&lt;/li&gt;
&lt;li&gt;capture screenshot evidence,&lt;/li&gt;
&lt;li&gt;retry safely when upstream sites are slow,&lt;/li&gt;
&lt;li&gt;and avoid flooding the model context with irrelevant HTML.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where a web scraping API or crawler API becomes more useful than ad hoc browser scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  A practical pattern for agent web access
&lt;/h2&gt;

&lt;p&gt;For most AI agent workflows, I like to split web access into four steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Search first, crawl second
&lt;/h3&gt;

&lt;p&gt;Agents often do better when they first discover likely sources instead of starting with one URL. A search API for AI agents can return public web, news, image, video, or scholar results. The agent can then choose the highest-signal pages to read.&lt;/p&gt;

&lt;p&gt;This reduces unnecessary crawling and gives the model a better source set.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Use fetch before render
&lt;/h3&gt;

&lt;p&gt;Many pages do not need a headless browser. Documentation, blog posts, landing pages, legal pages, and static HTML often contain the useful content in the initial response.&lt;/p&gt;

&lt;p&gt;For those pages, a fetch-based web data extraction API is usually faster, cheaper, and more reliable.&lt;/p&gt;

&lt;p&gt;Use browser rendering only when the page depends on client-side JavaScript, hydration, or late network calls.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Convert pages to markdown
&lt;/h3&gt;

&lt;p&gt;Raw HTML is noisy. Agents usually need a compact representation:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;page title,&lt;/li&gt;
&lt;li&gt;main content,&lt;/li&gt;
&lt;li&gt;links,&lt;/li&gt;
&lt;li&gt;metadata,&lt;/li&gt;
&lt;li&gt;selected media,&lt;/li&gt;
&lt;li&gt;and readable markdown.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Website to markdown conversion is a simple change that often improves answer quality because the model sees content instead of layout scaffolding.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Capture screenshots when trust matters
&lt;/h3&gt;

&lt;p&gt;Text extraction is enough for many tasks, but not all of them. When an agent is checking visual layout, pricing evidence, legal copy, product UI, or compliance-sensitive content, a screenshot API gives a durable record of what the page looked like.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AnyCrawler fits
&lt;/h2&gt;

&lt;p&gt;I have been testing &lt;a href="https://anycrawler.com" rel="noopener noreferrer"&gt;AnyCrawler&lt;/a&gt; as an agent-facing web access layer. It combines public search, page crawling, markdown extraction, browser rendering, and screenshots behind API endpoints that are easier for agents to call than a full browser automation stack.&lt;/p&gt;

&lt;p&gt;The useful part is the routing model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;use &lt;a href="https://anycrawler.com/crawler/page/fetch" rel="noopener noreferrer"&gt;fetch crawling&lt;/a&gt; for static or content-first pages,&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://anycrawler.com/crawler/page/render" rel="noopener noreferrer"&gt;render crawling&lt;/a&gt; for JavaScript-heavy pages,&lt;/li&gt;
&lt;li&gt;use &lt;a href="https://anycrawler.com/crawler/screenshot" rel="noopener noreferrer"&gt;screenshots&lt;/a&gt; when visual evidence matters,&lt;/li&gt;
&lt;li&gt;and use &lt;a href="https://anycrawler.com/crawler/search/page" rel="noopener noreferrer"&gt;search&lt;/a&gt; before crawling when the source URL is not already known.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There is also an open skill package for agent runtimes here:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/AnyCrawler-com/AnyCrawler-Skill" rel="noopener noreferrer"&gt;https://github.com/AnyCrawler-com/AnyCrawler-Skill&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Design advice
&lt;/h2&gt;

&lt;p&gt;If you are adding web access to an AI agent, avoid making the browser the first tool for every task. A better default is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Search if the source is unknown.&lt;/li&gt;
&lt;li&gt;Fetch the page if content is likely available in HTML.&lt;/li&gt;
&lt;li&gt;Render only when fetch is incomplete.&lt;/li&gt;
&lt;li&gt;Convert the result to markdown.&lt;/li&gt;
&lt;li&gt;Capture screenshots only when the task needs visual proof.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That structure keeps workflows faster, less expensive, and easier to debug.&lt;/p&gt;

</description>
      <category>webscraping</category>
    </item>
  </channel>
</rss>
