<?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: Meterbase</title>
    <description>The latest articles on DEV Community by Meterbase (@usemeterbase).</description>
    <link>https://dev.to/usemeterbase</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%2F4159432%2Fb6e1add7-33db-4702-a706-dd80b02f36f7.jpg</url>
      <title>DEV Community: Meterbase</title>
      <link>https://dev.to/usemeterbase</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/usemeterbase"/>
    <language>en</language>
    <item>
      <title>The parts of a credit system nobody puts in the estimate</title>
      <dc:creator>Meterbase</dc:creator>
      <pubDate>Sun, 04 Oct 2026 07:21:17 +0000</pubDate>
      <link>https://dev.to/meterbase/the-parts-of-a-credit-system-nobody-puts-in-the-estimate-3063</link>
      <guid>https://dev.to/meterbase/the-parts-of-a-credit-system-nobody-puts-in-the-estimate-3063</guid>
      <description>&lt;p&gt;If your product sells credits, tokens, minutes or API calls, someone on your team has probably said "we'll just add a &lt;code&gt;credits&lt;/code&gt; column to the users table." It's a reasonable first move. It's also where most teams start building a billing system they never planned to own.&lt;/p&gt;

&lt;p&gt;Here's what that column turns into once real customers arrive.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The balance isn't one number
&lt;/h2&gt;

&lt;p&gt;A customer on a monthly plan has an allowance that resets. Then they buy a top-up pack, which shouldn't reset. Then you give them a signup bonus that expires in 30 days. Now "how many credits do they have?" is a sum over several buckets, each with its own rules, and you have to decide which bucket to spend first. Usually it's the one that expires soonest.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. "Monthly" means each customer's month
&lt;/h2&gt;

&lt;p&gt;Allowances renew on the customer's billing date, not on the 1st. A customer who signed up on the 17th renews on the 17th. A customer who upgraded on the 9th might renew on the 9th from now on, or keep the 17th, depending on how you prorate. Your renewal job now needs to know every customer's anchor date.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Plan changes mid-period
&lt;/h2&gt;

&lt;p&gt;Upgrade today, and what happens to the allowance? Do they get the full new amount now, a prorated share, or the new amount at the next renewal? Downgrades are worse: what if they've already used more than the smaller plan includes? Whatever you choose, you need a history of which plan applied when, or you can't explain an invoice later.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Two requests, one last credit
&lt;/h2&gt;

&lt;p&gt;Your app checks the balance, runs the job, then deducts. With parallel requests, two jobs can both see "10 credits left", both run, and both deduct. The customer ends up at −10. For cheap work that's fine. For a video render or a batch of image generations, it's real money. Fixing it means holding capacity before the work starts and settling after.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Failed jobs
&lt;/h2&gt;

&lt;p&gt;If you hold or deduct credits before a job and the job fails, the customer should get them back. If your worker crashes mid-job, nobody calls the refund. So holds need an expiry, and something has to clean them up.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Bursts
&lt;/h2&gt;

&lt;p&gt;A monthly allowance doesn't stop one customer's runaway script from spending the whole month in ten minutes. You need rate limits per customer as well as totals, often in more than one window, such as per minute and per hour.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Telling customers before they run out
&lt;/h2&gt;

&lt;p&gt;Customers would rather top up at 80% than find out they're at 100% when a job fails. That means tracking thresholds per customer per period and notifying once, not on every request after the line is crossed.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Usage for billing
&lt;/h2&gt;

&lt;p&gt;At the end of a period, finance wants an exact number: how much this customer used, how much was included, and how much was overage. That number has to stay put once the period closes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it adds up to
&lt;/h2&gt;

&lt;p&gt;None of these are hard on their own. Together they're a small, stateful, concurrency-sensitive system that sits in front of your most expensive operations, and it changes every time your pricing changes. That's the part that rarely makes it into the original estimate.&lt;/p&gt;

&lt;h2&gt;
  
  
  The short version
&lt;/h2&gt;

&lt;p&gt;This is the problem we're building Meterbase for. You set up meters, plans and allowances once, and your code makes one call before the work and one after:&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="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Meterbase&lt;/span&gt; &lt;span class="p"&gt;}&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;mbase-sdk&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;meterbase&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Meterbase&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;apiKey&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;METERBASE_API_KEY&lt;/span&gt;&lt;span class="o"&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;check&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;meterbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2&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;// an estimate&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;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;allowed&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="s2"&gt;`Refused: &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;check&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;reply&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;generateReply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;prompt&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;meterbase&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;track&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;customer_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;acct_1&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;meter_id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ai_tokens&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;quantity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;reply&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;total_tokens&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;Allowances, top-ups, plan changes, holds, rate limits and threshold webhooks sit behind those two calls. Meterbase isn't a billing system: Stripe or Paddle still take the money, and you read each customer's usage for a period to bill from.&lt;/p&gt;

&lt;p&gt;If you're building this right now, or you already built it and are tired of maintaining it, we'd love to hear how you handled it: &lt;a href="https://meterbase.tech" rel="noopener noreferrer"&gt;meterbase.tech&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>saas</category>
      <category>webdev</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
