<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged dev</title>
<link>https://coderlegion.com/tag/dev</link>
<description>Powered by Question2Answer</description>
<item>
<title>The Truck Driver Building His Own Programming Language- Meet Bryan MacLee</title>
<link>https://coderlegion.com/31246/the-truck-driver-building-his-own-programming-language-meet-bryan-maclee</link>
<description>&lt;h2 id=&quot;developer-spotlight-meet-bryan-maclee-the-truck-driver-building-his-own-programming-language&quot;&gt;Developer Spotlight: Meet Bryan MacLee — The Truck Driver Building His Own Programming Language&lt;/h2&gt;
&lt;p&gt;🚀 What if you worked long shifts in the oil and gas industry and spent your spare time building your own programming language?&lt;/p&gt;
&lt;p&gt;Meet &lt;strong&gt;Bryan MacLee&lt;/strong&gt;, the creator of &lt;strong&gt;Scriml&lt;/strong&gt;, a programming language designed to rethink how web applications are built.&lt;/p&gt;
&lt;p&gt;In this CoderLegion Developer Spotlight, host &lt;strong&gt;Tom Smith&lt;/strong&gt; talks with Bryan about his programming journey, the ideas behind Scriml, and the challenges of building a compiler independently.&lt;/p&gt;
&lt;h3 id=&quot;-what-you-ll-discover&quot;&gt;💡 What You'll Discover&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;The beginning:&lt;/strong&gt; How Bryan discovered programming as a teenager.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Life beyond coding:&lt;/strong&gt; Balancing oilfield truck-driving work with software development.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Building Scriml:&lt;/strong&gt; Why he decided to create his own programming language.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Simplifying web development:&lt;/strong&gt; His approach to frontend-backend communication, validation and compilation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The compiler challenge:&lt;/strong&gt; What it's like to build a programming language largely on your own.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;What's next:&lt;/strong&gt; Bryan's hopes for Scriml and getting other developers to try it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-watch-the-interview&quot;&gt;🎥 Watch the Interview&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;[Insert your YouTube video link here]&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&quot;-meet-the-people-behind-the-interview&quot;&gt;👨💻 Meet the People Behind the Interview&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Guest:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/Bryan+MacLee&quot;&gt;Bryan MacLee&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Host:&lt;/strong&gt; &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com/user/Tom+Smith&quot;&gt;Tom Smith&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;-building-something-of-your-own-&quot;&gt;🌟 Building Something of Your Own?&lt;/h3&gt;
&lt;p&gt;CoderLegion Developer Spotlight celebrates developers, independent builders and people exploring new ideas.&lt;/p&gt;
&lt;p&gt;If you're working on an interesting project, experimenting with a new technology or learning something challenging, we'd love to hear your story. Get involved in the community and you could be featured in a future Developer Spotlight.&lt;/p&gt;
&lt;p&gt;🌐 &lt;a rel=&quot;nofollow&quot; href=&quot;https://coderlegion.com&quot;&gt;Join the CoderLegion community&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&quot;-let-s-discuss&quot;&gt;💬 Let's Discuss&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;If you could redesign one part of modern web development, what would you change and why?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Share your thoughts in the comments!&lt;/p&gt;
&lt;h2 id=&quot;developerspotlight-programming-webdevelopment&quot;&gt;DeveloperSpotlight #Programming #WebDevelopment&lt;/h2&gt;
</description>
<category>Dev Stories</category>
<guid isPermaLink="true">https://coderlegion.com/31246/the-truck-driver-building-his-own-programming-language-meet-bryan-maclee</guid>
<pubDate>Thu, 15 Oct 2026 22:44:46 +0000</pubDate>
</item>
<item>
<title>Per-Tenant LLM Budgets: Stop One Customer From Spending Your Whole Model Bill</title>
<link>https://coderlegion.com/29520/per-tenant-llm-budgets-stop-one-customer-from-spending-your-whole-model-bill</link>
<description>&lt;p&gt;Picture a hypothetical B2B app with an AI assistant in every customer workspace. One customer wires it into a nightly job that loops over tens of thousands of documents, each sent with a long context and a generous &lt;code&gt;max_tokens&lt;/code&gt;. Every request is valid and under the per-user rate limit. By morning that customer has spent more on model calls than they pay you in a year, and possibly everyone else's share too: on the Claude API, an organization that reaches its monthly spend cap gets HTTP 429 on every request until 00:00 UTC on the first day of the next month, unless it gets a higher limit sooner.&lt;/p&gt;
&lt;p&gt;The fix is a per-tenant budget: the money each customer, and each user inside it, may spend on model calls in a window, checked before every call, settled from real usage after it, and reconciled against the provider's bill. It's the authorize-and-capture bookkeeping of card payments applied to tokens, and a standard piece of the multi-tenant AI products built in the production SaaS course at &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas&quot;&gt;Cursuri-AI.ro&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The code uses the official &lt;code&gt;anthropic&lt;/code&gt; Python SDK and Redis, with Postgres as the alternative. Retries, circuit breakers, agent stop conditions and tracing are separate layers and aren't repeated here. This is only about money: who spent it, how much is left, and what happens when it runs out.&lt;/p&gt;
&lt;h3 id=&quot;rate-limits-are-not-budgets&quot;&gt;Rate limits are not budgets&lt;/h3&gt;
&lt;p&gt;Most stacks already have a few kinds of limits. None of them is a budget.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The provider's rate limits&lt;/strong&gt; protect the provider's capacity. On the Claude API they are &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/api/rate-limits&quot;&gt;measured in requests, input tokens and output tokens per minute&lt;/a&gt;, per model, for your whole organization, with a token bucket that refills continuously. They don't know which customer used the capacity, and they don't measure money: for most Claude models, cache reads don't count toward the input-tokens-per-minute limit, yet every one of them is billed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The provider's spend limits&lt;/strong&gt; measure money at the wrong granularity: the organization cap, or a lower one you set yourself, switches AI off for your whole product at once. Workspaces can carry their own spend and rate limits, which helps to isolate one very large customer, but prompt caches are isolated per workspace on the Claude API, so splitting traffic across workspaces also splits your cache hits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your own per-request rate limits&lt;/strong&gt; bound how often someone calls, not what a call costs. Long context is billed at a flat per-token rate (&quot;A 900k-token request is billed at the same per-token rate as a 9k-token request&quot;), so cost scales with size. On Claude Opus 5.5, 2,000 input tokens and 500 output tokens cost just under 2 cents; about 870,000 input tokens plus the full 128K-token output cost about $6. Same endpoint, same rate-limit slot, a few hundred times the price.&lt;/p&gt;
&lt;p&gt;A budget is an amount of money per tenant per window, reduced by what each call actually cost and checked before the next one starts.&lt;/p&gt;
&lt;h3 id=&quot;budget-in-money-not-tokens&quot;&gt;Budget in money, not tokens&lt;/h3&gt;
&lt;p&gt;Tokens are the wrong unit because they don't have one price. A single Claude response reports up to five differently priced quantities:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Usage field &lt;/th&gt;&lt;th&gt;What it counts &lt;/th&gt;&lt;th&gt;Claude Opus 5.5, per million tokens&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;input_tokens&lt;/code&gt; &lt;/td&gt;&lt;td&gt;input after the last cache breakpoint &lt;/td&gt;&lt;td&gt;$4&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cache_creation_input_tokens&lt;/code&gt; &lt;/td&gt;&lt;td&gt;input written to the prompt cache (5-minute / 1-hour TTL) &lt;/td&gt;&lt;td&gt;$5 / $8&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;cache_read_input_tokens&lt;/code&gt; &lt;/td&gt;&lt;td&gt;input read from the cache &lt;/td&gt;&lt;td&gt;$0.20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;output_tokens&lt;/code&gt; &lt;/td&gt;&lt;td&gt;everything generated, thinking included &lt;/td&gt;&lt;td&gt;$20&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;&lt;code&gt;server_tool_use.web_search_requests&lt;/code&gt; &lt;/td&gt;&lt;td&gt;web searches, if you enable the tool &lt;/td&gt;&lt;td&gt;$10 per 1,000&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That's a 100× spread inside one model, and cache discounts differ by model: a cache hit costs 10% of the input price on most models, 5% on Claude Opus 5.5 and 2.5% on Claude Fable 5.1. Token quotas also drift. Per the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/about-claude/pricing&quot;&gt;pricing page&lt;/a&gt;, the tokenizer used by Claude 4.7 and later models &quot;produces approximately 30% more tokens for the same text&quot;, so a per-token quota written for an older model shrinks after a migration.&lt;/p&gt;
&lt;p&gt;Thinking is the part that catches people out. It is billed as output &quot;even when the thinking text isn't returned to you&quot;, and with summarized thinking &quot;the billed output token count does not match the count of tokens you see in the response&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/build-with-claude/thinking&quot;&gt;thinking docs&lt;/a&gt;). Adaptive thinking is always on for Claude Opus 5.5, so never estimate cost from visible text. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/api/messages/create&quot;&gt;Messages API reference&lt;/a&gt; calls &lt;code&gt;usage.output_tokens&lt;/code&gt; &quot;the inclusive, authoritative total used for billing&quot;, and &lt;code&gt;output_tokens_details.thinking_tokens&lt;/code&gt; says how much of it was reasoning.&lt;/p&gt;
&lt;p&gt;Keep budgets in integer micro-dollars. $4 per million tokens is exactly 4 micro-dollars per token, so pricing a response takes a few multiplications and one rounding step:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from decimal import Decimal, ROUND_CEILING

# USD per million tokens, from platform.claude.com/docs/en/about-claude/pricing
# (checked 2026-10-02). This is configuration, not code: reload it whenever prices or
# models change. $X per million tokens is exactly X micro-dollars per token.
_RAW_PRICES = {
    &quot;claude-fable-5-1&quot;:  {&quot;input&quot;: &quot;10&quot;, &quot;write_5m&quot;: &quot;12.50&quot;, &quot;write_1h&quot;: &quot;20&quot;, &quot;read&quot;: &quot;0.25&quot;, &quot;output&quot;: &quot;50&quot;},
    &quot;claude-opus-5-5&quot;:   {&quot;input&quot;: &quot;4&quot;,  &quot;write_5m&quot;: &quot;5&quot;,     &quot;write_1h&quot;: &quot;8&quot;,  &quot;read&quot;: &quot;0.20&quot;, &quot;output&quot;: &quot;20&quot;},
    &quot;claude-sonnet-5-5&quot;: {&quot;input&quot;: &quot;2&quot;,  &quot;write_5m&quot;: &quot;2.50&quot;,  &quot;write_1h&quot;: &quot;4&quot;,  &quot;read&quot;: &quot;0.20&quot;, &quot;output&quot;: &quot;10&quot;},
    &quot;claude-haiku-4-5-20251001&quot;: {&quot;input&quot;: &quot;1&quot;, &quot;write_5m&quot;: &quot;1.25&quot;, &quot;write_1h&quot;: &quot;2&quot;, &quot;read&quot;: &quot;0.10&quot;, &quot;output&quot;: &quot;5&quot;},
}
PRICES = {m: {k: Decimal(v) for k, v in row.items()} for m, row in _RAW_PRICES.items()}
WEB_SEARCH_MICROS = Decimal(10_000)   # $10 per 1,000 searches
US_ONLY = Decimal(&quot;1.1&quot;)              # inference_geo=&quot;us&quot;: 1.1x on every token category

def _ceil(x: Decimal) -&amp;gt; int:
    return int(x.to_integral_value(rounding=ROUND_CEILING))

def cost_micros(model: str, usage) -&amp;gt; int:
    &quot;&quot;&quot;What one response cost, in micro-dollars, from its usage block.&quot;&quot;&quot;
    p = PRICES[model]
    cc = getattr(usage, &quot;cache_creation&quot;, None)
    if cc is not None:
        w5, w1h = cc.ephemeral_5m_input_tokens, cc.ephemeral_1h_input_tokens
    else:
        w5, w1h = usage.cache_creation_input_tokens or 0, 0
    tokens = (
        (usage.input_tokens or 0) * p[&quot;input&quot;]
        + w5 * p[&quot;write_5m&quot;]
        + w1h * p[&quot;write_1h&quot;]
        + (usage.cache_read_input_tokens or 0) * p[&quot;read&quot;]
        + usage.output_tokens * p[&quot;output&quot;]          # thinking is in here
    )
    if getattr(usage, &quot;inference_geo&quot;, None) == &quot;us&quot;:
        tokens *= US_ONLY
    searches = usage.server_tool_use.web_search_requests if usage.server_tool_use else 0
    return _ceil(tokens + searches * WEB_SEARCH_MICROS)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;cache_creation&lt;/code&gt; object splits cache writes by TTL, and its values add up to &lt;code&gt;cache_creation_input_tokens&lt;/code&gt;; without it, the function assumes 5-minute writes. The Batch API (50% off) and fast mode (its own rates) need rows of their own if you use them.&lt;/p&gt;
&lt;h3 id=&quot;reserve-call-settle&quot;&gt;Reserve, call, settle&lt;/h3&gt;
&lt;p&gt;You can't know what a call costs until it's done, and you can't let it start unless the budget can absorb it. Card payments solved this long ago with authorization and capture:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Estimate&lt;/strong&gt; the most the call can cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reserve&lt;/strong&gt; that amount atomically against the tenant's budget and the user's budget. If either can't absorb it, refuse now, before a single token is generated.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Call&lt;/strong&gt; the model.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Settle&lt;/strong&gt;: replace the reservation with the real cost from &lt;code&gt;usage&lt;/code&gt;, which returns the unused part to the budget.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The estimate has two halves. Input comes from the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/build-with-claude/token-counting&quot;&gt;token counting endpoint&lt;/a&gt;, which takes the same input as a Messages request, isn't billed, and has its own requests-per-minute limit. Its result is an estimate that &quot;might differ by a small amount&quot; and ignores caching, so price counted input at the highest rate the request can incur: the cache-write rate if it uses &lt;code&gt;cache_control&lt;/code&gt;. Output is bounded by &lt;code&gt;max_tokens&lt;/code&gt;, which includes all thinking in the current turn and is enforced as a strict limit. In the docs' words: &quot;Effort is soft guidance. &lt;code&gt;max_tokens&lt;/code&gt; is a strict limit.&quot;&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import anthropic

client = anthropic.Anthropic()

COUNTABLE = {&quot;model&quot;, &quot;system&quot;, &quot;messages&quot;, &quot;tools&quot;, &quot;tool_choice&quot;,
             &quot;thinking&quot;, &quot;output_config&quot;, &quot;cache_control&quot;}
COUNT_MARGIN = Decimal(&quot;1.02&quot;)   # the count is an estimate; the margin is your choice

def hold_micros(request: dict, cache_ttl: str | None = None, likely_cached: int = 0) -&amp;gt; int:
    &quot;&quot;&quot;What to reserve for one call. With likely_cached=0 it is a true upper bound:
    every input token at the highest rate it can be billed at, plus max_tokens of output.&quot;&quot;&quot;
    p = PRICES[request[&quot;model&quot;]]
    counted = client.messages.count_tokens(
        **{k: v for k, v in request.items() if k in COUNTABLE}
    ).input_tokens
    in_rate = {None: p[&quot;input&quot;], &quot;5m&quot;: p[&quot;write_5m&quot;], &quot;1h&quot;: p[&quot;write_1h&quot;]}[cache_ttl]
    fresh = max(counted - likely_cached, 0)
    return _ceil(
        fresh * COUNT_MARGIN * in_rate
        + likely_cached * p[&quot;read&quot;]
        + request[&quot;max_tokens&quot;] * p[&quot;output&quot;]
    )
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The counting endpoint rejects requests that use server tools (other than the advisor tool), the MCP connector, or images and documents passed by &lt;code&gt;url&lt;/code&gt; or &lt;code&gt;file&lt;/code&gt;; for those features, reserve from a configured ceiling. And count with the model you're about to call, since tokenizers differ between generations.&lt;/p&gt;
&lt;p&gt;The guarded call is short; &lt;code&gt;reserve&lt;/code&gt; and &lt;code&gt;settle&lt;/code&gt; come next.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def call_with_budget(c: &quot;Caller&quot;, request: dict, request_id: str, hold: int | None = None):
    keys = budget_keys(c, request_id)
    hold = hold if hold is not None else hold_micros(request)
    reserve(c, keys, hold)                    # raises BudgetExceeded or DuplicateRequest
    try:
        response = client.messages.create(**request)
    except anthropic.APIStatusError:
        settle(keys, 0)        # an error response: no message, no usage to settle
        raise
    except anthropic.APIConnectionError:
        settle(keys, hold)     # outcome unknown (timeout, dropped connection): charge the hold
        raise
    settle(keys, cost_micros(request[&quot;model&quot;], response.usage))
    record_in_ledger(c, request_id, request[&quot;model&quot;], response.usage)
    return response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;request_id&lt;/code&gt; is the idempotency key, so it must stay stable across retries of one logical request: take it from the client's idempotency header or your job ID, never from a fresh UUID per attempt. Getting &lt;code&gt;usage&lt;/code&gt; right across caching, tools and streaming is much of the day-to-day work of &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/advanced-llm-integration-in-production&quot;&gt;running LLM integrations in production&lt;/a&gt;, and it's the only reliable input a budget has.&lt;/p&gt;
&lt;h3 id=&quot;the-reservation-one-atomic-step&quot;&gt;The reservation: one atomic step&lt;/h3&gt;
&lt;p&gt;The check and the decrement must be one operation. Two concurrent requests that both read &quot;enough left&quot; and both proceed is the classic overspend, and parallel agent calls make it common. In Redis, a Lua script does it: &quot;Redis guarantees the script's atomic execution&quot;, and a script &quot;can perform conditional updates across multiple keys&quot; (&lt;a rel=&quot;nofollow&quot; href=&quot;https://redis.io/docs/latest/develop/programmability/eval-intro/&quot;&gt;Redis docs&lt;/a&gt;), here the tenant's budget and the user's budget in one step.&lt;/p&gt;
&lt;p&gt;Each budget is a hash with three fields: &lt;code&gt;limit&lt;/code&gt;, &lt;code&gt;committed&lt;/code&gt; (settled spend) and &lt;code&gt;held&lt;/code&gt; (open reservations). Each reservation is a small hash of its own, indexed by deadline in a per-tenant sorted set so abandoned ones can be found.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- reserve.lua
-- KEYS[1] tenant budget   KEYS[2] user budget   KEYS[3] hold   KEYS[4] holds index
-- ARGV[1] amount in micro-USD   ARGV[2] hold deadline (unix ms)
if redis.call('EXISTS', KEYS[3]) == 1 then
  return 'duplicate'                      -- this request ID already reserved
end

local amount = tonumber(ARGV[1])
for i, key in ipairs({KEYS[1], KEYS[2]}) do
  local b = redis.call('HMGET', key, 'limit', 'committed', 'held')
  if not b[1] then
    return 'no_budget'                    -- fail closed: no limit configured
  end
  local left = tonumber(b[1]) - (tonumber(b[2]) or 0) - (tonumber(b[3]) or 0)
  if amount &amp;gt; left then
    if i == 1 then return 'tenant' end
    return 'user'
  end
end

redis.call('HINCRBY', KEYS[1], 'held', ARGV[1])
redis.call('HINCRBY', KEYS[2], 'held', ARGV[1])
redis.call('HSET', KEYS[3], 'amount', ARGV[1], 'state', 'held',
           'tenant_key', KEYS[1], 'user_key', KEYS[2])
redis.call('ZADD', KEYS[4], ARGV[2], KEYS[3])
return 'ok'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Three details matter. The script fails closed: a budget without a &lt;code&gt;limit&lt;/code&gt; refuses. Every key it touches arrives through &lt;code&gt;KEYS&lt;/code&gt;, since Redis requires that &quot;all names of keys that a script accesses must be explicitly provided as input key arguments&quot;. And every key name carries the tenant ID as a hash tag, &lt;code&gt;{tenant}&lt;/code&gt;, which puts all of a tenant's keys in one &lt;a rel=&quot;nofollow&quot; href=&quot;https://redis.io/docs/latest/operate/oss_and_stack/reference/cluster-spec/&quot;&gt;Redis Cluster hash slot&lt;/a&gt;, so the multi-key script survives sharding.&lt;/p&gt;
&lt;p&gt;The Python side builds the keys, opens the month's budgets lazily, and turns verdicts into exceptions:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;import time
from dataclasses import dataclass
from datetime import datetime
from zoneinfo import ZoneInfo

import redis

r = redis.Redis(decode_responses=True)
reserve_script = r.register_script(open(&quot;reserve.lua&quot;).read())
settle_script = r.register_script(open(&quot;settle.lua&quot;).read())

HOLD_TTL_MS = 15 * 60 * 1000    # longer than your slowest call, retries included
TOMBSTONE_S = 7 * 24 * 3600     # how long a finished request ID stays deduplicated

@dataclass
class Caller:
    tenant_id: str
    user_id: str
    tz: str               # the tenant's billing time zone, e.g. &quot;Europe/Bucharest&quot;
    tenant_limit: int     # micro-USD per month, from the tenant's plan
    user_limit: int       # micro-USD per month for this user; equal to tenant_limit = no user cap

class BudgetExceeded(Exception):
    def __init__(self, scope: str, resets_at: datetime | None = None):
        super().__init__(scope)
        self.scope, self.resets_at = scope, resets_at

class DuplicateRequest(Exception):
    pass

def budget_keys(c: Caller, request_id: str) -&amp;gt; list[str]:
    period = datetime.now(ZoneInfo(c.tz)).strftime(&quot;%Y-%m&quot;)
    t = &quot;{&quot; + c.tenant_id + &quot;}&quot;   # hash tag: one cluster slot per tenant
    return [f&quot;budget:{t}:{period}&quot;, f&quot;budget:{t}:user:{c.user_id}:{period}&quot;,
            f&quot;hold:{t}:{request_id}&quot;, f&quot;holds:{t}&quot;]

def next_reset(tz: str) -&amp;gt; datetime:
    now = datetime.now(ZoneInfo(tz))
    year, month = (now.year + 1, 1) if now.month == 12 else (now.year, now.month + 1)
    return datetime(year, month, 1, tzinfo=ZoneInfo(tz))

def reserve(c: Caller, keys: list[str], amount: int) -&amp;gt; None:
    r.hsetnx(keys[0], &quot;limit&quot;, c.tenant_limit)   # open this month's budgets on first use
    r.hsetnx(keys[1], &quot;limit&quot;, c.user_limit)
    deadline = int(time.time() * 1000) + HOLD_TTL_MS
    verdict = reserve_script(keys=keys, args=[amount, deadline])
    if verdict == &quot;duplicate&quot;:
        raise DuplicateRequest(keys[2])
    if verdict != &quot;ok&quot;:
        raise BudgetExceeded(verdict, next_reset(c.tz))

def settle(keys: list[str], cost: int, mode: str = &quot;settle&quot;) -&amp;gt; str:
    return settle_script(keys=keys, args=[cost, mode, TOMBSTONE_S])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new month opens the first time someone spends in it, at the limit the tenant's plan has at that moment. A plan change mid-month is a plain &lt;code&gt;HSET&lt;/code&gt; of &lt;code&gt;limit&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id=&quot;settlement-idempotent-and-never-refused&quot;&gt;Settlement: idempotent, and never refused&lt;/h3&gt;
&lt;p&gt;Settlement follows two rules. It must be &lt;strong&gt;idempotent&lt;/strong&gt;, because whatever calls it will be retried: your queue, your web framework, a user clicking twice. And it must &lt;strong&gt;never refuse&lt;/strong&gt;: by the time you settle, the money is spent, and the budget's job is to record it. If the real cost exceeds the hold (the input count was an estimate), the budget goes slightly over; the overshoot is bounded by your margin and visible in the metrics.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-lua&quot;&gt;-- settle.lua
-- KEYS: the same four as reserve.lua
-- ARGV[1] real cost in micro-USD   ARGV[2] 'settle' or 'expire'   ARGV[3] tombstone TTL (s)
local h = redis.call('HMGET', KEYS[3], 'amount', 'state')
local held, state = tonumber(h[1]), h[2]
if not state then return 'unknown' end            -- never reserved, or tombstone gone
if state == 'settled' then return 'duplicate' end
if ARGV[2] == 'expire' and state ~= 'held' then return 'noop' end

local cost = tonumber(ARGV[1])
if ARGV[2] == 'expire' then cost = held end       -- the worker vanished: assume the worst

local delta = cost
if state == 'held' then
  redis.call('HINCRBY', KEYS[1], 'held', -held)
  redis.call('HINCRBY', KEYS[2], 'held', -held)
else                                              -- 'expired': the full hold was charged
  delta = cost - held
end
redis.call('HINCRBY', KEYS[1], 'committed', delta)
redis.call('HINCRBY', KEYS[2], 'committed', delta)
local final_state = 'settled'
if ARGV[2] == 'expire' then final_state = 'expired' end
redis.call('HSET', KEYS[3], 'state', final_state, 'cost', cost)
redis.call('ZREM', KEYS[4], KEYS[3])
redis.call('EXPIRE', KEYS[3], ARGV[3])
return 'ok'
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finished holds stay behind as tombstones with a TTL, so a duplicate settle does nothing. A settle that arrives after its hold was expired (next section) corrects the charge from the full hold down to the real cost. And because the budget keys live in the hold, a call reserved at 23:59 on the last day of the month settles against the month it was reserved in.&lt;/p&gt;
&lt;p&gt;Keep a durable record next to the gate. The Redis Cluster specification says &quot;usually there are small windows where acknowledged writes can be lost&quot;, which is acceptable for a gate and not for a ledger. Write every settled call to an append-only table, and rebuild &lt;code&gt;committed&lt;/code&gt; from it if Redis is ever lost or wrong:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE llm_ledger (
    request_id   TEXT        PRIMARY KEY,   -- the same ID as the Redis hold
    tenant_id    TEXT        NOT NULL,
    user_id      TEXT        NOT NULL,
    model        TEXT        NOT NULL,
    usage        JSONB       NOT NULL,      -- the raw usage block, so history can be re-priced
    cost_micros  BIGINT      NOT NULL,
    created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- record_in_ledger() is INSERT ... ON CONFLICT (request_id) DO NOTHING
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Storing the raw &lt;code&gt;usage&lt;/code&gt;, not only the cost, turns a mistake in the price table into a re-computation instead of a loss.&lt;/p&gt;
&lt;h3 id=&quot;holds-that-never-come-back&quot;&gt;Holds that never come back&lt;/h3&gt;
&lt;p&gt;A worker that reserves and then dies (OOM-killed, deployed over, cut off by the network) leaves a hold nobody will settle, and those holds eat the budget until a tenant is refused with money left on paper. Give each hold a deadline longer than your slowest legitimate call, retries included, and sweep:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def sweep_expired_holds(tenant_id: str, batch: int = 100) -&amp;gt; int:
    &quot;&quot;&quot;Run every minute or so, for each tenant with a holds index (SCAN for holds:*).&quot;&quot;&quot;
    index = &quot;holds:{&quot; + tenant_id + &quot;}&quot;
    now_ms = int(time.time() * 1000)
    expired = 0
    for hold_key in r.zrangebyscore(index, &quot;-inf&quot;, now_ms, start=0, num=batch):
        h = r.hgetall(hold_key)
        if not h:
            r.zrem(index, hold_key)
            continue
        keys = [h[&quot;tenant_key&quot;], h[&quot;user_key&quot;], hold_key, index]
        if settle_script(keys=keys, args=[0, &quot;expire&quot;, TOMBSTONE_S]) == &quot;ok&quot;:
            expired += 1
    return expired
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Charging the full hold is the conservative default: the worker may have finished the call before it died, and there's no &lt;code&gt;usage&lt;/code&gt; to prove otherwise. If you'd rather absorb that cost, release the hold at zero instead. Either way, count expired holds; a rising number means something crashes between reserve and settle.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If Postgres is your gate.&lt;/strong&gt; The same design works without Redis. A conditional &lt;code&gt;UPDATE&lt;/code&gt; is the check, the row lock makes it atomic, and a missing row in &lt;code&gt;RETURNING&lt;/code&gt; means refused:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;-- One row per budget: (tenant, period) and (tenant, user, period).
-- In one transaction: commit if both rows come back, roll back otherwise.
UPDATE llm_budget
   SET held = held + %(amount)s
 WHERE budget_key IN (%(tenant_key)s, %(user_key)s)
   AND committed + held + %(amount)s &amp;lt;= limit_micros
RETURNING budget_key;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You get durability, and the ledger insert can share the transaction. The price is that every request from one tenant serializes on that tenant's budget row, so measure before you pick a side.&lt;/p&gt;
&lt;h3 id=&quot;streaming-the-bill-arrives-at-the-end&quot;&gt;Streaming: the bill arrives at the end&lt;/h3&gt;
&lt;p&gt;Streaming changes when you learn the cost, not what it is. The &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/build-with-claude/streaming&quot;&gt;documented event flow&lt;/a&gt; is &lt;code&gt;message_start&lt;/code&gt;, the content blocks, one or more &lt;code&gt;message_delta&lt;/code&gt; events, then &lt;code&gt;message_stop&lt;/code&gt;. Input usage, cache reads and writes included, sits in the &lt;code&gt;Message&lt;/code&gt; inside &lt;code&gt;message_start&lt;/code&gt;. The output count comes in &lt;code&gt;message_delta&lt;/code&gt;, and &quot;the token counts shown in the &lt;code&gt;usage&lt;/code&gt; field of the &lt;code&gt;message_delta&lt;/code&gt; event are &lt;em&gt;cumulative&lt;/em&gt;.&quot; Two consequences follow.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Settle from the final usage, not the first.&lt;/strong&gt; The SDK's accumulator overwrites its snapshot with each cumulative count, so settle on the &lt;code&gt;usage&lt;/code&gt; of &lt;code&gt;stream.get_final_message()&lt;/code&gt;. Input can change too: the docs' server-tool example shows a larger &lt;code&gt;input_tokens&lt;/code&gt; in the final &lt;code&gt;message_delta&lt;/code&gt; than in &lt;code&gt;message_start&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is no running output meter.&lt;/strong&gt; Content deltas carry text, not token counts, and when thinking is omitted or summarized the visible text undercounts what you pay for. So you can't stop a stream &quot;when it hits the budget&quot; by counting what you've seen. The hard stop for one call is &lt;code&gt;max_tokens&lt;/code&gt;, set before it starts: when a tenant is low, size &lt;code&gt;max_tokens&lt;/code&gt; from what's left (shown below) and let the API enforce it.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;def stream_with_budget(c: Caller, request: dict, request_id: str, hold: int | None = None):
    keys = budget_keys(c, request_id)
    hold = hold if hold is not None else hold_micros(request)
    reserve(c, keys, hold)
    settled = False
    try:
        with client.messages.stream(**request) as stream:
            for text in stream.text_stream:
                yield text            # a client disconnect arrives here as GeneratorExit
            final = stream.get_final_message()
        settle(keys, cost_micros(request[&quot;model&quot;], final.usage))
        settled = True
        record_in_ledger(c, request_id, request[&quot;model&quot;], final.usage)
    finally:
        if not settled:
            # Aborted or failed mid-stream: the final message_delta never arrived, and
            # thinking you never saw was still generated. Charge the hold; reconcile later.
            settle(keys, hold)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When the user closes the tab, you have two honest options: charge the hold (conservative) or estimate from what you streamed (generous, and blind to thinking). Choose deliberately, and let reconciliation show what the choice costs.&lt;/p&gt;
&lt;h3 id=&quot;agent-loops-budget-the-step-not-the-session&quot;&gt;Agent loops: budget the step, not the session&lt;/h3&gt;
&lt;p&gt;An agent run is a series of model calls with tools in between, and its context grows at every step. Reserving for the whole session up front looks tidy and behaves badly: the pessimistic total is locked away from the tenant's other users for the whole run, and a crash leaks all of it. Reserve per step instead, each step with its own reserve, call, settle and idempotency key, and give the run a ceiling of its own on top.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;STEP_MAX_TOKENS = 8_000      # per-step output cap, whatever the feature's default is

def agent_step(c: Caller, run: dict, request: dict, cache_ttl: str | None = &quot;5m&quot;):
    request = {**request, &quot;max_tokens&quot;: min(request[&quot;max_tokens&quot;], STEP_MAX_TOKENS)}
    hold = hold_micros(request, cache_ttl, likely_cached=run[&quot;cached_prefix&quot;])
    if run[&quot;spent&quot;] + hold &amp;gt; run[&quot;ceiling&quot;]:
        raise BudgetExceeded(&quot;run&quot;)           # the run's own cap, separate from the month
    response = call_with_budget(c, request, f&quot;{run['id']}:{run['step']}&quot;, hold=hold)
    u = response.usage
    run[&quot;spent&quot;] += cost_micros(request[&quot;model&quot;], u)
    run[&quot;cached_prefix&quot;] = (u.cache_read_input_tokens or 0) + (u.cache_creation_input_tokens or 0)
    run[&quot;step&quot;] += 1
    return response
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The per-step &lt;code&gt;max_tokens&lt;/code&gt; cap stops one runaway step from eating the run's allowance. &lt;code&gt;likely_cached&lt;/code&gt; fixes a quieter problem: after the first step, most of an agent's input is the conversation so far, already in the cache. On Claude Opus 5.5 that prefix costs $0.20 per million tokens as a cache read and $5 as a 5-minute cache write. Reserve it at the write rate and late-run reservations come out many times too large, refusing tenants who still have budget; price it as a read and accept a small overshoot when the cache misses. The run's other stop conditions belong to the loop, which is the territory of &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/ai-agents-architecture-and-automation&quot;&gt;designing agents that stay inside their limits&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;soft-limits-degrade-hard-limits-refuse&quot;&gt;Soft limits degrade, hard limits refuse&lt;/h3&gt;
&lt;p&gt;Give every budget two thresholds. Past the &lt;strong&gt;soft&lt;/strong&gt; one, keep serving but spend less:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Route new conversations to a cheaper model.&lt;/strong&gt; Claude Sonnet 5.5 costs $2/$10 per million input/output tokens against $4/$20 for Opus 5.5, but its cache reads cost the same $0.20, so cache-heavy workloads save less than the headline suggests. Don't switch in the middle of a conversation: Sonnet 5.5 can't read Opus 5.5's thinking blocks, so the earlier turns' reasoning is dropped.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lower the effort.&lt;/strong&gt; On Claude Opus 5.5 the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/build-with-claude/effort&quot;&gt;effort docs&lt;/a&gt; call effort &quot;the primary control for how much the model reasons and what a request costs&quot;. Changing it invalidates the cached message blocks, so this too is a decision for the start of a conversation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Shrink &lt;code&gt;max_tokens&lt;/code&gt;&lt;/strong&gt; to what the budget can still pay for.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A cheaper model isn't automatically a cheaper answer. Sonnet 5.5 defaults to &lt;code&gt;high&lt;/code&gt; effort where Opus 5.5 defaults to &lt;code&gt;medium&lt;/code&gt;, so compare cost per finished task on your own traffic, not price per token.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;SOFT_LIMIT = Decimal(&quot;0.80&quot;)   # a starting point; tune it per product
CHEAPER = {&quot;claude-fable-5-1&quot;: &quot;claude-opus-5-5&quot;, &quot;claude-opus-5-5&quot;: &quot;claude-sonnet-5-5&quot;}
MIN_USEFUL_OUTPUT = 1_024      # below this, refuse instead of returning a stub

def headroom(c: Caller, keys: list[str]) -&amp;gt; tuple[int, Decimal]:
    &quot;&quot;&quot;(micro-USD left, share used) for the tighter of the two budgets. For routing only:
    the reservation script remains the only check that counts.&quot;&quot;&quot;
    left, used = None, Decimal(0)
    for key, default in ((keys[0], c.tenant_limit), (keys[1], c.user_limit)):
        limit, committed, held = r.hmget(key, &quot;limit&quot;, &quot;committed&quot;, &quot;held&quot;)
        limit = int(limit or default)
        spent = int(committed or 0) + int(held or 0)
        left = limit - spent if left is None else min(left, limit - spent)
        used = max(used, Decimal(spent) / limit if limit &amp;gt; 0 else Decimal(1))
    return left, used

def plan_call(c: Caller, keys: list[str], request: dict, new_conversation: bool) -&amp;gt; tuple[dict, int]:
    left, used = headroom(c, keys)
    if used &amp;gt;= SOFT_LIMIT and new_conversation and request[&quot;model&quot;] in CHEAPER:
        request = {**request, &quot;model&quot;: CHEAPER[request[&quot;model&quot;]]}
    p = PRICES[request[&quot;model&quot;]]
    input_hold = hold_micros({**request, &quot;max_tokens&quot;: 0})
    affordable = int(Decimal(left - input_hold) / p[&quot;output&quot;])
    # Never go below the floor: if even that doesn't fit, the reservation refuses,
    # and it knows which budget (tenant or user) ran out.
    max_tokens = max(MIN_USEFUL_OUTPUT, min(request[&quot;max_tokens&quot;], affordable))
    return {**request, &quot;max_tokens&quot;: max_tokens}, input_hold + _ceil(max_tokens * p[&quot;output&quot;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The floor matters on models with thinking always on: a call squeezed to a few hundred output tokens can spend them all reasoning and stop with &lt;code&gt;stop_reason: &quot;max_tokens&quot;&lt;/code&gt; before saying anything useful. The tenant pays and gets nothing.&lt;/p&gt;
&lt;p&gt;Past the &lt;strong&gt;hard&lt;/strong&gt; threshold, refuse, clearly and honestly. Not a generic 500, not an endless spinner, not a silent switch to worse answers. A good refusal says what happened, whose budget it was, when it resets and who can change it, with an error code that tells clients not to retry:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;REFUSALS = {
    &quot;tenant&quot;: &quot;Your organization has used this month's AI budget.&quot;,
    &quot;user&quot;: &quot;You've used your share of your organization's AI budget for this month.&quot;,
    &quot;run&quot;: &quot;This task reached its spending cap before it finished.&quot;,
}

def refusal_body(e: BudgetExceeded) -&amp;gt; dict:
    reason = REFUSALS.get(e.scope, REFUSALS[&quot;tenant&quot;])
    resets = e.resets_at
    return {
        &quot;error&quot;: &quot;ai_budget_exhausted&quot;,     # distinct code: clients must not auto-retry
        &quot;scope&quot;: e.scope,
        &quot;resets_at&quot;: resets.isoformat() if resets else None,
        &quot;message&quot;: reason + (f&quot; It resets on {resets.day} {resets:%B %Y}.&quot; if resets else &quot;&quot;)
                   + &quot; An administrator can raise the limit in billing settings.&quot;,
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The message never implies the allowance was open-ended. The plan had an AI budget, and this is it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Two levels, because the tenant pays and users spend.&lt;/strong&gt; The tenant budget protects your margin; the per-user budget inside it protects the tenant's other employees from one colleague's batch job. Let the tenant's administrator set the per-user share, and check both in one atomic step, as the Lua script does, so a reservation that fits the user but not the tenant never half-commits.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Windows need a time zone.&lt;/strong&gt; &quot;Resets on the 1st&quot; should mean midnight where the customer is, so the window key uses the tenant's billing time zone. Keep ledger timestamps in UTC anyway: the provider's cost buckets are UTC days.&lt;/p&gt;
&lt;h3 id=&quot;reconcile-against-the-provider-s-numbers&quot;&gt;Reconcile against the provider's numbers&lt;/h3&gt;
&lt;p&gt;Your ledger is a model of the bill, so check it against the bill. Anthropic's &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/manage-claude/usage-cost-api&quot;&gt;Usage and Cost Admin API&lt;/a&gt; has two endpoints. &lt;code&gt;/v1/organizations/usage_report/messages&lt;/code&gt; reports tokens in 1-minute, 1-hour or 1-day buckets, groupable by API key, workspace, model, service tier and a few more dimensions. &lt;code&gt;/v1/organizations/cost_report&lt;/code&gt; reports USD costs in daily buckets, groupable by workspace or by description, which carries the model. Both need Admin API credentials such as an Admin API key (&lt;code&gt;sk-ant-admin01-...&lt;/code&gt;), they aren't available to individual accounts, and data &quot;typically appears within 5 minutes of API request completion&quot;.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;from collections import defaultdict

import httpx

def provider_cost_by_model(starting_at: str, ending_at: str, admin_key: str) -&amp;gt; dict[str, Decimal]:
    &quot;&quot;&quot;Micro-dollars per model (or per non-token cost type) for whole UTC days.&quot;&quot;&quot;
    totals: dict[str, Decimal] = defaultdict(Decimal)
    params = {&quot;starting_at&quot;: starting_at, &quot;ending_at&quot;: ending_at, &quot;group_by[]&quot;: &quot;description&quot;}
    while True:
        resp = httpx.get(
            &quot;https://api.anthropic.com/v1/organizations/cost_report&quot;,
            params=params,
            headers={&quot;x-api-key&quot;: admin_key, &quot;anthropic-version&quot;: &quot;2023-06-01&quot;},
        )
        resp.raise_for_status()
        body = resp.json()
        for bucket in body[&quot;data&quot;]:
            for item in bucket[&quot;results&quot;]:
                # &quot;amount&quot; is a decimal string in cents: &quot;123.45&quot; means $1.23
                totals[item[&quot;model&quot;] or item[&quot;cost_type&quot;]] += Decimal(item[&quot;amount&quot;]) * 10_000
        if not body[&quot;has_more&quot;]:
            return totals
        params[&quot;page&quot;] = body[&quot;next_page&quot;]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Compare the totals per model and UTC day with the sum of &lt;code&gt;cost_micros&lt;/code&gt; in your ledger. None of the provider's dimensions is your customer, so reconciliation checks totals and your ledger does the attribution. When they disagree, the usual suspects are a stale price table, a code path that calls the model around the guard (background jobs and new features are the classics), aborted streams and expired holds, or a pricing modifier you didn't model. Priority Tier costs aren't in the cost endpoint; track them through the usage endpoint. Pick a drift threshold and alert on it.&lt;/p&gt;
&lt;h3 id=&quot;four-numbers-to-watch&quot;&gt;Four numbers to watch&lt;/h3&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Settled cost per tenant, against time elapsed in the window.&lt;/strong&gt; A tenant at 60% of the month's budget on day 5 is a conversation for this week, not day 12. That's burn rate, the idea SRE teams apply to error budgets, and &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/ai-for-devops-and-sre-aiops&quot;&gt;operating AI workloads with SRE discipline&lt;/a&gt; takes it well beyond cost.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Refusal and degrade rates, split by scope&lt;/strong&gt; (tenant, user, run). A rising user-scope rate inside one tenant usually means the per-user shares are wrong, not that anyone is misbehaving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reserved versus settled&lt;/strong&gt;, per feature. Reserving ten times what you spend means refusing tenants who still have budget; tighten that feature's &lt;code&gt;max_tokens&lt;/code&gt; rather than raising the limit.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Expired holds and reconciliation drift.&lt;/strong&gt; Both should sit near zero and stay flat. Either one climbing means something crashes between reserve and settle, or calls the model around the guard.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Keep per-tenant cost in the ledger rather than as a label on a high-cardinality time series. If you trace model calls already, put the tenant ID and settled cost on the call's span, so an expensive request leads straight to its customer.&lt;/p&gt;
&lt;h3 id=&quot;faq&quot;&gt;FAQ&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Why not count tokens per tenant instead of money?&lt;/strong&gt;&lt;br&gt;
Because tokens don't have one price: inside one model an output token costs 100 times a cache read, and newer tokenizers change the count for the same text. Money is the only unit that matches the bill you pay.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Redis or Postgres for the budget?&lt;/strong&gt;&lt;br&gt;
Redis for the gate when request rates are high and latency matters, Postgres for the ledger in every case. Postgres alone is a fine gate while no single tenant's traffic makes its budget row a hot spot.&lt;/p&gt;
&lt;h3 id=&quot;the-short-version&quot;&gt;The short version&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Provider rate limits and spend caps protect the provider and your organization as a whole. Neither knows your customers.&lt;/li&gt;
&lt;li&gt;Budget in micro-dollars, priced from every &lt;code&gt;usage&lt;/code&gt; field, with thinking counted as output.&lt;/li&gt;
&lt;li&gt;Reserve the worst case atomically against tenant and user, call, then settle idempotently from real usage. Settlement never refuses.&lt;/li&gt;
&lt;li&gt;Expire abandoned holds and keep a durable ledger behind the Redis gate.&lt;/li&gt;
&lt;li&gt;In streams, usage arrives at the end, so the hard stop is &lt;code&gt;max_tokens&lt;/code&gt; sized from what's left.&lt;/li&gt;
&lt;li&gt;Reserve per agent step, cap each step, and price the cached prefix as a cache read.&lt;/li&gt;
&lt;li&gt;Degrade at the soft limit, refuse honestly at the hard one, and reconcile daily against the Cost API.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;None of this makes a call cheaper. It makes every call's cost belong to someone, which you need before you can price the feature, sell it to larger customers, or sleep through a customer's nightly batch job.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Metering, tenancy and billing are the unglamorous half of every AI product. For the full path from prototype to paying customers, see &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/en/courses/build-and-ship-a-production-ai-saas&quot;&gt;the hands-on course on shipping a production AI SaaS&lt;/a&gt; on Cursuri-AI.ro, Eastern Europe's AI education platform.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/29520/per-tenant-llm-budgets-stop-one-customer-from-spending-your-whole-model-bill</guid>
<pubDate>Fri, 02 Oct 2026 11:41:58 +0000</pubDate>
</item>
<item>
<title>Claude Fable 5.1 Made Your Conversation History Append-Only</title>
<link>https://coderlegion.com/26081/claude-fable-5-1-made-your-conversation-history-append-only</link>
<description>&lt;p&gt;There is a 400 error in Claude Fable 5.1 that you will probably not see in development, will not see in staging, and will see in production — from users whose API accounts are newer than yours.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;messages.5.content.0: Invalid `signature` in `thinking` block. The block is
bound to a different conversation. Remove the block, or set
`thinking.block_binding.prefix_mismatch_behavior` to &quot;drop_block&quot;.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It fires when your code does something that has been completely normal for two years: modifying the conversation history you send back to the model. Trimming an old turn. Swapping in a summary. Rebuilding the &lt;code&gt;tools&lt;/code&gt; array between requests. Editing a previous assistant message before replaying it.&lt;/p&gt;
&lt;p&gt;On &lt;code&gt;claude-fable-5-1&lt;/code&gt;, released 1 September 2026, conversation history is effectively &lt;strong&gt;append-only&lt;/strong&gt;. This piece is about what that means in code, which patterns just became illegal, what replaced them, and why the person who maintains an agent framework is the last to find out.&lt;/p&gt;
&lt;h3 id=&quot;what-a-thinking-block-is-actually-bound-to&quot;&gt;What a thinking block is actually bound to&lt;/h3&gt;
&lt;p&gt;Fable 5.1 runs adaptive thinking always on — it cannot be disabled, and &lt;code&gt;thinking: {type: &quot;disabled&quot;}&lt;/code&gt; returns a 400 at any effort level. Every response therefore carries &lt;code&gt;thinking&lt;/code&gt; blocks, and you pass them back on subsequent turns.&lt;/p&gt;
&lt;p&gt;Those blocks are not inert payload. Each one carries a signature, and the documentation is precise about what it covers:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;Each &lt;code&gt;thinking&lt;/code&gt; block from Claude Fable 5.1 is valid only against the &lt;code&gt;system&lt;/code&gt; prompt, &lt;code&gt;tools&lt;/code&gt;, and conversation history that preceded it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Three things, then, are frozen for the life of a thinking block:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The &lt;strong&gt;system prompt&lt;/strong&gt; as it was when the block was produced.&lt;/li&gt;
&lt;li&gt;The &lt;strong&gt;tools&lt;/strong&gt; array as it was when the block was produced.&lt;/li&gt;
&lt;li&gt;Every &lt;strong&gt;message that came before it&lt;/strong&gt;, byte for byte.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Change any one and send the block back, and the request is rejected. Not degraded — rejected, with a permanent error for that request body. As the docs put it: an automatic retry loop won't clear it.&lt;/p&gt;
&lt;p&gt;There is a second binding on top of the first: blocks record which model produced them. Fable 5.1 reads its own blocks and those from Mythos 5.1, Opus 5, Fable 5, Mythos 5, and earlier models — so a conversation moving &lt;em&gt;up&lt;/em&gt; to Fable 5.1 keeps its reasoning. The relationship is one-way. Apart from Mythos 5.1, none of those models can read Fable 5.1's blocks, so a conversation moving &lt;em&gt;down&lt;/em&gt; to Opus 5 drops them.&lt;/p&gt;
&lt;h3 id=&quot;why-you-will-find-out-last&quot;&gt;Why you will find out last&lt;/h3&gt;
&lt;p&gt;This is the part worth reading twice, because it inverts the usual order of discovery.&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The API enforces the check for new accounts created on or after August 31, 2026. For accounts created earlier, the API records the mismatch but doesn't act on it unless the request sets &lt;code&gt;thinking.block_binding.prefix_mismatch_behavior&lt;/code&gt;, which opts into enforcement.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Your account is almost certainly older than 31 August 2026. Your CI account is older. Your team's accounts are older. On all of them, the API quietly notes the mismatch and serves the request anyway.&lt;/p&gt;
&lt;p&gt;Your users' accounts are not older. Anyone who signed up this week is enforced by default.&lt;/p&gt;
&lt;p&gt;Anthropic states the trap plainly: if you ship a tool or framework that people run with their own API key, test with the field set before launch, because your key is probably on an older account and your users on new ones hit the check before you do. And the reprieve is temporary — enforcement is planned for every account on future models.&lt;/p&gt;
&lt;p&gt;So the practical reading is: &lt;strong&gt;this is a deadline, not an outage.&lt;/strong&gt; You have one model generation to make your history handling append-only, and a free way to test against the future today.&lt;/p&gt;
&lt;p&gt;To find out where your own account stands, send a request that edits history without the beta header. A 400 that names the header means you're enforced.&lt;/p&gt;
&lt;h3 id=&quot;the-patterns-that-are-now-illegal&quot;&gt;The patterns that are now illegal&lt;/h3&gt;
&lt;p&gt;If Claude Code, claude.ai, Claude Managed Agents, or the Claude Agent SDK owns your conversation history, they already keep the prefix intact and none of this applies. &lt;strong&gt;If your code builds the &lt;code&gt;messages&lt;/code&gt; array itself, all of it applies.&lt;/strong&gt; Here is the list, roughly in order of how common it is in real codebases.&lt;/p&gt;
&lt;h4 id=&quot;1-client-side-truncation-and-summarization&quot;&gt;1. Client-side truncation and summarization&lt;/h4&gt;
&lt;p&gt;The classic loop: when the conversation exceeds N tokens, drop or summarize the oldest turns and send the shortened array. Every thinking block after the edit point is now bound to a prefix that no longer exists.&lt;/p&gt;
&lt;h4 id=&quot;2-snipping-a-turn-out-of-the-middle&quot;&gt;2. Snipping a turn out of the middle&lt;/h4&gt;
&lt;p&gt;The worst case, and the one with no client-side workaround at all. The docs are blunt: don't do it — that invalidates every later thinking block and no client-side shape avoids it.&lt;/p&gt;
&lt;h4 id=&quot;3-rebuilding-system-or-tools-per-request&quot;&gt;3. Rebuilding &lt;code&gt;system&lt;/code&gt; or &lt;code&gt;tools&lt;/code&gt; per request&lt;/h4&gt;
&lt;p&gt;Easy to do by accident. A system prompt that interpolates the current date, a tools array assembled from a dict in non-deterministic order, a feature flag that adds a tool mid-session — each one changes the bound prefix. This is also, not coincidentally, the same class of bug that destroys your prompt cache hit rate; the two failures have exactly one root cause.&lt;/p&gt;
&lt;h4 id=&quot;4-keep-tail-compaction&quot;&gt;4. Keep-tail compaction&lt;/h4&gt;
&lt;p&gt;Summarize the old turns, keep the last few verbatim behind the summary. Those recent turns carry thinking produced against the &lt;em&gt;full&lt;/em&gt; history, and they now sit behind a summary that replaced it. They fail.&lt;/p&gt;
&lt;h4 id=&quot;5-background-compaction&quot;&gt;5. Background compaction&lt;/h4&gt;
&lt;p&gt;Build the summary off the critical path, swap it in when ready. Every turn produced between the snapshot and the swap carries thinking that predates the new prefix.&lt;/p&gt;
&lt;h4 id=&quot;6-editing-assistant-turns-for-control&quot;&gt;6. Editing assistant turns for control&lt;/h4&gt;
&lt;p&gt;Rewriting a previous assistant message to steer the model, injecting synthetic assistant turns as few-shot examples mid-conversation, replaying a &quot;corrected&quot; version of what the model said. This family of techniques is what the change is aimed at — Anthropic frames it as an anti-distillation measure, describing it as no longer being possible for new API accounts to manually edit Claude's prior context in a multi-turn conversation while preserving the transcript of Claude's prior thinking.&lt;/p&gt;
&lt;p&gt;Note that assistant prefill was already removed across the 4.6+ family, returning a 400. If you were reaching for prefill as the workaround here, that door closed first.&lt;/p&gt;
&lt;h3 id=&quot;the-three-legal-shapes-for-client-side-compaction&quot;&gt;The three legal shapes for client-side compaction&lt;/h3&gt;
&lt;p&gt;You do not have to give up compaction. You have to pick a shape that carries no stale thinking. The documentation names three:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Simple compaction — the recommended one.&lt;/strong&gt; Replace the entire history with one summary message plus the new user turn, and replay nothing else. No thinking blocks carry over, so nothing can mismatch. The docs note that Claude models are trained on long-horizon tasks with exactly this scheme and that it performs comparably to more elaborate approaches for most workloads. If you are choosing today, choose this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Keep-tail compaction.&lt;/strong&gt; If you insist on keeping recent turns verbatim behind a summary, strip the &lt;code&gt;thinking&lt;/code&gt; and &lt;code&gt;redacted_thinking&lt;/code&gt; blocks from those turns — text and tool calls can stay — or opt into &lt;code&gt;drop_block&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Background compaction.&lt;/strong&gt; If the summary is built asynchronously and swapped in later, send &lt;code&gt;drop_block&lt;/code&gt; on every request that still carries thinking produced before the swap, strip those blocks yourself, or compact synchronously and accept the latency.&lt;/p&gt;
&lt;h3 id=&quot;the-better-answer-stop-editing-on-the-client&quot;&gt;The better answer: stop editing on the client&lt;/h3&gt;
&lt;p&gt;All three shapes above are ways to keep doing the work in the wrong place. The cleaner fix is to move context management to the server, where it doesn't count as an edit at all:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Server-side compaction&lt;/strong&gt; (beta header &lt;code&gt;compact-2026-01-12&lt;/code&gt;) summarizes earlier context when the conversation approaches a threshold, and its &lt;code&gt;instructions&lt;/code&gt; parameter accepts your own summarization prompt — so you keep editorial control over what survives.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Context editing&lt;/strong&gt; (beta &lt;code&gt;context-management-2025-06-27&lt;/code&gt;) clears old tool results or thinking blocks with strategies like &lt;code&gt;clear_tool_uses_20250919&lt;/code&gt; and &lt;code&gt;clear_thinking_20251015&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The reason these are safe is precise and worth internalizing: the history check compares the conversation &lt;strong&gt;as you sent it&lt;/strong&gt;. Anything the server removes after that point cannot invalidate a later thinking block.&lt;/p&gt;
&lt;p&gt;One caveat if you adopt server-side compaction: append &lt;code&gt;response.content&lt;/code&gt; back to your messages on every turn, not just the extracted text. Compaction blocks in the response are how the API replaces the compacted history on the next request. Pull out the string and append that, and you silently lose the compaction state.&lt;/p&gt;
&lt;p&gt;For the mid-session changes you used to make by rewriting the prefix, there are now first-class channels. A &lt;code&gt;role: &quot;system&quot;&lt;/code&gt; message appended to &lt;code&gt;messages[]&lt;/code&gt; changes instructions without touching the cached prefix. Tool changes go through &lt;code&gt;tool_addition&lt;/code&gt; / &lt;code&gt;tool_removal&lt;/code&gt; blocks (beta &lt;code&gt;mid-conversation-tool-changes-2026-07-01&lt;/code&gt;, with the full tool set declared at session start). Per-turn reminders go in turn-scoped system messages that you leave in the history — they stop rendering after the next user message and cost no tokens once cleared.&lt;/p&gt;
&lt;p&gt;That is the mental shift the whole release is asking for: &lt;strong&gt;you no longer edit the conversation, you append instructions to it.&lt;/strong&gt; If your agent architecture treats &lt;code&gt;messages&lt;/code&gt; as a mutable buffer you rewrite each turn, that assumption is what needs to change — a design question we spend real time on in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/courses/ai-agents-automatizare&quot;&gt;agent architecture course&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;the-escape-hatch-and-why-it-isn-t-a-fix&quot;&gt;The escape hatch, and why it isn't a fix&lt;/h3&gt;
&lt;p&gt;Send the beta header &lt;code&gt;thinking-binding-controls-2026-08-01&lt;/code&gt; and set &lt;code&gt;prefix_mismatch_behavior&lt;/code&gt; to &lt;code&gt;&quot;drop_block&quot;&lt;/code&gt; (the default is &lt;code&gt;&quot;error&quot;&lt;/code&gt;):&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-python&quot;&gt;response = client.messages.create(
    model=&quot;claude-fable-5-1&quot;,
    max_tokens=16000,
    thinking={
        &quot;type&quot;: &quot;adaptive&quot;,
        &quot;block_binding&quot;: {&quot;prefix_mismatch_behavior&quot;: &quot;drop_block&quot;},
    },
    messages=messages,
)

for t in getattr(response, &quot;input_transformations&quot;, []) or []:
    if t.reason == &quot;prefix_binding_mismatch&quot;:
        log.warning(&quot;dropped thinking block: %s&quot;, t)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Instead of a 400, the API drops the mismatched block &lt;strong&gt;and every thinking block after it&lt;/strong&gt;, reporting each one in the response's &lt;code&gt;input_transformations&lt;/code&gt; array with &lt;code&gt;reason: &quot;prefix_binding_mismatch&quot;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Understand the trade. You are not fixing the mismatch, you are discarding the model's preserved reasoning from that point forward and paying for it in quality on exactly the long-horizon tasks you bought Fable 5.1 for. Use &lt;code&gt;drop_block&lt;/code&gt; as a shock absorber and a diagnostic — not as your architecture.&lt;/p&gt;
&lt;h3 id=&quot;the-other-breaking-change-in-the-same-release&quot;&gt;The other breaking change in the same release&lt;/h3&gt;
&lt;p&gt;While you're in the migration, there is a second one that hits a completely different code path. &lt;strong&gt;Forced tool choice is gone.&lt;/strong&gt; Fable 5 accepted &lt;code&gt;tool_choice&lt;/code&gt; values &lt;code&gt;auto&lt;/code&gt;, &lt;code&gt;none&lt;/code&gt;, &lt;code&gt;any&lt;/code&gt;, and &lt;code&gt;tool&lt;/code&gt;. On &lt;code&gt;claude-fable-5-1&lt;/code&gt;, &lt;code&gt;{type: &quot;any&quot;}&lt;/code&gt; and &lt;code&gt;{type: &quot;tool&quot;, name: &quot;...&quot;}&lt;/code&gt; return a 400:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-text&quot;&gt;tool_choice: type &quot;tool&quot; and &quot;any&quot; are not supported for this model.
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The check applies on the Messages API, the Message Batches API, and the token counting endpoint — so a token-counting call in your cost estimator will fail too, which is a fun way to discover it.&lt;/p&gt;
&lt;p&gt;If you used forced tool choice to guarantee structured output, the replacement is &lt;code&gt;{type: &quot;auto&quot;}&lt;/code&gt; plus an explicit instruction and &lt;code&gt;strict: true&lt;/code&gt; on the tool, or structured outputs via &lt;code&gt;output_config.format&lt;/code&gt;. If your application genuinely requires a specific call on the current turn, put the instruction in the &lt;code&gt;user&lt;/code&gt; turn or in a mid-conversation &lt;code&gt;role: &quot;system&quot;&lt;/code&gt; message. Structured extraction pipelines built on forced tool choice are the most likely thing in your codebase to break on the model ID swap alone — &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/courses/advanced-llm-integration&quot;&gt;the structured outputs approach&lt;/a&gt; is the durable replacement.&lt;/p&gt;
&lt;h3 id=&quot;a-20-minute-audit&quot;&gt;A 20-minute audit&lt;/h3&gt;
&lt;p&gt;Run this before you switch traffic, not after.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Decide whether you're exposed at all.&lt;/strong&gt; Does your code construct the &lt;code&gt;messages&lt;/code&gt; array? If a managed harness owns it, you're done.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grep for the mutation.&lt;/strong&gt; Look for anything that slices, pops, filters, or rewrites a messages list; anything that rebuilds &lt;code&gt;system&lt;/code&gt; or &lt;code&gt;tools&lt;/code&gt; per request; anything that sorts or serializes tool schemas non-deterministically.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Turn on enforcement deliberately.&lt;/strong&gt; Run a representative multi-turn session with &lt;code&gt;thinking-binding-controls-2026-08-01&lt;/code&gt; and &lt;code&gt;prefix_mismatch_behavior: &quot;drop_block&quot;&lt;/code&gt;, and log every entry in &lt;code&gt;input_transformations&lt;/code&gt;. Each &lt;code&gt;prefix_binding_mismatch&lt;/code&gt; is a real bug in your history handling. &lt;code&gt;model_binding_mismatch&lt;/code&gt; entries after a deliberate model switch are expected and fine.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Fix the shape, not the symptom.&lt;/strong&gt; Move trimming server-side, or adopt simple compaction.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Freeze the prefix.&lt;/strong&gt; System prompt and tools set once at session start; every mid-session change goes through a &lt;code&gt;role: &quot;system&quot;&lt;/code&gt; message.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Replace forced &lt;code&gt;tool_choice&lt;/code&gt;.&lt;/strong&gt; Including in token-counting calls.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pick a production &lt;code&gt;prefix_mismatch_behavior&lt;/code&gt; and monitor it.&lt;/strong&gt; &lt;code&gt;&quot;error&quot;&lt;/code&gt; fails loudly; &lt;code&gt;&quot;drop_block&quot;&lt;/code&gt; degrades quietly. Both are defensible — an unmonitored default is not.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;If you ship a framework, test with the field set.&lt;/strong&gt; Your users' accounts are newer than yours.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Steps 2 and 3 are the ones that find real bugs. Everything else is bookkeeping.&lt;/p&gt;
&lt;h3 id=&quot;why-this-is-happening&quot;&gt;Why this is happening&lt;/h3&gt;
&lt;p&gt;It is worth understanding the motivation, because it tells you which direction the next release moves.&lt;/p&gt;
&lt;p&gt;The ability to hand a model an edited transcript of its own prior reasoning is the core primitive in distillation attacks — you reconstruct a frontier model's reasoning by feeding it modified versions of what it previously produced and observing how it continues. Binding each thinking block to the exact prefix that produced it closes that off without removing the block from the API surface.&lt;/p&gt;
&lt;p&gt;It sits alongside the other provenance measure shipped in the same release: models released after 2 August 2026 carry a watermark in their text output — described as a numerical way of determining the likelihood that Claude was involved in writing a piece of text — invisible to anyone without the detection API.&lt;/p&gt;
&lt;p&gt;Both point the same way. The frontier vendors are hardening the boundary around model outputs, and the API surface is getting less malleable with each release. Building on the assumption that you can freely rewrite what the model produced is now building against the direction of travel. That is a security and architecture judgement as much as an API one — the kind of reasoning we work through in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/courses/ai-security-ethics&quot;&gt;AI security course&lt;/a&gt;, and in the &lt;a rel=&quot;nofollow&quot; href=&quot;https://cursuri-ai.ro/courses/claude-code-mastery-coding-agentic&quot;&gt;Claude Code and agentic coding course&lt;/a&gt; for teams building harnesses of their own.&lt;/p&gt;
&lt;h3 id=&quot;the-short-version-1&quot;&gt;The short version&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;On Fable 5.1, each &lt;code&gt;thinking&lt;/code&gt; block is bound to the &lt;code&gt;system&lt;/code&gt;, &lt;code&gt;tools&lt;/code&gt;, and history that preceded it. Edit any of them and the request is a 400.&lt;/li&gt;
&lt;li&gt;Enforced by default for accounts created on or after &lt;strong&gt;31 August 2026&lt;/strong&gt;. Older accounts are unaffected &lt;em&gt;for now&lt;/em&gt; — Anthropic plans to enforce it for everyone on future models.&lt;/li&gt;
&lt;li&gt;If you ship a framework, your users are enforced before you are. Test with &lt;code&gt;thinking-binding-controls-2026-08-01&lt;/code&gt; and &lt;code&gt;prefix_mismatch_behavior: &quot;drop_block&quot;&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Client-side trimming, keep-tail summaries, background compaction swaps and mid-transcript snipping all break. Simple compaction, server-side compaction, and context editing don't.&lt;/li&gt;
&lt;li&gt;Forced &lt;code&gt;tool_choice&lt;/code&gt; (&lt;code&gt;any&lt;/code&gt; / &lt;code&gt;tool&lt;/code&gt;) is also gone — a 400, including on token counting.&lt;/li&gt;
&lt;li&gt;Freeze the prefix; append instructions instead of editing history.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;If you're planning a context strategy change off the back of this, there's a money-side companion to it: Fable 5.1 also cut cache reads to $0.25/MTok, which makes summarizing-to-save-money a losing trade in most cases. I wrote that up on dev.to as &lt;em&gt;&quot;Fable 5.1 Cache Reads Cost $0.25/MTok.&quot;&lt;/em&gt; The two decisions interact — the cheap path and the legal path happen to be the same one, which is unusually convenient.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;Every behavior, error string, header and date in this article was checked against Anthropic's official documentation on 2 September 2026 — principally the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/models/fable-5-1/migration-guide&quot;&gt;Fable 5.1 migration guide&lt;/a&gt; and the &lt;a rel=&quot;nofollow&quot; href=&quot;https://platform.claude.com/docs/en/build-with-claude/preserved-thinking&quot;&gt;preserved thinking guide&lt;/a&gt;. Beta headers and enforcement dates change; verify against current docs before shipping.&lt;/em&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/26081/claude-fable-5-1-made-your-conversation-history-append-only</guid>
<pubDate>Wed, 02 Sep 2026 13:45:11 +0000</pubDate>
</item>
<item>
<title>I released **oteldoctor v0.1.0** — an open-source Go CLI for checking OpenTelemetry Collector config</title>
<link>https://coderlegion.com/18690/released-oteldoctor-v0-an-open-source-go-cli-for-checking-opentelemetry-collector-config</link>
<description>&lt;p&gt;OpenTelemetry Collector configs are easy to grow and hard to reason about.&lt;/p&gt;
&lt;p&gt;A config can be valid and still have production risks:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;exporters:
  otlphttp:
    endpoint: http://backend.example.com
    headers:
      X-API-KEY: &quot;hardcoded-secret&quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;or:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-yaml&quot;&gt;service:
  pipelines:
    traces:
      processors: [batch, memory_limiter]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;These are the kinds of things oteldoctor is designed to catch.&lt;/p&gt;
&lt;h3 id=&quot;what-it-does&quot;&gt;What it does&lt;/h3&gt;
&lt;p&gt;oteldoctor checks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;undefined receivers/processors/exporters&lt;/li&gt;
&lt;li&gt;missing &lt;code&gt;memory_limiter&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;wrong processor order&lt;/li&gt;
&lt;li&gt;missing retry/queue config&lt;/li&gt;
&lt;li&gt;hardcoded secrets&lt;/li&gt;
&lt;li&gt;plain HTTP exporters&lt;/li&gt;
&lt;li&gt;debug exporters in production&lt;/li&gt;
&lt;li&gt;high-cardinality attributes&lt;/li&gt;
&lt;li&gt;missing sampling/filtering hints&lt;/li&gt;
&lt;li&gt;semantic convention issues&lt;/li&gt;
&lt;li&gt;Kubernetes readiness gaps&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;commands&quot;&gt;Commands&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;oteldoctor analyze ./deploy --profile production
oteldoctor analyze ./deploy --format json
oteldoctor analyze ./deploy --format sarif &amp;gt; oteldoctor.sarif
oteldoctor graph collector.yaml --format mermaid
oteldoctor explain OTEL-SEC-202
oteldoctor fix collector.yaml --dry-run
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;install&quot;&gt;Install&lt;/h3&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;go install github.com/firfircelik/oteldoctor/cmd/oteldoctor@v0.1.0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&quot;why-it-exists&quot;&gt;Why it exists&lt;/h3&gt;
&lt;p&gt;This is not a replacement for Collector validation.&lt;/p&gt;
&lt;p&gt;It is more like a production-readiness layer for Collector configs.&lt;/p&gt;
&lt;p&gt;The goal is to catch reliability, security, cost, semantic, and Kubernetes deployment risks earlier — ideally in CI, before the config is deployed.&lt;/p&gt;
&lt;p&gt;GitHub: &lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/firfircelik/oteldoctor&quot;&gt;https://github.com/firfircelik/oteldoctor&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;If you work with OpenTelemetry Collector, I’d really appreciate feedback, issue reports, and real-world config edge cases.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/18690/released-oteldoctor-v0-an-open-source-go-cli-for-checking-opentelemetry-collector-config</guid>
<pubDate>Fri, 22 May 2026 14:34:21 +0000</pubDate>
</item>
<item>
<title>Nowadays, LLM is based and major for me</title>
<link>https://coderlegion.com/17360/nowadays-llm-is-based-and-major-for-me</link>
<description>&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/topstar-ai/OpenLLM&quot;&gt;https://github.com/topstar-ai/OpenLLM&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://bentoml.com&quot;&gt;https://bentoml.com&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Run any open-source LLMs, such as DeepSeek and Llama, as OpenAI compatible API endpoint in the cloud.&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://coderlegion.com/?qa=blob&amp;amp;qa_blobid=8591791232814553939&quot; alt=&quot;&quot;&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/17360/nowadays-llm-is-based-and-major-for-me</guid>
<pubDate>Mon, 11 May 2026 04:56:55 +0000</pubDate>
</item>
<item>
<title>Why Users Don’t Trust Your Product (Even When It Works Perfectly)</title>
<link>https://coderlegion.com/16413/why-users-dont-trust-your-product-even-when-it-works-perfectly</link>
<description>&lt;p&gt;A product can work perfectly…&lt;/p&gt;
&lt;p&gt;And still feel unreliable.&lt;/p&gt;
&lt;p&gt;I’ve seen this happen more times than people realize.&lt;/p&gt;
&lt;p&gt;No bugs.&lt;br&gt;
No crashes.&lt;br&gt;
No major issues.&lt;/p&gt;
&lt;p&gt;But users hesitate.&lt;/p&gt;
&lt;p&gt;They second-guess.&lt;br&gt;
They get stuck.&lt;br&gt;
They don’t move forward with confidence.&lt;/p&gt;
&lt;p&gt;The Problem Isn’t Always Technical&lt;/p&gt;
&lt;p&gt;Most teams assume:&lt;/p&gt;
&lt;p&gt;“If users don’t trust the product, something must be broken.”&lt;/p&gt;
&lt;p&gt;But often, nothing is broken.&lt;/p&gt;
&lt;p&gt; It just doesn’t feel clear.&lt;/p&gt;
&lt;p&gt;Where Trust Actually Comes From&lt;/p&gt;
&lt;p&gt;Users don’t trust your product because it works.&lt;/p&gt;
&lt;p&gt;They trust it because they understand what’s happening.&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;p&gt;• They know what to do next&lt;br&gt;
• They understand what just happened&lt;br&gt;
• They can predict what will happen after&lt;/p&gt;
&lt;p&gt;Without that…&lt;/p&gt;
&lt;p&gt;Even a working system feels confusing.&lt;/p&gt;
&lt;p&gt;What Breaks Trust&lt;/p&gt;
&lt;p&gt;Here are a few subtle things that quietly reduce user confidence:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Unclear next steps&lt;br&gt;
After completing an action, users don’t know what to do next.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Silent systems&lt;br&gt;
The product does something… but doesn’t explain it.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Overloaded explanations&lt;br&gt;
Too much information at once makes users unsure what matters.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Assumed knowledge&lt;br&gt;
Users are expected to understand things that were never explained.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;What I’ve Learned Working on Documentation&lt;/p&gt;
&lt;p&gt;When users feel lost, they don’t say:&lt;/p&gt;
&lt;p&gt;“This system lacks clarity.”&lt;/p&gt;
&lt;p&gt;They say:&lt;/p&gt;
&lt;p&gt;“This feels confusing.”&lt;br&gt;
“I’m not sure if I’m doing this right.”&lt;/p&gt;
&lt;p&gt;And that’s where trust starts breaking.&lt;/p&gt;
&lt;p&gt;The Fix Is Simpler Than You Think&lt;/p&gt;
&lt;p&gt;You don’t always need more features.&lt;/p&gt;
&lt;p&gt;You don’t even need more content.&lt;/p&gt;
&lt;p&gt;You need:&lt;/p&gt;
&lt;p&gt; Clear guidance at the right moment&lt;/p&gt;
&lt;p&gt;That means:&lt;/p&gt;
&lt;p&gt;• Showing users what to do next&lt;br&gt;
• Confirming when they’ve done something right&lt;br&gt;
• Reducing unnecessary decisions&lt;/p&gt;
&lt;p&gt;A Simple Test&lt;/p&gt;
&lt;p&gt;Open your product or documentation and ask:&lt;/p&gt;
&lt;p&gt; “Would a first-time user feel confident here?”&lt;/p&gt;
&lt;p&gt;If the answer isn’t a clear yes…&lt;/p&gt;
&lt;p&gt;That’s where the work is.&lt;/p&gt;
&lt;p&gt;Final Thought&lt;/p&gt;
&lt;p&gt;Trust isn’t built by functionality alone.&lt;/p&gt;
&lt;p&gt;It’s built by clarity.&lt;/p&gt;
&lt;p&gt;If your product works but users still hesitate or get stuck…&lt;/p&gt;
&lt;p&gt;That’s usually where I come in.&lt;/p&gt;
&lt;p&gt;I help turn working systems into clear, trusted experiences.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/16413/why-users-dont-trust-your-product-even-when-it-works-perfectly</guid>
<pubDate>Sat, 02 May 2026 14:16:11 +0000</pubDate>
</item>
<item>
<title>I Built a Pneumonia Detection AI on My MacBook — Here's Exactly How It Works</title>
<link>https://coderlegion.com/15808/i-built-a-pneumonia-detection-ai-on-my-macbook-heres-exactly-how-it-works</link>
<description>&lt;p&gt;I just finished building a deep learning system that identifies pneumonia from chest X-rays with 96% accuracy and an AUC-ROC of 0.99. I ran the entire training process on my MacBook Pro M5 using the GPU acceleration provided by Apple's Metal Performance Shaders (MPS). This project wasn't about complex math; it was about using transfer learning to turn a consumer laptop into a medical diagnostic tool. Understanding how to handle messy data and verify what an AI is actually &quot;seeing&quot; is more important than having a massive server room.&lt;/p&gt;
&lt;h3 id=&quot;what-i-actually-built&quot;&gt;WHAT I ACTUALLY BUILT&lt;/h3&gt;
&lt;p&gt;At its core, I built a computer program that acts like a specialized set of eyes for doctors. You feed it a digital chest X-ray image, and in less than a second, it tells you whether it sees signs of pneumonia or a normal, healthy lung. It doesn't just guess; it provides a confidence percentage for its decision. The system is designed to catch cases that might be subtle to the human eye, acting as a second pair of eyes to reduce diagnostic errors.&lt;/p&gt;
&lt;h3 id=&quot;the-dataset-problem-nobody-mentions&quot;&gt;THE DATASET PROBLEM NOBODY MENTIONS&lt;/h3&gt;
&lt;p&gt;When I first looked at the data, I found a massive problem: class imbalance. The training set had 1,341 &quot;Normal&quot; images but a whopping 3,875 &quot;Pneumonia&quot; images. If I had trained the model as-is, it would have quickly learned that &quot;guessing pneumonia every time&quot; results in 74% accuracy without actually learning a single thing about lungs. This is a common trap in AI where a high accuracy score hides a completely useless model.&lt;/p&gt;
&lt;p&gt;To fix this, I used two specific techniques to level the playing field. First, I implemented a &lt;code&gt;WeightedRandomSampler&lt;/code&gt;, which you can think of as &quot;photocopying rare examples.&quot; It ensures that during training, the model sees the &quot;Normal&quot; images more frequently so it doesn't forget what a healthy lung looks like. Second, I added class weights to the loss function—essentially a &quot;bigger penalty for missing rare cases.&quot; If the model misclassifies a &quot;Normal&quot; lung as pneumonia, the error signal it receives is mathematically amplified, forcing it to pay closer attention to those specific patterns.&lt;/p&gt;
&lt;h3 id=&quot;why-i-didn-t-train-from-scratch&quot;&gt;WHY I DIDN'T TRAIN FROM SCRATCH&lt;/h3&gt;
&lt;p&gt;I didn't start with a blank slate. Instead, I used a technique called transfer learning with an architecture known as ResNet-18. Think of it like this: a radiologist who already knows what edges, textures, and shapes look like doesn't need to relearn basic vision from scratch. They already have the &quot;visual foundation&quot; from years of looking at the world; they just need to learn what sick lungs look like specifically. ResNet-18 comes pre-trained on millions of everyday images (like dogs, cars, and trees), so it already understands how to detect lines and textures.&lt;/p&gt;
&lt;p&gt;I used a two-phase training strategy to refine this pre-existing knowledge. In Phase 1 (Epochs 1-5), I froze most of the model and only trained the very last layer. This allowed the model to get a &quot;feel&quot; for the new medical data without overwriting its basic visual skills. In Phase 2 (Epochs 6-20), I unfroze everything and used a tiny learning rate to fine-tune the entire network. At the start of Phase 2, I saw a temporary spike in the loss curve. This is normal and expected—it's the mathematical equivalent of the model being slightly &quot;confused&quot; as it starts adjusting its deep-seated visual patterns to the nuances of X-ray tissue.&lt;/p&gt;
&lt;h3 id=&quot;can-you-trust-it-grad-cam-explainability&quot;&gt;CAN YOU TRUST IT? GRAD-CAM EXPLAINABILITY&lt;/h3&gt;
&lt;p&gt;A 96% accurate AI is useless if it's a &quot;black box&quot; that you can't verify. In medical AI, you have to know &lt;em&gt;why&lt;/em&gt; a decision was made. I implemented Grad-CAM (Gradient-weighted Class Activation Mapping), which is a tool that &quot;shows you which pixels the model was looking at when it made its decision.&quot; Without this, a model might achieve 99% accuracy just by learning that images with a certain hospital's patient ID label in the corner are usually the pneumonia cases.&lt;/p&gt;
&lt;p&gt;When I ran Grad-CAM on my results, the heatmaps were revealing. For pneumonia cases, the &quot;heat&quot; (red and yellow zones) was concentrated directly on the lung tissue where opacities usually appear. For normal cases, the focus was much more diffuse across the entire chest cavity. This gave me the confidence that the model was actually learning medical features, not just memorizing background noise or image artifacts.&lt;/p&gt;
&lt;h3 id=&quot;the-results&quot;&gt;THE RESULTS&lt;/h3&gt;
&lt;p&gt;After 20 epochs and about 45 minutes of training on my MacBook, here is how the system performed on the 624 images in the test set:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th align=&quot;left&quot;&gt;Metric &lt;/th&gt;&lt;th align=&quot;left&quot;&gt;Score&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;Test Accuracy &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;96%&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;AUC-ROC &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.99&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;NORMAL F1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.94&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;PNEUMONIA F1 &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;0.96&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;False Negatives &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;15/390&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td align=&quot;left&quot;&gt;False Positives &lt;/td&gt;&lt;td align=&quot;left&quot;&gt;13/234&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Let's break these down into plain English. &lt;strong&gt;Test Accuracy&lt;/strong&gt; means the model was right 96 times out of 100 overall. &lt;strong&gt;AUC-ROC&lt;/strong&gt; measures how well the model can distinguish between the two classes across different confidence levels; 0.99 is nearly perfect separation. &lt;strong&gt;F1 Score&lt;/strong&gt; is a balanced average of precision (not flagging healthy people as sick) and recall (not missing sick people). &lt;/p&gt;
&lt;p&gt;Most importantly, we have to look at the &lt;strong&gt;15 missed pneumonia cases&lt;/strong&gt; (False Negatives). In a clinical context, missing a sick patient is far worse than accidentally flagging a healthy one. This is why &quot;Recall&quot; matters more than &quot;Accuracy&quot; in medical AI. While 15 misses out of 390 is low, it highlights that this system is a diagnostic assistant, not a replacement for a human doctor who would catch those edge cases.&lt;/p&gt;
&lt;h3 id=&quot;what-i-learned&quot;&gt;WHAT I LEARNED&lt;/h3&gt;
&lt;p&gt;This project taught me a few genuine technical lessons that go beyond the usual tutorials:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;The Validation Set Trap&lt;/strong&gt;: The original dataset only had 16 images in the validation folder. This made the validation accuracy bounce around wildly and become meaningless during training. You need a representative validation set to know if your model is actually improving.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch the Loss, Not the Accuracy&lt;/strong&gt;: Accuracy is a &quot;lagging indicator.&quot; The loss curve tells you the &quot;quality&quot; of the model's learning. If the loss is still going down but accuracy is flat, you're still making progress.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Grad-CAM is Mandatory&lt;/strong&gt;: For medical AI, explainability isn't a &quot;nice to have.&quot; It's the difference between a useful tool and a legal liability. If you can't see the heatmaps, you shouldn't trust the predictions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Apple Silicon is Ready&lt;/strong&gt;: Training this on MPS (Metal Performance Shaders) was surprisingly fast. For this size of workload, you don't need a dedicated Linux server with a massive GPU; a modern MacBook Pro handles it in under an hour.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&quot;what-s-next&quot;&gt;WHAT'S NEXT&lt;/h3&gt;
&lt;p&gt;I'm not finished with this system yet. My next steps are specific:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Fix the Validation Set&lt;/strong&gt;: I'm going to move about 500 images from the training set into the validation set to get more reliable feedback during training.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Try DenseNet-121&lt;/strong&gt;: This architecture is the current gold standard in chest X-ray research papers because of how it handles feature reuse.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Build a Web UI&lt;/strong&gt;: I want to use Streamlit to create a simple drag-and-drop interface so anyone can test the model without looking at code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Kaggle Notebook&lt;/strong&gt;: I've already published a self-contained version of this project as a Kaggle notebook for the community to play with.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&quot;closing&quot;&gt;CLOSING&lt;/h3&gt;
&lt;p&gt;This project demonstrated that you don't need a supercomputer to build high-performing medical AI. By using transfer learning and being smart about how you handle imbalanced data, you can achieve professional-grade results on consumer hardware. It's a testament to how accessible deep learning has become, provided you focus on the data and the &quot;why&quot; behind the predictions.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;: This model is for educational purposes only and is not intended for clinical use.&lt;/p&gt;
&lt;p&gt;&lt;a rel=&quot;nofollow&quot; href=&quot;https://www.kaggle.com/code/sreenandhunair/pneumonia-detection-with-deep-learning-96-acc&quot;&gt;View the full notebook on Kaggle&lt;/a&gt;&lt;br&gt;
&lt;a rel=&quot;nofollow&quot; href=&quot;https://github.com/saucynandhu/medical-image-diagnosis&quot;&gt;View the code on GitHub&lt;/a&gt;&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/15808/i-built-a-pneumonia-detection-ai-on-my-macbook-heres-exactly-how-it-works</guid>
<pubDate>Sat, 25 Apr 2026 14:15:21 +0000</pubDate>
</item>
<item>
<title>The New Employment Market Reality – It’s Much Worse Than You Think!</title>
<link>https://coderlegion.com/11990/the-new-employment-market-reality-its-much-worse-than-you-think</link>
<description>&lt;p&gt;I truly don't know where to begin. This is the one of the worst employment markets I have ever seen!&lt;/p&gt;
&lt;p&gt;Recruiters and companies are being bombarded with qualified candidates from all over the world. The mass layoffs left 100s of thousands people stranded!&lt;/p&gt;
&lt;p&gt;The once was considered golden job with real stability and security had been obliterated by AI!&lt;/p&gt;
&lt;p&gt;This doesn't mean that tech doesn't need humans, it simply shifted or transformed to different needs. Corporations no longer need coders, they want designers, architects and AI oriented professionals.&lt;/p&gt;
&lt;p&gt;The problem is that, what corporations missed, is that they no longer understand why Devs wanted to be Devs.&lt;/p&gt;
&lt;p&gt;People regardless of their background, are always looking for good jobs where they feel respected and valued. The problem with the current market dynamics, employers no longer feel the need to care about their employees or care about having a healthy workplace environment.&lt;/p&gt;
&lt;p&gt;There is a very huge rise in serious problems such as workplace bullying, mobbing and harassment! What happened?&lt;/p&gt;
&lt;p&gt;The truth is that unfortunately, when there is 1000 person ready to take your job, those in leadership lose the incentive to put effort in retention of employees. At the same time, employers realized that a lot of roles specially white collar jobs are no longer needed, so they want to get rid of as many people as possible and remain small. This is why, statistically speaking, the layoff stats far exceeded new job creation. You will hear a lot that employers are being &quot;Cautious&quot;.&lt;/p&gt;
&lt;p&gt;Do you share my sentiment, do you feel the employment market has become unsustainable? Is it time for Devs to be self employed first?&lt;/p&gt;
&lt;p&gt;Share your thoughts in the comments!&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/11990/the-new-employment-market-reality-its-much-worse-than-you-think</guid>
<pubDate>Sat, 21 Feb 2026 04:01:17 +0000</pubDate>
</item>
<item>
<title>Why Developers Should Build Homelabs</title>
<link>https://coderlegion.com/10489/why-developers-should-build-homelabs</link>
<description>&lt;p&gt;In an era dominated by cloud platforms, managed services, and abstracted infrastructure, it’s easy for developers to forget what actually happens beneath the surface. Yet, some of the most capable engineers today share a common trait: they run &lt;strong&gt;homelabs&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;A homelab isn’t about expensive servers or enterprise-grade hardware. It’s about creating a personal playground where you can experiment, break things safely, and truly understand how systems work. Whether you’re a backend developer, DevOps engineer, or full-stack builder, a homelab can quietly become one of the most valuable tools in your growth journey.&lt;/p&gt;
&lt;h3 id=&quot;what-is-a-homelab-&quot;&gt;What Is a Homelab?&lt;/h3&gt;
&lt;p&gt;A homelab is a self-hosted environment usually running on spare hardware, a mini PC, a Raspberry Pi, or even an old laptop where you experiment with real world systems. Think Linux servers, Docker containers, CI pipelines, monitoring tools, reverse proxies, and networking setups, all under your control.&lt;/p&gt;
&lt;p&gt;Unlike tutorials or sandbox environments, a homelab forces you to deal with &lt;strong&gt;real constraints&lt;/strong&gt;: limited resources, outages, misconfigurations, and upgrades. That’s where the real learning happens.&lt;/p&gt;
&lt;h3 id=&quot;1-you-learn-how-things-actually-work&quot;&gt;1. You Learn How Things Actually Work&lt;/h3&gt;
&lt;p&gt;Cloud platforms make deployment easy, but they also hide complexity. In a homelab, you don’t get that luxury.&lt;/p&gt;
&lt;p&gt;You’ll learn:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;How Linux services start and fail&lt;/li&gt;
&lt;li&gt;What systemd actually does&lt;/li&gt;
&lt;li&gt;How networking, ports, firewalls, and DNS interact&lt;/li&gt;
&lt;li&gt;Why disk space suddenly disappears at 2 a.m.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This foundational knowledge makes you a stronger developer even if you never plan to become a full-time DevOps engineer.&lt;/p&gt;
&lt;h3 id=&quot;2-safe-space-to-break-and-fix-things&quot;&gt;2. Safe Space to Break (and Fix) Things&lt;/h3&gt;
&lt;p&gt;Production systems punish mistakes. Homelabs reward them.&lt;/p&gt;
&lt;p&gt;You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Crash a server and rebuild it&lt;/li&gt;
&lt;li&gt;Misconfigure Nginx and debug it&lt;/li&gt;
&lt;li&gt;Experiment with security hardening&lt;/li&gt;
&lt;li&gt;Practice backups and disaster recovery&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Breaking things in a homelab builds confidence. When something goes wrong at work, it’s no longer panic, it’s déjà vu.&lt;/p&gt;
&lt;h3 id=&quot;3-real-world-devops-skills-without-the-pressure&quot;&gt;3. Real-World DevOps Skills Without the Pressure&lt;/h3&gt;
&lt;p&gt;Many developers struggle to get hands-on DevOps experience because production access is limited (for good reason). A homelab fills that gap.&lt;/p&gt;
&lt;p&gt;With it, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Run Docker and Docker Compose&lt;/li&gt;
&lt;li&gt;Set up CI/CD pipelines&lt;/li&gt;
&lt;li&gt;Monitor systems with Prometheus or Netdata&lt;/li&gt;
&lt;li&gt;Practice zero-downtime deployments&lt;/li&gt;
&lt;li&gt;Experiment with systemd, cron, and logging&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are highly marketable skills and a homelab lets you learn them at your own pace.&lt;/p&gt;
&lt;h3 id=&quot;4-better-understanding-of-performance-and-scaling&quot;&gt;4. Better Understanding of Performance and Scaling&lt;/h3&gt;
&lt;p&gt;When you run everything yourself, inefficiencies become obvious.&lt;/p&gt;
&lt;p&gt;You start asking:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Why is this container using so much memory?&lt;/li&gt;
&lt;li&gt;Do I really need this service running?&lt;/li&gt;
&lt;li&gt;How can I optimize startup times?&lt;/li&gt;
&lt;li&gt;What happens when traffic spikes?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This mindset translates directly into writing more efficient applications and designing better architectures.&lt;/p&gt;
&lt;h3 id=&quot;5-a-portfolio-that-speaks-louder-than-certificates&quot;&gt;5. A Portfolio That Speaks Louder Than Certificates&lt;/h3&gt;
&lt;p&gt;A homelab isn’t just for learning it’s proof.&lt;/p&gt;
&lt;p&gt;You can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Document your setup in blog posts&lt;/li&gt;
&lt;li&gt;Share architecture diagrams&lt;/li&gt;
&lt;li&gt;Open Source configs and scripts&lt;/li&gt;
&lt;li&gt;Talk confidently about real infrastructure challenges&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When you say “I’ve run my own services,” it carries weight especially in interviews.&lt;/p&gt;
&lt;h3 id=&quot;6-it-encourages-curiosity-and-long-term-growth&quot;&gt;6. It Encourages Curiosity and Long Term Growth&lt;/h3&gt;
&lt;p&gt;Homelabs naturally evolve. What starts as “just a Linux server” turns into:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A personal VPN&lt;/li&gt;
&lt;li&gt;A monitoring stack&lt;/li&gt;
&lt;li&gt;A self-hosted dashboard&lt;/li&gt;
&lt;li&gt;Automated alerts and backups&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You stop chasing trends and start understanding systems. That curiosity compounds over time.&lt;/p&gt;
&lt;h3 id=&quot;you-don-t-need-much-to-start&quot;&gt;You Don’t Need Much to Start&lt;/h3&gt;
&lt;p&gt;This is the best part.&lt;/p&gt;
&lt;p&gt;You can start with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;An old laptop&lt;/li&gt;
&lt;li&gt;A mini PC&lt;/li&gt;
&lt;li&gt;A Raspberry Pi&lt;/li&gt;
&lt;li&gt;Even a small VPS&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Install Linux, pick one tool (Docker, Nginx, or systemd), and build from there. The value isn’t in how powerful the hardware is, it’s in what you learn along the way.&lt;/p&gt;
&lt;h3 id=&quot;final-thoughts&quot;&gt;Final Thoughts&lt;/h3&gt;
&lt;p&gt;Building a homelab isn’t about replacing the cloud or rejecting modern tooling. It’s about &lt;strong&gt;understanding the foundations&lt;/strong&gt; that everything else is built on.&lt;/p&gt;
&lt;p&gt;In 2026, developers who stand out aren’t just those who can write code, they’re the ones who understand the systems running it.&lt;/p&gt;
&lt;p&gt;If you’ve ever wanted to go deeper, gain confidence, and sharpen your engineering instincts, a homelab is one of the best investments you can make.&lt;/p&gt;
&lt;p&gt;If this post resonated with you, share it with another developer and if you’re already running a homelab, you’re ahead of the curve.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/10489/why-developers-should-build-homelabs</guid>
<pubDate>Mon, 26 Jan 2026 07:42:18 +0000</pubDate>
</item>
<item>
<title>Why Building Projects Beats Just Watching Tutorials</title>
<link>https://coderlegion.com/8459/why-building-projects-beats-just-watching-tutorials</link>
<description>&lt;p&gt;When I started learning AI and Machine Learning, I thought watching tutorials was enough. Models, algorithms, code examples — everything looked clear while watching. But the moment I tried to build something on my own, reality hit.&lt;/p&gt;
&lt;p&gt;❌ The Tutorial Comfort Zone&lt;/p&gt;
&lt;p&gt;Tutorials made everything feel easy:&lt;/p&gt;
&lt;p&gt;Clean datasets&lt;/p&gt;
&lt;p&gt;Pre-written logic&lt;/p&gt;
&lt;p&gt;Perfect outputs&lt;/p&gt;
&lt;p&gt;But once the video ended, I realized I didn’t truly understand how things worked. &lt;/p&gt;
&lt;p&gt; What Changed: Building Real Projects&lt;/p&gt;
&lt;p&gt;Things changed when I started building my own AI &amp;amp; ML projects — even small ones.&lt;/p&gt;
&lt;p&gt;I faced:&lt;/p&gt;
&lt;p&gt;Dataset issues&lt;/p&gt;
&lt;p&gt;Model giving wrong predictions&lt;/p&gt;
&lt;p&gt;Errors I had never seen before&lt;/p&gt;
&lt;p&gt;Performance problems&lt;/p&gt;
&lt;p&gt;At first, it was frustrating. But slowly, I learned:&lt;/p&gt;
&lt;p&gt;How to debug ML code&lt;/p&gt;
&lt;p&gt;Why preprocessing matters&lt;/p&gt;
&lt;p&gt;How model choice affects results&lt;/p&gt;
&lt;p&gt;How AI systems behave in real-world scenarios&lt;/p&gt;
&lt;p&gt;That struggle taught me more than any tutorial ever could.&lt;/p&gt;
&lt;p&gt; Learning by Doing (My Approach)&lt;/p&gt;
&lt;p&gt;This is the method that worked best for me:&lt;/p&gt;
&lt;p&gt;Learn the basic concept&lt;/p&gt;
&lt;p&gt;Build a simple AI/ML project&lt;/p&gt;
&lt;p&gt;Break it by experimenting&lt;/p&gt;
&lt;p&gt;Improve accuracy or features&lt;/p&gt;
&lt;p&gt;Repeat with a new idea&lt;/p&gt;
&lt;p&gt;Each project improved my confidence and understanding.&lt;/p&gt;
&lt;p&gt; Final Thought&lt;/p&gt;
&lt;p&gt;AI and ML are not learned by just watching — they are learned by building, failing, and fixing.&lt;/p&gt;
&lt;p&gt;If you’re learning AI or ML, don’t wait to be perfect.&lt;br&gt;
Start small, build projects, and let mistakes teach you.&lt;/p&gt;
&lt;p&gt;Build → Fail → Learn → Improve &lt;/p&gt;
&lt;p&gt;Happy Coding &lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/8459/why-building-projects-beats-just-watching-tutorials</guid>
<pubDate>Sun, 14 Dec 2025 16:37:38 +0000</pubDate>
</item>
<item>
<title>Why I Couldn’t Launch My EdTech MVP Without a Real Developer: My Accelerator Heartbreak in Lagos</title>
<link>https://coderlegion.com/7484/why-couldnt-launch-my-edtech-mvp-without-real-developer-my-accelerator-heartbreak-lagos</link>
<description>&lt;p&gt;Ever had this wild, messy dream you couldn’t shake? That was me, 2021. The world was upside-down, and I had an idea for an EdTech platform buzzing in my head. Not just any idea… the kind that keeps you up at night, sketching screens on scraps of paper while the rain hammers the windows.&lt;/p&gt;
&lt;p&gt;The Spark: From Idea to Accelerator&lt;br&gt;
So I ran with it. I pitched my heart out. Somehow, the stars lined up—I got accepted into an accelerator program in the USA right from here in Lagos. There were other smart founders in the cohort, but I was sure my EdTech angle had legs. I could already see the impact: students leveling up, teachers connecting, a real shift in how we learn.&lt;/p&gt;
&lt;p&gt;But let’s pause for a second. Ya know that rush when someone says “Congratulations!” and you realize, oh man, now the real work starts? That’s when I hit my first speed bump.&lt;/p&gt;
&lt;p&gt;Playing Matchmaker in the Tech Scene&lt;br&gt;
If you’ve ever tried to launch a tech product in Lagos, you know developers are kings and queens. They’ve got options. I started asking around—friends from uni, that guy my cousin knows who codes in Python, a few folks recommended through the accelerator’s Slack group. Everyone was busy, everyone wanted equity, and everyone needed convincing.&lt;/p&gt;
&lt;p&gt;Believe it or not, I actually managed to get a developer to sign an agreement. The thrill! The relief! I thought, “Finally, a pro on my side. We’re gonna build this MVP and crush it.” But weeks turned into months. DMs went unanswered. Zoom calls got rescheduled, then ghosted. It was like trying to herd cats—but the cats were also lead singers in seven different bands.&lt;/p&gt;
&lt;p&gt;The No-Code Mirage&lt;br&gt;
Here’s where my DIY optimism took a nosedive. People online kept saying “No-code tools are the way forward! You don’t need to be a developer.” Great advice, if you know what those words mean. I’d open Bubble, stare at all those workflows. Tried Adalo, too. Still, nothing “clicked.” No tutorial really felt made for raw beginners, especially in Nigeria where so many tools were blocked or cost a fortune to access.&lt;/p&gt;
&lt;p&gt;I was stuck. There was money on the table—real funding from the accelerator! But every deadline felt more like a tsunami crashing over me. I’d read stories about founders building MVPs in a weekend. Meanwhile, my idea was stuck on Post-it notes and half-baked Google Docs.&lt;/p&gt;
&lt;p&gt;Losing The Shot&lt;br&gt;
After six months (and a thousand heated WhatsApp chats), it was gone. The accelerator moved on. My shot at funding vanished. I felt like I’d wasted not just an opportunity but all the trust people placed in me. Nothing stings more than being close enough to touch your dream, only to watch it slip away because the one thing you couldn’t get—real dev muscle—just wouldn’t show up.&lt;/p&gt;
&lt;p&gt;Lessons I Wish I’d Known&lt;br&gt;
So… why couldn’t I build my MVP alone, even with help lined up?&lt;/p&gt;
&lt;p&gt;Developers are swamped, and sometimes equity isn’t enough to get them onboard—especially if you’re a first-time founder with only big ideas and no code chops.&lt;/p&gt;
&lt;p&gt;No-code tools sound life-changing. In practice? If you can’t pick them up fast enough, they might just create more headaches for non-tech founders.&lt;/p&gt;
&lt;p&gt;Signed agreements don’t mean squat if there’s no trust, shared vision, or urgency. Papers look nice, but momentum gets things shipped.&lt;/p&gt;
&lt;p&gt;If You’re Dreaming Big in Lagos…&lt;br&gt;
I’d say: learn just enough tech to ask the smart questions. Start ridiculously small. Build a landing page. Sketch a user flow. Don’t wait for the “perfect” dev—or, at least, don’t bet everything on someone else’s bandwidth.&lt;/p&gt;
&lt;p&gt;Opportunities don’t always circle back. Take your shot (even the ugly, unfinished ones). Because the only thing worse than regret is never getting in the ring at all.&lt;/p&gt;
&lt;p&gt;Let’s commiserate below—have you struggled to build a tech MVP in Nigeria? Got tips or stories? Drop them in the comments.&lt;/p&gt;
&lt;p&gt;That’s the journey. Messy, real, and full of stuff nobody puts in the official how-to guides.&lt;/p&gt;
</description>
<category>Career Roadmaps</category>
<guid isPermaLink="true">https://coderlegion.com/7484/why-couldnt-launch-my-edtech-mvp-without-real-developer-my-accelerator-heartbreak-lagos</guid>
<pubDate>Tue, 18 Nov 2025 19:47:24 +0000</pubDate>
</item>
<item>
<title>Why You Should Build Projects Before You Feel “Ready”</title>
<link>https://coderlegion.com/7414/why-you-should-build-projects-before-you-feel-ready</link>
<description>&lt;p&gt;When I first started learning tech, I had this habit:&lt;br&gt;
I kept waiting.&lt;/p&gt;
&lt;p&gt;Waiting to fully understand a language…&lt;br&gt;
Waiting to finish a course…&lt;br&gt;
Waiting to feel “confident enough.”&lt;/p&gt;
&lt;p&gt;But here’s what I learned along the way:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;You will never feel 100% ready — and that’s exactly why you should start building early.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;building-is-the-fastest-way-to-learn&quot;&gt;Building Is the Fastest Way to Learn&lt;/h3&gt;
&lt;p&gt;Courses give you theory.&lt;br&gt;
Tutorials guide your steps.&lt;br&gt;
But projects?&lt;/p&gt;
&lt;p&gt;Projects push you into real problem-solving.&lt;/p&gt;
&lt;p&gt;Every time I built something — even tiny things that looked “meh” — I grew faster than any tutorial could ever teach me.&lt;/p&gt;
&lt;p&gt;You learn by doing, by breaking things, and by fixing them.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;projects-reveal-what-you-don-t-know&quot;&gt;Projects Reveal What You Don’t Know&lt;/h3&gt;
&lt;p&gt;It’s funny how gaps show up the moment you start building:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;“Wait… how do I fetch an API again?”&lt;/li&gt;
&lt;li&gt;“Why is this bug refusing to vanish?”&lt;/li&gt;
&lt;li&gt;“How do people structure these files neatly?” &lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And honestly?&lt;br&gt;
That’s &lt;em&gt;good&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Because now you know exactly what you need to learn next.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;small-projects-build-big-confidence&quot;&gt;Small Projects Build Big Confidence&lt;/h3&gt;
&lt;p&gt;Your first project won’t be perfect.&lt;br&gt;
It doesn’t have to be.&lt;/p&gt;
&lt;p&gt;What matters is that it’s &lt;strong&gt;yours&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;That little spark of “I made this” pushes you into your next project.&lt;/p&gt;
&lt;p&gt;And soon:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Button → Page&lt;/li&gt;
&lt;li&gt;Page → App&lt;/li&gt;
&lt;li&gt;App → Portfolio&lt;/li&gt;
&lt;li&gt;Portfolio → Opportunities&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That’s how developers grow.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;projects-make-you-visible&quot;&gt;Projects Make You Visible&lt;/h3&gt;
&lt;p&gt;Posting your work — GitHub, CodePen, LinkedIn — changes everything.&lt;/p&gt;
&lt;p&gt;People don’t just want to &lt;em&gt;hear&lt;/em&gt; about your learning.&lt;br&gt;
They want to &lt;em&gt;see&lt;/em&gt; your progress.&lt;/p&gt;
&lt;p&gt;Your projects become proof.&lt;br&gt;
Receipts.&lt;br&gt;
Your growth timeline.&lt;/p&gt;
&lt;p&gt;And that is how opportunities find you.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;how-to-start-even-if-you-don-t-feel-ready-&quot;&gt;How To Start (Even If You Don’t Feel Ready)&lt;/h3&gt;
&lt;p&gt;Pick something tiny:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A calculator&lt;/li&gt;
&lt;li&gt;A to-do app&lt;/li&gt;
&lt;li&gt;A card UI&lt;/li&gt;
&lt;li&gt;A simple landing page&lt;/li&gt;
&lt;li&gt;A fun animation&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Start small.&lt;br&gt;
Start messy.&lt;br&gt;
Just start.&lt;/p&gt;
&lt;p&gt;You’ll figure out the rest along the journey — we all did.&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&quot;final-words&quot;&gt;Final Words&lt;/h3&gt;
&lt;p&gt;Don’t wait for confidence.&lt;br&gt;
Don’t wait for perfection.&lt;br&gt;
Don’t wait for the “right time.”&lt;/p&gt;
&lt;p&gt;Start now.&lt;/p&gt;
&lt;p&gt;Because readiness doesn’t come &lt;em&gt;before&lt;/em&gt; action — it comes &lt;em&gt;from&lt;/em&gt; action.&lt;/p&gt;
&lt;p&gt;Every developer you admire started the same way:&lt;br&gt;
messy, confused, curious… but brave enough to begin.&lt;/p&gt;
&lt;p&gt;Keep building.&lt;br&gt;
Keep growing.&lt;/p&gt;
&lt;p&gt;You’re not just building projects —&lt;br&gt;
&lt;strong&gt;you’re building yourself.&lt;/strong&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;strong&gt;Signature:&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Duchess of Hackers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full-Stack Developer | Digital Marketer| Community Builder&lt;/strong&gt;&lt;/p&gt;
</description>
<category>Full-stack Dev</category>
<guid isPermaLink="true">https://coderlegion.com/7414/why-you-should-build-projects-before-you-feel-ready</guid>
<pubDate>Sun, 16 Nov 2025 02:57:09 +0000</pubDate>
</item>
<item>
<title>What is Web3?</title>
<link>https://coderlegion.com/3225/what-is-web3</link>
<description>&lt;p&gt;&lt;strong&gt;Web1 (1980’s - early 2000’s)&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first phase of the Internet, Web1, was mainly about providing the everyday consumer with online content and information.&lt;/p&gt;
&lt;p&gt;As consumers could only read information or content online, and not yet interact with it, Web1 was incredibly static.&lt;/p&gt;
&lt;p&gt;When you think about Web1, think Internet Explorer, Yahoo, or Netscape. While web1 was read-only, the companies we associate with web1 were built on open protocols (meaning pretty much any person or organization could build on the internet and know they were subject to the same rules as the next person or organization).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is Web2?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Web2 is the version of the internet most of us know and use today. Where Web1 was static and “read-only,” Web2 is “read-write,” and interactive. Under Web2, the internet became more usable: Web2 was dynamic and users could consume, interact with, and create content on the internet themselves.&lt;/p&gt;
&lt;p&gt;Along the way, the internet became largely dominated by the four behemoths we know today as Apple, Amazon, Facebook, and Google. Web2 also saw an explosion in the use of smartphones, and most of internet use was through mobile apps and hardware built by these companies. While this meant more people could participate in the internet, it also meant the internet was becoming increasingly controlled by the leading digital platforms.&lt;/p&gt;
&lt;p&gt;Why is this a problem? In the centralized internet we know today, Apple can take a 30% cut on all paid-app downloads and in-app purchases, Twitter and Facebook can de-platform the President of the United States, and the everyday consumer has less privacy, security, and control over their online information than ever before.&lt;/p&gt;
&lt;p&gt;We also see a lot of data breaches happening all across Web2 leading to reduced security and privacy for one's personal data. When a user's data gets breached it's easy for them to become a victim of identity theft, personal attacks etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is Web3?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Web3, the future internet we’re moving towards, is a decentralized internet. Under Web3, the internet is shared online and governed by the collective “we,” rather than owned by centralized entities. The Web3 world is one that has open-source protocols at its foundation. Web3 is about rearchitecting internet services and products so that they benefit people rather than entities.&lt;/p&gt;
&lt;p&gt;Web3 enhances the web we know today by making it decentralized, distributed, open, trustless and permissionless.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;It is getting built such that everything would happen in a decentralized distributed way giving no central authority access to control the system.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘&lt;em&gt;Open&lt;/em&gt;’ as it would be open sourced software built by an open and accessible community of developers and executed in full view of the world.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘&lt;em&gt;Trustless&lt;/em&gt;’ in that the network itself allows participants to interact publicly or privately without a trusted third party.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;‘&lt;em&gt;Permissionless&lt;/em&gt;’ in that anyone, both users and suppliers, can participate without authorization from a governing body.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Summary&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Web1&lt;/em&gt;: Read &lt;/p&gt;
&lt;p&gt;&lt;em&gt;Web2&lt;/em&gt;: Read-Write  ️&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Web3&lt;/em&gt;: Read-Write-Own  ️ &lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/3225/what-is-web3</guid>
<pubDate>Wed, 18 Jun 2025 12:53:37 +0000</pubDate>
</item>
<item>
<title>Wait! Don’t Start Your Server Before MongoDB Connects</title>
<link>https://coderlegion.com/3354/wait-dont-start-your-server-before-mongodb-connects</link>
<description>&lt;p&gt;&lt;em&gt;Hey everyone! &lt;/em&gt;&lt;br&gt;
In the &lt;strong&gt;previous&lt;/strong&gt; article, I explained how to connect &lt;strong&gt;MongoDB to your Node.js app using Mongoose&lt;/strong&gt;.&lt;br&gt;
&lt;em&gt;But if you noticed&lt;/em&gt; — we didn’t talk about starting the server yet!&lt;/p&gt;
&lt;p&gt;We’ll see how to &lt;strong&gt;start the Express server only after your database connection is successful&lt;/strong&gt; — a small but important step to avoid future errors and follow best practices.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt; Current Project Structure Recap&lt;/strong&gt;&lt;br&gt;
So far, here’s how your folder structure looks:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;your-project/
├── index.js            // Main entry point (will start the server)
├── db/server.js        // MongoDB connection
├── constants/idbname.js  // DB name or other constants
├── .env
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;❓ Before We Start Coding the Server...&lt;/strong&gt;&lt;br&gt;
There’s one important question that needs to be answered first:&lt;/p&gt;
&lt;p&gt;Should we start the server &lt;strong&gt;before connecting to the database&lt;/strong&gt;, or after the &lt;strong&gt;connection is successful?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Answer: Always Start the Server After the Database Connection&lt;/strong&gt;&lt;br&gt;
&lt;em&gt;Why?&lt;/em&gt; &lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;p&gt;The server &lt;em&gt;starts immediately&lt;/em&gt; — it assumes everything is ready.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Meanwhile, the &lt;em&gt;MongoDB connection is still being established&lt;/em&gt; (and it's an asynchronous operation).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;If a user hits an API route that depends on the database — for example, /users, /posts, or /login —&lt;br&gt;
&lt;em&gt;Your server will try to access the database that hasn’t connected yet.&lt;/em&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;This leads to errors Or even app crashes in worst cases.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Basically:&lt;/em&gt; Your server is accepting requests it isn’t ready to handle yet. That’s like&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;First connect to MongoDB&lt;/strong&gt;, and then only if that’s successful, start the Express server.&lt;/p&gt;
&lt;p&gt;Now that &lt;strong&gt;we know why the server should start only after a successful DB connection&lt;/strong&gt;, let’s set up the Express server step-by-step.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;1. Install Express&lt;/strong&gt;&lt;br&gt;
First, let’s install Express using npm:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;npm install express
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will add Express to your &lt;em&gt;project dependencies&lt;/em&gt; so you can create and run your server.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;2. Create an app.js File&lt;/strong&gt;&lt;br&gt;
Inside your root folder, create a new file named app.js. This file will handle creating the Express app instance&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Set &quot;type&quot;: &quot;module&quot; in package.json&lt;/strong&gt;&lt;br&gt;
This tells Node.&lt;em&gt;js&lt;/em&gt; to treat .js files as ES modules, &lt;strong&gt;enabling import and export.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;⚠️ Without This?&lt;/strong&gt;&lt;br&gt;
If you don’t set &quot;type&quot;: &quot;module&quot;, then writing this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import express from &quot;express&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;will throw an error like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SyntaxError: Cannot use import statement outside a module
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;3 Simple Steps to Initialize Express in app.js&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 1: Import Express&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import express from &quot;express&quot;;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Step 2: Create the App Instance&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;const app = express();
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This app variable is now your entire Express application. You’ll use it to define routes, middlewares, etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Step 3: Export the App&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Exporting the app allows you to import and use it in your index.js file (where you'll start the server after connecting to MongoDB).&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;import express from &quot;express&quot;
const app = express();
export default app;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;for now, this minimal &lt;strong&gt;setup is perfect.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Quick Reminder from the Previous Article&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In our previous article, we created an a&lt;strong&gt;sync function called connectionDB()&lt;/strong&gt; that handles the MongoDB connection logic.&lt;br&gt;
We then called this &lt;strong&gt;function in index.js to actually initiate the connection&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;But here’s something important you should &lt;em&gt;understand&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;When you create an async function like this:&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;async function connectionDB() {
  // your logic here
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;...it always returns a &lt;strong&gt;Promise&lt;/strong&gt; by default, whether you explicitly return something or not.&lt;/p&gt;
&lt;p&gt;This means — when we call this function:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connectionDB()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We can attach .then() and .catch() to it, &lt;em&gt;just like any other Promise:&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;connectionDB()
  .then(() =&amp;gt; {
    // Code to run after DB is connected
  })
  .catch((err) =&amp;gt; {
    // Handle DB connection error
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;strong&gt;Why We Use .then() and .catch() for Database Connection&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;.then() — If DB Connection is Successful&lt;/strong&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt; connectionDB()
  .then(() =&amp;gt; {
    // If connection is successful, start the server
    app.listen(3000, () =&amp;gt; {
      console.log(` Server started successfully`);
    });
  })
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;This means:&lt;/strong&gt;&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;If MongoDB connects successfully&lt;/li&gt;
&lt;li&gt;Then we start our Express server using app.listen().&lt;/li&gt;
&lt;li&gt;This ensures the server starts only when the DB is ready.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;.&lt;strong&gt;catch() — If DB Connection Fails&lt;/strong&gt;&lt;br&gt;
But what if the connection fails? For example:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Wrong MongoDB URI, &lt;/li&gt;
&lt;li&gt;Incorrect username or password,&lt;/li&gt;
&lt;li&gt;Or internet issues?&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In that case, the .catch() block runs:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;.catch((err) =&amp;gt; {
  console.log(&quot; Server failed to start due to DB connection error:&quot;, err);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This &lt;em&gt;stops the server from running without a working database,&lt;/em&gt; which could lead to serious bugs or crashes in your app.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// index.js

import dotenv from &quot;dotenv&quot;;
import app from &quot;./app.js&quot;;
import connectionDB from &quot;./db/server.js&quot;;

// Load environment variables from .env file
dotenv.config();
 
// Connect to the database first
connectionDB()
  .then(() =&amp;gt; {
    // If connection is successful, start the server
    app.listen(3000, () =&amp;gt; {
      console.log(` Server started successfully`);
    });
  })
  .catch((err) =&amp;gt; {
    // If connection fails, do NOT start the server
    console.log(&quot; Server failed to start due to DB connection error:&quot;, err);
  });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;strong&gt;Wrapping Up&lt;/strong&gt;&lt;br&gt;
And that’s how you start your server only after your &lt;em&gt;database connection is successful&lt;/em&gt; — the clean, professional way.&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Remember:&lt;/strong&gt;&lt;br&gt;
Make sure to import your app.js file into index.js like this:&lt;/p&gt;
&lt;p&gt;i&lt;code&gt;mport app from &quot;./app.js&quot;;&lt;/code&gt;&lt;br&gt;
This ensures your Express server is ready to run once everything else (like DB connection) is set.&lt;/p&gt;
&lt;p&gt; &lt;em&gt;This setup is not too lengthy&lt;/em&gt; — it's actually a very compact and organized way to structure your backend.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;With this approach, your project is:&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Modular&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Error-resilient&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Production-ready&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt; Bonus Tip: Special Characters in Your MongoDB Password&lt;/strong&gt;&lt;br&gt;
Let me share a quick issue I faced (and many beginners do too):&lt;/p&gt;
&lt;p&gt;&lt;em&gt;❓ What if your MongoDB password contains special characters like @, #, or $?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;If your password has characters like @, the MongoDB connection &lt;em&gt;will fail unless you encode them properly in the URI.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;The Fix:&lt;/strong&gt;&lt;br&gt;
Let’s say your password is:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myPass@123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;em&gt;Your MongoDB URI will break at @, because @ has a special meaning in URLs.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt; Instead, you must URL-encode the special characters.&lt;br&gt;
&lt;em&gt;For example, @ becomes %40.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;So your password becomes:&lt;/em&gt;&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;myPass%40123
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And your connection string should look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;mongodb+srv://username:myPass%*Emails are not allowed*/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt; &lt;em&gt;Pro Tip: If you don’t want to mess with encoding, just avoid using special characters in your DB password — or change them to something simpler.&lt;/em&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;If this article helped you even a little, drop a like ❤️ — it really motivates me to share more practical content like this!*&lt;/li&gt;
&lt;/ul&gt;
</description>
<category>Performance</category>
<guid isPermaLink="true">https://coderlegion.com/3354/wait-dont-start-your-server-before-mongodb-connects</guid>
<pubDate>Tue, 17 Jun 2025 22:54:46 +0000</pubDate>
</item>
<item>
<title>FIRST STEPS, BIG LEAPS</title>
<link>https://coderlegion.com/1220/first-steps-big-leaps</link>
<description>&lt;h2 id=&quot;first-steps-big-leaps&quot;&gt;FIRST STEPS, BIG LEAPS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Taking the leap&lt;/strong&gt; is one thing—stopping others in the air and not making them to reach for the stars cause&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt; &quot;how can the sky be the limit when they are people on the moon&quot; - Paul Brandt.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt; After making the decision to build my startup, I quickly realised that dreaming big was easy, but turning those dreams into reality? That was the real challenge.&lt;/p&gt;
&lt;p&gt; This was where things got &lt;strong&gt;real&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;strong&gt;first steps&lt;/strong&gt; were messy, exciting, and terrifying all at once. One minute, I was pumped with motivation (David Goggins Type shii ), ready to change the world . The next, I was staring at my screen, wondering where to even begin.&lt;/p&gt;
&lt;p&gt;I wasn’t just a teenager with a dream anymore—I was a teenager with a mission. But how do you turn a mission into something real?&lt;/p&gt;
&lt;h2 id=&quot;from-vision-to-action&quot;&gt;&lt;strong&gt;FROM VISION TO ACTION&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;A dream is nothing without &lt;strong&gt;action&lt;/strong&gt;. I knew that if I wanted to be one of the youngest tech startup founders, I couldn’t just sit around and hope for things to happen.&lt;/p&gt;
&lt;p&gt;But let’s be real… where do you even start when you’re 15? Balancing school which I am already managing with the 65% pass mark, and have limited resources, which was just me my Laptop, phone and a Dream so what should I do?&lt;/p&gt;
&lt;p&gt;At first, my mind was all over the place. One second, I wanted to build an app, then a website, then an AI-powered platform —my ideas were running wild. But I knew I had to simplify.&lt;/p&gt;
&lt;p&gt;So, I took my first real step: writing things down.&lt;/p&gt;
&lt;p&gt; I grabbed a notebook (yes, a real one, not an app this time ) and started listing out what I needed to do:&lt;/p&gt;
&lt;p&gt;✅ Understand the problem I wanted to solve – A startup isn’t just about an idea; it’s about solving a real problem that people have in there every-day life that I could Also Relate to.&lt;br&gt;
✅ Research similar products – What’s already out there? How can I make something better?&lt;br&gt;
✅ Figure out what I needed to learn – Coding? Business? Marketing? Time to hit the books (Actual Books ).&lt;br&gt;
✅ Sketch my first wireframes – Even though my designs looked like a five-year-old drew them, I had to start somewhere . (Personally, I think they looked good  but back to what I was saying) &lt;/p&gt;
&lt;p&gt; Lesson learned: &lt;strong&gt;The key&lt;/strong&gt; wasn’t knowing everything from the start—it was just getting started.&lt;/p&gt;
&lt;h2 id=&quot;-reality-check-facing-the-first-hurdles&quot;&gt; &lt;strong&gt;REALITY CHECK: FACING THE FIRST HURDLES&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Taking the first step felt like an achievement… but reality hit FAST.&lt;/p&gt;
&lt;p&gt;I had to face some uncomfortable truths:&lt;/p&gt;
&lt;p&gt; I didn’t know everything. Shocking, right?  I thought I knew how to &lt;strong&gt;code&lt;/strong&gt;, but the more I dived into real-world projects, the more I realised there was so much I didn’t know.&lt;/p&gt;
&lt;p&gt; Self-doubt creeped in. Was I even capable of this? Would anyone take me seriously? Would this idea even work?&lt;/p&gt;
&lt;p&gt;⏳ Time became my biggest enemy. Balancing school, homework, and my startup felt impossible. Some days, I barely had time to breathe.&lt;/p&gt;
&lt;p&gt;At this point, I had &lt;strong&gt;two choices&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;1️⃣ Let the &lt;strong&gt;doubts&lt;/strong&gt; and &lt;strong&gt;challenges&lt;/strong&gt; stop me.&lt;br&gt;
2️⃣ Keep &lt;strong&gt;pushing forward&lt;/strong&gt;, no matter what.&lt;/p&gt;
&lt;p&gt;You already know which one I chose.  (And no It's not No:1 )&lt;/p&gt;
&lt;h2 id=&quot;-learning-by-doing&quot;&gt;&lt;strong&gt; LEARNING BY DOING&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;I stopped overthinking and started learning through action.&lt;/p&gt;
&lt;p&gt; I dived deep into research—watching hours of YouTube tutorials, reading startup blogs, and joining online coding communities (I basically Just started Tutorial Hell, till I got to this [https://roadmap.sh][1] )&lt;/p&gt;
&lt;p&gt; I started small projects—not for money, not for recognition, but just to test my skills (It was hard ). Every line of code I wrote, every feature I built, was a step forward.&lt;/p&gt;
&lt;p&gt; I made mistakes. A lot of them (I can't even give an estimate) . But every error taught me something new.&lt;/p&gt;
&lt;p&gt;And that’s when I discovered a secret:&lt;/p&gt;
&lt;blockquote&gt;&lt;p&gt;The best way to learn isn’t by reading or planning—it’s by doing&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Saw it in a motivational Video.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h2 id=&quot;-small-wins-big-motivation&quot;&gt; SMALL WINS, BIG MOTIVATION&lt;/h2&gt;
&lt;p&gt;Then, it happened—my first real win.&lt;/p&gt;
&lt;p&gt;I built a working prototype of my idea.&lt;/p&gt;
&lt;p&gt;Was it perfect? No.&lt;br&gt;
Did it crash a few times? Absolutely.&lt;br&gt;
Did I feel like a genius anyway? HECK YES. &lt;/p&gt;
&lt;p&gt; That moment was &lt;strong&gt;everything&lt;/strong&gt;. Seeing my idea come to life, even in its simplest form, was the proof I needed that I was on the right path.&lt;/p&gt;
&lt;p&gt;It didn’t matter if it wasn’t 'perfect'. What mattered was that it was real.&lt;/p&gt;
&lt;p&gt;Every small win gave me a massive confidence boost. Every piece of progress reminded me why I started.&lt;/p&gt;
&lt;h2 id=&quot;-the-bigger-picture-why-this-matters&quot;&gt;&lt;strong&gt; THE BIGGER PICTURE: WHY THIS MATTERS&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;The hardest part of any journey is &lt;strong&gt;starting&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We all have big dreams, but most people never take that first step because they’re scared of failing, scared of looking stupid, or waiting for the “perfect” time.&lt;/p&gt;
&lt;p&gt; Let me tell you something: There is &lt;strong&gt;NO&lt;/strong&gt; perfect time.&lt;/p&gt;
&lt;p&gt;If you wait until you have everything figured out, you’ll never start.&lt;/p&gt;
&lt;p&gt;I realised that momentum is everything. Once you start taking action—no matter how small—you build confidence, experience, and resilience.&lt;/p&gt;
&lt;p&gt;And before you know it…&lt;/p&gt;
&lt;p&gt; You’re no longer just a dreamer.&lt;br&gt;
 You’re a doer.&lt;/p&gt;
&lt;h4 id=&quot;-the-road-ahead-&quot;&gt;⏭️ THE ROAD AHEAD…&lt;/h4&gt;
&lt;p&gt;I won’t lie—this is just the beginning.&lt;/p&gt;
&lt;p&gt;I still have so much to learn.&lt;br&gt;
I still face challenges every single day.&lt;br&gt;
I still have moments of doubt.&lt;/p&gt;
&lt;p&gt;But I’ve made it this far—and that means I’m capable of going even further.&lt;/p&gt;
&lt;p&gt;Each small step forward is another big leap toward my dream.&lt;/p&gt;
&lt;p&gt; The journey continues… and I can’t wait to share what’s next.&lt;/p&gt;
&lt;p&gt; Stay tuned for Episode 5!&lt;/p&gt;
&lt;p&gt; To anyone reading this: Your dream is waiting. The only question is—will you take the first step?&lt;/p&gt;
&lt;p&gt; &lt;strong&gt;Your Turn!&lt;/strong&gt;&lt;br&gt;
 What’s &lt;strong&gt;a dream&lt;/strong&gt; you’ve been putting off?&lt;br&gt;
 What’s stopping you from taking '&lt;strong&gt;The first step&lt;/strong&gt;'?&lt;/p&gt;
&lt;h3 id=&quot;-&quot;&gt;-&lt;/h3&gt;
&lt;h3 id=&quot;--1&quot;&gt;-&lt;/h3&gt;
&lt;h3 id=&quot;--2&quot;&gt;-&lt;/h3&gt;
&lt;p&gt;Drop a comment—I’d love to hear your thoughts! &lt;/p&gt;
&lt;p&gt;I also wanna Thank those who commented On my Last Post It was really Inspiring&lt;/p&gt;
</description>
<category>Web Dev</category>
<guid isPermaLink="true">https://coderlegion.com/1220/first-steps-big-leaps</guid>
<pubDate>Sun, 23 Feb 2025 20:37:35 +0000</pubDate>
</item>
</channel>
</rss>