<?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: neo xia</title>
    <description>The latest articles on DEV Community by neo xia (@neo_xia_3f4c019330af5fb9d).</description>
    <link>https://dev.to/neo_xia_3f4c019330af5fb9d</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%2F3961904%2F1a660dc3-e1fb-4caf-b9fe-c28d09fd5a72.png</url>
      <title>DEV Community: neo xia</title>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/neo_xia_3f4c019330af5fb9d"/>
    <language>en</language>
    <item>
      <title>Why marketplace product photos get rejected: white-background image requirements, and a 1-minute browser fix</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Sun, 04 Oct 2026 16:00:57 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/why-marketplace-product-photos-get-rejected-white-background-image-requirements-and-a-1-minute-38md</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/why-marketplace-product-photos-get-rejected-white-background-image-requirements-and-a-1-minute-38md</guid>
      <description>&lt;p&gt;If you sell on marketplaces — Amazon, Shopify, Etsy, app stores, or any product directory — you have probably had a listing image rejected or down-ranked for one reason: the background.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I build &lt;a href="https://cleanwhitebg.com" rel="noopener noreferrer"&gt;CleanWhiteBG&lt;/a&gt;, a free browser tool for exactly this problem. This post explains the requirements themselves; the tool is one way to hit them. No signup, no watermark, and images never leave your device.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "white background" is a hard requirement
&lt;/h2&gt;

&lt;p&gt;Most marketplaces normalize their catalog grids around a consistent look. A white (or pure transparent) background lets them:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Composite your photo into any layout&lt;/strong&gt; — tiles, carousels, comparison tables — without visible seams.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pass automated checks&lt;/strong&gt;: several pipelines measure background uniformity and reject images with busy or colored backgrounds.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Keep perceived quality high&lt;/strong&gt;: studio-white reads as "professional" at thumbnail size, where most buying decisions happen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What the requirements usually say
&lt;/h2&gt;

&lt;p&gt;The wording varies, but the checks look like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The product fills 80–90% of the frame.&lt;/li&gt;
&lt;li&gt;The background is pure white (#FFFFFF) or transparent PNG.&lt;/li&gt;
&lt;li&gt;No watermarks, logos, or inset text on the image.&lt;/li&gt;
&lt;li&gt;Sharp edges — no halo or color fringing left behind by a bad cutout.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Point 4 is where DIY jobs fail: a tolerance-based "magic wand" removal leaves gray fringes around hair, glassware, or jewelry, and reviewers (human or automated) notice.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 1-minute fix in the browser
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Open &lt;a href="https://cleanwhitebg.com" rel="noopener noreferrer"&gt;https://cleanwhitebg.com&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Drop the product photo in (nothing is uploaded — it runs locally in your browser via WebGPU/WASM).&lt;/li&gt;
&lt;li&gt;Pick the mode: &lt;strong&gt;Fast&lt;/strong&gt; for everyday shots, &lt;strong&gt;Precision&lt;/strong&gt; for complex edges like hair or jewelry.&lt;/li&gt;
&lt;li&gt;Download the flattened white-background PNG and upload it to the marketplace.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Because the processing is local, it also works for images you would rather not upload to a cloud service — product shots under NDA, unreleased packaging, client work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist before you re-upload
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Product fills the frame, centered&lt;/li&gt;
&lt;li&gt;[ ] Background is exactly #FFFFFF (pick the white-flatten option, not transparent)&lt;/li&gt;
&lt;li&gt;[ ] Zoom to 200% and check edges for fringing&lt;/li&gt;
&lt;li&gt;[ ] No leftover watermark from stock previews&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That is the whole loop. If you batch 50 SKUs a week, the difference between a 5-minute-per-image desktop workflow and a 30-second browser one adds up fast.&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially edge cases where the cutout struggles (glass, fur, reflective packaging).&lt;/p&gt;

</description>
      <category>design</category>
      <category>productivity</category>
      <category>tools</category>
    </item>
    <item>
      <title>Four Marketplaces, One Listing Workflow: What Amazon, eBay, Etsy and Shopify Expect From Your Copy</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Sat, 03 Oct 2026 19:38:42 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/four-marketplaces-one-listing-workflow-what-amazon-ebay-etsy-and-shopify-expect-from-your-copy-3n8c</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/four-marketplaces-one-listing-workflow-what-amazon-ebay-etsy-and-shopify-expect-from-your-copy-3n8c</guid>
      <description>&lt;p&gt;If you sell on more than one marketplace, you already know the copy you write for Amazon doesn't survive a trip to Etsy. Same product, different rules — and the rules are not cosmetic.&lt;/p&gt;

&lt;p&gt;I build listing tools for a living, and here's the cheat sheet I wish someone had handed me before I rewrote the same SKU four times.&lt;/p&gt;

&lt;h2&gt;
  
  
  What each platform actually expects
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Amazon&lt;/strong&gt; — Titles get truncated hard on mobile, and Rufus surfaces content that answers buyer questions directly. Front-load the searchable keywords; save the adjectives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;eBay&lt;/strong&gt; — Title length is capped at 80 characters, and buyers filter by item specifics. Condition-forward descriptions reduce disputes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Etsy&lt;/strong&gt; — Buyers respond to the maker story and materials. Tags are multi-word phrases, and the tone is handmade, not corporate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Shopify&lt;/strong&gt; — Your own brand voice applies, and the most common failure is shipping the manufacturer's thin spec sheet as a product description.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why paste-reformatting breaks down
&lt;/h2&gt;

&lt;p&gt;The usual loop looks like this: ask a general AI writer for copy, paste the answer into the marketplace form, then fix the character limit, reorder the bullets, rewrite the description, and swap images by hand. Every SKU repeats the loop, and the loop scales with your catalog.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we built instead
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;AI Product Page Optimization&lt;/a&gt; is a Chrome extension that reads the live listing page you already have open, detects which marketplace you're on, and generates copy that respects that platform's rules. One install covers Amazon, Shopify (including custom domains), eBay, and Etsy. Optional image enhancement runs in the same pass, and results land on a review dashboard so nothing publishes until you approve it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pricing facts
&lt;/h2&gt;

&lt;p&gt;Text optimization costs 1 credit; successfully enhanced images cost 2. New accounts get 5 free credits, no card required. Subscriptions start at $19.9/month.&lt;/p&gt;

&lt;p&gt;If you manage listings across channels: which platform's rules eat most of your time? Comments are open.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ecommerce</category>
      <category>shopify</category>
      <category>marketing</category>
    </item>
    <item>
      <title>Ship an Editor, Not a Generator: Designing Review-First AI for Marketplace Listings</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Fri, 02 Oct 2026 19:25:32 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/ship-an-editor-not-a-generator-designing-review-first-ai-for-marketplace-listings-151d</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/ship-an-editor-not-a-generator-designing-review-first-ai-for-marketplace-listings-151d</guid>
      <description>&lt;p&gt;Most AI writing tools optimize for one moment: the instant text appears. But for marketplace sellers, that moment is the least important part of the workflow. What matters is everything after — reading the suggestion, comparing it to the original, and deciding what actually goes live on Amazon, Shopify, eBay, or Etsy.&lt;/p&gt;

&lt;p&gt;That gap is why we built &lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;AI Product Page Optimization&lt;/a&gt; around an editor, not a generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The trust problem with generated text
&lt;/h2&gt;

&lt;p&gt;A generated title is a guess. A reviewed title is a decision. Sellers don't publish what an AI writes — they publish what they approve. So the core UX question isn't "how good is the model?" but "how fast can a seller judge the output?"&lt;/p&gt;

&lt;p&gt;Our answer: every optimization run lands on a web dashboard as a before/after comparison. Original on one side, suggestion on the other. The seller edits, accepts, or discards. Nothing touches the listing until a human says so.&lt;/p&gt;

&lt;h2&gt;
  
  
  The loop: extract → draft → review → publish
&lt;/h2&gt;

&lt;p&gt;The Chrome extension reads the live product page the seller already has open — Amazon, Shopify (including custom domains), eBay, or Etsy — using structured data plus platform-specific selectors. The AI drafts rewrites that respect each channel's constraints: Amazon's mobile truncation, eBay title length, Etsy's handmade tone, Shopify brand voice.&lt;/p&gt;

&lt;p&gt;Then control moves to the dashboard:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Before/after comparison for every field&lt;/li&gt;
&lt;li&gt;History of past runs, reusable across SKUs&lt;/li&gt;
&lt;li&gt;Optional public share links for team feedback&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Paste-ready output, human-approved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the same pass handles images
&lt;/h2&gt;

&lt;p&gt;Copy alone rarely fixes a weak listing. The same run can enhance product photos or swap backgrounds — and keeping the original image is free. That pricing detail matters more than it sounds: it makes "do nothing" a legitimate choice, which keeps the review loop honest.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we learned
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Constraints are features.&lt;/strong&gt; Character limits and platform tone rules make suggestions &lt;em&gt;more&lt;/em&gt; useful, not less — they turn generated text into paste-ready text.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review speed beats generation speed.&lt;/strong&gt; A suggestion that takes 30 seconds to judge is worth more than three alternatives that take 10 minutes to compare.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;History turns AI output into an asset.&lt;/strong&gt; Sellers who can reopen last month's runs stop treating AI as a slot machine.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you sell across marketplaces and want to see the review-first flow yourself, the extension is free to try — 5 credits on signup, no credit card: &lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;https://aiproductpageoptimization.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>ux</category>
      <category>productivity</category>
    </item>
    <item>
      <title>Reading any marketplace product page from a Chrome extension: JSON-LD first, selectors second</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:35:47 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/reading-any-marketplace-product-page-from-a-chrome-extension-json-ld-first-selectors-second-c2i</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/reading-any-marketplace-product-page-from-a-chrome-extension-json-ld-first-selectors-second-c2i</guid>
      <description>&lt;p&gt;If you have ever tried to build a tool that "reads" a product page — an Amazon listing, a Shopify storefront, an eBay item, an Etsy shop page — you already know the problem: every marketplace renders the same conceptual data (title, bullets, description, images, price) in completely different DOM shapes.&lt;/p&gt;

&lt;p&gt;We ran into this while building &lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;AI Product Page Optimization&lt;/a&gt;, a Chrome extension that reads the product page you already have open and returns marketplace-ready title, description, and image suggestions in about 30 seconds. This post is the technical breakdown of how the reading step works. (This article is disclosed as AI-assisted; the extraction design and constraints below are from our actual implementation.)&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with JSON-LD, always
&lt;/h2&gt;

&lt;p&gt;Most marketplaces embed structured data in the page. Before touching a single DOM selector, we look for &lt;code&gt;application/ld+json&lt;/code&gt; blocks and parse them for &lt;code&gt;Product&lt;/code&gt;-shaped objects:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ldjson&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;querySelectorAll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;script[type="application/ld+json"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&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;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textContent&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="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="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why JSON-LD first:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It survives layout changes. Amazon can reshuffle its DOM next week; the &lt;code&gt;Product&lt;/code&gt; schema block rarely moves.&lt;/li&gt;
&lt;li&gt;It normalizes the shape. A title is a &lt;code&gt;name&lt;/code&gt;, images are an &lt;code&gt;image&lt;/code&gt; array, and offers carry price/currency regardless of which marketplace you are on.&lt;/li&gt;
&lt;li&gt;It handles encoding edge cases (HTML entities, unicode in internationalized catalogs) better than regex-over-HTML ever will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But JSON-LD is never the whole story. Marketplaces routinely put &lt;em&gt;richer or more current&lt;/em&gt; data in the DOM than in their structured data: bullet points that are not in any schema field, variant-specific copy, A/B-tested titles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Selectors as a scoped fallback
&lt;/h2&gt;

&lt;p&gt;After JSON-LD, we apply platform-specific DOM selectors to fill the gaps — and the key word is &lt;em&gt;scoped&lt;/em&gt;: we detect the marketplace from the URL first, then load only that platform's selector set.&lt;/p&gt;

&lt;p&gt;The four surfaces we read today:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon&lt;/strong&gt; — deep, nested DOM with frequent class-name churn; stable attributes and landmark patterns beat styling hooks&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify (including custom domains)&lt;/strong&gt; — the most uniform target; Shopify themes share enough structure that extraction is reliable even on stores with custom themes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eBay&lt;/strong&gt; — item-specific layouts where the description block is seller-authored HTML, which changes the parsing strategy entirely&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Etsy&lt;/strong&gt; — listing copy with a distinct handmade/creative tone that also matters downstream for generation&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Constraints matter more than extraction
&lt;/h2&gt;

&lt;p&gt;Reading the page is half the job. The other half is knowing what each marketplace will accept when you write the optimized copy back:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Amazon truncates aggressively on mobile — character limits are hard product constraints, not suggestions&lt;/li&gt;
&lt;li&gt;Shopify storefronts often carry thin manufacturer text that needs brand-voice cleanup rather than keyword stuffing&lt;/li&gt;
&lt;li&gt;eBay titles have a hard length cap, and condition-forward descriptions convert better&lt;/li&gt;
&lt;li&gt;Etsy rewards handmade-tone phrasing with stronger search phrases — generic copy reads as dropshipping&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;One more detail that surprised us: output language follows the detected page language. Cross-border catalogs mix English with other locales, and returning English suggestions for a Japanese listing page is worse than useless — it breaks the seller's workflow.&lt;/p&gt;

&lt;h2&gt;
  
  
  MV3 practicalities
&lt;/h2&gt;

&lt;p&gt;Two things worth knowing if you build this on Manifest V3:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The extraction needs to run in the page context (JSON-LD and DOM both), so your content script is the workhorse; the service worker only orchestrates.&lt;/li&gt;
&lt;li&gt;Keep the extension stateless about page data — read the live page on demand, send only what the user approves onward. It keeps you compatible with platform policies and keeps the data footprint small.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Review before publish
&lt;/h2&gt;

&lt;p&gt;We deliberately never auto-paste anything. Extracted data and generated suggestions land on a web dashboard with a before/after comparison, history, and optional share links — the seller copies and publishes only what they approve. Optimization is credit-based (text 1 credit, AI-enhanced image 2, keeping an original image is free; a typical product page uses about 5 credits, and new accounts start with 5 free credits).&lt;/p&gt;

&lt;p&gt;If you are building anything that reads product pages — price trackers, feed tools, listing optimizers — the pattern is the same: structured data first, scoped selectors second, platform constraints third, and a human review step before anything goes back to the marketplace.&lt;/p&gt;

&lt;p&gt;Try the extension: &lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;aiproductpageoptimization.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>extensions</category>
    </item>
    <item>
      <title>Rehearse the conversations you dread: what building an AI practice simulator taught me</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Wed, 30 Sep 2026 17:46:17 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/rehearse-the-conversations-you-dread-what-building-an-ai-practice-simulator-taught-me-4i08</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/rehearse-the-conversations-you-dread-what-building-an-ai-practice-simulator-taught-me-4i08</guid>
      <description>&lt;p&gt;Most of us prepare for big conversations by... not preparing. We rehearse a few lines in our heads, maybe mutter them in the shower, then walk into the meeting and freeze.&lt;/p&gt;

&lt;p&gt;I built &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill&lt;/a&gt; to fix that. It's an AI communication simulator: you rehearse high-stakes conversations — salary negotiations, performance feedback, difficult personal talks — by actually talking with realistic AI personas before they happen.&lt;/p&gt;

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

&lt;ol&gt;
&lt;li&gt;Pick a scenario: asking for a raise, delivering hard feedback, saying no to your boss, or an entirely personal one.&lt;/li&gt;
&lt;li&gt;The AI plays the other person. It pushes back, changes tone, and asks the questions you're afraid of.&lt;/li&gt;
&lt;li&gt;You talk it through, out loud, then look back at what worked and where you got stuck.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Three things I learned building and using it
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Rehearsal beats research.&lt;/strong&gt; Reading ten articles about negotiating doesn't help much when the other person says "we'll see." Practicing your counter-move does. A simulator gives you the counter-move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The second attempt is where the skill is.&lt;/strong&gt; The first run through a scenario is rough. By the third replay the rhythm changes — you stop defending and start steering. That's the whole point of practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Saying it out loud matters.&lt;/strong&gt; Typing an argument is easy. Speaking it to an AI boss that interrupts and pushes back is a different skill — and the one you actually need in the room.&lt;/p&gt;

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

&lt;p&gt;cosskill is free to try: a few messages without an account, more after sign-in, all personas and scenarios included. Unlimited use is on Pro.&lt;/p&gt;

&lt;p&gt;If you have a hard conversation coming up, rehearse it first at &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;. Two of our guides go deeper: &lt;a href="https://cosskill.com/guides/how-to-ask-for-a-raise" rel="noopener noreferrer"&gt;how to ask for a raise&lt;/a&gt; and &lt;a href="https://cosskill.com/guides/how-to-give-negative-feedback" rel="noopener noreferrer"&gt;how to give negative feedback&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>softskills</category>
    </item>
    <item>
      <title>How We Built an AI Listing Optimizer for Amazon, Shopify, eBay &amp; Etsy (Chrome Extension Architecture)</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Tue, 22 Sep 2026 19:46:39 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/how-we-built-an-ai-listing-optimizer-for-amazon-shopify-ebay-etsy-chrome-extension-3dn1</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/how-we-built-an-ai-listing-optimizer-for-amazon-shopify-ebay-etsy-chrome-extension-3dn1</guid>
      <description>&lt;p&gt;Multi-channel sellers live in a painful loop: copy a listing into ChatGPT or Jasper, get decent text, then spend another 20 minutes reformatting it for Amazon's character limits, eBay's title rules, or Etsy's story style — per SKU, per channel. Amazon-only tools don't follow them to Shopify. Generic AI writers ignore marketplace rules.&lt;/p&gt;

&lt;p&gt;We built &lt;strong&gt;AI Product Page Optimization&lt;/strong&gt; to close that gap, and this post walks through how it works and the architecture behind it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The core idea: run on the live page
&lt;/h2&gt;

&lt;p&gt;Instead of another dashboard with CSV imports, the tool is a Chrome extension that runs on the product page you already have open. It:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Detects the marketplace from the URL (Amazon, Shopify — including custom domains, eBay, Etsy)&lt;/li&gt;
&lt;li&gt;Extracts title, bullets, body copy, and images using structured data (JSON-LD) plus platform-specific DOM selectors&lt;/li&gt;
&lt;li&gt;Generates rewrites that respect each channel's constraints&lt;/li&gt;
&lt;li&gt;Returns results to a web dashboard for before/after review before anything is published&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A typical run takes about 30–60 seconds per product page.&lt;/p&gt;

&lt;h2&gt;
  
  
  Platform-aware rules, not generic copy
&lt;/h2&gt;

&lt;p&gt;The interesting part isn't the LLM — it's the constraints we bake in:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Amazon&lt;/strong&gt;: mobile truncation limits and Rufus-friendly phrasing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shopify&lt;/strong&gt;: brand-voice cleanup for thin manufacturer text&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;eBay&lt;/strong&gt;: title length limits and condition-forward descriptions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Etsy&lt;/strong&gt;: handmade tone with stronger search phrases&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Output language follows the detected page language, which matters for cross-border catalogs that mix locales.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Extension&lt;/strong&gt;: Manifest V3, dynamic script injection for live-page extraction&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web app&lt;/strong&gt;: Next.js, React, Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Cloudflare Workers, D1 (SQLite), R2 for image storage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: LLMs for copy optimization; image models for enhancement or background changes (keeping the original image is free)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auth &amp;amp; billing&lt;/strong&gt;: Google sign-in or email magic link; Creem for subscriptions and credit top-ups&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Pricing model
&lt;/h2&gt;

&lt;p&gt;Text optimization costs 1 credit; successfully enhanced images cost 2. A typical product page uses about 5 credits. New users get 5 free trial credits with no credit card required; paid plans start at $19.9/month.&lt;/p&gt;

&lt;p&gt;If you sell on any of these marketplaces and want to try the workflow: &lt;a href="https://aiproductpageoptimization.com" rel="noopener noreferrer"&gt;https://aiproductpageoptimization.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Feedback welcome — especially from anyone who maintains listings across multiple platforms.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>ecommerce</category>
      <category>chromeextension</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built an AI simulator for the conversations people avoid</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Tue, 22 Sep 2026 17:43:48 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-simulator-for-the-conversations-people-avoid-f6i</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-simulator-for-the-conversations-people-avoid-f6i</guid>
      <description>&lt;p&gt;The hardest conversations in your life don't get retries. A salary negotiation, critical feedback to a teammate, a breakup, telling your boss you're quitting — you get one take, live, with a real person whose reaction you can't control.&lt;/p&gt;

&lt;p&gt;So most of us rehearse in the shower, say the wrong thing anyway, and then spend weeks replaying it.&lt;/p&gt;

&lt;p&gt;I kept running into this problem as a founder, so I built &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill&lt;/a&gt;: an AI communication simulator where you rehearse high-stakes conversations with realistic AI personas &lt;em&gt;before&lt;/em&gt; they happen. This post is about what the product does, and what I learned building it — including the parts that surprised me.&lt;/p&gt;

&lt;h2&gt;
  
  
  What rehearsing actually looks like
&lt;/h2&gt;

&lt;p&gt;You pick a scenario (negotiate a raise, deliver negative feedback, end a relationship, quit gracefully), pick a persona, and talk. You can type or speak. The AI doesn't roleplay a pushover — it pushes back, interrupts, gets defensive, and responds like an actual person would.&lt;/p&gt;

&lt;p&gt;Then you get an evaluation of the conversation: what landed, what didn't, and how it moved. You can rewind to any point and try the moment again — including the hardest 30 seconds.&lt;/p&gt;

&lt;p&gt;There are 20 personas across operators, strategists, philosophers, and psychologists, plus a mission mode with live meters that track how the conversation is going.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Pricing, plainly:&lt;/strong&gt; it's free to try — 3 messages without an account, 10 after signing in, with all personas and scenarios available. Pro is $12.90/month or $129/year for unlimited.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three things building it taught me
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. A roleplay bot that agrees with you is useless
&lt;/h3&gt;

&lt;p&gt;Early versions were too polite. The "other side" folded too easily, and practicing against it trained avoidance, not skill. The single biggest product decision was making personas that genuinely push back. If your rehearsed opener survives a realistic counterparty, it'll survive a real one — or at least you'll know what the counter-move looks like.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The rewind is the feature
&lt;/h3&gt;

&lt;p&gt;Live conversations don't have undo. That's exactly why practice should. Being able to redo the worst moment of a conversation — immediately, with a persona that reacts differently each time — is where most of the learning happens. Repetition builds fluency; fluency is what keeps you articulate when you're actually nervous.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Feedback has to be concrete
&lt;/h3&gt;

&lt;p&gt;"You did great!" is worthless. The evaluation had to name specific moves: you opened with an apology that undercut your ask; you stated your number once and then kept talking; you de-escalated a personal attack without conceding the point. Concrete diagnosis is what you can act on in the retry.&lt;/p&gt;

&lt;h2&gt;
  
  
  A 10-minute rehearsal you can do today
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Pick the conversation you've been avoiding.&lt;/li&gt;
&lt;li&gt;Write down your ask in one sentence, with your number/decision stated once.&lt;/li&gt;
&lt;li&gt;Run the scenario with a persona that will push back.&lt;/li&gt;
&lt;li&gt;When it goes sideways, rewind and redo that moment — don't restart the whole thing.&lt;/li&gt;
&lt;li&gt;Read the evaluation, adjust one thing, run it once more.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you want a deeper framework, we published free guides like &lt;a href="https://cosskill.com/guides/how-to-have-a-difficult-conversation" rel="noopener noreferrer"&gt;How to Have a Difficult Conversation&lt;/a&gt; and &lt;a href="https://cosskill.com/guides/how-to-negotiate-salary" rel="noopener noreferrer"&gt;How to Negotiate Your Salary&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;An AI persona is not your manager, your partner, or your teammate. It can't predict how a specific person will react, and it shouldn't be a substitute for real advice on legal, medical, or financial decisions. What it &lt;em&gt;can&lt;/em&gt; do is take the edge of panic off — you walk into the real conversation having already heard the pushback, so you're responding instead of improvising.&lt;/p&gt;

&lt;p&gt;That's the whole bet: fluency comes from rehearsal, and hard conversations deserve a rehearsal.&lt;/p&gt;

&lt;p&gt;If you try it, I'd genuinely like to hear what the AI pushed back with — that's the part we iterate on fastest.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;cosskill: &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;https://cosskill.com&lt;/a&gt; — free to try, no account needed for the first few messages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>career</category>
      <category>softskills</category>
      <category>productivity</category>
    </item>
    <item>
      <title>White or Transparent? A Practical Guide to Product Image Backgrounds</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Sun, 20 Sep 2026 03:14:32 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/white-or-transparent-a-practical-guide-to-product-image-backgrounds-33ko</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/white-or-transparent-a-practical-guide-to-product-image-backgrounds-33ko</guid>
      <description>&lt;p&gt;Most product grids don't look broken because of bad photography. They look broken because the backgrounds don't agree with each other.&lt;/p&gt;

&lt;p&gt;One card with a warm beige backdrop in a row of pure-white cards reads as a mistake, even when that photo is technically the better shot. Designers tend to blame the layout. The layout is usually fine. The background is the variable nobody specified.&lt;/p&gt;

&lt;p&gt;This guide covers what the major platforms actually require, when transparent beats white, and a checklist you can hand to whoever produces the images.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why background choice is a layout decision
&lt;/h2&gt;

&lt;p&gt;A product grid is a repeated component. Its visual rhythm depends on every cell sharing the same visual weight, and background colour is one of the strongest contributors to that weight.&lt;/p&gt;

&lt;p&gt;Three things break the rhythm:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Mixed backgrounds.&lt;/strong&gt; Lifestyle, studio-grey, and pure-white images in one row. Each is fine alone; together they look unplanned.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed crops.&lt;/strong&gt; A product that fills 60% of one frame and 90% of the next makes the grid look like it has two different paddings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mixed colour temperature.&lt;/strong&gt; White balance drift across a photo set reads as inconsistent backgrounds even when every image is nominally "on white".&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of these are solved in CSS. You can letterbox, crop, or apply a filter, but you cannot recover a consistent background from inconsistent source images. The decision has to happen before the asset reaches the component.&lt;/p&gt;

&lt;p&gt;That is the useful reframe: a white background is a &lt;strong&gt;specification&lt;/strong&gt;, in the same way a spacing scale or a type ramp is. It belongs in the design system, not in a photographer's judgement call.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the platforms actually require
&lt;/h2&gt;

&lt;p&gt;If your product appears on a marketplace, the specification is not yours to choose.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Amazon&lt;/strong&gt; requires main images on a pure white background — RGB 255, 255, 255 — with the product occupying 85% of the frame. Amazon states the reason directly: a consistent background gives buyers a consistent experience across search results and detail pages.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Google Merchant Center&lt;/strong&gt; is slightly more permissive but adds a trap worth knowing. Its image link guidance says to "use a solid white or transparent background", noting that these ensure the image "works with a variety of design elements", and that staged or lifestyle images which clearly show the product are also acceptable.&lt;/p&gt;

&lt;p&gt;Then comes the note that catches people out:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't use a transparent background for images with light colors because they might show with a black background.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That single sentence explains a whole class of bug reports. A transparent PNG of a white ceramic mug looks correct in a design tool with a light canvas and disappears into a dark-mode card. Google is also tightening image size requirements: at least 500 x 500 pixels for all products from January 31, 2027, with 1500 x 1500 or above recommended for best performance.&lt;/p&gt;

&lt;p&gt;The practical takeaway is that "white" and "transparent" are not interchangeable defaults. They are two different specifications that suit different surfaces.&lt;/p&gt;

&lt;h2&gt;
  
  
  White or transparent: choosing per surface
&lt;/h2&gt;

&lt;p&gt;Use this as a decision rule rather than a preference.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Choose white when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The image will appear on a marketplace listing or shopping feed.&lt;/li&gt;
&lt;li&gt;The image sits in a grid alongside other products.&lt;/li&gt;
&lt;li&gt;The product itself is light-coloured and needs a defined silhouette.&lt;/li&gt;
&lt;li&gt;The image may be shared, screenshotted, or reposted somewhere you don't control.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Choose transparent when:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The image sits on a known, controlled background colour that you can guarantee.&lt;/li&gt;
&lt;li&gt;The product is dark or mid-tone, so it will never vanish into a light surface.&lt;/li&gt;
&lt;li&gt;The asset is a UI element — an icon, a badge, a floating product in a hero section.&lt;/li&gt;
&lt;li&gt;You genuinely need it to composite over more than one background.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Avoid transparent when&lt;/strong&gt; the product is white, cream, pale grey, or glass. This is the case Google's note warns about, and it is common in homeware, ceramics, cosmetics, and packaging.&lt;/p&gt;

&lt;p&gt;If you need both, generate both. Producing a white-background master and a transparent derivative from the same source image is cheaper than re-shooting later, and it removes the temptation to use one asset in the wrong place.&lt;/p&gt;

&lt;h2&gt;
  
  
  A checklist for a consistent grid
&lt;/h2&gt;

&lt;p&gt;Hand this to whoever produces or edits the images.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Define the background value once.&lt;/strong&gt; If you are not bound by a marketplace, pick a single value — pure white or a specific light neutral — and write it down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check the actual pixels, not the impression.&lt;/strong&gt; Sample a corner of each image. A set that "looks white" often contains several different values.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Standardise the product's share of the frame.&lt;/strong&gt; Match the ratio across the set so the grid has even optical padding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Normalise colour temperature across the batch.&lt;/strong&gt; Correct the set together rather than image by image.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Export both masters.&lt;/strong&gt; Keep a white-background version and a transparent version at the largest size you need.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test the transparent version on a dark surface before shipping it.&lt;/strong&gt; This is the step that catches the disappearing-mug bug.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Name files by variant.&lt;/strong&gt; A predictable suffix for background and size saves an enormous amount of guesswork later.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The background of a product image is a design token that happens to live in a bitmap. Treating it as one — with a defined value, a defined crop ratio, and a defined set of exports — removes the most common source of visual noise in product grids, and it keeps you compliant with the marketplaces that enforce their own specification.&lt;/p&gt;

&lt;p&gt;Transparent is not more sophisticated than white, and white is not lazier than transparent. They are two specifications for two different surfaces. The mistake is not choosing; it is leaving it unspecified.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://cleanwhitebg.com" rel="noopener noreferrer"&gt;CleanWhiteBG&lt;/a&gt;, a free tool that replaces an image background with white in one click. It is one way to produce the white-background master described above; the checklist stands on its own regardless of which tool you use.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Is a white background better than a transparent one for product photos?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It depends on the surface. Marketplaces such as Amazon require pure white for main images, while Google Merchant Center accepts either solid white or transparent. For grids and feeds, white is the safer default because it is self-contained; transparent only works reliably when you control the background it will sit on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does my transparent PNG look wrong in dark mode?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Because there is nothing behind the product. A transparent image inherits whatever is underneath it, so a light-coloured product can lose its edges against a dark card. Google's Merchant Center guidance flags exactly this risk. Export a white-background version for any surface you do not control.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What resolution should product images be?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Google Merchant Center requires at least 500 x 500 pixels for all products from January 31, 2027, and recommends 1500 x 1500 pixels or above for best performance across listing formats. Amazon requires the product to occupy 85% of the frame, so start from a larger master and crop rather than upscaling a small file.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>design</category>
      <category>css</category>
      <category>ux</category>
    </item>
    <item>
      <title>We put an ONNX background-removal container behind a Next.js Worker on Cloudflare</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Thu, 17 Sep 2026 04:48:33 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/we-put-an-onnx-background-removal-container-behind-a-nextjs-worker-on-cloudflare-3mb9</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/we-put-an-onnx-background-removal-container-behind-a-nextjs-worker-on-cloudflare-3mb9</guid>
      <description>&lt;h1&gt;
  
  
  We put an ONNX background-removal container behind a Next.js Worker on Cloudflare
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;Disclosure: I build &lt;a href="https://cleanwhitebg.com" rel="noopener noreferrer"&gt;CleanWhiteBG&lt;/a&gt;, the tool described here. This post is about the infrastructure, not a pitch — every number below comes from the running code.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Background removal looks like a one-liner in a demo: load an image, run a segmentation model, get a mask. In production it turns into a queueing problem, a cold-start problem, and a "why is my Worker crashing with error 1101" problem.&lt;/p&gt;

&lt;p&gt;Here is what our stack actually looks like after a few rounds of getting it wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  The constraint that shapes everything: 30 seconds of CPU
&lt;/h2&gt;

&lt;p&gt;Cloudflare Workers give you a hard CPU budget per invocation. We set ours to 30,000 ms in &lt;code&gt;wrangler.jsonc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"limits"&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;"cpu_ms"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;30000&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;On top of that, &lt;code&gt;waitUntil&lt;/code&gt; does not buy you a background thread that can run for a minute. Work that outlives the response is not reliable work. So you cannot simply accept an upload, run a 10-second inference in the background, and return &lt;code&gt;202 Accepted&lt;/code&gt; with a promise to email the user later.&lt;/p&gt;

&lt;p&gt;Our answer was to split the pipeline so that &lt;strong&gt;the expensive part runs inside a request the client is still holding open&lt;/strong&gt;, and the client drives the state machine.&lt;/p&gt;

&lt;h3&gt;
  
  
  Submit, then poll
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;POST /api/tool/background-to-white&lt;/code&gt; does almost nothing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Check the per-IP rate limit.&lt;/li&gt;
&lt;li&gt;Parse the multipart body.&lt;/li&gt;
&lt;li&gt;Validate the MIME type (JPEG, PNG, WebP) and a 10 MB size ceiling.&lt;/li&gt;
&lt;li&gt;Write the original to R2.&lt;/li&gt;
&lt;li&gt;Create a job row in D1 and return the &lt;code&gt;jobId&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That round trip is about a second. The actual inference happens later, when the client polls &lt;code&gt;GET&lt;/code&gt; with that job id. The first poll that "claims" a queued job is the request that actually runs the container inference, with a long-poll budget of 20 seconds and a 3-second client interval.&lt;/p&gt;

&lt;p&gt;It is a slightly unusual shape — the work happens in the GET, not the POST — but it means every request stays inside its CPU budget, and a dropped client connection just leaves the job to be reclaimed later.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cold starts: loading a model while the health check is watching
&lt;/h2&gt;

&lt;p&gt;The "Fast Cloud AI" path runs &lt;a href="https://huggingface.co/onnx-community/BiRefNet_lite-ONNX" rel="noopener noreferrer"&gt;BiRefNet-lite&lt;/a&gt; exported to ONNX, served by ONNX Runtime on CPU inside a Cloudflare Container.&lt;/p&gt;

&lt;p&gt;The container is a &lt;code&gt;python:3.12-slim&lt;/code&gt; image with the model &lt;strong&gt;baked in at build time&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nv"&gt;onnxruntime&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;1.21.1 &lt;span class="nv"&gt;Pillow&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;11.0.0 &lt;span class="nv"&gt;numpy&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;2.2.6 &lt;span class="nv"&gt;flask&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;3.1.1 &lt;span class="nv"&gt;gunicorn&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;23.0.0
&lt;span class="k"&gt;RUN &lt;/span&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--no-cache-dir&lt;/span&gt; &lt;span class="nv"&gt;huggingface_hub&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;0.34.4 &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="se"&gt;\
&lt;/span&gt;    python3 &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"from huggingface_hub import hf_hub_download; hf_hub_download(repo_id='onnx-community/BiRefNet_lite-ONNX', filename='onnx/model.onnx', local_dir='/app/models')"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two decisions here matter more than they look.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;First, the model is in the image, not fetched at boot.&lt;/strong&gt; Pulling it from Hugging Face on every cold start adds seconds you cannot control and a failure mode you cannot fix at 2 a.m.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Second, gunicorn binds the port before the model finishes loading.&lt;/strong&gt; If you load the model in the module body, the HTTP port stays closed while ONNX Runtime reads weights, and the platform's health check fails before your service ever answers. Instead we load it in a background thread and have requests wait:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;load_model_async&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ort&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;InferenceSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/app/models/birefnet_lite.onnx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                   &lt;span class="n"&gt;providers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;CPUExecutionProvider&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;ensure_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;60.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# block until the background load finishes, retry synchronously once on timeout
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The port is ready in milliseconds, health checks pass, and the first real request just waits a little longer.&lt;/p&gt;

&lt;p&gt;Even so, a genuinely cold container means pulling the image, starting the process, and loading the model — we measured that in the low tens of seconds. That is far too slow to sit behind a user's click.&lt;/p&gt;

&lt;p&gt;So we hide it: the tool page pings &lt;code&gt;GET /api/warm&lt;/code&gt; when it mounts and when the user starts choosing a file. That call is idempotent — it asks the Durable Object to start the container and returns immediately if it is already running. The cold start happens during the several seconds a human spends browsing for a photo, not after they hit submit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The landmine: do not name an R2 binding &lt;code&gt;IMAGES&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one cost us a production incident, so it is worth repeating.&lt;/p&gt;

&lt;p&gt;OpenNext's &lt;code&gt;/_next/image&lt;/code&gt; handler treats &lt;code&gt;env.IMAGES&lt;/code&gt; as a &lt;strong&gt;Cloudflare Images&lt;/strong&gt; binding and calls &lt;code&gt;.input()&lt;/code&gt; on it. If you also have an R2 bucket bound as &lt;code&gt;IMAGES&lt;/code&gt;, the runtime gets a bucket where it expects an image service, and the Worker dies with error 1101 — a crash, not a caught exception.&lt;/p&gt;

&lt;p&gt;Our binding is therefore called &lt;code&gt;R2_UPLOADS&lt;/code&gt;, with a comment in &lt;code&gt;wrangler.jsonc&lt;/code&gt; so nobody "tidies" the name later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&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;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"R2_UPLOADS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bucket_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;"cleanwhitebg-images"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="err"&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;"binding"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"IMAGES"&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="c1"&gt;// Cloudflare Images binding for /_next/image — fixed name&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The general lesson: binding names are part of the framework's API surface, not your namespace.&lt;/p&gt;

&lt;h2&gt;
  
  
  Not every edge needs the same model
&lt;/h2&gt;

&lt;p&gt;A small ONNX model is fast and cheap and handles most photos. It is also visibly worse on the cases people actually care about — flyaway hair, jewellery, fur, anything translucent with a busy background.&lt;/p&gt;

&lt;p&gt;Rather than run one big model for everything and pay for it on every request, we route on the quality parameter the user picked:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;standard&lt;/strong&gt; → the self-hosted BiRefNet-lite container, CPU, cheap, fast.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;quality&lt;/strong&gt; → pre-process only when needed (downscale beyond 2K, pad when the aspect ratio is extreme), then hand off to a larger hosted model and poll its progress.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Same API, same job table, two very different cost profiles. The user chooses the tradeoff, and we do not pay the expensive path for a passport photo that did not need it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Cleanup is a cron, not a best-effort delete
&lt;/h2&gt;

&lt;p&gt;Uploaded images are stored in R2 and the public promise is that they are gone within two hours. We do not rely on the request path to guarantee that. A Cron Trigger sweeps expired uploads every 15 minutes, and a second cron (every minute) resumes orphaned batch jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json-doc"&gt;&lt;code&gt;&lt;span class="nl"&gt;"triggers"&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;"crons"&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;"*/15 * * * *"&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="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;If cleanup only ran when a request happened to finish successfully, a crash would leave user images sitting in a bucket indefinitely. A cron makes the retention promise independent of traffic.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would tell someone starting today
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Decide early whether your work fits in one request. If it does not, design the job table and the polling contract before you write the model code.&lt;/li&gt;
&lt;li&gt;Load models in a background thread so health checks pass. A container that never becomes healthy is worse than a slow one.&lt;/li&gt;
&lt;li&gt;Bake weights into the image. Network fetches at boot are a reliability tax with no upside.&lt;/li&gt;
&lt;li&gt;Read the framework's binding names. &lt;code&gt;IMAGES&lt;/code&gt; is taken.&lt;/li&gt;
&lt;li&gt;Hydrate the expensive resource before the user needs it, during a gap where they are already waiting on something else.&lt;/li&gt;
&lt;li&gt;Make retention a scheduled job, not a side effect.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The whole thing is a Next.js 16 app running through OpenNext on Workers, with D1 for job state, R2 for objects, and one Python container doing the segmentation. It is several moving parts — but each one is doing a job the others genuinely cannot.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why not run the ONNX model inside the Worker itself?&lt;/strong&gt;&lt;br&gt;
Workers do not ship a general-purpose CPU inference runtime for arbitrary ONNX models, and even if you got one working, the 30-second CPU ceiling and memory limits make it a poor fit for a 1024×1024 segmentation pass. A container gives us the real runtime and lets the Worker stay a router.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why does the client poll instead of the server pushing?&lt;/strong&gt;&lt;br&gt;
Because the work is tied to a request's lifetime. Long-polling keeps the inference inside a request that is still open, which is the only place we can safely spend that CPU. A push model would need the work to outlive the response, which is exactly what we cannot rely on.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Is the inference running on a GPU?&lt;/strong&gt;&lt;br&gt;
No — CPU only, ONNX Runtime with &lt;code&gt;CPUExecutionProvider&lt;/code&gt;. That is why the model is the "lite" variant and why the expensive edge cases are routed to a hosted model instead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What happens if a user closes the tab mid-job?&lt;/strong&gt;&lt;br&gt;
The job stays in D1 in a claimed state. A stale-claim timeout lets a later poll or the batch cron reclaim it, so abandoned work gets cleaned up rather than stuck forever.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>ai</category>
      <category>showdev</category>
      <category>nextjs</category>
    </item>
    <item>
      <title>Next.js 16 on Cloudflare Workers: what broke and what didn't</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Wed, 22 Jul 2026 03:23:39 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/nextjs-16-on-cloudflare-workers-what-broke-and-what-didnt-5bka</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/nextjs-16-on-cloudflare-workers-what-broke-and-what-didnt-5bka</guid>
      <description>&lt;p&gt;I shipped a Next.js 16 app on Cloudflare Workers via OpenNext. Not a demo. A real product with streaming chat, server components, D1 at the edge, and anonymous user sessions.&lt;/p&gt;

&lt;p&gt;Here is what broke, what barely worked, and what turned out to be surprisingly fine.&lt;/p&gt;

&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Next.js 16.2 (App Router)&lt;/li&gt;
&lt;li&gt;@opennextjs/cloudflare 1.19&lt;/li&gt;
&lt;li&gt;D1 for SQLite at the edge&lt;/li&gt;
&lt;li&gt;Streaming chat via the AI binding (DeepSeek-V3 through a Workers proxy)&lt;/li&gt;
&lt;li&gt;React 19&lt;/li&gt;
&lt;li&gt;Tailwind CSS 4&lt;/li&gt;
&lt;li&gt;No auth wall, no OAuth, no database on the origin&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site runs a few thousand sessions a week across ~30 persona pages, blog posts, guides, and learning content. Most pages are statically generated. The chat interaction is server-rendered components with streaming responses.&lt;/p&gt;

&lt;h2&gt;
  
  
  What worked surprisingly well
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Static generation and ISR
&lt;/h3&gt;

&lt;p&gt;Pages, blogs, guides, persona pages — everything that does not need user-specific rendering — runs as static HTML at deploy time.&lt;/p&gt;

&lt;p&gt;Next.js 16 with &lt;code&gt;generateStaticParams&lt;/code&gt; and &lt;code&gt;fetch&lt;/code&gt; caching worked without modification. OpenNext handles the Cloudflare output format. The build step produces something Workers can serve. Revalidations are limited to Workers' cache API, but since most content changes at deploy time, I never hit that limit in production.&lt;/p&gt;

&lt;p&gt;The one caveat: &lt;code&gt;revalidateTag()&lt;/code&gt; does not work the same way in a Workers runtime. Tags are Node.js memory constructs, and Workers are stateless. If you depend on tag-based revalidation for content updates, you need to either trigger deploys or accept stale-while-revalidate behavior from the CDN.&lt;/p&gt;

&lt;h3&gt;
  
  
  D1 at the edge
&lt;/h3&gt;

&lt;p&gt;D1 was the least surprising part of the stack. SQL queries from Next.js route handlers feel like calling a regular database. Sessions store in D1, messages store in D1, and the latency is low enough that restoring a full chat thread from 30 messages takes under 200ms cold.&lt;/p&gt;

&lt;p&gt;The only sharp edge: D1 connections count against your Worker's concurrent request limit in development. With Next.js making its own fetch calls for compilation, I hit the D1 connection ceiling faster than expected. The fix was moving &lt;code&gt;wrangler dev&lt;/code&gt; to use &lt;code&gt;--experimental-json-config&lt;/code&gt; early, but the dev experience for D1 + Next.js hot reload is still rougher than it should be.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming chat responses
&lt;/h3&gt;

&lt;p&gt;The app streams model responses token by token. In dev, this was unreliable — the stream would drop mid-response on roughly 1 in 15 requests. I spent two days tracing it before realizing it was a local &lt;code&gt;wrangler dev&lt;/code&gt; issue with HTTP chunked transfer encoding, not a production problem.&lt;/p&gt;

&lt;p&gt;In production, streaming over Workers works. The AI binding handles the model request inside the Worker, and the response streams back through the Next.js route handler to the client without a hitch.&lt;/p&gt;

&lt;h3&gt;
  
  
  Anonymous user sessions
&lt;/h3&gt;

&lt;p&gt;The product ships without required sign-in. Users get a UUID from &lt;code&gt;crypto.randomUUID()&lt;/code&gt; stored in localStorage, sent as &lt;code&gt;X-User-Id&lt;/code&gt; on every chat request. On the server, D1 writes sessions against that ID.&lt;/p&gt;

&lt;p&gt;Workers not having durable session state actually helps here. The identity arrives in headers. No sticky sessions. No session table. The Worker pulls the user ID, checks D1 for existing sessions or quota limits, and proceeds.&lt;/p&gt;

&lt;h2&gt;
  
  
  What broke
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Middleware and edge runtime mismatch
&lt;/h3&gt;

&lt;p&gt;Next.js 16 middleware runs on every request. On Cloudflare Workers, middleware executes in the edge runtime, not the Node.js runtime. This means any middleware that imports &lt;code&gt;crypto&lt;/code&gt; or uses APIs outside the Workers subset will fail at request time, not at build time.&lt;/p&gt;

&lt;p&gt;I had a middleware that checked rate limits using a counter in D1. Simple enough. But the D1 binding is not available in the middleware context through OpenNext the way it is in route handlers. I had to move rate limiting to a Cloudflare WAF rule instead of doing it in Next.js middleware. Not a dealbreaker, but the middleware → Workers gap is larger than the documentation suggests.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;next/image&lt;/code&gt; and Workers
&lt;/h3&gt;

&lt;p&gt;Static images work fine. Dynamic image optimization through &lt;code&gt;next/image&lt;/code&gt; does not, because Workers lack the image processing libraries that Node.js uses.&lt;/p&gt;

&lt;p&gt;The fix: I pre-processed all images at build time and served them as static assets. No runtime optimization needed. For an app with fewer than 50 unique images, this was trivial. For an app with user-uploaded content, it would be a hard problem.&lt;/p&gt;

&lt;h3&gt;
  
  
  Environment variables in client components
&lt;/h3&gt;

&lt;p&gt;Environment variables prefixed with &lt;code&gt;NEXT_PUBLIC_&lt;/code&gt; are baked into the client bundle at build time. That works fine. But runtime environment variables accessed through &lt;code&gt;process.env&lt;/code&gt; in server components behave differently in Workers.&lt;/p&gt;

&lt;p&gt;Cloudflare Workers use a &lt;code&gt;env&lt;/code&gt; binding, not &lt;code&gt;process.env&lt;/code&gt;. OpenNext bridges this, but the bridge is not seamless. Variables I set in &lt;code&gt;wrangler.toml&lt;/code&gt; were available in the Worker context but not through the &lt;code&gt;process.env&lt;/code&gt; API that Next.js server components expect at runtime. The workaround was importing the Cloudflare bindings through the &lt;code&gt;@opennextjs/cloudflare&lt;/code&gt; types and passing them explicitly.&lt;/p&gt;

&lt;h3&gt;
  
  
  The first-deploy cold start
&lt;/h3&gt;

&lt;p&gt;First deploy after building with OpenNext takes roughly 45-60 seconds for the first Worker request. After the initial cold start, response times drop to normal.&lt;/p&gt;

&lt;p&gt;This is a known Workers characteristic compounded by Next.js route chunking. The more routes your app has, the more chunks the Worker needs to load on the first request. My app has about 30 routes. A smaller app with 5 routes would cold-start faster.&lt;/p&gt;

&lt;p&gt;Subsequent deploys are faster because Cloudflare caches the compilation output. But the first deploy of the day always has a cold start window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Streaming + D1 in the same route handler
&lt;/h3&gt;

&lt;p&gt;This one was subtle. A route handler that reads from D1 and then streams a response — for example, loading session history and then starting the chat stream — sometimes lost the D1 response before the stream completed.&lt;/p&gt;

&lt;p&gt;The issue: Workers terminate the request context after the response is sent. If you read from D1 inside the same handler that starts a streaming response, the D1 bindings can close before the stream finishes if the stream outlasts the initial response resolution.&lt;/p&gt;

&lt;p&gt;The fix: resolve all D1 reads before starting the stream. Load session data early, store it in a closure, then begin streaming.&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="c1"&gt;// Before: D1 read interleaved with streaming&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM sessions WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;streamToReadableStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// After: resolve D1 reads first, then stream&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;POST&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;prepare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;SELECT * FROM sessions WHERE id = ?&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;bind&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sessionId&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;first&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;messagesWithHistory&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...(&lt;/span&gt;&lt;span class="nx"&gt;session&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;messages&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]),&lt;/span&gt; &lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="nx"&gt;newMessages&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;stream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;ai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;beta&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messagesWithHistory&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;stream&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="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Response&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;streamToReadableStream&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;stream&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;Straightforward fix once I recognized the pattern. Cost me two evenings of debugging before I found it.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I am still watching
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;ISR revalidation at scale.&lt;/strong&gt; I have not reached the threshold where Workers cache API limits matter. If the app grows to hundreds of content pages with frequent revalidations, I may need to revisit the caching strategy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;D1 row limits.&lt;/strong&gt; A few thousand sessions with 10-15 messages each is well under D1's limits. If the app crosses 100k+ sessions, I will need either a message archiving strategy or a move to R2 for stored messages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;OpenNext churn.&lt;/strong&gt; The OpenNext → Cloudflare pipeline changes version to version. Two minor bumps have already required &lt;code&gt;wrangler.toml&lt;/code&gt; changes. The stack is stable enough for production but not mature enough to set and forget.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest take
&lt;/h2&gt;

&lt;p&gt;Next.js 16 on Cloudflare Workers works for a real production app. The static generation path is smooth. D1 is solid. Streaming is fine after you learn the D1-resolution-before-stream rule.&lt;/p&gt;

&lt;p&gt;The rough edges are in the gaps between ecosystems: middleware runtimes, environment variable access patterns, and image optimization. Each gap has a workaround, but the workarounds are not always documented.&lt;/p&gt;

&lt;p&gt;The OpenNext team has done the heavy lift of bridging Next.js to Workers. The remaining friction is mostly about accepting that you are on a serverless platform with different constraints than Vercel. Once you internalize those constraints — resolve D1 first, pre-process images, avoid Node.js APIs in middleware — the stack holds.&lt;/p&gt;

&lt;p&gt;For a production app with streaming chat, anonymous sessions, and static content: &lt;a href="https://cosskill.com?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=week8-nextjs-workers" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;. The stack file is pinned on the repo if you want the full picture.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>nextjs</category>
      <category>cloudflarechallenge</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Why I Chose DeepSeek Over GPT-4 for a Free AI Conversation App</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Mon, 22 Jun 2026 06:43:39 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/why-i-chose-deepseek-over-gpt-4-for-a-free-ai-conversation-app-3gci</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/why-i-chose-deepseek-over-gpt-4-for-a-free-ai-conversation-app-3gci</guid>
      <description>&lt;p&gt;I did not choose DeepSeek because I think GPT-4 is bad. I chose it because I was building a free app, and free apps teach you what actually matters pretty fast.&lt;/p&gt;

&lt;p&gt;The question was simple: how do I keep sessions cheap enough that people can practice a lot without me lighting money on fire?&lt;/p&gt;

&lt;p&gt;The answer pushed me toward DeepSeek-V3 (and later R1 for specific tasks).&lt;/p&gt;

&lt;h2&gt;
  
  
  The real constraint was volume
&lt;/h2&gt;

&lt;p&gt;The app is a conversation practice tool. People come in to rehearse hard talks, not to admire the model.&lt;/p&gt;

&lt;p&gt;A single practice session runs 8-15 turns. Each turn is roughly 300-600 tokens in, 100-300 out. Multiply that by five sessions a week per active user and the costs start compounding.&lt;/p&gt;

&lt;p&gt;Here is what the math looked like when I was choosing (mid-2026 pricing):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Model&lt;/th&gt;
&lt;th&gt;Input cost (per 1M tokens)&lt;/th&gt;
&lt;th&gt;Output cost (per 1M tokens)&lt;/th&gt;
&lt;th&gt;Cost per 10-turn session (est.)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4o&lt;/td&gt;
&lt;td&gt;$2.50&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;~$0.04-0.06&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GPT-4 Turbo&lt;/td&gt;
&lt;td&gt;$10.00&lt;/td&gt;
&lt;td&gt;$30.00&lt;/td&gt;
&lt;td&gt;~$0.12-0.18&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-V3&lt;/td&gt;
&lt;td&gt;$0.27&lt;/td&gt;
&lt;td&gt;$1.10&lt;/td&gt;
&lt;td&gt;~$0.004-0.007&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DeepSeek-R1&lt;/td&gt;
&lt;td&gt;$0.55&lt;/td&gt;
&lt;td&gt;$2.19&lt;/td&gt;
&lt;td&gt;~$0.008-0.012&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;At scale, the difference between $0.005 and $0.05 per session is the difference between running a free product and needing a paywall after three conversations. I wanted people to come back daily without hitting a wall.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DeepSeek handled well
&lt;/h2&gt;

&lt;p&gt;It stayed in character for 10-15 turns. It pushed back when the user got vague. It followed persona heuristics (numbered if/then rules in the system prompt) about as reliably as GPT-4o did for our use case.&lt;/p&gt;

&lt;p&gt;For salary negotiation rehearsal, the model needs to say "that's not in the budget" and hold that position for three more turns while the user tries different approaches. DeepSeek-V3 did this. Not perfectly, but reliably enough that sessions felt real.&lt;/p&gt;

&lt;p&gt;It also made the app easier to run as a free product. People can try, fail, reset, and try again without me worrying about per-session cost.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where GPT-4 was still better
&lt;/h2&gt;

&lt;p&gt;GPT-4 (and 4o) is smoother with nuanced emotional wording. When a conversation gets subtle, loaded with subtext, or requires picking up on implied meaning, GPT-4 catches more.&lt;/p&gt;

&lt;p&gt;For the breakup text persona, GPT-4o noticed when a user's "kind" message was actually passive-aggressive. DeepSeek missed that about 20% more often in my informal testing across ~100 sessions.&lt;/p&gt;

&lt;p&gt;But polish was not the main bottleneck for this product. The main bottleneck was getting people enough reps to build actual comfort with discomfort.&lt;/p&gt;

&lt;h2&gt;
  
  
  The tradeoff I actually cared about
&lt;/h2&gt;

&lt;p&gt;Do I want one beautiful session, or ten useful ones?&lt;/p&gt;

&lt;p&gt;For this app, ten useful ones. Every time.&lt;/p&gt;

&lt;p&gt;So I took the cheaper model, put the engineering effort into the prompt architecture (persona seed, heuristics, mode wrapper, boundaries), and accepted that 85-90% quality at 10x the volume was a better product than 95% quality at 1x.&lt;/p&gt;

&lt;p&gt;The model matters. The scaffolding around it matters more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I changed to make DeepSeek work
&lt;/h2&gt;

&lt;p&gt;A few things made the choice viable:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Tighter system prompts. DeepSeek drifts more with long, loose instructions. Shorter seed, more numbered rules.&lt;/li&gt;
&lt;li&gt;Lower temperature (0.55 for roleplay, 0.2 for scoring). Kept persona variation without character breaks.&lt;/li&gt;
&lt;li&gt;Max reply length cap in the mode wrapper. DeepSeek's default is wordier than GPT-4o, so I had to constrain it explicitly.&lt;/li&gt;
&lt;li&gt;Built retries into the flow. A bad response does not kill the session; the user gets a fresh turn.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The last one is underrated for any practice app. The experience should not feel fragile.&lt;/p&gt;

&lt;h2&gt;
  
  
  My actual takeaway
&lt;/h2&gt;

&lt;p&gt;If you are building a free AI app, the best model is not always the smartest one. It is the one that lets people come back tomorrow.&lt;/p&gt;

&lt;p&gt;Not bragging rights. Not benchmark charts. Whether the app stays affordable enough to be used like a tool instead of a demo.&lt;/p&gt;

&lt;p&gt;For cosskill, DeepSeek made more sense. It let me build something people use five times a week instead of once and forget. Which is usually the whole game for a practice product anyway.&lt;/p&gt;

&lt;p&gt;If you want to see the product, it is at &lt;a href="https://cosskill.com?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=week5-deepseek" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>productivity</category>
    </item>
    <item>
      <title>I built an AI conversation simulator because I kept chickening out of real talks</title>
      <dc:creator>neo xia</dc:creator>
      <pubDate>Mon, 01 Jun 2026 03:43:42 +0000</pubDate>
      <link>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-conversation-simulator-because-i-kept-chickening-out-of-real-talks-2jmc</link>
      <guid>https://dev.to/neo_xia_3f4c019330af5fb9d/i-built-an-ai-conversation-simulator-because-i-kept-chickening-out-of-real-talks-2jmc</guid>
      <description>&lt;p&gt;Last year I needed to ask for a raise. I knew my number, I'd read the guides, I had bullet points in my notes app. Then my manager said "let's chat about your goals for next quarter" and I said "sounds great, looking forward to it" and hung up. Never brought up money.&lt;/p&gt;

&lt;p&gt;Same thing kept happening elsewhere. Coworker taking credit for my work, I said nothing. Relationship that should've ended months earlier, I kept postponing. I always knew what to say. I just couldn't say it with someone actually looking at me.&lt;/p&gt;

&lt;p&gt;So I started building a thing to practice on. That thing became &lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually is
&lt;/h2&gt;

&lt;p&gt;You pick a persona, tell it the situation in a sentence, and start talking. The persona doesn't help you. It holds position and pushes back. You practice not folding.&lt;/p&gt;

&lt;p&gt;Think of it as a flight simulator for hard conversations. You rehearse until your opener comes out steady, then go do the real thing. 20 personas across five categories:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Operators (Musk, Jobs): first-principles thinking, harsh product feedback&lt;/li&gt;
&lt;li&gt;Strategists (Trump, Buffett): treat everything as a deal or a bet&lt;/li&gt;
&lt;li&gt;Relationship (Ex, Coworker): breakups, workplace friction, family money&lt;/li&gt;
&lt;li&gt;Philosophy (Socrates, Aurelius, Confucius, Sun Tzu, four more): each tradition frames problems differently&lt;/li&gt;
&lt;li&gt;Psychology (Rogers, Rosenberg, Ellis, Frankl, Kahneman, Jung): therapeutic frameworks on real situations&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These aren't celebrity impressions. The Buffett persona won't hype your startup idea. It'll ask "what's the downside?" and keep asking until you have something concrete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech stack
&lt;/h2&gt;

&lt;p&gt;Next.js 16 on Cloudflare Workers. DeepSeek for inference. Cloudflare D1 (SQLite at edge) for the bits that need to persist. No user accounts, chat history lives in localStorage.&lt;/p&gt;

&lt;p&gt;Monthly cost stays low enough that the free tier (10 messages/day) doesn't worry me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I made these choices
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;DeepSeek instead of GPT-4/Claude.&lt;/strong&gt; Each conversation is 10-30 messages. At GPT-4 pricing a free product bleeds money. DeepSeek gives maybe 90% of the quality for a fraction of the cost on this specific task, which is maintaining persona consistency across a back-and-forth.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No accounts.&lt;/strong&gt; Every signup form is friction between "I need to practice this talk" and actually practicing it. If I add a login wall, some percentage of people close the tab and go back to rehearsing in the shower. I'd rather have them practice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Cloudflare Workers instead of Vercel.&lt;/strong&gt; D1 is genuinely good for this. One database at edge, no connection pooling, no separate DB service. DX is slightly worse for Next.js specifically but the deploy simplicity makes up for it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Personas that resist instead of a chatbot that helps.&lt;/strong&gt; A generic AI assistant will agree with you. That's the problem. You don't need agreement. You need someone saying "that's not in the budget" while you practice not conceding immediately. Each persona has hardcoded positions and pushback patterns. They're useful by being difficult.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I actually learned building this
&lt;/h2&gt;

&lt;p&gt;Prompting a persona to stay in character is a solved problem. The hard part was figuring out what "practice" means for conversations.&lt;/p&gt;

&lt;p&gt;Scripts don't work because they sound robotic and shatter on contact with a real human. Free-form chatting doesn't work because there's no improvement loop. What actually works: have one sentence you want to say, say it to something that pushes back, adjust based on what happens, try again. The first two minutes of any hard conversation set the rest. So you practice those two minutes.&lt;/p&gt;

&lt;p&gt;After a few runs, people say the real conversation feels less scary. Not because they memorized a script. Because they already heard the worst response and survived. "That's not in the budget" doesn't feel like a gut punch when you've heard it four times from an AI and practiced not caving.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where it's at
&lt;/h2&gt;

&lt;p&gt;Early. Traffic is growing organically. The pages that get the most hits are salary negotiation practice and breakup text help. Draw your own conclusions about what people actually struggle with.&lt;/p&gt;

&lt;p&gt;Free tier handles most users. Pro ($9.90/month annual) is for people who want unlimited messages and custom personas.&lt;/p&gt;

&lt;h2&gt;
  
  
  Go try it
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://cosskill.com" rel="noopener noreferrer"&gt;cosskill.com&lt;/a&gt;. No signup, just pick a persona and start. If you have a salary conversation coming up, try Buffett. Need to set a boundary with a coworker, try the Coworker persona. Want someone to tear your startup pitch apart, try Jobs.&lt;/p&gt;

&lt;p&gt;Genuinely curious: what conversations do devs specifically need to practice that I haven't thought of? And what persona doesn't exist yet but should?&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>ai</category>
      <category>nextjs</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
