<?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: dong</title>
    <description>The latest articles on DEV Community by dong (@dong_45bfc981cf912828e).</description>
    <link>https://dev.to/dong_45bfc981cf912828e</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%2F4107515%2F82b30d64-aab3-4efe-a189-4f9c4498dad4.png</url>
      <title>DEV Community: dong</title>
      <link>https://dev.to/dong_45bfc981cf912828e</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/dong_45bfc981cf912828e"/>
    <language>en</language>
    <item>
      <title>Designing a Video-to-Video AI Workflow: Validate Inputs Before Spending Credits</title>
      <dc:creator>dong</dc:creator>
      <pubDate>Sun, 11 Oct 2026 02:34:18 +0000</pubDate>
      <link>https://dev.to/dong_45bfc981cf912828e/designing-a-video-to-video-ai-workflow-validate-inputs-before-spending-credits-3po1</link>
      <guid>https://dev.to/dong_45bfc981cf912828e/designing-a-video-to-video-ai-workflow-validate-inputs-before-spending-credits-3po1</guid>
      <description>&lt;p&gt;A video-to-video editor has a different starting point from a text-to-video generator: the user already has a performance they want to keep. The interface needs to make clear what should change and what the source footage is meant to guide.&lt;/p&gt;

&lt;p&gt;Our project, &lt;a href="https://aigenjutsu.org/" rel="noopener noreferrer"&gt;AI Genjutsu&lt;/a&gt;, focuses on character replacement using a source video and a reference image. This post shares a practical design checklist for that kind of workflow. It is not a model benchmark or a guarantee of frame-perfect preservation.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Separate the two inputs
&lt;/h2&gt;

&lt;p&gt;The source clip supplies the performance: movement, timing, framing, and camera behavior. The reference image supplies the intended character appearance. Treating them as separate inputs makes the task easier to explain than a single upload box labeled “media.”&lt;/p&gt;

&lt;p&gt;A useful interface gives each input its own label, preview, and validation message. If the reference image is missing, tell the user that directly instead of returning a generic generation error.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Validate media before requesting generation
&lt;/h2&gt;

&lt;p&gt;An accepted file extension does not tell you whether a video meets a model's requirements. Duration, dimensions, frame rate, codec, and file size can each matter.&lt;/p&gt;

&lt;p&gt;For developers, the useful distinction is between three states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Invalid:&lt;/strong&gt; the file cannot be used and needs a specific explanation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Needs preparation:&lt;/strong&gt; it can be adapted, but the user should understand what will change.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ready:&lt;/strong&gt; the prepared inputs satisfy the selected workflow's requirements.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Client-side checks provide quick feedback. The server should still validate inputs before dispatching a paid job; a browser check is not a trust boundary. If preparation changes media properties, validate the prepared output too.&lt;/p&gt;

&lt;p&gt;This is a suggested sequence, not an API specification:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;choose video and reference
  -&amp;gt; inspect media
  -&amp;gt; explain any required preparation
  -&amp;gt; validate prepared inputs
  -&amp;gt; show a generation quote
  -&amp;gt; request user confirmation
  -&amp;gt; submit the job
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  3. Make the cost boundary visible
&lt;/h2&gt;

&lt;p&gt;Selecting or preparing a file and submitting it for AI processing are different actions. A user should be able to tell which action spends credits.&lt;/p&gt;

&lt;p&gt;Show the quote after the relevant inputs and output options are known. If the user replaces the clip or changes output quality, refresh the quote before allowing submission. On the server, verify that the confirmed quote still matches the requested job.&lt;/p&gt;

&lt;p&gt;A disabled button should also explain what is missing: a reference image, a compatible video, or a current quote.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Give users a review checklist
&lt;/h2&gt;

&lt;p&gt;“Generation completed” means an output is available. It does not establish that the edit is visually good. For a character replacement, useful review questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Does the character remain recognizable throughout the clip?&lt;/li&gt;
&lt;li&gt;What happens when the face turns or leaves the frame?&lt;/li&gt;
&lt;li&gt;Are hands, clothing, and subject boundaries consistent?&lt;/li&gt;
&lt;li&gt;Does the movement still follow the intended performance?&lt;/li&gt;
&lt;li&gt;Did the background change in a distracting way?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Start with a short, continuous shot and a clearly visible subject. A complicated shot with overlapping people makes it harder to isolate why an output looks wrong. Use footage and references you have permission to edit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where AI Genjutsu fits
&lt;/h2&gt;

&lt;p&gt;AI Genjutsu is our independent web project for this source-video-plus-reference-image workflow. Generation uses paid credits; it is not a free rendering service. It is not affiliated with or endorsed by Higgsfield.&lt;/p&gt;

&lt;p&gt;The product page explains the current upload requirements and workflow. The broader design question is useful beyond our project: &lt;strong&gt;how can an AI media interface help users understand the inputs, expected changes, and cost before they start a job?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;What validation or preview step has saved your users the most failed generations?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Disclosure: this article was generated by an AI agent at the project owner's request, using the project's public product information. It has not been edited by a human.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
    </item>
    <item>
      <title>Why an AI Image Generation Timeout Should Not Automatically Trigger a Retry</title>
      <dc:creator>dong</dc:creator>
      <pubDate>Thu, 08 Oct 2026 08:39:53 +0000</pubDate>
      <link>https://dev.to/dong_45bfc981cf912828e/why-an-ai-image-generation-timeout-should-not-automatically-trigger-a-retry-kp1</link>
      <guid>https://dev.to/dong_45bfc981cf912828e/why-an-ai-image-generation-timeout-should-not-automatically-trigger-a-retry-kp1</guid>
      <description>&lt;p&gt;An image-generation request can time out even after the provider has accepted the job.&lt;/p&gt;

&lt;p&gt;That creates an awkward situation: the application has no result, but another generation may already be running. Automatically repeating the request can create a second paid job.&lt;/p&gt;

&lt;p&gt;This article examines that boundary using the current provider adapter in &lt;a href="https://nano-banana-2-1.com/" rel="noopener noreferrer"&gt;Nano Banana 2.1&lt;/a&gt;, a project I work on. It describes implementation choices and remaining failure cases, not a claim that every recovery scenario is solved.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate submission from polling
&lt;/h2&gt;

&lt;p&gt;The adapter exposes two operations:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;submit()&lt;/code&gt; creates a prediction and returns its identifier.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;poll()&lt;/code&gt; reads the status of that existing prediction.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A simplified version of the interface looks like this:&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="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ImageProvider&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;submit&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;ImageRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="nf"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;taskId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;
    &lt;span class="o"&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="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;pending&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&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="nl"&gt;imageUrl&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&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="nl"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This separation matters because the operations have different consequences.&lt;/p&gt;

&lt;p&gt;Repeating a status request asks about the same job. Repeating submission may create another job.&lt;/p&gt;

&lt;p&gt;The adapter performs a single creation request and validates that the response contains a usable prediction ID. It does not contain an automatic retry loop around that POST.&lt;/p&gt;

&lt;h2&gt;
  
  
  A timeout is an unknown outcome
&lt;/h2&gt;

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

&lt;ol&gt;
&lt;li&gt;The application sends a creation request.&lt;/li&gt;
&lt;li&gt;The provider accepts it.&lt;/li&gt;
&lt;li&gt;The connection fails before the application receives the prediction ID.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The application cannot safely infer that nothing happened.&lt;/p&gt;

&lt;p&gt;Avoiding an automatic resubmission reduces duplicate-job risk, but it leaves a recovery problem: without the ID, the application cannot use its normal polling path.&lt;/p&gt;

&lt;p&gt;A more complete recovery design would need a documented provider idempotency mechanism or another way to reconcile the original request. Those capabilities should be verified rather than assumed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate successful responses too
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;succeeded&lt;/code&gt; status is not enough by itself.&lt;/p&gt;

&lt;p&gt;In this adapter, the output can be a URL string or an array. The code selects the first array entry when necessary, then checks that the result is an HTTPS URL.&lt;/p&gt;

&lt;p&gt;Malformed output becomes an explicit error instead of an apparently successful generation with an unusable result.&lt;/p&gt;

&lt;p&gt;That check is only basic validation. It does not prove the URL is reachable, contains a valid image, or has been copied into application storage.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep error details out of user records
&lt;/h2&gt;

&lt;p&gt;Provider error bodies can contain prompts or reference-image URLs.&lt;/p&gt;

&lt;p&gt;For failed predictions, the adapter keeps a small operational error containing the provider, processing stage, category, and failure code. It does not copy the provider's raw error body into the returned failure message.&lt;/p&gt;

&lt;p&gt;This preserves enough information to classify the failure without unnecessarily duplicating user input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test the uncertainty boundary
&lt;/h2&gt;

&lt;p&gt;Useful tests for this design include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A creation request fails without returning an ID: no automatic second POST.&lt;/li&gt;
&lt;li&gt;A creation response lacks a valid ID: polling does not begin.&lt;/li&gt;
&lt;li&gt;A prediction remains in progress: return &lt;code&gt;pending&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A prediction succeeds with malformed output: return an explicit error.&lt;/li&gt;
&lt;li&gt;A prediction fails: return a sanitized failure rather than the raw response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The main design lesson is to distinguish “the request failed” from “the work never started.” In an asynchronous, paid workflow, those are different states.&lt;/p&gt;




&lt;p&gt;Disclosure: This article was drafted with AI assistance using the project's current source code. The interface example is simplified for explanation.&lt;/p&gt;

</description>
      <category>webdev</category>
    </item>
    <item>
      <title>What's the simplest way to change a few words baked into an image?</title>
      <dc:creator>dong</dc:creator>
      <pubDate>Sun, 27 Sep 2026 14:02:15 +0000</pubDate>
      <link>https://dev.to/dong_45bfc981cf912828e/whats-the-simplest-way-to-change-a-few-words-baked-into-an-image-58mg</link>
      <guid>https://dev.to/dong_45bfc981cf912828e/whats-the-simplest-way-to-change-a-few-words-baked-into-an-image-58mg</guid>
      <description>&lt;p&gt;I keep running into a small, specific image-editing need: a screenshot or graphic has one or two outdated words, and I want to fix the copy without taking on the learning curve of a full image editor.&lt;/p&gt;

&lt;p&gt;The right approach depends on how the image was made. If the original file still has an editable text layer, changing the words is straightforward. If the text has been flattened into the image, it becomes a retouching problem too: the old letters need to be removed or covered, the background repaired, and the replacement text made to fit. Adobe's guidance explains the editable-text case: &lt;a href="https://helpx.adobe.com/photoshop/desktop/text-typography/get-started-with-text/edit-text.html" rel="noopener noreferrer"&gt;https://helpx.adobe.com/photoshop/desktop/text-typography/get-started-with-text/edit-text.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For someone who only needs this occasionally, what workflow would you recommend? Is there a lightweight tool that handles this narrow job without requiring me to learn a large editor first?&lt;/p&gt;

&lt;p&gt;I've been using PhotextAI for this kind of small text change and found it helpful, so I'm sharing it in case it helps someone else: &lt;a href="https://photextai.com/" rel="noopener noreferrer"&gt;https://photextai.com/&lt;/a&gt; . I'm a user recommending a tool I've found useful, and I'm interested in other simple workflows people use.&lt;/p&gt;

</description>
      <category>discuss</category>
    </item>
    <item>
      <title>Designing a Guest AI Image Generation Flow Without Letting Free Usage Run Wild</title>
      <dc:creator>dong</dc:creator>
      <pubDate>Thu, 03 Sep 2026 08:02:42 +0000</pubDate>
      <link>https://dev.to/dong_45bfc981cf912828e/designing-a-guest-ai-image-generation-flow-without-letting-free-usage-run-wild-3b7l</link>
      <guid>https://dev.to/dong_45bfc981cf912828e/designing-a-guest-ai-image-generation-flow-without-letting-free-usage-run-wild-3b7l</guid>
      <description>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Disclosure:&lt;/strong&gt; I used AI to help organize and edit this article. The implementation details and engineering decisions come from my own project, and I reviewed the final draft for accuracy.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Letting a visitor try an AI image tool before creating an account sounds simple. Give them a text box, call an image API, and show the result.&lt;/p&gt;

&lt;p&gt;That version works until real traffic arrives.&lt;/p&gt;

&lt;p&gt;Image generation has a direct cost. Requests take long enough for users to refresh, double-click, or close the tab. Providers fail. Workers restart. A person can switch between anonymous and signed-in sessions. If the free path is only a boolean such as &lt;code&gt;hasUsedTrial&lt;/code&gt;, it will eventually charge twice, run twice, or become easy to abuse.&lt;/p&gt;

&lt;p&gt;I ran into these problems while building the guest flow behind &lt;a href="https://piceditor.org/" rel="noopener noreferrer"&gt;PicEditor&lt;/a&gt;, a browser-based AI image editor. The useful lesson was that a free generation should not be treated like a coupon. It should be treated like a small distributed job with a reservation, a lifecycle, and a cost owner.&lt;/p&gt;

&lt;h2&gt;
  
  
  The request needs a lifecycle
&lt;/h2&gt;

&lt;p&gt;The first design question was not “Is this user free?” It was “What state is this request in?”&lt;/p&gt;

&lt;p&gt;I ended up using a short list of states that correspond to things the server can actually prove:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;reserved -&amp;gt; pending -&amp;gt; processing -&amp;gt; completed
                                \-&amp;gt; failed
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The browser can display &lt;code&gt;queued&lt;/code&gt; as part of the user experience, but the source of truth remains on the server. I deliberately avoided fake progress percentages and messages such as “analyzing your idea” when no such step exists.&lt;/p&gt;

&lt;p&gt;This matters more than it may seem. If a visitor refreshes the page after submission, the client should reconnect to the same job instead of creating another provider request. If the same anonymous identity submits again while a job is active, the server should return the existing job or reject the duplicate.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reserve before calling the provider
&lt;/h2&gt;

&lt;p&gt;Before an image request reaches the model provider, the server performs several checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Is guest generation currently enabled?&lt;/li&gt;
&lt;li&gt;Did the browser complete the anti-bot challenge?&lt;/li&gt;
&lt;li&gt;Is this identity or network already over its free allowance?&lt;/li&gt;
&lt;li&gt;Does the identity already have a generation in progress?&lt;/li&gt;
&lt;li&gt;Is there enough room in the daily provider-cost budget?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important part is the order. I do not want two concurrent requests to both read “budget available” and then both spend it.&lt;/p&gt;

&lt;p&gt;The simplified flow looks like this:&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;reservation&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;reserveIdentitySlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;identity&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;reserveCostBudget&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;estimatedProviderCost&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;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;await&lt;/span&gt; &lt;span class="nf"&gt;restoreIdentitySlot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;reservation&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;throw&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The identity claim uses a conflict-safe database write. The budget uses a conditional update that succeeds only while the new reserved total remains under the cap. If the second operation fails, the first reservation is restored.&lt;/p&gt;

&lt;p&gt;It is not a magical global transaction. It is a small reservation protocol with an explicit compensation step. That distinction made the failure cases much easier to reason about.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep access policy separate from billing
&lt;/h2&gt;

&lt;p&gt;Another decision that helped was separating &lt;em&gt;how a request is allowed to run&lt;/em&gt; from &lt;em&gt;how it is paid for&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;The generation record carries an access tier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;AccessTier&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;paid&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free_first&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;free_slow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The first successful guest generation can enter immediately. Later free requests can use a slower queue. Paid requests follow the credit path.&lt;/p&gt;

&lt;p&gt;This is cleaner than sprinkling checks such as &lt;code&gt;if (isGuest)&lt;/code&gt; throughout the workflow. The workflow receives a concrete policy and acts on it. It also means signing in does not silently reset a person's completed guest history.&lt;/p&gt;

&lt;p&gt;For the free path, the server also fixes the expensive parameters to a known model, resolution, quality, provider, and one output image. Letting the client choose arbitrary settings would make the cost cap meaningless.&lt;/p&gt;

&lt;h2&gt;
  
  
  The queue is part of the product
&lt;/h2&gt;

&lt;p&gt;I did not want the second free request to hit a dead end. A slower queue is a more honest trade-off: the request exists, has a real execution time, and will eventually run.&lt;/p&gt;

&lt;p&gt;The delay is calculated when the job is created and stored with that job. If an administrator changes the queue configuration later, existing jobs keep their original schedule. That prevents the UI from showing a moving target.&lt;/p&gt;

&lt;p&gt;The workflow can wait for either of two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the free delay to expire;&lt;/li&gt;
&lt;li&gt;a verified upgrade event that allows the same logical job to continue sooner.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The second path must not create a replacement provider request. Payment callbacks, page refreshes, and repeated clicks all need to converge on the same job.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the expensive work in a durable workflow
&lt;/h2&gt;

&lt;p&gt;An HTTP request is a poor owner for an image-generation job. The connection may disappear long before the provider finishes.&lt;/p&gt;

&lt;p&gt;I use a Cloudflare Workflow for the long-running path. The request handler validates the input, creates a pending history record, and enqueues the workflow. From that point, the workflow owns the job:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;validate request
  -&amp;gt; reserve guest entitlement and budget
  -&amp;gt; create generation history
  -&amp;gt; enqueue workflow
  -&amp;gt; submit to image provider
  -&amp;gt; poll for completion
  -&amp;gt; store result
  -&amp;gt; finalize history
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Provider submission, polling, and storage are retryable steps. The browser only polls the history ID, so reconnecting does not restart the generation.&lt;/p&gt;

&lt;p&gt;Paid generations consume credits inside the workflow. If a later step fails, the compensation step refunds the same usage transaction and marks the job failed. Guest jobs do not need a credit refund, but their claim still needs an accurate final state.&lt;/p&gt;

&lt;p&gt;Keeping debit and refund under the same workflow owner removed a class of bugs where the API returned an error but a background request continued spending money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Anonymous should not mean unowned
&lt;/h2&gt;

&lt;p&gt;Guest files still need ownership and privacy.&lt;/p&gt;

&lt;p&gt;Reference images are uploaded to private storage and attached to a server-side anonymous identity. The server checks ownership before using a file in a generation request. Generated outputs also belong to that identity rather than living at an untracked public URL.&lt;/p&gt;

&lt;p&gt;If the visitor later signs in, the system links the guest claim, history, and files to the verified account. The same completed guest generation still counts after the identity changes.&lt;/p&gt;

&lt;p&gt;Anonymous content is temporary. In my current design it is eligible for cleanup after 72 hours, so the product tells visitors to download anything important. “No sign-up” should not be presented as “permanent storage without an account.”&lt;/p&gt;

&lt;h2&gt;
  
  
  Fail before spending whenever possible
&lt;/h2&gt;

&lt;p&gt;The cheapest provider call is the one that never needs to happen.&lt;/p&gt;

&lt;p&gt;File type, file size, ownership, model capabilities, prompt policy, anti-bot verification, concurrency, and budget checks all happen before provider submission. A rejected prompt should not consume the guest allowance. A malformed upload should not create a generation job.&lt;/p&gt;

&lt;p&gt;There is still no perfect abuse-prevention rule. IP limits can affect shared networks, identity limits can be evaded, and CAPTCHA adds friction. I found it more useful to layer several imperfect signals and keep a global kill switch than to pretend one identifier is reliable.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I would keep if I rebuilt it
&lt;/h2&gt;

&lt;p&gt;The code will change, but I would keep these ideas:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Model free usage as a reservation, not a boolean.&lt;/li&gt;
&lt;li&gt;Give each generation a durable server-side identity.&lt;/li&gt;
&lt;li&gt;Make duplicate requests converge on the same job.&lt;/li&gt;
&lt;li&gt;Reserve estimated cost before provider submission.&lt;/li&gt;
&lt;li&gt;Put failure compensation beside the work it compensates.&lt;/li&gt;
&lt;li&gt;Keep anonymous files private and temporary.&lt;/li&gt;
&lt;li&gt;Show only states and waiting times the server can prove.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The surprising part was that the image model was not the hardest component. The hard part was defining who owns a request while the user, the browser, the worker, and the provider can all disappear independently.&lt;/p&gt;

&lt;p&gt;Once the free generation became a proper job instead of a marketing exception, the rest of the system became much easier to explain and test.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>architecture</category>
      <category>ai</category>
      <category>cloudflareworkers</category>
    </item>
  </channel>
</rss>
