<?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: YEB</title>
    <description>The latest articles on DEV Community by YEB (@yebto).</description>
    <link>https://dev.to/yebto</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3819014%2Ffa6944f7-8059-494e-84c4-9592e91a1146.jpeg</url>
      <title>DEV Community: YEB</title>
      <link>https://dev.to/yebto</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yebto"/>
    <language>en</language>
    <item>
      <title>Building a Pay-Per-Use SaaS: Lessons from a Caption Tool</title>
      <dc:creator>YEB</dc:creator>
      <pubDate>Wed, 25 Mar 2026 22:00:01 +0000</pubDate>
      <link>https://dev.to/yebto/building-a-pay-per-use-saas-lessons-from-a-caption-tool-47n4</link>
      <guid>https://dev.to/yebto/building-a-pay-per-use-saas-lessons-from-a-caption-tool-47n4</guid>
      <description>&lt;p&gt;The original frustration was simple: I was paying €10/month for a caption generator and using it three times. That is over €3 per video for a tool that does a single, stateless job — take a video file, produce a subtitle file, done.&lt;/p&gt;

&lt;p&gt;So I built a pay-per-use alternative. What I did not expect was how much the billing model would reshape every technical decision from the ground up.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Architecture Difference
&lt;/h2&gt;

&lt;p&gt;A subscription SaaS and a pay-per-use SaaS look similar on the surface — users, auth, a product feature — but the payment and job model diverges significantly.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Subscription model:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User pays monthly → gets a "member" flag → unlimited (or quota-limited) access&lt;/li&gt;
&lt;li&gt;Auth middleware checks membership status&lt;/li&gt;
&lt;li&gt;Usage tracking is optional (for analytics, not billing)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Pay-per-use model:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User has a credit balance (or card on file)&lt;/li&gt;
&lt;li&gt;Every job deducts a defined cost before processing starts&lt;/li&gt;
&lt;li&gt;If balance is insufficient, the job is rejected before any compute runs&lt;/li&gt;
&lt;li&gt;The credit deduction is atomic with the job record creation&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That last point matters more than it sounds. If a user submits a job and payment succeeds but the job fails, you need a reliable refund path. If payment fails but the job runs anyway, you have a revenue leak. The deduction and job creation need to be in the same database transaction, or you need a compensating transaction pattern.&lt;/p&gt;

&lt;h2&gt;
  
  
  Job State Machine
&lt;/h2&gt;

&lt;p&gt;Caption generation (or any media processing job) is inherently async. The user uploads, you enqueue, you process, you deliver. The job moves through states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending → queued → processing → completed
                              → failed
                              → refunded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;refunded&lt;/code&gt; state is specific to pay-per-use. If the job hits &lt;code&gt;failed&lt;/code&gt;, a background process evaluates whether the failure was on the user's side (bad file format, invalid input — no refund) or on the system's side (processing error, timeout — full refund).&lt;/p&gt;

&lt;p&gt;This introduces a &lt;code&gt;failure_reason&lt;/code&gt; enum on the job model, not just a boolean failed flag.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credit System Design
&lt;/h2&gt;

&lt;p&gt;A few options for implementing credits:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option A — Integer credits (simplest)&lt;/strong&gt;&lt;br&gt;
Store credits as an integer (e.g., 100 credits = €10). Each job costs N credits. Top up in packs. This is easy to display, easy to deduct, and easy to audit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option B — Real currency balance&lt;/strong&gt;&lt;br&gt;
Store balance in cents (integer). Each job has a price in cents. Top up via Stripe with exact amount. More transparent to users, slightly more complex for pricing tiers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Option C — Prepaid tokens&lt;/strong&gt;&lt;br&gt;
User buys a pack of "jobs" (e.g., 10 video captions for €5). Tokens decrement per use. Simple, but inflexible if you want variable pricing based on video length or quality settings.&lt;/p&gt;

&lt;p&gt;I went with Option A. It is the most flexible for adding new tools with different costs without changing the billing infrastructure.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Checkout Flow Problem
&lt;/h2&gt;

&lt;p&gt;Subscription tools can afford friction in signup. The user is making a commitment — they will tolerate a few extra steps to set up billing.&lt;/p&gt;

&lt;p&gt;Pay-per-use needs a faster on-ramp. If a user lands on the tool, uploads a file, and then hits a "please create an account and add payment info" wall, conversion dies there.&lt;/p&gt;

&lt;p&gt;The flow I landed on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;User uploads file (no auth required at this step — preview/analysis only)&lt;/li&gt;
&lt;li&gt;Show result preview + credit cost&lt;/li&gt;
&lt;li&gt;One-click purchase option (guest checkout with email) OR login if they have credits already&lt;/li&gt;
&lt;li&gt;Job runs immediately after payment confirms&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Guest checkout with saved email means return users can top up without a full account if they choose. It reduces the commitment required for a first transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Subscription Billing Hides
&lt;/h2&gt;

&lt;p&gt;Building pay-per-use forced me to confront costs I could ignore with a subscription model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Processing time&lt;/strong&gt; — a 30-minute video costs more to transcribe than a 2-minute one. Flat pricing hides this.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Failure rate&lt;/strong&gt; — with a subscription, failed jobs are annoying but not a billing event. With pay-per-use, every failure is a potential support ticket about money.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage&lt;/strong&gt; — output files need to be available for download for a reasonable window. With pay-per-use, storing files indefinitely makes no sense. I auto-delete outputs after 48 hours and tell users this upfront.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are not complications — they are clarifications. The model forces you to understand your own cost structure.&lt;/p&gt;




&lt;p&gt;Read the full backstory that started this build: &lt;a href="https://yeb.to/captions-ai-charged-me-10-euro-a-month-for-3-videos-so-i-built-my-own-tool" rel="noopener noreferrer"&gt;https://yeb.to/captions-ai-charged-me-10-euro-a-month-for-3-videos-so-i-built-my-own-tool&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try the tool: &lt;a href="https://captions.yeb.to" rel="noopener noreferrer"&gt;https://captions.yeb.to&lt;/a&gt;&lt;/p&gt;

</description>
      <category>saas</category>
      <category>architecture</category>
      <category>indiedev</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
