<?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: Sanjeev Pulakurthi </title>
    <description>The latest articles on DEV Community by Sanjeev Pulakurthi  (@sanjeev_pulakurthi).</description>
    <link>https://dev.to/sanjeev_pulakurthi</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%2F4045197%2F739df2a9-3ce6-4ca7-9018-5c01e3f4b5d3.png</url>
      <title>DEV Community: Sanjeev Pulakurthi </title>
      <link>https://dev.to/sanjeev_pulakurthi</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sanjeev_pulakurthi"/>
    <language>en</language>
    <item>
      <title>The Extraction Layer That Cut My LLM Token Bill by 96%</title>
      <dc:creator>Sanjeev Pulakurthi </dc:creator>
      <pubDate>Mon, 17 Aug 2026 20:56:00 +0000</pubDate>
      <link>https://dev.to/sanjeev_pulakurthi/the-extraction-layer-that-cut-my-llm-token-bill-by-96-322l</link>
      <guid>https://dev.to/sanjeev_pulakurthi/the-extraction-layer-that-cut-my-llm-token-bill-by-96-322l</guid>
      <description>&lt;p&gt;I have been building a content pipeline where automated agents read the web — pull down a competitor page, work out what it covers, and report back on what is missing. It worked on the first try, which should have been the warning sign. The API bill climbed steeply, responses took several seconds to start streaming, and the agents kept citing URLs that did not exist anywhere on the page they had supposedly just read.&lt;/p&gt;

&lt;p&gt;All three symptoms had one cause: I was passing raw page HTML straight into the model. The fix was not a better prompt or a bigger context window. It was an extraction layer in front of the model, and it turned out to be the single highest-leverage piece of plumbing in the whole system.&lt;/p&gt;

&lt;p&gt;Two different jobs, both called extraction&lt;br&gt;
The word "extractor" gets used for two unrelated tasks, and conflating them is why this area is more confusing than it should be. The first job is pulling links out of text — you have a sitemap, a search-results dump, a newsletter, or a page of documentation, and you want the URLs it contains as a clean list. That is what a URL extractor does: messy text in, one URL per line out.&lt;/p&gt;

&lt;p&gt;The second job is fetching one of those URLs and reducing the page to its readable content — stripping the navigation, the cookie banner, the inlined stylesheets, the analytics snippets, and the eight variants of the same Open Graph tag, leaving the article. Both stages are necessary and they fail in completely different ways. The first decides what your agent reads. The second decides what it costs.&lt;/p&gt;

&lt;p&gt;What raw HTML actually costs&lt;br&gt;
On a typical article page, the content you want is under 10% of the document. Measured across the competitor pages my own pipeline reads, the raw HTML ran 40,000 to 80,000 tokens each — roughly 150 to 350 kilobytes of markup, before counting the images and script bundles the browser would fetch separately. The same articles, once reduced to plain markdown, came to 1,500 to 3,000 tokens. That is a 93 to 96% reduction in input tokens carrying the same information.&lt;/p&gt;

&lt;p&gt;The arithmetic scales the way you would expect: pages per day, times average tokens per page, divided by a million, times whatever your provider charges per million input tokens. At ten thousand pages a day, the difference between a 50,000-token average and a 2,000-token average is the difference between 500 million and 20 million tokens daily. Input pricing varies by more than an order of magnitude between models and changes often enough that quoting a rate here would be actively misleading — but the ratio holds regardless of which model you are billed by, and the ratio is the part worth designing around.&lt;/p&gt;

&lt;p&gt;There is a latency win that is easier to miss. Time to first token is dominated by the prefill phase, and prefill scales with prompt length. Cutting a prompt by 25 times cuts the wait before the first character streams back. In an agent loop that makes several calls in sequence, that saving compounds at every hop.&lt;/p&gt;

&lt;p&gt;Context dilution is the expensive half&lt;br&gt;
Cost you can budget for. The quality problem is worse. Boilerplate lowers the signal-to-noise ratio of the entire prompt, and in a retrieval system it does real damage further downstream: chunk a raw HTML page and you end up with vectors that encode navigation structure and cookie-consent copy rather than meaning. Retrieval then confidently returns the chunk whose markup happened to be closest in the embedding space.&lt;/p&gt;

&lt;p&gt;The hallucinated citations came from the same place. Buried in the markup of every page are URLs that have nothing to do with the content — analytics endpoints, CDN paths, preconnect hints, schema identifiers. Given a prompt where 90% of the tokens are markup containing dozens of such URLs, a model asked to cite its sources will sometimes produce one of them. It is not really a hallucination; the URL was right there in the context I gave it. I had asked the model to find the signal in a haystack I built myself.&lt;/p&gt;

&lt;p&gt;The delimiter bug that corrupted every URL&lt;br&gt;
Stage one looks like a five-minute regex job, and I have the commit message proving otherwise. The URL extractor on this site matched http and https links, plus www-prefixed URLs written without a scheme, and stopped at whitespace, closing parens, and closing brackets — so it handled sentence punctuation and markdown links correctly. Its delimiter character class was [^\s)&amp;gt;]], which excludes whitespace, a closing paren, a greater-than, and a closing bracket.&lt;/p&gt;

&lt;p&gt;Then I pasted this site's own sitemap into it. Every single entry came back as &lt;a href="https://devtoolstack.io/tool/extract-urls" rel="noopener noreferrer"&gt;https://devtoolstack.io/tool/extract-urls&lt;/a&gt;&amp;lt;/loc — the URL with a fragment of markup welded to the end. The class excluded the greater-than character but not the less-than, so a URL sitting inside a tag ran straight past its own closing tag and only stopped at the next greater-than it found. Quoted attributes broke the same way: the namespace declaration xmlns="&lt;a href="http://www.sitemaps.org/schemas/sitemap/0.9" rel="noopener noreferrer"&gt;http://www.sitemaps.org/schemas/sitemap/0.9&lt;/a&gt;" yielded a URL with a trailing double quote still attached, because the class did not exclude quotes either.&lt;/p&gt;

&lt;p&gt;The fix needed two character classes rather than one. The body of a URL is [^\s&amp;lt;&amp;gt;"'`)]] — everything except whitespace, angle brackets, quotes, a backtick, and closing brackets. The final character uses a stricter class that additionally refuses to end on .,;:!? so that sentence punctuation does not get glued onto the link. An XML sitemap, a quoted href, a markdown link, a URL in backticks, and a URL wrapped in parentheses all extract cleanly now, and none of the cases that already worked regressed.&lt;/p&gt;

&lt;p&gt;The uncomfortable part is why this survived so long. The test suite for these tools was thorough and entirely green. It had simply never fed an extractor any markup — and markup is the single most common thing anyone pastes into a URL extractor. The bug was not subtle, it was untested. The real fix was adding markup as an input class to the suite, along with an invariant that an extracted value must never retain an angle bracket or a quote, then confirming that check failed against the old pattern before trusting the new one.&lt;/p&gt;

&lt;p&gt;Why bare domains are deliberately out of scope&lt;br&gt;
One limitation is intentional. The extractor does not match bare domains like example.com written with no scheme and no www prefix, and it never will. Matching those reliably requires a real list of valid top-level domains, because without one you also match config.json, version strings like 2.5.1, and the word Node.js. Every naive attempt trades a false negative you can see for a swarm of false positives you cannot. Requiring http, https, or www is the honest boundary, and stating it on the page is better than a tool that quietly guesses wrong.&lt;/p&gt;

&lt;p&gt;Reducing the page itself&lt;br&gt;
For stage two, do not write your own. Separating article text from boilerplate is a genuinely hard problem, and the maintained libraries encode years of accumulated heuristics about how real pages are structured. Most tutorials still reach for newspaper3k; its last release was in 2020 and it has known installation problems on current Python versions. Trafilatura is the better default — actively maintained, stronger on boilerplate removal, and able to emit markdown directly, which is already the shape you want a prompt in.&lt;/p&gt;

&lt;p&gt;The setting that mattered most was preferring precision over recall. When output feeds a model, dropping a sidebar you might have wanted costs you nothing, while keeping a cookie banner costs tokens and accuracy at the same time. Bias every ambiguous call toward discarding. It is also worth extracting the page metadata — title, author, publication date — as separate fields rather than leaving them embedded in the text, because a model reasoning about whether a source is current does much better with an explicit date field than with one buried in a byline.&lt;/p&gt;

&lt;p&gt;Deduplicate before fetching, not after. Sitemaps and search-result dumps are full of URLs that differ only by a trailing slash, a tracking parameter, or a fragment, and every duplicate you do not catch is a paid round trip and a wasted rate-limit slot. Normalising with a URL parser to compare hosts and paths while ignoring the query string caught more duplicates than a plain string comparison did.&lt;/p&gt;

&lt;p&gt;Four things that cost me time&lt;br&gt;
Pages rendered entirely in JavaScript return nothing useful to a static fetch — you get an empty shell and no error. A headless browser solves it but is expensive enough that running it by default would eat the savings, so it belongs behind a check: if static extraction returned less than some minimum length, escalate to the browser, otherwise do not.&lt;/p&gt;

&lt;p&gt;Cache aggressively, keyed on the normalised URL, with a sensible expiry. Agent loops revisit the same handful of pages constantly, and re-fetching costs money, adds latency, and raises your odds of getting rate-limited by a site that was being perfectly reasonable about it.&lt;/p&gt;

&lt;p&gt;Respect robots.txt and terms of service. Python ships a robots parser in its standard library, so there is no excuse for skipping the check. Set a User-Agent that identifies you and provides a way to get in touch, and rate-limit per host rather than globally.&lt;/p&gt;

&lt;p&gt;Extraction failures are silent by default, which is the trap that bit hardest. Paywalls and JavaScript shells return an empty string or a stub rather than raising, so without a minimum-length assertion after every extraction you will eventually hand a model an empty context and get a fluent, confident answer about nothing at all. Assert on the output length and fail loudly.&lt;/p&gt;

&lt;p&gt;What ties this together&lt;br&gt;
Extraction reads like plumbing you bolt on once the interesting parts work. It is not. It determines what your model costs to run, how fast it responds, and whether the answers it produces are traceable to anything real. A 25-fold reduction in prompt size and a measurable drop in invented citations came out of two regex character classes and one well-chosen library — no model change, no prompt engineering, no larger context window.&lt;/p&gt;

&lt;p&gt;The wider lesson is the same one that keeps surfacing in this codebase: a green test suite tells you the cases you thought of are handled. It says nothing whatsoever about the case that matters most, which is the thing a normal person actually pastes in on a normal Tuesday. For a URL extractor, that thing was a sitemap, and it had been broken the whole time.&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to Generate Realistic Fake Phone Numbers for Test Data</title>
      <dc:creator>Sanjeev Pulakurthi </dc:creator>
      <pubDate>Sat, 01 Aug 2026 07:13:10 +0000</pubDate>
      <link>https://dev.to/sanjeev_pulakurthi/how-to-generate-realistic-fake-phone-numbers-for-test-data-3kd0</link>
      <guid>https://dev.to/sanjeev_pulakurthi/how-to-generate-realistic-fake-phone-numbers-for-test-data-3kd0</guid>
      <description>&lt;h2&gt;
  
  
  Ten random digits is not a phone number
&lt;/h2&gt;

&lt;p&gt;US phone numbers follow the North American Numbering Plan (NANP), and the format XXX-XXX-XXXX has real constraints baked into it:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Area code (NPA) — the first digit must be 2–9. 0 and 1 are reserved as trunk prefixes.&lt;/li&gt;
&lt;li&gt;Exchange / central office code (NXX) — same rule. First digit must be 2–9.&lt;/li&gt;
&lt;li&gt;N11 codes are reserved — 211, 311, 411, 511, 611, 711, 811, 911 are service codes, not area codes or exchanges.&lt;/li&gt;
&lt;li&gt;Line numb
er — the last four digits are genuinely unconstrained, 0000–9999.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So 073-118-4492 is ten digits and it is not a phone number. Any validator worth using — libphonenumber, most form libraries, the majority of hand-rolled regexes — will reject it. Naive random generation produces an invalid area code roughly 20% of the time, and an invalid exchange another 20% on top of that.&lt;/p&gt;

&lt;p&gt;If you're seeding a few hundred rows, that's a meaningful chunk of your fixtures silently failing.&lt;/p&gt;

&lt;h3&gt;
  
  
  A generator that actually respects the rules (Our Tool)
&lt;/h3&gt;

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

&lt;h2&gt;
  
  
  The wider point about test data
&lt;/h2&gt;

&lt;p&gt;Phone numbers are a small example of a pattern that shows up everywhere in fixtures:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Credit card numbers need to pass the Luhn checksum, or your payment form rejects them&lt;/li&gt;
&lt;li&gt;UUIDs have version and variant bits in fixed positions&lt;/li&gt;
&lt;li&gt;ISBNs, IMEIs, VINs, and IBANs all carry check digits&lt;/li&gt;
&lt;li&gt;Even em
ail addresses have rules that asdf@asdf quietly violates&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Random bytes shaped like the right thing are not the right thing. Any field with a validator on it needs a generator that knows the same rules the validator does — otherwise you're just generating a slower way to fail.&lt;/p&gt;

&lt;h2&gt;
  
  
  About DevToolStack
&lt;/h2&gt;

&lt;p&gt;The phone generator is one of 118+ free tools on DevToolStack — a collection I built for exactly this kind of small, recurring developer task that doesn't justify writing a script.&lt;/p&gt;

&lt;p&gt;Everything runs client-side in your browser. No accounts, no uploads, no limits, and nothing you paste is transmitted to a server — which matters more than it sounds when the thing you're decoding is a JWT from production.&lt;/p&gt;

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

&lt;p&gt;&lt;em&gt;A few that get the most use:&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devtoolstack.io/tool/hash-generator/" rel="noopener noreferrer"&gt;Generators&lt;/a&gt; — random Base64 and hex strings, UUIDs, PINs, colors, dates, user agents&lt;br&gt;
&lt;a href="https://devtoolstack.io/tool/base64-converter/" rel="noopener noreferrer"&gt;Data Encoding&lt;/a&gt; — Base64, URL, HTML entity encode/decode&lt;br&gt;
&lt;a href="https://devtoolstack.io/tool/json-formatter/" rel="noopener noreferrer"&gt;Formatters&lt;/a&gt; — JSON, SQL, XML&lt;br&gt;
&lt;a href="https://devtoolstack.io/tool/text-diff/" rel="noopener noreferrer"&gt;Text Diff Checker&lt;/a&gt; — Compare two blocks of text and see line-by-line differences highlighted instantly&lt;/p&gt;

&lt;p&gt;&lt;a href="https://devtoolstack.io/" rel="noopener noreferrer"&gt;Browse all the tools → https://devtoolstack.io/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>developertool</category>
      <category>testing</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Dev Tool Stack</title>
      <dc:creator>Sanjeev Pulakurthi </dc:creator>
      <pubDate>Fri, 31 Jul 2026 10:29:09 +0000</pubDate>
      <link>https://dev.to/sanjeev_pulakurthi/dev-tool-stack-3dka</link>
      <guid>https://dev.to/sanjeev_pulakurthi/dev-tool-stack-3dka</guid>
      <description>&lt;p&gt;Site Link: &lt;a href="https://devtoolstack.io/" rel="noopener noreferrer"&gt;https://devtoolstack.io/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;em&gt;&lt;a href="https://devtoolstack.io/" rel="noopener noreferrer"&gt;DevToolStack &lt;/a&gt;&lt;/em&gt;&lt;/strong&gt;is a collection of 100+ open, fast, and dependency-free developer tools—all running client-side in your browser. From encoding and generation to formatting and conversion, every tool loads instantly with zero sign-ups or backend dependence. Build faster. Think less about infrastructure. Spend more time coding. Why this works: Emphasizes speed, no friction, and the "zero backend" differentiation. Appeals to both indie developers and teams tired of scattered tool ecosystems.&lt;/p&gt;

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

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

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

</description>
      <category>productivity</category>
      <category>devtools</category>
      <category>nocode</category>
      <category>tooling</category>
    </item>
  </channel>
</rss>
