<?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: Lucas Santos Rodrigues</title>
    <description>The latest articles on DEV Community by Lucas Santos Rodrigues (@lusrodri).</description>
    <link>https://dev.to/lusrodri</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%2F3392595%2F28b337e0-f46a-4df4-8e05-5074b91133e4.JPG</url>
      <title>DEV Community: Lucas Santos Rodrigues</title>
      <link>https://dev.to/lusrodri</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lusrodri"/>
    <language>en</language>
    <item>
      <title>A PlayStation 1 burger: posterized fog, ordered dithering and vertex jitter in pure CSS</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:56:04 +0000</pubDate>
      <link>https://dev.to/lusrodri/a-playstation-1-burger-posterized-fog-ordered-dithering-and-vertex-jitter-in-pure-css-5fam</link>
      <guid>https://dev.to/lusrodri/a-playstation-1-burger-posterized-fog-ordered-dithering-and-vertex-jitter-in-pure-css-5fam</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, CSS Art&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Inspiration
&lt;/h2&gt;

&lt;p&gt;Two comfort foods collided here.&lt;/p&gt;

&lt;p&gt;One is &lt;strong&gt;Méqui&lt;/strong&gt; — what every Brazilian actually calls McDonald's. Not a fancy dish: the burger you begged your parents for on a Sunday afternoon.&lt;/p&gt;

&lt;p&gt;The other is the way games looked when I was a kid. Chunky low-poly geometry, colours banding because there weren't enough of them, dithering scattered over every gradient, coloured fog swallowing the far plane, and that faint wobble PSX hardware had because it snapped vertices to integers.&lt;/p&gt;

&lt;p&gt;So: a comfort-food burger, rendered like it's running on 1998 hardware.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://lusrodri.github.io/mequi/" rel="noopener noreferrer"&gt;Live&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://github.com/LuSrodri/mequi" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/strong&gt; (MIT)&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fkijbkajdu8lm081uf5qs.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%2Fkijbkajdu8lm081uf5qs.png" alt="Low-poly cel-shaded burger with black outlines on a purple posterized background" width="534" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Scroll and it comes apart, labelling itself as it goes:&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%2Fendcmced3q8j868kitep.jpg" 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%2Fendcmced3q8j868kitep.jpg" alt="The same burger separated into seven labelled layers" width="799" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Upfront:&lt;/strong&gt; the burger's geometry is SVG, not CSS shapes — ~370 polygons emitted by a build-time script I wrote. Every &lt;em&gt;look&lt;/em&gt; on this page, though, is CSS: the fog, the grain, the outlines, the motion. That's the part worth showing, because most of it is four lines each.&lt;/p&gt;

&lt;h3&gt;
  
  
  Posterization is just gradients with hard stops
&lt;/h3&gt;

&lt;p&gt;Banding is usually the enemy. Here it's the entire point. Every colour stop is doubled so there's no interpolation between bands:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fx--fog&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;screen&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;58%&lt;/span&gt; &lt;span class="m"&gt;44%&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;12%&lt;/span&gt; &lt;span class="m"&gt;8%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#ff2d95&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;6%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#a8215f&lt;/span&gt; &lt;span class="m"&gt;6%&lt;/span&gt; &lt;span class="m"&gt;13%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="m"&gt;#5a1140&lt;/span&gt; &lt;span class="m"&gt;13%&lt;/span&gt; &lt;span class="m"&gt;22%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#0000&lt;/span&gt; &lt;span class="m"&gt;40%&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;radial-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;52%&lt;/span&gt; &lt;span class="m"&gt;40%&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;88%&lt;/span&gt; &lt;span class="m"&gt;22%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#16d6ff&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;5%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#0f88b8&lt;/span&gt; &lt;span class="m"&gt;5%&lt;/span&gt; &lt;span class="m"&gt;12%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="m"&gt;#0a4a70&lt;/span&gt; &lt;span class="m"&gt;12%&lt;/span&gt; &lt;span class="m"&gt;21%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#0000&lt;/span&gt; &lt;span class="m"&gt;38%&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;#ff2d95 0 6%, #a8215f 6% 13%&lt;/code&gt; — each colour holds flat, then snaps. Stack four of those and you get coloured fog that looks quantized to an 8-bit palette.&lt;/p&gt;

&lt;h3&gt;
  
  
  Ordered dithering is two conic gradients
&lt;/h3&gt;

&lt;p&gt;My favourite four lines on the page. A 2px checkerboard and a 3px checkerboard, offset from each other, blended over everything:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.fx--dither&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="py"&gt;mix-blend-mode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;overlay&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;opacity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.22&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conic-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;90deg&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#0000&lt;/span&gt; &lt;span class="m"&gt;25%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#000&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="n"&gt;conic-gradient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from&lt;/span&gt; &lt;span class="m"&gt;90deg&lt;/span&gt; &lt;span class="n"&gt;at&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#fff0&lt;/span&gt; &lt;span class="m"&gt;25%&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;#fff&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nl"&gt;background-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt; &lt;span class="m"&gt;2px&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt; &lt;span class="m"&gt;3px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;background-position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="m"&gt;1px&lt;/span&gt; &lt;span class="m"&gt;1px&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;Two coprime grid sizes never line up, so the interference reads as ordered dither instead of a visible checkerboard. It breaks up the fog's bands and puts "low-resolution render" grain back into an otherwise perfectly crisp vector page.&lt;/p&gt;

&lt;h3&gt;
  
  
  Vertex jitter needs steps(), and that's the whole joke
&lt;/h3&gt;

&lt;p&gt;PSX consoles had no sub-pixel precision, so geometry shimmered. The charm is in &lt;strong&gt;not interpolating&lt;/strong&gt; — a smooth version of this just looks like drift:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="k"&gt;@keyframes&lt;/span&gt; &lt;span class="n"&gt;psx-jitter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="err"&gt;0&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;16&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.9px&lt;/span&gt; &lt;span class="m"&gt;-0.6px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;33&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0.7px&lt;/span&gt; &lt;span class="m"&gt;0.5px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;50&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.4px&lt;/span&gt; &lt;span class="m"&gt;0.9px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;66&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;-0.9px&lt;/span&gt; &lt;span class="m"&gt;-0.4px&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="err"&gt;83&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;translate&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0.6px&lt;/span&gt; &lt;span class="m"&gt;0.7px&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="nc"&gt;.burger-jitter&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;psx-jitter&lt;/span&gt; &lt;span class="m"&gt;1.1s&lt;/span&gt; &lt;span class="n"&gt;steps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;jump-none&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;infinite&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;steps(1, jump-none)&lt;/code&gt; holds each keyframe and snaps to the next. Sub-pixel amounts, but your eye catches it instantly.&lt;/p&gt;

&lt;h3&gt;
  
  
  One light, five tones
&lt;/h3&gt;

&lt;p&gt;The cel shading isn't hand-picked colour. The generator computes a normal per face, dots it with &lt;strong&gt;one&lt;/strong&gt; warm key light, then &lt;strong&gt;quantizes&lt;/strong&gt; the result to a 5-step ramp mixed toward that light and a single cool ambient.&lt;/p&gt;

&lt;p&gt;Quantization is what makes it read as cel-shaded — the banding falls out of the renderer instead of being painted on top. And because all seven ingredients derive from the same two colours, they look like one render instead of seven stickers.&lt;/p&gt;

&lt;h3&gt;
  
  
  Black outlines, no soft edges, no round corners
&lt;/h3&gt;

&lt;p&gt;PSX-era art has hard everything. No &lt;code&gt;border-radius&lt;/code&gt; anywhere — corners are chamfered with &lt;code&gt;clip-path: polygon()&lt;/code&gt;. No blurred shadows — &lt;code&gt;box-shadow: 8px 9px 0&lt;/code&gt; with zero blur. Text outlines are a ring of eight zero-blur &lt;code&gt;text-shadow&lt;/code&gt;s rather than &lt;code&gt;-webkit-text-stroke&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  The one that fought me
&lt;/h3&gt;

&lt;p&gt;The big MÉQUI wordmark uses &lt;code&gt;background-clip: text&lt;/code&gt; for its cel-shading bands, plus a stack of &lt;code&gt;drop-shadow()&lt;/code&gt; filters for the outline. It rendered &lt;strong&gt;solid black&lt;/strong&gt;: Chrome drops the text clip when a filter lands on the same element.&lt;/p&gt;

&lt;p&gt;The fix was to stop asking one element to do two jobs — solid colour and the &lt;code&gt;text-shadow&lt;/code&gt; ring on the base element, gradient clip on an &lt;code&gt;::after&lt;/code&gt; with &lt;code&gt;content: attr(data-txt)&lt;/code&gt; sitting exactly on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Motion
&lt;/h3&gt;

&lt;p&gt;The layers separate with a &lt;strong&gt;CSS scroll-driven animation&lt;/strong&gt; on a named view-timeline, ranged &lt;code&gt;contain 0%&lt;/code&gt; → &lt;code&gt;contain 100%&lt;/code&gt; — which, for a section taller than the viewport, is exactly the window where its sticky child is pinned. No scroll listeners, no library. It's wrapped in &lt;code&gt;@supports (animation-timeline: view())&lt;/code&gt; with a real fallback, since Firefox doesn't ship it yet.&lt;/p&gt;

&lt;p&gt;The page carries one line of runtime JS: the CSS import Vite needs as an entry point.&lt;/p&gt;

&lt;p&gt;Thanks for reading! The fog and dither snippets are genuinely drop-in — steal them.&lt;/p&gt;

</description>
      <category>frontendchallenge</category>
      <category>devchallenge</category>
      <category>css</category>
      <category>svg</category>
    </item>
    <item>
      <title>Méqui — the story of McDonald's in Brazil, told by a burger that comes apart as you scroll</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 03 Aug 2026 13:53:16 +0000</pubDate>
      <link>https://dev.to/lusrodri/mequi-the-story-of-mcdonalds-in-brazil-told-by-a-burger-that-comes-apart-as-you-scroll-3b2l</link>
      <guid>https://dev.to/lusrodri/mequi-the-story-of-mcdonalds-in-brazil-told-by-a-burger-that-comes-apart-as-you-scroll-3b2l</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for &lt;a href="https://dev.to/challenges/frontend-2026-07-29"&gt;Frontend Challenge - Comfort Food Edition, Perfect Landing&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;In Brazil, nobody says "McDonald's". They say &lt;strong&gt;Méqui&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;On August 30th, 2019, the company quietly changed the sign on two restaurants to read MÉQUI, issued no press release, and let the country figure it out. The mystery spread on its own. The nickname stuck so hard it went on the packaging, the tray liners and the menu.&lt;/p&gt;

&lt;p&gt;So the comfort food I picked isn't a dish. It's the burger every Brazilian kid grew up begging for on a Sunday — and the 46 years of history stacked behind it.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;&lt;a href="https://lusrodri.github.io/mequi/" rel="noopener noreferrer"&gt;Live site&lt;/a&gt;&lt;/strong&gt; · &lt;strong&gt;&lt;a href="https://github.com/LuSrodri/mequi" rel="noopener noreferrer"&gt;Source&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The page is one continuous scroll: a hero, a giant SVG burger that comes apart layer by layer as it travels down the screen, then a timeline from the first store in Copacabana in 1979 to the day the sign changed.&lt;/p&gt;

&lt;p&gt;It's written in Portuguese on purpose. The whole piece is about a country renaming a global brand in its own accent — translating it would defeat the point.&lt;/p&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;The burger arrives assembled:&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%2Fkijbkajdu8lm081uf5qs.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%2Fkijbkajdu8lm081uf5qs.png" alt="The assembled low-poly burger" width="534" height="588"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;…and comes apart as it descends, labelling itself on the way down:&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%2Fendcmced3q8j868kitep.jpg" 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%2Fendcmced3q8j868kitep.jpg" alt="The burger fully separated into seven labelled layers" width="799" height="370"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The one trick worth stealing
&lt;/h2&gt;

&lt;p&gt;The separation is a &lt;strong&gt;CSS scroll-driven animation&lt;/strong&gt; anchored to a named view-timeline. The section is 460svh tall and holds a &lt;code&gt;position: sticky&lt;/code&gt; child:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight css"&gt;&lt;code&gt;&lt;span class="nc"&gt;.stage&lt;/span&gt;      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="py"&gt;view-timeline-name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--burger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;460&lt;/span&gt;&lt;span class="n"&gt;svh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="nc"&gt;.stage__pin&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nl"&gt;position&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;sticky&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;inset-block-start&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="py"&gt;block-size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="n"&gt;svh&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nc"&gt;.bx-layer&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;animation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;peel&lt;/span&gt; &lt;span class="n"&gt;linear&lt;/span&gt; &lt;span class="nb"&gt;both&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-timeline&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;--burger&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="py"&gt;animation-range&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;contain&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="n"&gt;contain&lt;/span&gt; &lt;span class="n"&gt;var&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;--b&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The part that made it click: for a subject &lt;strong&gt;taller than the viewport&lt;/strong&gt;, the &lt;code&gt;contain&lt;/code&gt; range runs from "the section's top edge meets the top of the screen" to "its bottom edge meets the bottom of the screen" — which is, point for point, exactly the window in which the sticky child is pinned. So &lt;code&gt;contain 0%&lt;/code&gt; → &lt;code&gt;contain 100%&lt;/code&gt; means &lt;em&gt;"animate only while the burger is parked on screen"&lt;/em&gt;, with no magic numbers.&lt;/p&gt;

&lt;p&gt;Each layer gets its own range, offset 5% from the one above. That stagger is the whole reason the sandwich opens top-down instead of all at once.&lt;/p&gt;

&lt;p&gt;One more thing that surprised me: &lt;strong&gt;no &lt;code&gt;timeline-scope&lt;/code&gt; is needed&lt;/strong&gt;. The SVG layers are descendants of &lt;code&gt;.stage&lt;/code&gt;, and timeline name lookup already walks up the ancestors.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accessibility and the Firefox problem
&lt;/h2&gt;

&lt;p&gt;Scroll-driven animations are &lt;strong&gt;Baseline: limited&lt;/strong&gt; — Chrome/Edge 115+ and Safari 26+ have them; Firefox is still preview-only. So every scroll-driven rule lives inside &lt;code&gt;@supports (animation-timeline: view())&lt;/code&gt;, with a &lt;code&gt;not (...)&lt;/code&gt; branch that collapses the 460svh section back to auto height, un-sticks the pin, and shows the burger assembled with all labels visible.&lt;/p&gt;

&lt;p&gt;I tested that branch by forcing the &lt;code&gt;@supports&lt;/code&gt; query to fail: nothing is hidden, and nobody scrolls through 460svh of nothing.&lt;/p&gt;

&lt;p&gt;Other things I cared about:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;prefers-reduced-motion&lt;/code&gt; kills the PSX jitter, the fog and the scanlines. The layer separation &lt;strong&gt;stays&lt;/strong&gt;, because it's the content of the section and the user drives it directly — but the travel is damped to 40% so it isn't a full-screen sweep.&lt;/li&gt;
&lt;li&gt;On narrow screens the side labels don't fit, so they disappear and the layers come back as a real text list at the bottom of the stage.&lt;/li&gt;
&lt;li&gt;Skip link, semantic landmarks, no heading-level skips, gold focus rings that survive every background in the palette, &lt;code&gt;scroll-margin&lt;/code&gt; so anchor targets don't hide behind the fixed header.&lt;/li&gt;
&lt;li&gt;The burger SVG is &lt;code&gt;role="img"&lt;/code&gt; with a &lt;code&gt;&amp;lt;title&amp;gt;&lt;/code&gt;/&lt;code&gt;&amp;lt;desc&amp;gt;&lt;/code&gt;; the fog, dither and scanline overlays are &lt;code&gt;aria-hidden&lt;/code&gt; and &lt;code&gt;pointer-events: none&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Journey
&lt;/h2&gt;

&lt;p&gt;Two bugs are worth confessing.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The wordmark rendered black.&lt;/strong&gt; I had &lt;code&gt;background-clip: text&lt;/code&gt; for the cel-shading bands plus a &lt;code&gt;filter: drop-shadow()&lt;/code&gt; stack for the outline. Chrome loses the text clip when a filter is on the same element. Fix: split the job — solid colour and a &lt;code&gt;text-shadow&lt;/code&gt; ring on the base element, gradient clip on an &lt;code&gt;::after&lt;/code&gt; with &lt;code&gt;content: attr(data-txt)&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizontal scroll on mobile.&lt;/strong&gt; The stage's grid column was sizing to the SVG's natural width — 509px inside a 369px viewport. I reached for &lt;code&gt;overflow-x: clip&lt;/code&gt; on &lt;code&gt;html&lt;/code&gt; and it did &lt;strong&gt;not&lt;/strong&gt; stop the viewport scrolling. The actual fix was &lt;code&gt;grid-template-columns: minmax(0, 1fr)&lt;/code&gt;, because &lt;code&gt;auto&lt;/code&gt; tracks grow to max-content. Clipping was treating a symptom.&lt;/p&gt;

&lt;p&gt;The whole page ships &lt;strong&gt;one line of runtime JavaScript&lt;/strong&gt; — the &lt;code&gt;import&lt;/code&gt; of the CSS that Vite needs as an entry module. Everything else, including the burger, is CSS.&lt;/p&gt;

&lt;p&gt;MIT licensed. Fork it, take the &lt;code&gt;contain&lt;/code&gt; trick, ignore my colour choices.&lt;/p&gt;

</description>
      <category>devchallenge</category>
      <category>frontendchallenge</category>
      <category>webdev</category>
      <category>css</category>
    </item>
    <item>
      <title>I generated 207 MCP tools from an OpenAPI spec. Generating them was the easy part.</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Thu, 30 Jul 2026 00:23:08 +0000</pubDate>
      <link>https://dev.to/lusrodri/i-generated-207-mcp-tools-from-an-openapi-spec-generating-them-was-the-easy-part-1n8c</link>
      <guid>https://dev.to/lusrodri/i-generated-207-mcp-tools-from-an-openapi-spec-generating-them-was-the-easy-part-1n8c</guid>
      <description>&lt;p&gt;Every MCP server I've read that wraps a third-party REST API has the same shape: someone picked fifteen or twenty endpoints that seemed useful, hand-wrote a Zod schema for each, and shipped it.&lt;/p&gt;

&lt;p&gt;That works for about three months.&lt;/p&gt;

&lt;p&gt;Then the API adds a field, deprecates an enum value, renames a query parameter. The wrapper doesn't notice, because nothing in it is connected to the API's own description of itself. The schemas drift. The model starts getting rejected by the upstream API for reasons it can't see, and you get to debug an LLM guessing at a shape that stopped being true in February.&lt;/p&gt;

&lt;p&gt;The other failure is quieter. You need endpoint twenty-one — the one about persistent disks, or scaling, or environment groups — and the author skipped it. Now you're back to &lt;code&gt;curl&lt;/code&gt;, except the agent is holding half the context and you're holding the other half.&lt;/p&gt;

&lt;p&gt;I wanted neither, so I built &lt;a href="https://github.com/LuSrodri/render-useful-mcp" rel="noopener noreferrer"&gt;render-useful-mcp&lt;/a&gt;: an MCP server for &lt;a href="https://render.com" rel="noopener noreferrer"&gt;Render&lt;/a&gt; where every API tool is generated from Render's own OpenAPI document. All 207 endpoints, no curation.&lt;/p&gt;

&lt;p&gt;The generating part took a weekend. Everything after that was the actual work.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the generator refuse to guess
&lt;/h2&gt;

&lt;p&gt;The tempting way to write a spec-to-tools generator is to make it forgiving. Skip what you can't parse, fall back to &lt;code&gt;{ type: "object" }&lt;/code&gt; when a &lt;code&gt;$ref&lt;/code&gt; gets hairy, log a warning and move on. You get 207 tools on the first run and feel great.&lt;/p&gt;

&lt;p&gt;You also get tools that lie. A parameter typed as a free-form object when the API actually wants one of four enum values is worse than no tool at all — the model will confidently produce garbage, and the failure surfaces three layers away from the cause.&lt;/p&gt;

&lt;p&gt;So the generator is fail-closed. It aborts the build, loudly, on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;An operation whose tag doesn't map to a known toolset.&lt;/strong&gt; Render added a resource category and I haven't classified it yet. That's my problem to solve, not something to paper over.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A tool name collision.&lt;/strong&gt; Two operations deriving the same name means my naming scheme is wrong.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A cyclic &lt;code&gt;$ref&lt;/code&gt; it can't flatten.&lt;/strong&gt; JSON Schema handles recursion; the "just inline everything" shortcut doesn't. Better to know.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A path parameter that doesn't appear in the URL template&lt;/strong&gt;, or appears in the template but isn't declared. Either direction means I'd build a URL wrong at runtime.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;CI runs the generator. A spec change that the generator can't handle correctly fails the build instead of shipping a subtly broken tool. That's the entire value proposition — a hand-written wrapper &lt;em&gt;cannot&lt;/em&gt; have this property, because there's nothing to check it against.&lt;/p&gt;

&lt;p&gt;The payoff is that Render's own constraints reach the model intact. Enums stay enums. String patterns stay patterns. Required fields stay required. The model gets rejected by &lt;em&gt;my&lt;/em&gt; schema, locally, with a readable message, instead of by Render's API after a round trip.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem nobody warns you about
&lt;/h2&gt;

&lt;p&gt;207 API tools plus 4 workflow tools plus a meta-tool is 212 entries in &lt;code&gt;tools/list&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;You cannot ship that.&lt;/p&gt;

&lt;p&gt;Every tool definition — name, description, full input schema — goes into the model's context on every single turn. 212 of them is tens of thousands of tokens spent before the user has typed anything. Worse than the cost is the dilution: a model choosing between 212 near-neighbours picks wrong far more often than one choosing between fifteen. Complete coverage, delivered naively, makes the server &lt;em&gt;less&lt;/em&gt; useful than the curated subset I was trying to beat.&lt;/p&gt;

&lt;p&gt;The fix is to separate what the server &lt;em&gt;can&lt;/em&gt; do from what it &lt;em&gt;exposes&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The 207 tools are grouped into 17 toolsets by the tag on their originating operation: &lt;code&gt;services&lt;/code&gt;, &lt;code&gt;postgres&lt;/code&gt;, &lt;code&gt;disks&lt;/code&gt;, &lt;code&gt;blueprints&lt;/code&gt;, &lt;code&gt;logs&lt;/code&gt;, &lt;code&gt;metrics&lt;/code&gt;, &lt;code&gt;webhooks&lt;/code&gt;, &lt;code&gt;network&lt;/code&gt;, &lt;code&gt;maintenance&lt;/code&gt;, &lt;code&gt;workflows&lt;/code&gt;, and so on. Everything is on by default, because a server that hides capability by default is a server that fails silently for the person who needed the hidden thing. But one environment variable narrows it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;RENDER_MCP_TOOLSETS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;services,logs,metrics
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now the model sees a few dozen tools instead of 212, chosen by you, for the job you're actually doing. There's a &lt;code&gt;RENDER_MCP_READ_ONLY=true&lt;/code&gt; on top of that, which drops every mutating operation — the right default for anything you'd point at production.&lt;/p&gt;

&lt;p&gt;The interesting constraint is that this selection is read &lt;strong&gt;once, at startup, and never changes&lt;/strong&gt;. I'll come back to why.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four tools that aren't in the spec
&lt;/h2&gt;

&lt;p&gt;Generated tools are a faithful mirror of the API, which means they inherit the API's ergonomics. Some sequences that are one thought for a human are four calls for an agent, and agents are bad at four-call sequences.&lt;/p&gt;

&lt;p&gt;So there are four hand-written tools on top:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;render_find_service&lt;/code&gt; — you know the service as "the api", Render knows it as &lt;code&gt;srv-d1a2b3c4e5f6g7h8i9j0&lt;/code&gt;. Fuzzy-matches by name and returns close alternatives when there's no exact hit, instead of failing.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render_wait_for_deploy&lt;/code&gt; — blocks until a deploy reaches a terminal state, so the agent stops re-polling in a loop and burning turns.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render_service_status&lt;/code&gt; — one call that answers "is this thing healthy?" by combining service detail, latest deploy, and recent error logs. This is the tool I use most.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;render_recent_logs&lt;/code&gt; — the log query you actually want, without constructing a filter object by hand.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the split I'd defend generally: &lt;strong&gt;generate the surface, hand-write the intent.&lt;/strong&gt; The spec knows what the API can do. Only you know what people are trying to accomplish with it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keeping up with an API you don't control
&lt;/h2&gt;

&lt;p&gt;A GitHub Action runs weekly. It fetches Render's current OpenAPI document, regenerates the catalogue, and diffs the result against what's committed.&lt;/p&gt;

&lt;p&gt;If the tool set is unchanged, it exits quietly. No noise. If something changed, it opens a PR with the diff and a summary — and flags whether the change is breaking, meaning removed operations or tightened constraints, versus additive.&lt;/p&gt;

&lt;p&gt;Small detail with a lesson in it: Render's OpenAPI document isn't served from a stable URL. The documented &lt;code&gt;.json&lt;/code&gt; and &lt;code&gt;.yaml&lt;/code&gt; endpoints 404. So the fetcher extracts the spec from the docs HTML and validates the extracted result before handing it to the generator, because scraping something structural without validating it is just a slower way to be wrong.&lt;/p&gt;

&lt;p&gt;If you're building anything similar against an API whose spec isn't formally published: assume the retrieval path will break, and make it fail loudly when it does.&lt;/p&gt;

&lt;h2&gt;
  
  
  The 2026-07-28 rewrite
&lt;/h2&gt;

&lt;p&gt;MCP's &lt;a href="https://modelcontextprotocol.io/" rel="noopener noreferrer"&gt;2026-07-28 revision&lt;/a&gt; turned the protocol's core stateless — request/response, no session handshake, no long-lived connection. Great for anyone deploying to serverless. For a stdio server like mine, the migration was smaller than expected but forced one real design decision.&lt;/p&gt;

&lt;p&gt;Under the new model, list endpoints don't vary per connection. &lt;code&gt;tools/list&lt;/code&gt; responses carry cache hints — a TTL and a scope — so clients can treat the tool list as a cacheable resource rather than re-fetching it every turn. Mine ships a 5-minute TTL.&lt;/p&gt;

&lt;p&gt;That's fundamentally incompatible with a tool list that mutates mid-session. The earlier design let the model call &lt;code&gt;render_toolsets&lt;/code&gt; to &lt;em&gt;enable&lt;/em&gt; a toolset at runtime, emitting &lt;code&gt;notifications/tools/list_changed&lt;/code&gt; so new tools appeared immediately. Clever, and now wrong: you cannot advertise a list as cacheable and then change it underneath the cache.&lt;/p&gt;

&lt;p&gt;So the enabled set became immutable for the process lifetime, and &lt;code&gt;render_toolsets&lt;/code&gt; was demoted to reporting — it tells you which toolsets exist and which are on, and tells you to change the env var and restart. Less magical, correct, and honestly easier to reason about. The server declares no &lt;code&gt;listChanged&lt;/code&gt; capability at all, and a test asserts that.&lt;/p&gt;

&lt;p&gt;The general lesson: caching semantics and runtime mutability are the same design decision wearing two hats. Pick one.&lt;/p&gt;

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



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npx render-useful-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It needs &lt;code&gt;RENDER_API_KEY&lt;/code&gt;. It's on npm and in the MCP Registry, published with OIDC trusted publishing and provenance attestation — no long-lived npm token exists to steal. There's a Claude Code plugin in the repo. No telemetry, no analytics, no backend; the process contacts exactly one host, and your key is never written to disk and is redacted from log output.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Positioning, honestly:&lt;/strong&gt; Render maintains an &lt;a href="https://render.com/docs/mcp-server" rel="noopener noreferrer"&gt;official MCP server&lt;/a&gt;, hosted, with OAuth, and if it covers what you need you should use it. Their docs are upfront about the boundaries — creation for a subset of service types, and for existing resources only deploys and environment variables. Mine covers the operational surface outside that: scaling, disks, blueprints, environment groups, custom domains, webhooks, maintenance, workflows. I'm not affiliated with Render.&lt;/p&gt;

&lt;p&gt;MIT. Issues and PRs welcome, particularly from anyone who has fought the tool-count problem and solved it differently — I don't think static toolsets are the last word on it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/LuSrodri/render-useful-mcp" rel="noopener noreferrer"&gt;github.com/LuSrodri/render-useful-mcp&lt;/a&gt;&lt;/p&gt;

</description>
      <category>mcp</category>
      <category>ai</category>
      <category>typescript</category>
      <category>devops</category>
    </item>
    <item>
      <title>How I Rebuilt Educando.app's Core: Tavily + GPT-5.4 nano Search Pipeline</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Sun, 19 Apr 2026 02:22:50 +0000</pubDate>
      <link>https://dev.to/lusrodri/how-i-rebuilt-educandoapps-core-tavily-gpt-54-nano-search-pipeline-28g6</link>
      <guid>https://dev.to/lusrodri/how-i-rebuilt-educandoapps-core-tavily-gpt-54-nano-search-pipeline-28g6</guid>
      <description>&lt;p&gt;After a security incident forced Educando.app offline, I took the opportunity to rethink the entire product. The original version was a Gemini-powered activity generator for Brazilian teachers. The new version is an intelligent content repository with a semantic search pipeline. This post is about the technical decisions behind that rebuild.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Generator Had to Go
&lt;/h2&gt;

&lt;p&gt;The original architecture was simple: user submits a prompt → Next.js API route → Gemini Flash → response streamed to the UI. It worked, but it had two structural problems.&lt;/p&gt;

&lt;p&gt;First, a compromised API key triggered unauthorized generation at scale. The key was client-accessible during an early dev iteration and never fully locked down server-side. Lesson: even keys that "should" be server-only need to be audited regularly — especially when Vercel environment variable scoping can be misconfigured silently.&lt;/p&gt;

&lt;p&gt;Second, the value was never in real-time generation. Teachers don't want to prompt an LLM. They want to find a good activity fast, trust it, and use it. A repository with good search solves that better than a generator.&lt;/p&gt;

&lt;h2&gt;
  
  
  The New Search Pipeline
&lt;/h2&gt;

&lt;p&gt;The core is a three-layer pipeline:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User query → Tavily search → GPT-5.4 nano evaluation → ranked JSON → UI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Layer 1: Tavily&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Tavily handles the actual search. It returns structured results with titles, descriptions, and URLs. The query goes in raw — no preprocessing — and Tavily does the semantic heavy lifting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: GPT-5.4 nano evaluation&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Each result batch gets passed to GPT-5.4 nano with a structured prompt asking it to score and rank the results as JSON. The model evaluates relevance, content quality, and match to the Brazilian curriculum (BNCC codes). The prompt enforces strict JSON output — no preamble, no markdown fences — so the response can be parsed directly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;evaluationPrompt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`
You are evaluating educational activity search results for Brazilian teachers.
Score each result from 0-10 based on: relevance, pedagogical quality, BNCC alignment.
Return ONLY a JSON array: [{ "index": 0, "score": 8, "reason": "..." }, ...]
Results: &lt;/span&gt;&lt;span class="p"&gt;${&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;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;searchResults&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;
Query: "&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userQuery&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"
`&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GPT-5.4 nano is the right model here — it's fast, cheap, and the task is classification/ranking, not generation. No need for a larger model.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: Supabase cache&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Results are cached in Supabase keyed by a normalized query hash. Cache hit = skip Tavily + GPT entirely. This keeps costs predictable and latency low for repeated queries.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;queryHash&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;createHash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;md5&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;normalizedQuery&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cached&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;supabase&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;search_cache&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;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;results&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;eq&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;query_hash&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;queryHash&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;single&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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;cached&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;results&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A separate &lt;code&gt;search_telemetry&lt;/code&gt; table logs every query and click for future ranking improvements.&lt;/p&gt;

&lt;h2&gt;
  
  
  Content Schema
&lt;/h2&gt;

&lt;p&gt;The old schema was flat — just the generated text blob. The new schema is structured:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;create&lt;/span&gt; &lt;span class="k"&gt;table&lt;/span&gt; &lt;span class="n"&gt;activities&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;id&lt;/span&gt; &lt;span class="n"&gt;uuid&lt;/span&gt; &lt;span class="k"&gt;primary&lt;/span&gt; &lt;span class="k"&gt;key&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;gen_random_uuid&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="n"&gt;title&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;theme&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt; &lt;span class="k"&gt;not&lt;/span&gt; &lt;span class="k"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;short_description&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;long_description&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;bncc_codes&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="k"&gt;type&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;image_url&lt;/span&gt; &lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;created_at&lt;/span&gt; &lt;span class="n"&gt;timestamptz&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="n"&gt;now&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;BNCC codes are the Brazilian national curriculum identifiers (e.g. &lt;code&gt;EF05LP01&lt;/code&gt;). Having them as a queryable array field lets the search layer filter by curriculum alignment without relying purely on semantic matching.&lt;/p&gt;

&lt;h2&gt;
  
  
  Image Processing with Replicate + Qwen
&lt;/h2&gt;

&lt;p&gt;Activity thumbnails go through a &lt;code&gt;qwen/qwen-image-2-pro&lt;/code&gt; pipeline on Replicate. The model handles resizing, quality normalization, and description extraction — the output feeds back into the activity metadata for better search indexing.&lt;/p&gt;

&lt;h2&gt;
  
  
  Abuse Prevention
&lt;/h2&gt;

&lt;p&gt;The original version had FingerprintJS + IP hashing. It was complex and brittle — VPN, browser switch, new tab, and the identity broke.&lt;/p&gt;

&lt;p&gt;The new version uses &lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; at the edge combined with server-side rate limiting per IP. Turnstile runs before the request hits the Next.js API route, so abuse is stopped at the CDN level. Simpler, more reliable, and no client-side fingerprinting library to maintain.&lt;/p&gt;

&lt;p&gt;No auth in this version. The Saved Activities feature is scoped to v2, which will include Supabase Auth + Google SSO. There was no reason to ship an auth system without a paywall to motivate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js + TypeScript&lt;/strong&gt; — unchanged&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supabase&lt;/strong&gt; — database, cache, telemetry&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Tavily&lt;/strong&gt; — search&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GPT-5.4 nano&lt;/strong&gt; — result evaluation and ranking&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Replicate (Qwen)&lt;/strong&gt; — image processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cloudflare Turnstile&lt;/strong&gt; — abuse prevention&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vercel&lt;/strong&gt; — deployment&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What's Next
&lt;/h2&gt;

&lt;p&gt;The premium tier — download/print behind a paywall, Supabase Auth, Google SSO — is designed but not yet shipped. The repository needs enough content before a paywall makes sense.&lt;/p&gt;

&lt;p&gt;The security incident was painful. Rebuilding was the right call.&lt;/p&gt;

</description>
      <category>nextjs</category>
      <category>typescript</category>
      <category>supabase</category>
      <category>ai</category>
    </item>
    <item>
      <title>Building an image processing pipeline with the Watermark Removal API</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Sat, 18 Apr 2026 18:26:00 +0000</pubDate>
      <link>https://dev.to/lusrodri/building-an-image-processing-pipeline-with-the-watermark-removal-api-2pl5</link>
      <guid>https://dev.to/lusrodri/building-an-image-processing-pipeline-with-the-watermark-removal-api-2pl5</guid>
      <description>&lt;p&gt;If you work with product photos, real estate listings, or any user-generated content at scale, chances are you've dealt with watermarked images. Removing them manually doesn't scale. This post walks through building a simple but production-ready image processing pipeline using the &lt;a href="https://rapidapi.com/lusrodri/api/goodbye-watermark-api" rel="noopener noreferrer"&gt;Goodbye Watermark API&lt;/a&gt; — available on RapidAPI.&lt;/p&gt;

&lt;p&gt;The API accepts an image (URL or base64), runs it through an AI inpainting model, and returns a clean PNG. No UI, no manual steps — just HTTP.&lt;/p&gt;




&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A pipeline that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Takes a list of image URLs&lt;/li&gt;
&lt;li&gt;Sends each one to the Watermark Removal API&lt;/li&gt;
&lt;li&gt;Saves the cleaned images to disk&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Simple, composable, and easy to drop into any existing workflow.&lt;/p&gt;




&lt;h2&gt;
  
  
  cURL
&lt;/h2&gt;

&lt;p&gt;Before writing any code, let's confirm the API works:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-rapidapi-key: YOUR_RAPIDAPI_KEY"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"x-rapidapi-host: goodbye-watermark-api.p.rapidapi.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s1"&gt;'{ "image": "https://example.com/photo-with-watermark.jpg" }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll get back:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAA..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"format"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"png"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;image&lt;/code&gt; field is a data URI. From here, you can decode the base64 and write it to disk.&lt;/p&gt;




&lt;h2&gt;
  
  
  Node.js pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;fetch&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;node-fetch&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;RAPIDAPI_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;RAPIDAPI_KEY&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;images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/product-1.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/product-2.jpg&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://example.com/product-3.jpg&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;removeWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark&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="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-rapidapi-key&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-rapidapi-host&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;goodbye-watermark-api.p.rapidapi.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/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="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&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;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&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;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;success&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&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;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;image&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// data URI&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;saveDataUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dataUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;filename&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;base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;dataUri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^data:image&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;png;base64,/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;writeFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Buffer&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="nx"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;base64&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="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;runPipeline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&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;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Processing image &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;removeWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;images&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
      &lt;span class="nf"&gt;saveDataUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;`output-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✓ Saved output-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`✗ Failed: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;runPipeline&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Python pipeline
&lt;/h2&gt;



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

&lt;span class="n"&gt;RAPIDAPI_KEY&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;images&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;https://example.com/product-1.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/product-2.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://example.com/product-3.jpg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;remove_watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-rapidapi-key&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;RAPIDAPI_KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;x-rapidapi-host&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;goodbye-watermark-api.p.rapidapi.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Content-Type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;application/json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_url&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;success&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Unknown error&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;image&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="c1"&gt;# data URI
&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;save_data_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data_uri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;base64_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data_uri&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;,&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nf"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wb&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;b64decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base64_data&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Processing image &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;...&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;remove_watermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;output-&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;.png&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
        &lt;span class="nf"&gt;save_data_uri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✓ Saved &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;filename&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;✗ Failed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  PHP pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?php&lt;/span&gt;

&lt;span class="nv"&gt;$apiKey&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getenv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"RAPIDAPI_KEY"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$images&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"https://example.com/product-1.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"https://example.com/product-2.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"https://example.com/product-3.jpg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;removeWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$imageUrl&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$ch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;curl_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"https://goodbye-watermark-api.p.rapidapi.com/api/v1/remove-watermark"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nb"&gt;curl_setopt_array&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="no"&gt;CURLOPT_RETURNTRANSFER&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;CURLOPT_POST&lt;/span&gt;           &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;CURLOPT_TIMEOUT&lt;/span&gt;        &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="no"&gt;CURLOPT_HTTPHEADER&lt;/span&gt;     &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="s2"&gt;"x-rapidapi-key: &lt;/span&gt;&lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"x-rapidapi-host: goodbye-watermark-api.p.rapidapi.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="no"&gt;CURLOPT_POSTFIELDS&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$imageUrl&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
    &lt;span class="p"&gt;]);&lt;/span&gt;

    &lt;span class="nv"&gt;$response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;curl_exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&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="nb"&gt;curl_close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$ch&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="s2"&gt;"Unknown error"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nv"&gt;$response&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt; &lt;span class="c1"&gt;// data URI&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt; &lt;span class="n"&gt;saveDataUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$dataUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;string&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kt"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$base64&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;explode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;","&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$dataUri&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="nb"&gt;file_put_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;base64_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$base64&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$images&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nv"&gt;$num&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$i&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Processing image &lt;/span&gt;&lt;span class="nv"&gt;$num&lt;/span&gt;&lt;span class="s2"&gt;/"&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nb"&gt;count&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$images&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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="nv"&gt;$result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;removeWatermark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$apiKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="nv"&gt;$filename&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"output-&lt;/span&gt;&lt;span class="nv"&gt;$num&lt;/span&gt;&lt;span class="s2"&gt;.png"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="nf"&gt;saveDataUri&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✓ Saved &lt;/span&gt;&lt;span class="nv"&gt;$filename&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Exception&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"✗ Failed: "&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="nv"&gt;$e&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="nf"&gt;getMessage&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="mf"&gt;.&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Error handling tips
&lt;/h2&gt;

&lt;p&gt;The API returns structured errors on non-2xx responses:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"success"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"human-readable description"&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;A few things worth handling in production:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;429&lt;/strong&gt; — you've hit your plan's rate limit. Add a delay between requests or upgrade your plan.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;500&lt;/strong&gt; — upstream inference failure. Safe to retry with exponential backoff.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;503&lt;/strong&gt; — the API is temporarily down. Hit &lt;code&gt;/api/v1/ping&lt;/code&gt; to check status before retrying.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;This pipeline covers the basics. From here you can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Process local files&lt;/strong&gt; — read from disk and send as base64 instead of URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fan out in parallel&lt;/strong&gt; — use &lt;code&gt;Promise.all&lt;/code&gt; in Node.js or &lt;code&gt;ThreadPoolExecutor&lt;/code&gt; in Python for faster batch processing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upload to CDN&lt;/strong&gt; — pipe the output directly to S3, Cloudflare R2, or any storage provider instead of saving to disk&lt;/li&gt;
&lt;/ul&gt;




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

&lt;p&gt;The Goodbye Watermark API is live on RapidAPI with a pay-per-use model — no subscription lock-in.&lt;/p&gt;

&lt;p&gt;👉 &lt;a href="https://rapidapi.com/lusrodri/api/goodbye-watermark-api" rel="noopener noreferrer"&gt;goodbye-watermark-api on RapidAPI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;See more here: &lt;a href="https://goodbyewatermark.com/" rel="noopener noreferrer"&gt;Goodbye Watermark&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

</description>
      <category>node</category>
      <category>python</category>
      <category>api</category>
      <category>webdev</category>
    </item>
    <item>
      <title>How to Use Replicate the Right Way in Your Next.js App (And Ship a Real Product With It)</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 06 Apr 2026 23:52:21 +0000</pubDate>
      <link>https://dev.to/lusrodri/how-to-use-replicate-the-right-way-in-your-nextjs-app-and-ship-a-real-product-with-it-38dg</link>
      <guid>https://dev.to/lusrodri/how-to-use-replicate-the-right-way-in-your-nextjs-app-and-ship-a-real-product-with-it-38dg</guid>
      <description>&lt;p&gt;Most tutorials show you how to &lt;em&gt;call&lt;/em&gt; Replicate. Few show you how to &lt;em&gt;use it well&lt;/em&gt; inside a real production app. This article covers the mistakes I made and the patterns that actually work — using &lt;a href="https://goodbyewatermark.com" rel="noopener noreferrer"&gt;Goodbye Watermark&lt;/a&gt; as a real-world case study.&lt;/p&gt;




&lt;h2&gt;
  
  
  What Is Replicate, Really?
&lt;/h2&gt;

&lt;p&gt;Replicate is a cloud API that lets you run AI models — image generation, video, audio, vision — without owning a single GPU. You send an HTTP request, a model runs on their infrastructure, and you get the result back.&lt;/p&gt;

&lt;p&gt;The business model is pay-per-prediction: you're charged for the time the model actually runs, not idle time. That means cold boots don't affect your cost — only your latency.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Understand the Prediction Lifecycle Before Writing Any Code
&lt;/h2&gt;

&lt;p&gt;Every Replicate call creates a &lt;strong&gt;prediction&lt;/strong&gt; — an object with a lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;starting → processing → succeeded (or failed / canceled)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;starting&lt;/code&gt;: model is booting (cold start happens here)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;processing&lt;/code&gt;: &lt;code&gt;predict()&lt;/code&gt; is actively running&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;succeeded&lt;/code&gt;: output is ready — but &lt;strong&gt;files are deleted after 1 hour&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point is critical. If you're not saving outputs immediately, you'll lose them. More on that below.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Polling vs. Webhooks: Choose the Right Strategy
&lt;/h2&gt;

&lt;p&gt;Replicate gives you three ways to handle async predictions:&lt;/p&gt;

&lt;h3&gt;
  
  
  Polling (simplest, fine for most apps)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Create the prediction&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prediction&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predictions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner/model-name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// Poll until done&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="nx"&gt;result&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&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;Works well for short-lived predictions (under ~15s). Simple to implement. The tradeoff: you're making repeated requests even when nothing has changed.&lt;/p&gt;

&lt;h3&gt;
  
  
  Webhooks (better for longer or background tasks)
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;prediction&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predictions&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="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner/model-name&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;imageUrl&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;webhook&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;VERCEL_URL&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/api/webhooks`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;webhook_events_filter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;completed&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// only fire when done&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Replicate POSTs to your URL when the prediction finishes. No polling loop. If there are network issues, they retry automatically.&lt;/p&gt;

&lt;p&gt;Use webhooks when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Predictions take more than ~10-15 seconds&lt;/li&gt;
&lt;li&gt;You want to persist results to a database&lt;/li&gt;
&lt;li&gt;You're building background processing flows&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Tip:&lt;/strong&gt; Add query params to your webhook URL to carry context:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;https://yourapp.com/api/webhooks?userId=abc123&amp;amp;predictionType=watermark
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  When to use each
&lt;/h3&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Scenario&lt;/th&gt;
&lt;th&gt;Use&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Fast model, UX waits for result&lt;/td&gt;
&lt;td&gt;Polling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Slow model, fire and notify&lt;/td&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Background job, store to DB&lt;/td&gt;
&lt;td&gt;Webhooks&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Quick prototype&lt;/td&gt;
&lt;td&gt;Polling&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  3. Cold Starts Are Real — Here's How to Handle Them
&lt;/h2&gt;

&lt;p&gt;When a model hasn't been used recently, it needs to "boot up." This can add several seconds of latency on the first request after idle time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For casual traffic:&lt;/strong&gt; Cold boots are fine. You only pay for actual compute, not boot time.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For production apps with consistent traffic:&lt;/strong&gt; Use a &lt;strong&gt;Deployment&lt;/strong&gt; with &lt;code&gt;minInstances: 1&lt;/code&gt;:&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;// Via the Replicate dashboard or API:&lt;/span&gt;
&lt;span class="c1"&gt;// Create a deployment for your model with min_instances = 1&lt;/span&gt;
&lt;span class="c1"&gt;// This keeps the model warm 24/7&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This costs more (you're paying to keep the instance warm) but eliminates cold start latency entirely.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;For Goodbye Watermark&lt;/strong&gt;, I don't use a deployment because the traffic is spread across the day and a few seconds of latency on first boot is acceptable. But if you're building something with strict SLA requirements — use deployments.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Save Outputs Immediately — They Expire in 1 Hour
&lt;/h2&gt;

&lt;p&gt;This is the gotcha that trips up everyone:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Input and output files are automatically deleted after 1 hour for any predictions created through the API.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If your app doesn't save the result right after &lt;code&gt;succeeded&lt;/code&gt;, it's gone. Your options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A: Stream back to the client immediately&lt;/strong&gt;&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;// Next.js API route&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;GET&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="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;output&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner/model&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="nx"&gt;input&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="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// stream back to client&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Option B: Save to your own storage (Supabase Storage, S3, etc.)&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;output&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;owner/model&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="nx"&gt;input&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;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;output&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// download from Replicate&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;buffer&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;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;arrayBuffer&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;supabase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;storage&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;outputs&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;upload&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;.png`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For Goodbye Watermark, I stream the result directly back to the client. The user downloads it immediately. No storage needed, no expiry problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  5. Next.js Config: Don't Forget This
&lt;/h2&gt;

&lt;p&gt;If you're displaying output images from Replicate in a Next.js &lt;code&gt;&amp;lt;Image&amp;gt;&lt;/code&gt; component, add this to your config or you'll get a domain error:&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;// next.config.ts&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;nextConfig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;images&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;remotePatterns&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="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;replicate.delivery&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="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;hostname&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;*.replicate.delivery&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="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small thing, but it will bite you in production.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Error Handling That Doesn't Suck
&lt;/h2&gt;

&lt;p&gt;Real-world Replicate usage needs to handle:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network timeouts&lt;/li&gt;
&lt;li&gt;Model errors (bad input format, unsupported file type)&lt;/li&gt;
&lt;li&gt;Rate limits (429)&lt;/li&gt;
&lt;li&gt;Prediction timeouts (30 min hard cap)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;try&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;prediction&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predictions&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="p"&gt;...&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt;&lt;span class="p"&gt;)&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;prediction&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;error&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// poll with timeout safety&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;prediction&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;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 60s max wait&lt;/span&gt;

  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;succeeded&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Prediction timed out&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;504&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="nx"&gt;result&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;replicate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;failed&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Model failed&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;output&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output&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="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&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;NextResponse&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Unexpected error&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="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set your own deadline. Replicate's hard limit is 30 minutes, but your users don't want to wait more than ~60 seconds for most tasks.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Rate Limits to Know
&lt;/h2&gt;

&lt;p&gt;From Replicate's docs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Create prediction:&lt;/strong&gt; 600 requests/minute&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;All other endpoints:&lt;/strong&gt; 3000 requests/minute&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For most indie apps, you won't hit these. If you do, they return a &lt;code&gt;429&lt;/code&gt; — build retry logic with exponential backoff.&lt;/p&gt;




&lt;h2&gt;
  
  
  8. Choosing the Right Model
&lt;/h2&gt;

&lt;p&gt;Replicate hosts thousands of models. Two categories matter:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Official models&lt;/strong&gt; — maintained by Replicate, always warm, stable API, predictable per-output pricing. Best for production use.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Community models&lt;/strong&gt; — more variety, charged by compute time, may have cold starts, API can change between versions.&lt;/p&gt;

&lt;p&gt;For Goodbye Watermark, I use the &lt;strong&gt;Qwen model&lt;/strong&gt; for watermark removal. The choice came down to output quality and how well it handled semi-transparent watermarks — which are significantly harder than solid text watermarks. Testing a few models on realistic samples before committing to one is worth the extra hour.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real-World Case Study: Goodbye Watermark
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://goodbyewatermark.com" rel="noopener noreferrer"&gt;Goodbye Watermark&lt;/a&gt; is an AI watermark removal tool built with Next.js + Replicate + Vercel. The full stack is:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Frontend:&lt;/strong&gt; Next.js + Tailwind CSS&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI:&lt;/strong&gt; Replicate (Qwen model)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hosting:&lt;/strong&gt; Vercel&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Payments:&lt;/strong&gt; Stripe (two credit tiers)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The entire MVP was built in ~1 hour. The hardest part wasn't the UI — it was getting consistent output quality from the model across different watermark types.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Current results:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;~150 weekly organic users&lt;/li&gt;
&lt;li&gt;$0 paid acquisition&lt;/li&gt;
&lt;li&gt;Zero infrastructure management&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Replicate made the difference. Running my own GPU inference would have added weeks of setup and ongoing ops overhead. Instead, I spent that time on the UX and monetization.&lt;/p&gt;




&lt;h2&gt;
  
  
  TL;DR — The Patterns That Matter
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Understand the prediction lifecycle&lt;/strong&gt; — especially the 1-hour file expiry&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use polling for short tasks, webhooks for long/background ones&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Use Deployments&lt;/strong&gt; if cold start latency is a problem for your UX&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save or stream outputs immediately&lt;/strong&gt; after &lt;code&gt;succeeded&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Add replicate.delivery to your Next.js image domains&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set your own deadline&lt;/strong&gt; — don't wait 30 minutes for a user-facing request&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Test multiple models&lt;/strong&gt; before committing — quality varies significantly&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Replicate is genuinely one of the best tools for indie developers shipping AI products fast. Use it well and you can build something real in a weekend.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built something with Replicate? Drop it in the comments — always curious to see what people are shipping.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>typescript</category>
      <category>nextjs</category>
      <category>ai</category>
    </item>
    <item>
      <title>I built a free AI tool to remove watermarks from images — here's how it works</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Tue, 24 Mar 2026 18:25:53 +0000</pubDate>
      <link>https://dev.to/lusrodri/i-built-a-free-ai-tool-to-remove-watermarks-from-images-heres-how-it-works-3e23</link>
      <guid>https://dev.to/lusrodri/i-built-a-free-ai-tool-to-remove-watermarks-from-images-heres-how-it-works-3e23</guid>
      <description>&lt;p&gt;I got tired of watermarks ruining otherwise perfect images, so I built &lt;br&gt;
&lt;strong&gt;&lt;a href="https://goodbyewatermark.com" rel="noopener noreferrer"&gt;Goodbye Watermark&lt;/a&gt;&lt;/strong&gt; — a free AI-powered tool &lt;br&gt;
that removes watermarks from images in seconds.&lt;/p&gt;

&lt;h2&gt;
  
  
  How it works
&lt;/h2&gt;

&lt;p&gt;Upload your image, and the AI automatically detects and removes the watermark, &lt;br&gt;
reconstructing the background behind it. No manual selection, no Photoshop skills needed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech behind it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; for the frontend&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI inpainting model&lt;/strong&gt; to detect and fill the watermark region&lt;/li&gt;
&lt;li&gt;Fully client-friendly: just upload and download&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Why I built it
&lt;/h2&gt;

&lt;p&gt;Most watermark removal tools are either paid, slow, or require desktop software. &lt;br&gt;
I wanted something dead simple that runs in the browser with no friction.&lt;/p&gt;

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

&lt;p&gt;👉 &lt;a href="https://goodbyewatermark.com" rel="noopener noreferrer"&gt;goodbyewatermark.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Would love feedback from the community — especially if you hit edge cases &lt;br&gt;
where it struggles. Drop a comment below!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>ai</category>
      <category>opensource</category>
    </item>
    <item>
      <title>Why I Built "I Hate Background" — a Privacy-First Background Removal Service</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 15 Sep 2025 17:17:56 +0000</pubDate>
      <link>https://dev.to/lusrodri/why-i-built-i-hate-background-a-privacy-first-background-removal-service-1f6a</link>
      <guid>https://dev.to/lusrodri/why-i-built-i-hate-background-a-privacy-first-background-removal-service-1f6a</guid>
      <description>&lt;p&gt;I always liked the idea of removing an image background in one click.&lt;br&gt;
But I felt something was missing in most of the well-known services:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Privacy – many tools upload your image to their servers.&lt;/li&gt;
&lt;li&gt;Paywall – most limit free images or force subscriptions.&lt;/li&gt;
&lt;li&gt;Closed tech – no way to build something on top of it.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So I built &lt;a href="https://ihatebackground.com/en" rel="noopener noreferrer"&gt;I Hate Background&lt;/a&gt; – a privacy-first, AI-first tool that runs directly in your browser.&lt;br&gt;
Nothing is uploaded. Your image never leaves your computer.&lt;br&gt;
And it’s totally free to use on the website.&lt;/p&gt;

&lt;p&gt;If you’re a developer and want to integrate background removal in your product, there’s also an API (paid, powered by NVIDIA GPUs) for production-level needs: &lt;a href="https://rapidapi.com/LuSrodri/api/i-hate-background-api" rel="noopener noreferrer"&gt;I Hate Background – Rapid API&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Why I’m posting here
&lt;/h3&gt;

&lt;p&gt;I’m looking for honest feedback from developers and designers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the site feel fast and simple?&lt;/li&gt;
&lt;li&gt;Is the UX clear?&lt;/li&gt;
&lt;li&gt;Would you use it for quick edits or in your workflow?&lt;/li&gt;
&lt;li&gt;What could be improved (performance, docs, features)?&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Want to know more about my project? Starred my public repo: &lt;a href="https://github.com/LuSrodri/canva-copy" rel="noopener noreferrer"&gt;I Hate Background Repo&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>browser</category>
      <category>programming</category>
    </item>
    <item>
      <title>The Alternative to remove.bg API</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 01 Sep 2025 00:35:52 +0000</pubDate>
      <link>https://dev.to/lusrodri/the-alternative-to-removebg-api-5a7b</link>
      <guid>https://dev.to/lusrodri/the-alternative-to-removebg-api-5a7b</guid>
      <description>&lt;p&gt;If you’ve ever needed to remove image backgrounds programmatically, chances are you’ve heard of &lt;strong&gt;remove.bg&lt;/strong&gt;. It’s one of the most popular tools out there — but not the only one.&lt;/p&gt;

&lt;p&gt;For developers looking for &lt;strong&gt;a fast, reliable, and more flexible option&lt;/strong&gt;, let me introduce you to &lt;strong&gt;I Hate Background API&lt;/strong&gt;: an AI-powered background removal service designed with scalability and performance in mind.&lt;/p&gt;

&lt;h3&gt;
  
  
  🚀 API Overview
&lt;/h3&gt;

&lt;p&gt;I Hate Background API delivers fast and reliable background removal for images of any size or type.&lt;br&gt;
Powered by &lt;strong&gt;cutting-edge deep learning models&lt;/strong&gt; and accelerated on &lt;strong&gt;NVIDIA L4 GPUs&lt;/strong&gt;, it produces high-quality results in just a few seconds.&lt;/p&gt;
&lt;h3&gt;
  
  
  ⚡ Performance
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;⏱️ Average processing time: ~4s for 1024×1024 images&lt;/li&gt;
&lt;li&gt;🌍 Works with any image size or type (products, portraits, graphics, etc.)&lt;/li&gt;
&lt;li&gt;📈 Optimized for scalability and real-time applications&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🔑 Key Features
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;AI precision&lt;/strong&gt; → accurate edge detection &amp;amp; background separation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;High-quality output&lt;/strong&gt; → keeps fine details sharp, even with complex objects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flexible input&lt;/strong&gt; → supports JPEG, PNG, WEBP, and more&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Production-ready&lt;/strong&gt; → scalable API designed for demanding workloads&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  🛠️ Use Cases
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;E-commerce&lt;/strong&gt; → clean product photos with transparent backgrounds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Design &amp;amp; Creative apps&lt;/strong&gt; → enable users to edit/replace backgrounds instantly&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Media &amp;amp; Marketing&lt;/strong&gt; → automate image pipelines at scale&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation workflows&lt;/strong&gt; → integrate background removal into existing tools&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  📦 Example Request
&lt;/h3&gt;

&lt;p&gt;Here’s how simple it is to use the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl --request POST 
    --url https://i-hate-background-api.p.rapidapi.com/remove-background 
    --header 'Content-Type: multipart/form-data' 
    --header 'x-rapidapi-host: i-hate-background-api.p.rapidapi.com' 
    --header 'x-rapidapi-key: ****' 
    --form file=tree.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Within seconds, you’ll get a PNG image with the background removed.&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;Before&lt;/em&gt;
&lt;/h4&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.amazonaws.com%2Fuploads%2Farticles%2Fz7d3a00jga1tcc2aimwt.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.amazonaws.com%2Fuploads%2Farticles%2Fz7d3a00jga1tcc2aimwt.png" alt="A beautiful tree" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h4&gt;
  
  
  &lt;em&gt;After&lt;/em&gt;
&lt;/h4&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.amazonaws.com%2Fuploads%2Farticles%2F6phkugmvlbu7r2hfhdhd.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.amazonaws.com%2Fuploads%2Farticles%2F6phkugmvlbu7r2hfhdhd.png" alt="A beautiful tree without background" width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  🤔 Why use this API?
&lt;/h3&gt;

&lt;p&gt;Whether you’re building an online store, a photo editing tool, or an automation pipeline, &lt;strong&gt;I Hate Background API&lt;/strong&gt; makes it effortless to integrate professional-grade background removal.&lt;/p&gt;

&lt;p&gt;With &lt;strong&gt;modern GPU acceleration&lt;/strong&gt; and a robust backend, the API is built to handle production workloads while keeping response times low.&lt;/p&gt;

&lt;h3&gt;
  
  
  ✅ Try it out
&lt;/h3&gt;

&lt;p&gt;👉 You can check the docs and start experimenting here: &lt;a href="https://rapidapi.com/LuSrodri/api/i-hate-background-api" rel="noopener noreferrer"&gt;I Hate Background API&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;Background removal made simple.&lt;br&gt;
Fast, accurate, and developer-friendly.&lt;/p&gt;

&lt;p&gt;See more in &lt;a href="https://ihatebackground.com/en" rel="noopener noreferrer"&gt;I Hate Background Official Website&lt;/a&gt;&lt;/p&gt;

</description>
      <category>api</category>
    </item>
    <item>
      <title>I Hate Background!</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Mon, 18 Aug 2025 14:14:15 +0000</pubDate>
      <link>https://dev.to/lusrodri/i-hate-background-35h0</link>
      <guid>https://dev.to/lusrodri/i-hate-background-35h0</guid>
      <description>&lt;h2&gt;
  
  
  I Hate Background! 🚫🖼️
&lt;/h2&gt;

&lt;p&gt;Some time ago, I started exploring &lt;strong&gt;AI models&lt;/strong&gt; for image processing. One of my biggest frustrations was always: &lt;em&gt;"why is it so complicated to remove an image background in a simple and free way?"&lt;/em&gt;  &lt;/p&gt;

&lt;p&gt;That’s how &lt;strong&gt;I Hate Background&lt;/strong&gt; was born → &lt;a href="https://ihatebackground.com/en" rel="noopener noreferrer"&gt;ihatebackground.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;✨ With this tool, you can remove backgrounds from multiple images at once — just select everything and you’re done!&lt;/p&gt;

&lt;h2&gt;
  
  
  How does it work?
&lt;/h2&gt;

&lt;p&gt;The system uses &lt;strong&gt;advanced AI models&lt;/strong&gt; for image segmentation, trained on thousands of examples to identify what is &lt;em&gt;background&lt;/em&gt; and what is the &lt;em&gt;main object&lt;/em&gt;.&lt;br&gt;&lt;br&gt;
Processing happens in the cloud, hassle-free, and in just a few seconds you get the result ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Features
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🔎 &lt;strong&gt;Batch background removal&lt;/strong&gt; (no need to process one by one);&lt;/li&gt;
&lt;li&gt;⚡ &lt;strong&gt;Simplicity&lt;/strong&gt;: upload → process → save;&lt;/li&gt;
&lt;li&gt;🌱 Open-friendly project, built to help designers, content creators, and curious minds;&lt;/li&gt;
&lt;li&gt;💡 Built with &lt;strong&gt;Transformers.js&lt;/strong&gt; and &lt;strong&gt;Hugging Face&lt;/strong&gt; models.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What’s next?
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;I Hate Background&lt;/strong&gt; started as a personal experiment, but now I’m expanding it into something truly useful for creators, freelancers, and companies dealing with images every day.&lt;/p&gt;

&lt;p&gt;The roadmap includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An accessible &lt;strong&gt;API&lt;/strong&gt; for integrations.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔗 Try it for free: &lt;a href="https://ihatebackground.com/en" rel="noopener noreferrer"&gt;ihatebackground.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I’d love to hear feedback from the community!&lt;br&gt;&lt;br&gt;
👉 What do you miss the most in current &lt;strong&gt;background removal tools&lt;/strong&gt;?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>hf</category>
      <category>startup</category>
      <category>webdev</category>
    </item>
    <item>
      <title>I built a background remover that runs entirely in your browser using Transformers.js</title>
      <dc:creator>Lucas Santos Rodrigues</dc:creator>
      <pubDate>Sun, 27 Jul 2025 23:26:05 +0000</pubDate>
      <link>https://dev.to/lusrodri/a-new-way-to-remove-backgrounds-images-ae8</link>
      <guid>https://dev.to/lusrodri/a-new-way-to-remove-backgrounds-images-ae8</guid>
      <description>&lt;p&gt;I've always wanted to run AI directly in the browser — no servers, no API keys, just local, fast, and private.&lt;/p&gt;

&lt;p&gt;Thanks to &lt;a href="https://huggingface.co/docs/transformers.js" rel="noopener noreferrer"&gt;Hugging Face's Transformers.js&lt;/a&gt;, this is now totally possible. And I decided to test it by building a tool that many developers and content creators eventually need: a background remover.&lt;/p&gt;

&lt;p&gt;It runs entirely in-browser, using Web Workers and modern JS — no sign-up, no data sent to a server.&lt;/p&gt;

&lt;h4&gt;
  
  
  ⚙️ How it works
&lt;/h4&gt;

&lt;p&gt;Transformers.js provides a simple pipeline for background removal. Here's all you need:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Initialize the pipeline
const segmenter = await pipeline('background-removal', 'briaai/RMBG-1.4', { dtype: 'fp32' });

// Process the image
const result = await segmenter(imageURL);

// Done ✅ Use the result however you want
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I chose the RMBG-1.4 model with fp32 to keep compatibility high across devices (especially CPU) while maintaining solid quality.&lt;/p&gt;

&lt;p&gt;I turned this into a real app you can try instantly: &lt;a href="https://semfundos.lusrodri.me/en" rel="noopener noreferrer"&gt;Background free&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Want to explore the code? Here’s the source: &lt;a href="https://github.com/LuSrodri/canva-copy" rel="noopener noreferrer"&gt;Look at my GitHub canva-copy repository&lt;/a&gt;.&lt;/p&gt;




&lt;p&gt;If you’re curious, I also posted about the process in the Hugging Face community: &lt;a href="https://discuss.huggingface.co/t/built-a-privacy-first-background-removal-tool-running-entirely-in-the-browser-with-transformers-js" rel="noopener noreferrer"&gt;Discuss Thread&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>webdev</category>
      <category>programming</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
