DEV Community

babycat
babycat

Posted on

A 30M-Token Free Tier Needs a Token Ledger, Not Just a Budget Bar

Last week I linked a source-review agent to a free model tier and watched two numbers disagree: the client budget bar said 9.2M tokens remained, the provider dashboard said 24.7M, and the server still returned 429 budget_exceeded on the first retry. I spent the first hour blaming a quota throttle, because free tiers are supposed to break in exactly that way. The real defect was much closer to home. A streaming response was double-counted in my token ledger, and the budget bar was narrating a lie to screen-reader users.

The lesson from that afternoon is not that free AI is unreliable. It is that a large free grant makes accounting bugs invisible for much longer. A 30M-token pool will happily consume millions of tokens before a client-side double count crosses the failure boundary. By then the bug has touched every retry, every cache entry, and every accessibility announcement. I needed a token ledger, not another progress bar.

Disclosure: This article was prepared as part of MonkeyCode's product outreach. I used MonkeyCode's open-source project with its free model access and the free server path described in the operator materials. Those materials list a 30M-token pool and a no-cost server option; I treated those claims as ceilings to verify, not as product guarantees.

Start with the symptom table, not the provider

The conversation this week kept repeating a pattern. A request completed with HTTP 200, the reader finished, and the UI announced 79% of token budget remaining. Then a retry for the same prompt failed with budget_exceeded. The server's final usage said total tokens were 4.1M, while my ledger said 2.4M. Nothing about that failure pointed to a bug in the chunk reader except the shape of the numbers.

Source Token count What it claimed
Client ledger 2,410,388 9.2M remaining
Server usage 4,108,942 actual total
Dashboard after retry 26,115,400 quota remaining before failure

The gap between client and server was roughly the size of the cached prompt tokens plus one completion pass. That clue meant my retry path was counting prompt tokens twice, while the streaming path was counting completion tokens using character length instead of tokenizer output. I should have stopped trusting the client and started instrumenting the wire.

Build a replay harness before you touch the live UI

My first mistake was inspecting the production code in place. The second was assuming a free server would behave like a hosted endpoint with consistent final usage. When a serverless or free server path truncates the SSE stream, the last data: chunk can be missing. You then recalculate the budget from an estimate, and the drift becomes permanent. That is exactly when a free grant hides the failure, because the next 20M tokens run on top of the corrupted number.

A small replay harness makes the accounting observable. It replays the same JSON request body, reads the SSE stream, and records only the final usage when the provider sends one.

async function replayConversation(requestBody, endpoint, key) {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), 30_000);

  const response = await fetch(endpoint, {
    method: 'POST',
    headers: {
      'content-type': 'application/json',
      authorization: `Bearer ${key}`,
    },
    body: JSON.stringify(requestBody),
    signal: controller.signal,
  });

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${await response.text()}`);
  }

  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';
  let finalUsage = null;
  let completionChars = 0;

  while (true) {
    const { value, done } = await reader.read();
    if (done) break;
    buffer += decoder.decode(value, { stream: true });

    let boundary = buffer.indexOf('\n\n');
    while (boundary !== -1) {
      const eventText = buffer.slice(0, boundary);
      buffer = buffer.slice(boundary + 2);

      for (const line of eventText.split('\n')) {
        if (!line.startsWith('data:')) continue;
        const json = line.slice(5).trim();
        if (!json || json === '[DONE]') continue;

        const chunk = JSON.parse(json);
        const delta = chunk.choices?.[0]?.delta?.content ?? '';
        completionChars += delta.length; // not a token count
        if (chunk.usage) finalUsage = chunk.usage; // authoritative when present
      }

      boundary = buffer.indexOf('\n\n');
    }
  }

  clearTimeout(timeout);
  return { completionChars, finalUsage };
}
Enter fullscreen mode Exit fullscreen mode

Notice that completionChars is deliberately not used as the billing number. Character length and token length differ, so using delta.length to update a token budget creates drift that grows with every streamed response. The harness records it only as a cross-check.

Reconcile the ledger from one source of truth

The fix in the real UI came down to a state machine that accepts exactly one authoritative input: the final usage object. If the final usage is missing, the ledger is marked unverified, and the UI stops making replacement claims until a verified value arrives.

function getNextLedger({ previousLedger, finalUsage, failedAttempts }) {
  if (finalUsage && Number.isFinite(finalUsage.total_tokens)) {
    return {
      status: 'verified',
      used: previousLedger.used + finalUsage.total_tokens,
      source: 'server-usage',
    };
  }

  return {
    status: 'unverified',
    used: previousLedger.used,
    source: failedAttempts > 0 ? 'retry-pending' : 'stream-truncated',
    failedAttempts: failedAttempts + 1,
  };
}
Enter fullscreen mode Exit fullscreen mode

This changed the behavior in three places that users actually notice. A failed retry no longer says budget exceeded with a confident number. The budget bar no longer decreases from an estimate, because an estimate is never allowed to replace the server number. The screen-reader announcement now says token usage is unverified after a truncated response instead of reading a false percentage.

Make the budget boundary accessible, not just visible

The original budget bar used aria-valuenow with a number that was already wrong. When the server returned a corrected usage on the next request, the accessible name changed from 79% remaining to 43% remaining with no explanation. A user listening to that announcement assumes the model consumed 36% in one request. What actually happened is that the earlier number was fabricated.

A better pattern is to announce the transition, not the raw refresh. I added a visually hidden live region with aria-live='polite' and updated it only when the ledger status changed.

<p class='sr-only' aria-live='polite'>
  Token budget status: <span id='budget-status'>verified</span>.
</p>
Enter fullscreen mode Exit fullscreen mode

The visible meter can keep its role, but it should render as a progress bar only when the status is verified. In the unverified state it becomes plain text: Token usage is still being reconciled. If the next stream ends with a final usage, the screen reader then hears one clear transition from unverified to verified, not two competing numbers.

Where the free server made this harder

A free server option changes the failure surface. A cold start can delay the first token, a proxy can drop the final usage chunk, and a retry can consume another prompt pass before the UI knows the first attempt failed. Each of those conditions makes client-side token estimation more tempting, because the server number is absent right when the user is watching the budget bar. A token ledger that refuses to estimate is boring, but it remains honest when the network is not.

The operator-supplied MonkeyCode materials still describe a 30M-token pool and a free server option. I did not need that whole pool to find the bug; the defect appeared after a few million tokens because my retry path was broken, not because the quota was small. If you use a free model access path, capture the final usage before you render any remaining-balance claim.

Who should not copy this approach

Do not use a client token ledger for billing, quotas, or anything involving money or hard limits. Tokenizer equivalence is provider-specific, and a prompt that is cached can count differently than the same prompt after a cache miss. If your free server path can be restarted mid-stream, the final usage may belong to a partial generation. Treat the ledger as a UI reconciliation tool, not as a source of record.

The working rule I settled on is short: when a number can be absent, mark the state unverified; when a number exists, show the server value; when a retry masks the absence, block the next estimate. A free 30M-token grant makes arithmetic errors cheap to hide and expensive to debug. The harness is what makes them cheap to find again.

If you try the MonkeyCode free endpoint or free server path, run the replay harness before you publish a budget bar. The product is the open-source project and the no-cost server; the debugging lesson is that the next quota failure may be your own token math, not the quota.

Top comments (0)