<?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: nyx464664816-stack</title>
    <description>The latest articles on DEV Community by nyx464664816-stack (@nyx464664816stack).</description>
    <link>https://dev.to/nyx464664816stack</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%2F4207248%2Fb295b98d-eee0-4281-8251-579cc3a2a215.png</url>
      <title>DEV Community: nyx464664816-stack</title>
      <link>https://dev.to/nyx464664816stack</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nyx464664816stack"/>
    <language>en</language>
    <item>
      <title>Same page, two outputs: Markdown vs structured JSON for AI agents</title>
      <dc:creator>nyx464664816-stack</dc:creator>
      <pubDate>Mon, 26 Oct 2026 00:57:19 +0000</pubDate>
      <link>https://dev.to/nyx464664816stack/same-page-two-outputs-markdown-vs-structured-json-for-ai-agents-4019</link>
      <guid>https://dev.to/nyx464664816stack/same-page-two-outputs-markdown-vs-structured-json-for-ai-agents-4019</guid>
      <description>&lt;p&gt;I work on WebExtract, a pay-per-call API that turns a URL into data. It has two main endpoints: &lt;code&gt;GET /v1/markdown&lt;/code&gt; returns clean Markdown, and &lt;code&gt;GET /v1/extract&lt;/code&gt; returns the same Markdown plus fixed-schema fields (&lt;code&gt;product&lt;/code&gt;, &lt;code&gt;tables&lt;/code&gt;, &lt;code&gt;metadata&lt;/code&gt;). This post runs one product page and one table page through both and shows what an agent actually gets back. Every snippet below is copied from saved API responses in the &lt;a href="https://github.com/nyx464664816-stack/webextract/tree/main/examples/compare" rel="noopener noreferrer"&gt;examples/compare&lt;/a&gt; folder, not written by hand.&lt;/p&gt;

&lt;p&gt;One note on where the files came from: they were captured through the Base Sepolia sandbox routes (&lt;code&gt;/sandbox/v1/...&lt;/code&gt;, paid with test USDC) on 2026-10-25 (UTC). The sandbox runs the same extraction code as the live &lt;code&gt;/v1/...&lt;/code&gt; routes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page 1: a product page
&lt;/h2&gt;

&lt;p&gt;The page is &lt;code&gt;https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html&lt;/code&gt;, a practice site built for scraping. The page itself says so: "Prices and ratings here were randomly assigned and have no real meaning."&lt;/p&gt;

&lt;h3&gt;
  
  
  The Markdown
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/v1/markdown&lt;/code&gt; returned 1567 characters, 261 words. The end of it looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;&lt;span class="gu"&gt;## Product Information&lt;/span&gt;

| UPC | a897fe39b1053632 |
| --- | --- |
| Product Type | Books |
| Price (excl. tax) | £51.77 |
| Price (incl. tax) | £51.77 |
| Tax | £0.00 |
| Availability | In stock (22 available) |
| Number of reviews | 0 |
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is good input for a model that only needs to read the page. It is short, and the price, stock and table are all there. But look at the table. The HTML table has no header row, and a Markdown table needs one, so the first data row (&lt;code&gt;UPC&lt;/code&gt;) ends up in the header position. A model will probably read it correctly. A program that splits on pipes and treats row one as column names will not.&lt;/p&gt;

&lt;h3&gt;
  
  
  The structured JSON
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;/v1/extract&lt;/code&gt; returned the same Markdown (the two strings are identical) plus extra fields. Here is a &lt;code&gt;jq&lt;/code&gt; selection of the &lt;code&gt;product&lt;/code&gt; object:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;jq &lt;span class="s1"&gt;'.product | {name, price, currency, availability, rating, source}'&lt;/span&gt; out/product.extract.json
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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;"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;"A Light in the Attic"&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;51.77&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;"GBP"&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;"InStock"&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;"value"&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;"count"&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="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"heuristic"&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;&lt;code&gt;price&lt;/code&gt; is a number, &lt;code&gt;currency&lt;/code&gt; is a code, and &lt;code&gt;availability&lt;/code&gt; is a fixed value. Your code can compare or store these without any regex.&lt;/p&gt;

&lt;p&gt;The important field is &lt;code&gt;source&lt;/code&gt;. This page has no JSON-LD, OpenGraph product tags or microdata, so the price came from page heuristics. The response says so instead of hiding it:&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="nl"&gt;"evidence"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"first [class*=price] element; same amount in table row &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;Price (excl. tax)&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="s2"&gt;"heuristic_price_low_confidence"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="s2"&gt;"currency_inferred_from_symbol"&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;That is &lt;code&gt;product.price_detail.prices[0].evidence&lt;/code&gt; and &lt;code&gt;product.price_detail.warnings&lt;/code&gt;. The evidence tells you where the number came from (and that the product table shows the same amount). The warnings tell you the price came from heuristics and the currency was inferred from the &lt;code&gt;£&lt;/code&gt; symbol. If your agent is about to act on a price, check &lt;code&gt;price_detail.warnings&lt;/code&gt; first. We keep these warnings on purpose, even when the number turns out to be right, because heuristics can be wrong on other pages.&lt;/p&gt;

&lt;p&gt;The table comes back like this:&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="s2"&gt;"col_1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"col_2"&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;"UPC"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"a897fe39b1053632"&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="s2"&gt;"Product Type"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"Books"&lt;/span&gt;&lt;span class="p"&gt;],[&lt;/span&gt;&lt;span class="s2"&gt;"Price (excl. tax)"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"£51.77"&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;"row_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"column_count"&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="nl"&gt;"consistent_width"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Those are &lt;code&gt;tables[0].headers&lt;/code&gt;, the first three &lt;code&gt;rows&lt;/code&gt;, and &lt;code&gt;checks&lt;/code&gt;. Since the page has no header row, the headers are placeholders (&lt;code&gt;col_1&lt;/code&gt;, &lt;code&gt;col_2&lt;/code&gt;) and &lt;code&gt;UPC&lt;/code&gt; stays a data row. The &lt;code&gt;checks&lt;/code&gt; object lets you confirm the shape before using it: 7 rows, 2 columns, every row the same width.&lt;/p&gt;

&lt;h2&gt;
  
  
  Page 2: a table page
&lt;/h2&gt;

&lt;p&gt;The second page is &lt;code&gt;https://webscraper.io/test-sites/tables&lt;/code&gt;, a practice page with two standard HTML tables. &lt;code&gt;/v1/markdown&lt;/code&gt; returned 508 characters, 124 words, and here the Markdown tables look fine, because the page has real &lt;code&gt;&amp;lt;thead&amp;gt;&lt;/code&gt; rows.&lt;/p&gt;

&lt;p&gt;The difference is what &lt;code&gt;/v1/extract&lt;/code&gt; adds. Here is &lt;code&gt;tables[0]&lt;/code&gt;, verbatim:&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;"index"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"caption"&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;"headers"&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="s2"&gt;"#"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="s2"&gt;"First 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;"Last 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;"Username"&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;"rows"&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="s2"&gt;"1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Mark"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Otto"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"@mdo"&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="s2"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Jacob"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Thornton"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"@fat"&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="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Larry"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"the Bird"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"@twitter"&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;"checks"&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;"row_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;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;"column_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;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;"consistent_width"&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="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;In the Markdown the two tables sit one after the other with nothing between them. In the JSON they are &lt;code&gt;tables[0]&lt;/code&gt; and &lt;code&gt;tables[1]&lt;/code&gt;, each with its own headers and checks, so you don't have to work out where one ends and the next starts.&lt;/p&gt;

&lt;p&gt;This is not a product page, and &lt;code&gt;product&lt;/code&gt; is simply &lt;code&gt;null&lt;/code&gt;. Nothing is guessed. The response also explains why:&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;"reason"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no_product_markup"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"detail"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"no JSON-LD Product/ProductGroup, product:price meta, schema.org microdata, or price+title+cart heuristic match"&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;That is the &lt;code&gt;product_unresolved&lt;/code&gt; field. A &lt;code&gt;null&lt;/code&gt; with a reason is easier to handle than an empty-looking object.&lt;/p&gt;

&lt;h2&gt;
  
  
  Metadata
&lt;/h2&gt;

&lt;p&gt;Both pages also get a &lt;code&gt;metadata&lt;/code&gt; object. A selection for the table page:&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;"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;"Table Playground | Web Scraper Test Sites"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"language"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"en"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"canonical_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://webscraper.io/test-sites/tables"&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;"website"&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;The full object also has &lt;code&gt;description&lt;/code&gt;, &lt;code&gt;authors&lt;/code&gt;, &lt;code&gt;published_at&lt;/code&gt;, &lt;code&gt;modified_at&lt;/code&gt;, &lt;code&gt;site_name&lt;/code&gt; and &lt;code&gt;image&lt;/code&gt;. Fields the page doesn't provide are &lt;code&gt;null&lt;/code&gt; or empty, not filled in.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one should an agent use?
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;/v1/markdown&lt;/code&gt; when a model just needs to read the page: summarize it, answer a question about it, quote it. It costs $0.002 per call.&lt;/li&gt;
&lt;li&gt;Use &lt;code&gt;/v1/extract&lt;/code&gt; when your code needs the fields: compare prices, load a table into a dataframe, check whether something is in stock. It costs $0.005 per call. Because it also includes the Markdown, you don't need to call both.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Some limits to know about: &lt;code&gt;product&lt;/code&gt; comes from JSON-LD, OpenGraph, microdata or, as a last resort, page heuristics. &lt;code&gt;product.rating&lt;/code&gt; is filled only when the page has JSON-LD or star-rating markup. The image URL is kept as the page wrote it (&lt;code&gt;../../media/...&lt;/code&gt; here), so you may need to resolve it against &lt;code&gt;final_url&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;On REST, the free tier is opt-in: 20 free calls per day for each caller IP address, no signup. Add the header &lt;code&gt;X-Billing: free&lt;/code&gt;:&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;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Billing: free"&lt;/span&gt; &lt;span class="s2"&gt;"https://api.webextract.dev/v1/extract?url=https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or the query parameter &lt;code&gt;?free=1&lt;/code&gt; if your client can't set headers:&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;-G&lt;/span&gt; &lt;span class="s2"&gt;"https://api.webextract.dev/v1/markdown"&lt;/span&gt; &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"free=1"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"url=https://webscraper.io/test-sites/tables"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The free allowance is counted per caller IP address. If your outgoing IP changes between requests (a proxy, or a cloud host behind NAT), the calls are counted separately for each address, so &lt;code&gt;X-Free-Remaining&lt;/code&gt; can go up and down. Failed REST fetches are not charged. Without the free opt-in, a key or a payment, the endpoint answers HTTP 402 with x402 payment requirements. You can then pay per call in USDC or use prepaid credits.&lt;/p&gt;

&lt;p&gt;The folder also has four one-file clients: &lt;code&gt;compare_rest.py&lt;/code&gt; (both endpoints side by side), &lt;code&gt;langchain_tool.py&lt;/code&gt;, &lt;code&gt;mcp_client.py&lt;/code&gt; (against &lt;code&gt;https://api.webextract.dev/mcp&lt;/code&gt;, tools &lt;code&gt;page_markdown&lt;/code&gt; and &lt;code&gt;extract_url&lt;/code&gt;) and &lt;code&gt;x402_client.py&lt;/code&gt;. The x402 client only runs against the Base Sepolia sandbox with test USDC and refuses mainnet. The LangChain tools were tested by calling them directly, not inside a model-driven agent.&lt;/p&gt;

&lt;p&gt;If you run your own pages through both endpoints and the JSON gets something wrong, I'd like to hear about it. Wrong prices and broken tables are the bugs we most want to find.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example folder: &lt;a href="https://github.com/nyx464664816-stack/webextract/tree/main/examples/compare" rel="noopener noreferrer"&gt;https://github.com/nyx464664816-stack/webextract/tree/main/examples/compare&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Repository: &lt;a href="https://github.com/nyx464664816-stack/webextract" rel="noopener noreferrer"&gt;https://github.com/nyx464664816-stack/webextract&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Site: &lt;a href="https://webextract.dev/r/devto" rel="noopener noreferrer"&gt;https://webextract.dev/r/devto&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I'm on the WebExtract team, and WebExtract is our own product. This post was drafted with AI assistance. The JSON and Markdown excerpts are copied from saved API responses in the repository, and I checked them against those files before publishing.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;strong&gt;Update (2026-10-26):&lt;/strong&gt; I ran the same two endpoints over 10 public pages and put the raw files and scripts online. In the Base Sepolia sandbox, &lt;code&gt;/markdown&lt;/code&gt; succeeded on 38 of 40 calls and &lt;code&gt;/extract&lt;/code&gt; on 40 of 40, and the simple field checks passed 75/75 and 212/212. It is a small sample from one client, it covers easy pages only, and I did not run any other tool. Everything, including the two failures, is here: &lt;a href="https://webextract.dev/benchmark" rel="noopener noreferrer"&gt;https://webextract.dev/benchmark&lt;/a&gt; (code: &lt;a href="https://github.com/nyx464664816-stack/webextract/tree/main/examples/benchmark" rel="noopener noreferrer"&gt;https://github.com/nyx464664816-stack/webextract/tree/main/examples/benchmark&lt;/a&gt;).&lt;/p&gt;

</description>
      <category>ai</category>
      <category>api</category>
      <category>mcp</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Let your x402 agent read web pages: from a free test call to a paid call</title>
      <dc:creator>nyx464664816-stack</dc:creator>
      <pubDate>Sun, 25 Oct 2026 19:32:11 +0000</pubDate>
      <link>https://dev.to/nyx464664816stack/let-your-x402-agent-read-web-pages-from-a-free-test-call-to-a-paid-call-271o</link>
      <guid>https://dev.to/nyx464664816stack/let-your-x402-agent-read-web-pages-from-a-free-test-call-to-a-paid-call-271o</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I'm on the WebExtract team. This post was drafted with AI assistance, and the commands below were run by hand before publishing. WebExtract is our own product.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If you're building an agent that has to read arbitrary web pages, there are three things to settle: how to try the API without an account, how the payment handshake works, and how to pay from code. This walkthrough covers all three using an x402-enabled endpoint, &lt;a href="https://webextract.dev/r/devto" rel="noopener noreferrer"&gt;WebExtract&lt;/a&gt;, which turns a URL into clean Markdown (and, on the other endpoint, structured JSON). It costs $0.002 per call for Markdown and $0.005 per call for the structured extract.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Try it without a wallet
&lt;/h3&gt;

&lt;p&gt;REST is opt-in for the free tier. Add one header and you get 20 free calls a day per IP, no signup:&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="nt"&gt;-G&lt;/span&gt; &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"X-Billing: free"&lt;/span&gt; &lt;span class="s2"&gt;"https://api.webextract.dev/v1/markdown"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-urlencode&lt;/span&gt; &lt;span class="s2"&gt;"url=https://example.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get JSON back with &lt;code&gt;title&lt;/code&gt;, &lt;code&gt;markdown&lt;/code&gt;, &lt;code&gt;word_count&lt;/code&gt; and the final URL. The &lt;code&gt;x-free-remaining&lt;/code&gt; response header tells you how many free calls are left today. Failed fetches are not charged.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. See what the server wants for payment
&lt;/h3&gt;

&lt;p&gt;Call the endpoint without the header:&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="nt"&gt;-i&lt;/span&gt; &lt;span class="s2"&gt;"https://api.webextract.dev/v1/extract?url=https%3A%2F%2Fexample.com"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get HTTP 402. The x402 v2 payment requirements are in the &lt;code&gt;PAYMENT-REQUIRED&lt;/code&gt; response header, base64-encoded JSON. Decode it (the &lt;code&gt;base64 -d&lt;/code&gt; is the whole trick):&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="nt"&gt;-D&lt;/span&gt; - &lt;span class="nt"&gt;-o&lt;/span&gt; /dev/null &lt;span class="s2"&gt;"https://api.webextract.dev/v1/extract?url=https%3A%2F%2Fexample.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-i&lt;/span&gt; &lt;span class="s1"&gt;'^payment-required:'&lt;/span&gt; | &lt;span class="nb"&gt;cut&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-f2-&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;'\r\n'&lt;/span&gt; | &lt;span class="nb"&gt;base64&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; | python3 &lt;span class="nt"&gt;-m&lt;/span&gt; json.tool
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When I ran it, &lt;code&gt;accepts&lt;/code&gt; listed seven USDC options, Base first, then Polygon, Arbitrum, Avalanche, Optimism, World Chain and Monad. Every one has &lt;code&gt;amount: "5000"&lt;/code&gt;, which is $0.005 because USDC has 6 decimals. The response also carries a &lt;code&gt;Link: rel="payment"&lt;/code&gt; header and an &lt;code&gt;X-Free-Tier-Hint&lt;/code&gt; header pointing back at the free option, so an agent that hits the 402 first can find its way.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Pay from code (testnet first)
&lt;/h3&gt;

&lt;p&gt;The official Node packages handle the 402, signing and retry. Use a throwaway wallet and the sandbox, which only accepts Base Sepolia test USDC (free from &lt;a href="https://faucet.circle.com" rel="noopener noreferrer"&gt;Circle's faucet&lt;/a&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm i @x402/fetch @x402/evm viem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;wrapFetchWithPaymentFromConfig&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@x402/fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;ExactEvmScheme&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@x402/evm&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;privateKeyToAccount&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;viem/accounts&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;account&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;privateKeyToAccount&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TESTNET_PRIVATE_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// throwaway test wallet&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;wrapFetchWithPaymentFromConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;schemes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;network&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;eip155:84532&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;ExactEvmScheme&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;account&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pay&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://api.webextract.dev/sandbox/v1/markdown?url=https://example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;markdown&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Our own run of this pattern returned HTTP 200 with a settled 0.002 USDC payment on Base Sepolia (&lt;a href="https://sepolia.basescan.org/tx/0x6dc0bac36c611f4006f99ed68ccced930995aead7757b9dd497db3818251b26e" rel="noopener noreferrer"&gt;transaction&lt;/a&gt;).&lt;/p&gt;

&lt;p&gt;Two things we ran into with the SDK (version 2.28):&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Registering &lt;code&gt;eip155:*&lt;/code&gt; makes it pick the &lt;strong&gt;first&lt;/strong&gt; option the server lists, which is Base, wherever your USDC actually is. If your USDC is only on Polygon, register just &lt;code&gt;eip155:137&lt;/code&gt; or pass your own payment-requirements selector.&lt;/li&gt;
&lt;li&gt;Optimism (&lt;code&gt;eip155:10&lt;/code&gt;) and World Chain (&lt;code&gt;eip155:480&lt;/code&gt;) are rejected by the SDK's default spend controls because it has no default USDC asset for them. To pay there, add &lt;code&gt;spendControls: { allowedAssets: [...] }&lt;/code&gt; for that network.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  4. Go to mainnet
&lt;/h3&gt;

&lt;p&gt;Point the same code at &lt;code&gt;https://api.webextract.dev/v1/markdown&lt;/code&gt; (drop &lt;code&gt;/sandbox&lt;/code&gt;) and register the mainnet network where you hold USDC, for example &lt;code&gt;eip155:8453&lt;/code&gt; for Base. Start with a wallet that holds a few dollars, not your main one.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. If you'd rather not run a wallet
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;MCP:&lt;/strong&gt; add &lt;code&gt;https://api.webextract.dev/mcp&lt;/code&gt; as a remote server in Cursor, VS Code or Claude Desktop (via &lt;code&gt;mcp-remote&lt;/code&gt;). The tools are &lt;code&gt;extract_url&lt;/code&gt; and &lt;code&gt;page_markdown&lt;/code&gt;, and the free tier works through MCP without any header. x402 payment is not available through MCP; after the free calls run out, the tool tells you to switch to REST or credits.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Prepaid credits:&lt;/strong&gt; $1 = 200 credits by card, then send &lt;code&gt;Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code&gt;. Details at &lt;a href="https://webextract.dev/r/devto" rel="noopener noreferrer"&gt;webextract.dev&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;JS-heavy pages: use &lt;code&gt;/v1/render/extract&lt;/code&gt; ($0.008 per call) or &lt;code&gt;/v1/render/markdown&lt;/code&gt; ($0.004 per call); each IP gets 5 free renders a day.&lt;/p&gt;

&lt;h3&gt;
  
  
  What I'd like feedback on
&lt;/h3&gt;

&lt;p&gt;If the 402 handshake is confusing for your agent framework, tell me where it breaks: the client library, the headers, or the docs. That's the part we're still improving.&lt;/p&gt;

</description>
      <category>x402</category>
      <category>mcp</category>
      <category>webdev</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
