<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/">
    <channel>
        <title>Bashar Ayyash | Blog</title>
        <link>https://yabasha.dev</link>
        <description>Insights on AI engineering, Laravel, Next.js, React Native, and building modern software products for MENA businesses.</description>
        <lastBuildDate>Sat, 10 Oct 2026 10:11:41 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>Feed for Node.js</generator>
        <language>en</language>
        <image>
            <title>Bashar Ayyash | Blog</title>
            <url>https://yabasha.dev/og-image.jpg</url>
            <link>https://yabasha.dev</link>
        </image>
        <copyright>All rights reserved 2026, Bashar Ayyash</copyright>
        <item>
            <title><![CDATA[Findable Is Not Chosen — and That Résumé Won't Save You]]></title>
            <link>https://yabasha.dev/blog/findable-is-not-chosen-and-that-resume-wont-save-you</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/findable-is-not-chosen-and-that-resume-wont-save-you</guid>
            <pubDate>Tue, 15 Sep 2026 09:27:12 GMT</pubDate>
            <description><![CDATA[Your résumé gets you past the ATS filter. That's all 'findable' means — and it's priced at zero. Here's the 4-action sequence job seekers are using to get chosen in 2026.]]></description>
            <content:encoded><![CDATA[<h1>Findable Is Not Chosen — and That Résumé Won&#39;t Save You</h1>
<p>Thirteen applications since mid-August. Thirteen polite silences.</p>
<p>Every one had a tailored CV. Every one had a cover letter mapped line by line to the job description. By the rules hiring used to run on, that candidate did everything right.</p>
<p>If your inbox looks the same, so did you.</p>
<p>You&#39;re findable. Findable is not chosen. The rules are the problem — and sitting still won&#39;t fix them. The rest of this piece is what to do instead, in order, starting today.</p>
<h2>Your résumé is priced at zero. Here&#39;s why.</h2>
<p>A résumé was never proof. It was a proxy. &quot;Ten years at X&quot; stood in for &quot;can do the work.&quot; A degree stood in for &quot;learned the theory.&quot;</p>
<p>Proxies only hold while they&#39;re expensive to counterfeit. Both broke the moment a plausible CV for any role became a forty-second task. Every candidate generates one now. Every candidate looks perfect on paper. Hiring managers know it — so they discount the document to zero and hunt for the one thing a prompt can&#39;t fake: something you built, in public, with a trail.</p>
<p>I spent this month taking notes while hiring managers explained why they picked the person they picked. Not one mentioned a degree, a certificate, or a years count. All of them described something the candidate had built or fixed.</p>
<p>Your résumé still does one job: it gets you past the ATS filter. That&#39;s all &quot;findable&quot; means. Treating the filter as the decision is the mistake.</p>
<p><strong>Action 1 (today, 30 minutes):</strong> Audit your last 10 applications. If all you sent was a CV and a cover letter, you sent nothing a prompt couldn&#39;t produce. Stop tweaking the document. Start building the evidence.</p>
<h2>The three probes behind every AI interview question</h2>
<p>Every interview question about AI is a wrapper. Unwrap it and you find one of three probes:</p>
<ul>
<li><strong>Experiment like a scientist</strong> — hypothesis, test, accept the result, move fast. Sounds like: <em>&quot;How would you automate this?&quot;</em></li>
<li><strong>Stay close to the work</strong> — you can name which step of the workflow is broken, and which steps AI can actually improve. Sounds like: <em>&quot;Walk me through a process you rebuilt with AI. What didn&#39;t work?&quot;</em></li>
<li><strong>Stay curious</strong> — you ask the customer the right questions before building anything. Depth, not breadth. Sounds like: <em>&quot;What did the customer actually need, and how did you find out?&quot;</em></li>
</ul>
<p>What a fail sounds like, and what a pass sounds like:</p>
<ul>
<li><strong>Experiment.</strong> Fail: names a model and a prompt. Pass: &quot;I don&#39;t know yet — here&#39;s how I&#39;d find out, and how fast.&quot;</li>
<li><strong>Proximity.</strong> Fail: &quot;I&#39;ve used AI to improve workflows.&quot; Pass: names the step that breaks, then the step you actually automated — and why they weren&#39;t the same one.</li>
<li><strong>Curiosity.</strong> Fail: proposes the chatbot. Pass: went and asked the customer where the process loses them, before building anything.</li>
</ul>
<p>Notice what isn&#39;t probed: knowing the most models. Fluency is table stakes, and it&#39;s spreading so fast it stops being a differentiator before you finish listing it.</p>
<p><strong>Action 2 (this week):</strong> Write your three answers out loud. One experiment, one broken workflow step with names and numbers, one customer question you asked before building. If any of them still sounds like the &quot;fail&quot; column, that&#39;s your drill list.</p>
<h2>Build a portfolio of process — the five artefacts that get you chosen</h2>
<p>Chosen candidates don&#39;t show up with a better résumé. They show up with a portfolio of process. Five artefacts kept coming up in my notes:</p>
<ol>
<li><strong>One workflow you rebuilt with AI.</strong> Manual before, AI after, named and scoped. Yours — not a tutorial&#39;s.</li>
<li><strong>A public write-up of it.</strong> A post, a blog, a repo README. Anywhere a hiring manager finds it before the interview.</li>
<li><strong>A failure log.</strong> The automations that didn&#39;t work, and why. The most under-supplied artefact in hiring right now — and the most reliable signal of a scientist&#39;s mindset.</li>
<li><strong>A technical deep-dive.</strong> Not &quot;I use Claude Code.&quot; How the agent loop works. What your MCP server exposes. Where the tokens go.</li>
<li><strong>A day in the life with agents.</strong> Which agents, which tasks, what you still do by hand and why. Video is best — it proves communication skill in the same footage.</li>
</ol>
<p>The same pattern runs under all five: you walked into a process, found the broken step, fixed it with AI, and explained it to the team. That is the job now, whatever the title says.</p>
<p><strong>Action 3 (this month, one artefact per week):</strong></p>
<ul>
<li>Week 1: pick one workflow from your current or last job and rebuild a piece of it with AI. Small is fine. Real is required.</li>
<li>Week 2: publish the write-up. A failure log is the most under-supplied artefact in hiring right now — so include what broke.</li>
<li>Week 3: write the deep-dive. If you can&#39;t explain where the tokens go, learn it, then write it.</li>
<li>Week 4: record one video walkthrough. Post it. Posting is how you prove the knowledge in your head exists outside it.</li>
</ul>
<h2>Interviews changed on both sides of the table</h2>
<p>Candidates moved first. They record every interview, feed the transcript to an LLM, and ask where they were vague and what a stronger answer would have sounded like. Then they drill the weak answers before the next round. If you aren&#39;t doing this, the person you&#39;re competing with is.</p>
<p>Interviewers went the other way from what most candidates expect: deeper, not shallower. The coding round is less about syntax and more about one question — &quot;show me how you&#39;d do this with AI, and where you&#39;d stop trusting it.&quot;</p>
<p>The strongest answer in my notes ran like this:</p>
<blockquote>
<p>&quot;This is my domain. Here&#39;s the workflow, here&#39;s the step I automated, here&#39;s the agent that does it, here&#39;s what I&#39;d change next, and here&#39;s how I&#39;d hand it to the team.&quot;</p>
</blockquote>
<p>Now read the CV-bullet version: &quot;Experienced with LLMs and agentic workflows.&quot;</p>
<p>One of these earns a second call.</p>
<p><strong>Action 4 (every interview, from the next one on):</strong> Record it (where legal), transcribe it, and run one prompt: <em>where was I vague, and what would a stronger answer have sounded like?</em> Drill the weak spots before the next round. Fifteen minutes per interview. Your competition is already doing it.</p>
<h2>The uncomfortable part</h2>
<p>AI is opening roles faster than most people expected. For a lot of people it&#39;s the social elevator they&#39;ve been waiting on — a way to skip the credential queue and be judged on the work itself.</p>
<p>Good news, with one catch: the elevator only stops for people who put the work where it can be seen.</p>
<p>So here&#39;s the sequence. Stop rewriting the CV. Build one workflow. Publish the write-up — failures included. Record your interviews and drill the weak answers. Four actions, and not one of them is &quot;apply to more jobs.&quot;</p>
<p>Findable is the ATS. Chosen is the evidence.</p>
<p>Which artefact are you shipping first?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/1c012005ad9c0bdab0be415e867cd4e0/blog-cover-findable-is-not-chosen-hiring-in-2026.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Graph Engineering Is Mostly Airflow With A New Coat Of Paint]]></title>
            <link>https://yabasha.dev/blog/graph-engineering-is-mostly-airflow-with-a-new-coat-of-paint</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/graph-engineering-is-mostly-airflow-with-a-new-coat-of-paint</guid>
            <pubDate>Tue, 04 Aug 2026 07:40:26 GMT</pubDate>
            <description><![CDATA[The AI world just invented something called 'graph engineering' — and most of it is a rebrand of the DAG schedulers we've run for a decade. But one delta is real, and it's the only thing worth your attention. Here's the take from someone who's built rung 05 before, in Laravel queues, where only the workers changed.]]></description>
            <content:encoded><![CDATA[<h2>Graph Engineering Is Mostly Airflow With A New Coat Of Paint</h2>
<p>There&#39;s a new buzzword rolling through the AI world, and it landed with all the subtlety of a brick through a window: <strong>graph engineering</strong>. It&#39;s the top rung of what people are now calling a &quot;scope ladder&quot; — prompt, context, harness, loop, graph. Five labels since 2022, each one a bigger box for the same question: <em>how do I get a useful result out of a model?</em></p>
<p>I&#39;ve been a professional engineer for over twenty years. I&#39;ve scheduled ETL pipelines in banks, queued jobs through Laravel at 3am in Amman, and watched trend after trend arrive dressed as a revolution when it was really a reorg. So let me save you six months: <strong>most of graph engineering is Airflow with a new coat of paint.</strong> But one part of it is genuinely real, it maps embarrassingly well to the queues you already run, and it&#39;s the only thing worth your time. Let me show you which is which.</p>
<h2>First, the ladder is mostly one idea wearing five hats</h2>
<p>Every label in that ladder exists because the one below it hit a wall. That&#39;s not a conspiracy — it&#39;s just how the industry works.</p>
<ul>
<li><strong>Prompt</strong> → <strong>context</strong>: because perfect wording can&#39;t supply facts the model doesn&#39;t have. No sentence you write gets a model to know the date of your last production deploy.</li>
<li><strong>Context</strong> → <strong>harness</strong>: because a model that can only read can&#39;t change anything. It needed hands — tools, a filesystem — and a mirror: test output, compiler errors it could argue with.</li>
<li><strong>Harness</strong> → <strong>loop</strong>: because one pass through the tools rarely lands the job. Quality came from iteration count, not prompt cleverness. The people winning weren&#39;t writing better prompts; they were letting the agent fail and retry.</li>
<li><strong>Loop</strong> → <strong>graph</strong>: because one loop can&#39;t parallelise, can&#39;t specialise, can&#39;t independently check its own work. One context, one responsibility — and some jobs need an organisation, not a single worker.</li>
</ul>
<p>Rungs two, three, and four — context, harness, loop — are so entangled that working engineers use the words interchangeably. Only two boundaries hold weight: the shift from <em>instruction</em> to <em>information</em> (01→02), and the shift from <em>one worker</em> to <em>an organisation</em> (04→05).</p>
<p>That last one is graph engineering. The rest of the ladder is the same idea in five different coats.</p>
<h2>So what&#39;s actually new in graph engineering?</h2>
<p>At rung five, the question stops being &quot;how do I get one model to do a thing&quot; and becomes &quot;how do many loops wire together?&quot; One word: <strong>topology</strong>. The scope is the whole system — several rung-04 loops, deterministic functions, validators, tools, and humans, all wired together.</p>
<p>The levers are nodes, typed edges, shared state, parallel branches, verifiers, handoffs, stop conditions, budgets. And here&#39;s my contrarian bit: <strong>none of that structure is new.</strong> DAG schedulers and workflow engines have been drawing these graphs for a decade. Airflow has been orchestrating dependencies since 2015. If all you want is to draw boxes and arrows and run them on a schedule, you could have done that when most LLM developers were still in high school.</p>
<p>The only truly new thing — the entire delta, the whole novelty worth your attention — is that <strong>the nodes now interpret their tasks instead of following fixed rules.</strong></p>
<p>That&#39;s it. That&#39;s the whole revolution. And it&#39;s a big one, because it changes every implicit assumption you had. When a node is deterministic, you can trust it, unit test it, predict its cost. When a node is a model, it will occasionally lie to you — confidently, <em>politely</em> — and take as long and cost as much as it likes. Which means state, vetoes, and budgets stop being implied and have to become <strong>explicit</strong>. That&#39;s the real engineering. Everything else is paint.</p>
<h2>Two things called the same name</h2>
<p>Before you build anything, you need to know the term is split. &quot;Graph engineering&quot; means two genuinely different things, and a lot of the confusion in the hype is people using the same word for both:</p>
<ul>
<li><strong>Meaning A — Orchestration graph.</strong> Nodes are units of work, edges are permitted transitions. This is control flow. Ecosystem: LangGraph, Mastra, Temporal. This one sits genuinely above loop engineering. My favourite one-liner for it: <em>loops are subroutines, graphs are programs.</em></li>
<li><strong>Meaning B — Knowledge graph / GraphRAG.</strong> Nodes are entities, edges are typed relationships — caused, owns, wrote. This is data and retrieval. Ecosystem: Neo4j, GraphRAG pipelines. It&#39;s a smarter way to fill one context window. My favourite one-liner there: <em>vector search finds things that sound like your question; a graph finds things connected to your answer.</em></li>
</ul>
<p>The disambiguation question is brutally simple: <strong>are you routing work, or retrieving knowledge?</strong> A is control flow, B is data. They do compose — a node in graph A can retrieve by traversing graph B — but if you don&#39;t know which one you&#39;re building, you will build the wrong thing.</p>
<h2>Do you even need a graph? Be honest</h2>
<p>Here&#39;s the most useful thing I can hand you, and it&#39;s three questions:</p>
<ol>
<li>Does the work have two or more sub-tasks that could run in parallel, or need different specialised context?</li>
<li>Do you need an independent check on the output — something other than what produced it?</li>
<li>Is there a point where a human must approve before work continues?</li>
</ol>
<p>Two or more yeses? Build the graph. Zero or one? Stay on a single loop. <strong>A single well-guarded loop is cheaper, easier to debug, and good enough for most tasks.</strong> Reaching for a graph too early is the single most common way to turn a working agent into an unobservable mess. I&#39;ve watched teams install LangGraph and instantly lose the ability to explain what their own system did on any given run.</p>
<p>And here&#39;s my strong opinion, stated plainly: <strong>don&#39;t install a graph framework yet.</strong></p>
<p>A graph is a plain data structure plus a fifty-line runner. Build that first. You&#39;ll understand the failure modes from the inside, and you&#39;ll know exactly what a framework would actually buy you. There are genuine reasons to adopt LangGraph, Mastra, or Temporal later — durable checkpointing, human-in-the-loop pauses that survive days, time-travel replay, a visual trace UI. Those are real features. They are <em>not</em> reasons: &quot;this is how everyone does it,&quot; or wanting the word <em>graph</em> in your architecture diagram, which I promise you a VP will not read as deeply as you think.</p>
<h2>The five ways agent graphs die in the first month</h2>
<p>If you do build one, the first thirty days will throw five traps at you:</p>
<ol>
<li><strong>The verifier shares the drafter&#39;s model and context.</strong> It rubber-stamps its own work — a model that wrote a thing will nod approvingly at that same thing. Give the verifier fresh context and an explicit checklist, or make it deterministic code.</li>
<li><strong>Edges decided by free text.</strong> &quot;I think we should retry&quot; is not an edge. Return a validated enum; route anything unrecognised to escalate.</li>
<li><strong>No step budget.</strong> A two-node cycle with no cap is an unbounded invoice. Every graph needs a <em>global</em> ceiling, not just per-node retries.</li>
<li><strong>Every node is an agent.</strong> Threshold checks and enum routing belong in code. LLM nodes are for ambiguity only.</li>
<li><strong>Unlogged runs.</strong> Without a per-node trace you can&#39;t answer &quot;why did it do that?&quot; — and that question arrives, without fail, on day three, from your boss.</li>
</ol>
<p>The discipline that saves you from all five, in one line: <strong>an agent that runs out of budget escalates. It does not fail quietly.</strong></p>
<h2>Not every node should be an agent</h2>
<p>Listen, this is the part too many architecture diagrams get wrong. The model is not the star of every scene. The split is not &quot;use AI everywhere&quot; — it&#39;s about where ambiguity lives.</p>
<ul>
<li>If it&#39;s <strong>deterministic</strong>, plain code wins. Invoice approval thresholds, plan-active checks, routing on a known enum — anything you can unit test is code, full stop.</li>
<li>If there&#39;s <strong>ambiguity</strong>, an LLM earns its place. &quot;Is this email a refund request?&quot; Summarise, draft, translate. Planning under open conditions. Interpreting messy human input. That&#39;s model territory.</li>
</ul>
<p>And here&#39;s the budget angle nobody puts in the blog posts: <strong>each LLM node multiplies your cost per run by roughly its retry ceiling.</strong> A node that retries five times isn&#39;t one model call — it&#39;s up to five, plus the verifier, plus the retries on <em>that</em>. The code/LLM split isn&#39;t just architecture; it&#39;s the difference between pennies per run and a bill that lands like a licence renewal on a Friday.</p>
<h2>Here&#39;s the kicker: you&#39;ve already built this</h2>
<p>Now the part that should land hardest if you&#39;re a Laravel developer. Read this mapping and tell me it doesn&#39;t make you a little uncomfortable — because I already know the answer, I built this exact thing years ago:</p>
<ul>
<li>a <strong>node</strong> = a queued Job class (<code>ClassifyTicket</code>, <code>DraftReply</code>)</li>
<li>a <strong>typed edge</strong> = a PHP enum the job returns, validated before dispatch</li>
<li><strong>shared state</strong> = an <code>agent_runs</code> row</li>
<li>a <strong>trace</strong> = <code>agent_run_steps</code> rows: node, tokens, latency, cost</li>
<li>a <strong>step budget</strong> = a <code>steps_remaining</code> column, decremented atomically</li>
<li>a <strong>retry cap</strong> = <code>$tries</code> and <code>$backoff</code></li>
<li>a <strong>verifier</strong> = a Job allowed to re-dispatch its own predecessor</li>
<li>a <strong>parallel branch</strong> = <code>Bus::batch([...])</code></li>
<li>a <strong>join node</strong> = the batch&#39;s <code>then()</code> callback</li>
<li>a <strong>terminal state</strong> = a status enum: <code>done</code>, <code>escalated</code>, <code>failed</code></li>
<li>a <strong>human checkpoint</strong> = <code>awaiting_approval</code>, resumed by a controller action</li>
</ul>
<p>Read that table again, slowly. <strong>You already know rung 05.</strong> The wiring is Laravel queues. The orchestration, the retries, the state, the escalations, the sits-awaiting-a-human — it&#39;s all there. Graph engineering isn&#39;t new. It&#39;s the architecture you&#39;ve been building for twenty years, where the only thing that changed is the workers.</p>
<p>And that changed worker is the entire problem. Your old queue workers were deterministic — they did the same thing every time and failed predictably. Your new workers are <em>non-deterministic</em> and, occasionally, they will lie to you. Which is exactly why every edge now needs an explicit guard: a verifier, a hard attempt cap, a stop condition. That&#39;s not buzzword territory. That&#39;s just good engineering with the stakes raised, because the component in the middle of your pipeline has an opinion.</p>
<h2>Not a rebrand. A rebrand plus one hard truth</h2>
<p>So let me land this the way I started it. Most of graph engineering — the boxes, the arrows, the DAG you could draw in Airflow when Trump was still in his first term — is exactly what we&#39;ve always drawn. A rebrand. New paint, same skeleton, and a lot of people charging consulting fees to draw it for you.</p>
<p>The one delta is real, and it&#39;s not the graph at all — it&#39;s the <strong>nodes that interpret their tasks.</strong> That single change forces state, vetoes, and budgets to become explicit instead of implied, and that&#39;s where the genuine engineering work of the next decade is going to happen. The engineering lives in the edges, not the node count. Always has.</p>
<p>So my advice: don&#39;t chase the graph framework — chasing the word <em>graph</em> in your diagram is how money disappears. Build the plain data structure. Build the fifty-line runner. Give each node a guard. Log everything. And for the love of your own sanity, leave an escalate edge — every graph needs a door marked <em>give this to a human</em>.</p>
<p>Because here&#39;s the provocation I want to leave you with: <strong>the AI field is not discovering orchestration. It&#39;s rediscovering it, badly, and selling it back to you as new.</strong> The question isn&#39;t &quot;should I adopt graph engineering?&quot; The question is whether you&#39;ll notice you&#39;ve known the answer since your first queue worker. The only thing new under the sun is that the workers now have opinions. Tell me — how are you planning to verify theirs?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/fa763633c59e3772bfd3ce2b2c2be406/blog-cover-graph-engineering.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[The $18K Ceiling Breaker: Skills That Actually Move Your Number]]></title>
            <link>https://yabasha.dev/blog/the-18k-ceiling-breaker-skills-that-actually-move-your-number</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/the-18k-ceiling-breaker-skills-that-actually-move-your-number</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:37 GMT</pubDate>
            <description><![CDATA[Learn the seven high‑impact capabilities—incident ownership, systems thinking, AI orchestration, regulatory fluency, migration architecture, cost arbitrage, and decision documentation—that can boost a developer’s pay by $18K in MENA fintech.]]></description>
            <content:encoded><![CDATA[<h2>The Salary Gap Is a Skill Gap, But Not the Kind You Think</h2>
<p>I&#39;ve watched the same pattern for 20 years. Developer learns React. Developer learns Next.js. Developer learns the next abstraction layer. Salary plateaus anyway.</p>
<p>The ceiling isn&#39;t technical. It&#39;s architectural. It&#39;s the gap between &quot;I write code&quot; and &quot;I own outcomes.&quot;</p>
<p>At Alrajhi, I see this daily. Two engineers, same stack (Laravel, Next.js, React Native), same years of experience. One makes $60K. The other makes $78K-plus. The difference? Seven specific capabilities that have nothing to do with syntax.</p>
<h2>Skill 1: Production Incident Ownership</h2>
<p>Most developers run from production fires. The $18K premium goes to the ones who run toward them.</p>
<p>I&#39;m not talking about on-call rotation theater. I mean <strong>true ownership</strong>: the 3AM alert, the corrupted transaction state, the AI agent that&#39;s hallucinating payment authorizations. You stay until root cause. You write the post-mortem that actually changes system design.</p>
<p>In our banking infra, we had a Laravel queue processor that would silently fail on specific Arabic character encodings. Three teams knew about it. One engineer — now our platform lead — spent a weekend tracing through Horizon, MySQL binary logs, and downstream React Native sync states. Fixed it permanently.</p>
<p>That&#39;s worth $18K. The skill isn&#39;t debugging. It&#39;s <strong>consequence absorption</strong>.</p>
<h2>Skill 2: Cross-System Mental Models</h2>
<p>Framework expertise has a half-life. Laravel 11 looks different from Laravel 5. But the physics of distributed systems? That compounds.</p>
<p>The engineers who break salary ceilings can hold <strong>three system layers simultaneously</strong>:</p>
<ul>
<li>The Next.js frontend&#39;s hydration boundary</li>
<li>The Laravel API&#39;s queue concurrency and connection pool</li>
<li>The React Native bridge&#39;s async storage and offline conflict resolution</li>
</ul>
<p>And they see how changes in one create tsunamis in others.</p>
<p>In MENA fintech, this matters more than Silicon Valley. Our users have intermittent connectivity. Our regulators demand audit trails that cross mobile, web, and core banking. The engineer who can trace a transaction from biometric auth in Riyadh to ledger commit in Jordan — that&#39;s architectural thinking. That&#39;s premium pricing.</p>
<h2>Skill 3: AI Agent Orchestration (Not Just Prompting)</h2>
<p>Everyone&#39;s playing with ChatGPT wrappers. Few are building <strong>agent systems that survive enterprise reality</strong>.</p>
<p>Our AI agents at Alrajhi handle KYC document verification, fraud scoring, and customer service escalation. The $18K difference isn&#39;t who writes better prompts. It&#39;s who understands:</p>
<ul>
<li><strong>Failure modes</strong>: What happens when the LLM hallucifies a regulatory threshold?</li>
<li><strong>Observability</strong>: How do you trace an agent decision through to audit requirements?</li>
<li><strong>Human-in-the-loop design</strong>: Where does friction actually help vs. hurt?</li>
</ul>
<p>I built a Laravel-based agent orchestrator that routes between GPT-4, Claude, and local models based on latency, cost, and confidence thresholds. The hard part wasn&#39;t the API calls. It was the <strong>circuit breakers, the fallback chains, the rollback mechanisms</strong>.</p>
<p>That&#39;s infrastructure thinking. That&#39;s the premium.</p>
<h2>Skill 4: Regulatory Translation</h2>
<p>In emerging markets, compliance isn&#39;t overhead. It&#39;s <strong>competitive moat</strong>.</p>
<p>Saudi SAMA regulations. Jordanian CBJ requirements. UAE DFSA frameworks. The engineer who can read a 200-page circular and translate it into system constraints — database schemas, API rate limits, retention policies — becomes irreplaceable.</p>
<p>I once spent three weeks mapping SAMA&#39;s &quot;customer due diligence&quot; requirements to our Laravel models. Sounds boring. But that mapping became the foundation for our AI agent training data, our React Native offline sync logic, and our Next.js admin dashboards.</p>
<p>Regulatory fluency is <strong>domain expertise with teeth</strong>.</p>
<h2>Skill 5: Migration Physics</h2>
<p>Greenfield projects are rare. Brownfield resurrection is the real work.</p>
<p>The $18K skill is <strong>migration architecture</strong>: moving live systems without breaking the money pipe.</p>
<p>We recently migrated a core Laravel 8 monolith to a modular Laravel 11 structure with event sourcing. The technical patterns are documented. The hard part was:</p>
<ul>
<li><strong>Hot migration</strong>: Dual-write periods, verification scripts, rollback windows</li>
<li><strong>Stakeholder gravity</strong>: Convincing risk committees that &quot;zero downtime&quot; is achievable</li>
<li><strong>Team momentum</strong>: Keeping 40 engineers productive while the ground shifts</li>
</ul>
<p>Migration physics is about <strong>headroom</strong> — the space you create for others to work while you rebuild the floor.</p>
<h2>Skill 6: Cost-to-Complexity Arbitrage</h2>
<p>Cloud bills are the new performance optimization. But most engineers optimize locally.</p>
<p>The ceiling-breaker sees <strong>cost architecture</strong>: which systems need GPUs, which can run on spot instances, where caching actually pays off vs. where it&#39;s premature optimization.</p>
<p>I cut our AI inference costs 40% by moving from synchronous OpenAI calls to async Laravel queues with local model fallbacks. The React Native app got faster (less waiting on network). The Next.js dashboard got richer (more budget for complex queries). The agents got more reliable (rate limit protection).</p>
<p>Cost optimization is <strong>system design with economic constraints</strong>.</p>
<h2>Skill 7: The Documentation of Decisions</h2>
<p>Not code comments. Not wiki pages. <strong>Decision records that survive turnover</strong>.</p>
<p>Every $18K premium engineer I know maintains ADRs (Architecture Decision Records) that capture:</p>
<ul>
<li>What we considered</li>
<li>What we rejected and why</li>
<li>The consequences we&#39;re accepting</li>
<li>The conditions that would make us reconsider</li>
</ul>
<p>In Jordan&#39;s tight talent market, this is survival infrastructure. When your senior Laravel engineer moves to Dubai, the decision context doesn&#39;t leave with them.</p>
<p>I have 200+ ADRs in our repo. Some from 2019. I still reference them when new engineers ask &quot;why not GraphQL?&quot; or &quot;why not serverless?&quot; The answers are contextual, not absolute. Documented context preserves <strong>institutional velocity</strong>.</p>
<h2>The MENA Multiplier</h2>
<p>Here&#39;s what the generic &quot;7 skills&quot; articles miss: <strong>geography changes the value function</strong>.</p>
<p>In MENA fintech, these skills compound:</p>
<ul>
<li>Regulatory translation × emerging market complexity = rare expertise</li>
<li>Migration physics × legacy core banking = high-stakes differentiation</li>
<li>AI agent orchestration × Arabic NLP gaps = genuine innovation opportunity</li>
</ul>
<p>We&#39;re not implementing Silicon Valley playbooks. We&#39;re building on sand that shifts — regulatory, infrastructural, cultural.</p>
<p>The $18K premium isn&#39;t about being 20% better at React. It&#39;s about <strong>being the person who can navigate uncertainty while shipping systems that move money</strong>.</p>
<h2>The Real Ceiling</h2>
<p>After 20 years, here&#39;s my contrarian take: <strong>salary ceilings are consent structures</strong>.</p>
<p>Most developers accept the frame they&#39;re given. &quot;Senior&quot; means 5 years. &quot;Staff&quot; means 10 years. The ceiling is imaginary until someone breaks it.</p>
<p>The seven skills above aren&#39;t about climbing a ladder. They&#39;re about <strong>making the ladder irrelevant</strong>. When you can own production, translate regulation, orchestrate AI, and document decisions that outlast your tenure, you stop competing on years-of-experience.</p>
<p>You compete on <strong>risk absorption</strong>.</p>
<p>And banks — especially in MENA, especially in fintech — will pay for risk absorption. Because the alternative is systems that fail in ways that make headlines.</p>
<h2>What I&#39;m Building Now</h2>
<p>At Alrajhi, we&#39;re pushing these skills into our hiring and promotion frameworks. Not buzzwords. Specific demonstrations: incident post-mortems, migration plans, agent observability dashboards, ADR portfolios.</p>
<p>The engineers who show up with these? We fight to keep them. The ones who don&#39;t? We invest until they do, or we make hard decisions.</p>
<p>Your move: which of these seven are you actually demonstrating, not just claiming? And what&#39;s the one production system you&#39;re avoiding ownership of because the consequences feel too heavy?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/c1c0134adbb3a47e1bd38392aaf6d33e/blog-cover-the-18k-ceiling-breaker-skills-that-actually-move-your-number.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Why My AI Prompts Are 12 Words Long — And Yours Should Collapse Too]]></title>
            <link>https://yabasha.dev/blog/why-my-ai-prompts-are-12-words-long-and-yours-should-collapse-too</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/why-my-ai-prompts-are-12-words-long-and-yours-should-collapse-too</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:31 GMT</pubDate>
            <description><![CDATA[In "Why My AI Prompts Are 12 Words Long — And Yours Should Collapse Too" the author proves that trimming prompts to a concise 12‑word command can dramatically cut latency, cost, and hallucinations for banking AI agents, especially in Arabic‑focused markets.]]></description>
            <content:encoded><![CDATA[<h2>The Wall of Words</h2>
<p>I used to write prompts like I used to write Laravel controllers in 2014 — verbose, defensive, convinced that more lines meant more control.</p>
<p>Hundreds of tokens setting context. Chain-of-thought examples. Five-shot demonstrations. Personality frameworks. Temperature lectures. I was building <strong>prompt cathedrals</strong> when I needed <strong>sledgehammers</strong>.</p>
<p>Then I watched our AI agent handling KYC verification at Alrajhi Bank choke on its own instructions. 400-token prompt. 8-second latency. Context window bleeding. The agent was spending more energy parsing my prose than reasoning about the customer.</p>
<p>That&#39;s when I started cutting. Ruthlessly.</p>
<h2>Gravity as a Feature</h2>
<p>Here&#39;s what 20 years of system architecture taught me: <strong>constraint creates clarity</strong>. Not the other way around.</p>
<p>In Laravel, we learned this with Eloquent. In Next.js, we learned it with server components. In banking infrastructure, we learned it when every millisecond of latency costs real money and regulatory scrutiny.</p>
<p>My current prompt for our fraud-detection agent:</p>
<blockquote>
<p>&quot;Flag transaction. Explain in Arabic. Confidence 0-1. No speculation.&quot;</p>
</blockquote>
<p>Twelve words. The agent doesn&#39;t wander. The agent doesn&#39;t hallucinate policy interpretations. The agent does <strong>one thing</strong> with <strong>defined boundaries</strong>.</p>
<h2>The MENA Reality Check</h2>
<p>Let me be direct about where I&#39;m sitting. Jordan. Three hours from Riyadh. Building for a market where &quot;enterprise AI&quot; still means most vendors are selling snake oil wrapped in English-language documentation.</p>
<p>Our users don&#39;t need AI that sounds like a McKinsey deck. They need decisions. Fast. In Arabic. With audit trails.</p>
<p>The friction of verbose prompts isn&#39;t just latency. It&#39;s <strong>translation debt</strong>. English-heavy prompting creates a middle layer of cultural misalignment. Our agents were answering questions nobody asked because the prompts assumed Western banking contexts.</p>
<p>Twelve Arabic words — or twelve English words that map cleanly — cuts through that. No abstraction tsunami. No context collapse.</p>
<h2>What I Actually Built</h2>
<p>Our stack at Alrajhi: Laravel for core banking APIs, Next.js for internal dashboards, React Native for customer-facing apps. The AI layer sits between, orchestrating agents for credit scoring, document verification, compliance checks.</p>
<p>I rebuilt our three heaviest agents last quarter with the 12-word constraint.</p>
<p><strong>Before:</strong> 340-token average prompt, 4.2s response time, 12% hallucination rate on edge cases.</p>
<p><strong>After:</strong> 89-token average prompt, 1.8s response time, 3% hallucination rate.</p>
<p>The hallucinations didn&#39;t drop because the model got smarter. They dropped because <strong>I stopped confusing it</strong>.</p>
<h2>The Mechanics of Collapse</h2>
<p>How do you actually do this? Not by being clever. By being <strong>mechanical</strong>.</p>
<h3>1. One Domain Verb</h3>
<p>&quot;Flag.&quot; &quot;Extract.&quot; &quot;Compare.&quot; &quot;Route.&quot; Not &quot;analyze and determine whether there might be indicators suggesting.&quot; That&#39;s not precision. That&#39;s throat-clearing.</p>
<h3>2. One Output Format</h3>
<p>&quot;JSON with keys: decision, confidence, reasoning.&quot; Not &quot;please provide your response in a structured format that includes.&quot; The model knows JSON. Name the schema and move on.</p>
<h3>3. One Constraint Boundary</h3>
<p>&quot;No speculation&quot; is doing heavy lifting. It creates a wall. The agent hits it and stops. Without that wall, it keeps generating — confident-sounding nonsense about patterns it half-perceives.</p>
<h3>4. One Localization Anchor</h3>
<p>&quot;Explain in Arabic.&quot; Not optional. Not &quot;if relevant.&quot; Our regulators read Arabic. Our customers read Arabic. The default English bias of most LLMs is a <strong>compliance risk</strong> I&#39;m not taking.</p>
<h2>The Infrastructure Angle</h2>
<p>From an infra + UX lens, this matters more than it looks.</p>
<p>Short prompts mean smaller context windows. Smaller context windows mean:</p>
<ul>
<li><strong>Cheaper inference</strong> — we&#39;re seeing 40% cost reduction on our OpenAI bill</li>
<li><strong>Faster caching</strong> — similar prompts hit Redis more often</li>
<li><strong>Cleaner observability</strong> — I can actually read the prompts in our logs without scrolling</li>
<li><strong>Easier A/B testing</strong> — swapping 12 words is surgical; swapping 400 is archaeology</li>
</ul>
<p>In Next.js terms: it&#39;s the difference between server components that ship minimal JS and a bloated bundle that ships everything just in case. The browser — or the LLM — pays for your indecision.</p>
<h2>What I Cut (And Don&#39;t Miss)</h2>
<p>Gone: &quot;You are a helpful assistant.&quot; The model already knows this. It&#39;s either true and redundant, or false and ignored.</p>
<p>Gone: &quot;Think step by step.&quot; Chain-of-thought doesn&#39;t need an invitation. The model reasons or it doesn&#39;t. The instruction adds tokens, not capability.</p>
<p>Gone: Examples in the prompt. We moved to fine-tuned adapters. One-time cost, permanent behavior. Prompt examples are runtime tax.</p>
<p>Gone: Temperature settings in prose. &quot;Be creative but accurate&quot; is meaningless. We set temperature=0 in the API call and describe the task. The model doesn&#39;t need to be told its own hyperparameters.</p>
<h2>The React Native Parallel</h2>
<p>Our mobile team felt this immediately. They were building a chat interface for our AI agents and drowning in loading states.</p>
<p>Four-second prompts meant four-second spinners. In Jordan&#39;s mobile networks — where 3G still exists and patience is a currency — that&#39;s abandonment.</p>
<p>Collapsing to 12-word prompts cut perceived latency more than any optimization we tried. The physics are simple: less data over the wire, faster token generation, earlier streaming starts.</p>
<p>They didn&#39;t change the UI. I changed the gravity underneath it.</p>
<h2>When Long Still Lives</h2>
<p>I&#39;m not a fundamentalist. Some problems need walls of text.</p>
<p>Legal contract analysis. Multi-party negotiation. Creative writing where voice matters. Our credit policy agent still carries 200 tokens of regulatory framework because <strong>the complexity is real</strong>.</p>
<p>But I audit it monthly. I cut 10% every time. The framework creeps. Gravity wins by default.</p>
<p>The difference: <strong>intentional complexity versus accumulated cruft</strong>. Most long prompts I see are the latter. Written once, never reviewed, cargo-culted across systems.</p>
<h2>The Enterprise Trap</h2>
<p>Here&#39;s what frustrates me about banking AI discourse. Vendors selling &quot;enterprise solutions&quot; with prompt templates that read like terms of service. Layer upon layer of &quot;safety&quot; that just creates ambiguity.</p>
<p>Our procurement team loves these. Boxes checked. Compliance theater complete.</p>
<p>My engineering team hates them. We can&#39;t see what the agent actually does. We can&#39;t debug failures. We can&#39;t optimize.</p>
<p>Twelve-word prompts are <strong>un-auditable</strong> in the best way. They&#39;re so simple that review is instant. No hidden logic. No emergent behavior from prompt interactions.</p>
<p>In emerging markets, this matters double. We don&#39;t have the headcount to maintain prompt engineering teams. We need systems that <strong>stay comprehensible</strong> when the person who built them is on vacation or has left.</p>
<h2>The Fine-Tuning Pivot</h2>
<p>The real unlock came when we stopped trying to prompt our way to behavior and started <strong>training</strong> for it.</p>
<p>Laravel&#39;s ecosystem taught me this. You don&#39;t write 500 lines of validation logic. You write a FormRequest. You declare rules. The framework handles the rest.</p>
<p>We fine-tuned a small model on our 12-word patterns. Now the &quot;prompt&quot; is often just the verb and the data. The behavior lives in weights, not tokens.</p>
<p>Cost: $2,400 for the fine-tuning run. Benefit: 60% inference cost reduction, sub-second responses, behavior that&#39;s <strong>testable</strong> rather than <strong>promptable</strong>.</p>
<p>This is the direction. Prompts as interface, not implementation. The shorter the interface, the cleaner the contract.</p>
<h2>What I&#39;m Watching</h2>
<p>Claude&#39;s 3.5 Sonnet. Google&#39;s Flash. OpenAI&#39;s 4o-mini. The race to fast, cheap, capable is compressing the prompt window whether we like it or not.</p>
<p>Models are getting better at <strong>implied instruction</strong>. They&#39;re learning that &quot;flag transaction&quot; in a banking context carries assumptions about what matters. The prompt can shrink because the <strong>context is richer</strong>.</p>
<p>This is the tsunami coming. Developers still writing 500-token prompts are building on sand. The models don&#39;t need your scaffolding anymore. They need your <strong>intent</strong>, cleanly stated.</p>
<h2>The Question</h2>
<p>I cut my prompts to 12 words because I was forced to — by latency, by cost, by Arabic localization, by regulatory scrutiny in a market that doesn&#39;t forgive slowness.</p>
<p>What&#39;s forcing you?</p>
<p>Or are you still building cathedrals nobody asked for, waiting for the gravity you refuse to see?</p>
<hr>
<p><em>Building AI agents for banking in Amman. Laravel, Next.js, React Native. Twenty years of watching complexity collapse. Always looking for the 12-word version.</em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/74d399f85a589bd4b03ee175cd70879c/blog-cover-why-my-ai-prompts-are-12-words-long-and-yours-should-collapse-too.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Every Engineer Is Now a Manager — Whether You Signed Up For It or Not]]></title>
            <link>https://yabasha.dev/blog/every-engineer-is-now-a-manager-whether-you-signed-up-for-it-or-not</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/every-engineer-is-now-a-manager-whether-you-signed-up-for-it-or-not</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:25 GMT</pubDate>
            <description><![CDATA[Discover why engineers are becoming managers as AI agents reshape software development — shifting focus to governance, orchestration, and organizational design in regulated industries like fintech.]]></description>
            <content:encoded><![CDATA[<h2>The Wall I Hit in Amman</h2>
<p>Three months ago, I watched a Laravel queue worker I&#39;d written get replaced by an AI agent that wrote its own retry logic.</p>
<p>Not because the agent was better. Because I spent six hours in a meeting arguing whether our <strong>Next.js</strong> admin panel should show agent confidence scores or raw token probabilities. While the agent kept shipping.</p>
<p>That&#39;s when it hit me: <strong>I&#39;m not fighting AI for my job. I&#39;m fighting to stay technical while becoming a manager I never asked to be.</strong></p>
<h2>The Friction of &quot;Vibe Coding&quot;</h2>
<p>You&#39;ve seen the demos. Cursor, Claude Code, Lovable — type a prompt, get a deployable branch. The MENA startup ecosystem is drowning in founders who think they&#39;ve eliminated engineering.</p>
<p>They&#39;re wrong. But so are we.</p>
<p>The friction didn&#39;t disappear. It <strong>moved up the stack</strong>. Now I spend my mornings at <strong>Alrajhi Bank</strong> reviewing agent-generated <strong>React Native</strong> modules not for syntax errors — those are gone — but for:</p>
<ul>
<li><strong>Regulatory compliance</strong>: Does this KYC flow satisfy SAMA&#39;s sandbox requirements?</li>
<li><strong>Audit trails</strong>: Can we prove <em>why</em> the agent chose this biometric vendor?</li>
<li><strong>Failure modes</strong>: When the LLM hallucinates a Saudi IBAN format, who&#39;s on-call?</li>
</ul>
<p>The code is cheap. The <strong>context</strong> is expensive.</p>
<h2>From Artisan to Air Traffic Controller</h2>
<p>Twenty years ago, I measured productivity in commits. Now I measure it in <strong>decision velocity</strong>.</p>
<p>My team runs 12 autonomous agents across our banking infrastructure. Each agent has:</p>
<ul>
<li>A budget ceiling (inference tokens + API calls)</li>
<li>A risk profile (can it touch prod? customer data? core banking APIs?)</li>
<li>An escalation ladder (when does it wake a human?)</li>
</ul>
<p>Sound familiar? That&#39;s <strong>management</strong>. I&#39;m running a distributed team where half the &quot;engineers&quot; are stochastic and need constant performance reviews.</p>
<p>The <strong>Laravel</strong> applications I maintain now serve as <strong>governance layers</strong> — not business logic executors. My <strong>Next.js</strong> frontends are <strong>observability dashboards</strong> for agent behavior. The actual &quot;work&quot; happens in headless loops I can&#39;t step through with a debugger.</p>
<h2>The MENA Reality Check</h2>
<p>Jordan&#39;s tech scene taught me something Silicon Valley misses: <strong>infrastructure constraints breed better managers</strong>.</p>
<p>When your GPU cluster lives in Frankfurt because AWS Riyadh doesn&#39;t have the capacity, you learn to optimize for <strong>latency of decision-making</strong>, not lines of code. When your mobile banking app needs to function on 3-year-old Android devices with intermittent connectivity, you can&#39;t afford agent experiments that &quot;mostly work.&quot;</p>
<p>I see Egyptian fintechs, Emirati neobanks, Saudi payment rails — all converging on the same pattern. The engineers who thrive aren&#39;t the ones prompting hardest. They&#39;re the ones who <strong>architect the guardrails</strong>.</p>
<h2>What &quot;Managing&quot; Actually Means Now</h2>
<p>Let me be specific about what changed in my daily work:</p>
<p><strong>Budget ownership</strong>: Every agent invocation is a line item. I negotiate with finance whether our fraud detection agent gets GPT-4 or Claude 3.5 Sonnet based on latency/cost trade-offs. That&#39;s <strong>resource allocation</strong>.</p>
<p><strong>Stakeholder translation</strong>: Business wants &quot;AI personalization.&quot; I translate that into: agent prompt templates, A/B test frameworks, fallback rules when personalization violates <strong>SAMA</strong> guidelines. That&#39;s <strong>requirements management</strong>.</p>
<p><strong>Team health</strong>: My human engineers are burning out <em>faster</em> because the cognitive load shifted. They&#39;re not debugging code — they&#39;re debugging <strong>intent alignment</strong> between agents. That&#39;s <strong>people management</strong>.</p>
<p><strong>Risk posture</strong>: When an agent suggests a database migration based on schema inference, I&#39;m the one who says no. Not because it&#39;s wrong. Because <strong>I can&#39;t prove it&#39;s right</strong> to an auditor. That&#39;s <strong>governance</strong>.</p>
<h2>The Stack Becomes the Org Chart</h2>
<p>Here&#39;s what hurts: the tools I love are becoming <strong>management interfaces</strong>.</p>
<p><strong>Laravel</strong> used to be about elegant Eloquent relationships. Now it&#39;s about <em>authorization policies</em> for agent actions — can this service account trigger a wire transfer? Should it?</p>
<p><strong>Next.js</strong> used to be about SSR performance. Now it&#39;s about <em>streaming agent responses</em> — how do we render confidence intervals in real-time without freezing the UI?</p>
<p><strong>React Native</strong> used to be about bridge modules and native performance. Now it&#39;s about <em>offline-first agent synchronization</em> — how do we queue agent tasks when the user loses signal in rural Jordan?</p>
<p>The architecture diagrams I draw look less like layered cakes and more like <strong>organizational charts</strong> — with humans, agents, and legacy systems as nodes, and <em>trust boundaries</em> as the edges.</p>
<h2>The Headroom Problem</h2>
<p>There&#39;s a ceiling on how much complexity one human can orchestrate. I used to think that ceiling was cognitive — how much code can I understand?</p>
<p>Now I know it&#39;s <strong>attentional</strong>. How many agent contexts can I hold simultaneously while remaining accountable for outcomes?</p>
<p>At Alrajhi, we&#39;re experimenting with <strong>meta-agents</strong> — systems that manage other agents. This should solve the scaling problem. Instead, it created a <strong>recursive management burden</strong>: who manages the meta-agent? Who reviews its reviews?</p>
<p>The answer, so far, is me. And every engineer I know in similar roles.</p>
<h2>What We Lose, What We Keep</h2>
<p>I miss the gravity of production deploys. The certainty that <em>I</em> wrote the logic, <em>I</em> understood the edge cases, <em>I</em> could explain the failure mode to a regulator at 3 AM.</p>
<p>Now I ship systems I can only <strong>characterize statistically</strong>. &quot;This agent performs 94% accuracy on our test suite&quot; — that&#39;s not engineering. That&#39;s <strong>quality assurance for a black box</strong>.</p>
<p>But here&#39;s what I gained: <strong>leverage at a different scale</strong>. One engineer with good agent orchestration can cover what used to require a squad. In emerging markets where technical talent is scarce and expensive, that&#39;s not optional. That&#39;s <strong>survival</strong>.</p>
<h2>The Contrarian Take</h2>
<p>Everyone says &quot;learn to prompt better.&quot; I say: <strong>learn to manage better</strong>.</p>
<p>Prompt engineering is a local maximum. The engineers who will own the next decade are building:</p>
<ul>
<li><strong>Evaluation frameworks</strong> (how do you know your agent improved?)</li>
<li><strong>Fallback architectures</strong> (what happens when the LLM provider changes their model?)</li>
<li><strong>Audit infrastructure</strong> (how do you reconstruct a decision for compliance?)</li>
</ul>
<p>These aren&#39;t coding skills. They&#39;re <strong>organizational design skills</strong> applied to software.</p>
<h2>The Jordanian Perspective</h2>
<p>In Amman, we have a phrase: <em>&quot;El-maktaba elly ma feeha ketaab, ma feeha 3alam&quot;</em> — the library without books has no flags. Meaning: you need substance to signal status.</p>
<p>The global AI discourse is obsessed with <em>flags</em> — demos, benchmarks, hype. In MENA fintech, we&#39;re obsessed with <em>substance</em> — <strong>provable correctness, regulatory defensibility, operational resilience</strong>.</p>
<p>That makes us conservative. That also makes us <strong>better prepared</strong> for the actual future, where AI isn&#39;t a magic wand but a <strong>risk vector</strong> requiring constant supervision.</p>
<h2>Your New Job Description</h2>
<p>If you&#39;re still reading, you&#39;re probably wondering: what should I actually <em>do</em>?</p>
<p>Stop optimizing for code output. Start optimizing for <strong>system comprehension</strong>.</p>
<ul>
<li>Can you explain <em>why</em> an agent made a decision to a non-technical stakeholder?</li>
<li>Can you design a <strong>circuit breaker</strong> that fails safely when agents disagree?</li>
<li>Can you <strong>price the cost</strong> of an AI feature in tokens, latency, and cognitive load?</li>
</ul>
<p>These are management questions. They&#39;re also <strong>engineering questions</strong> now.</p>
<h2>The Tsunami Isn&#39;t Coming. It&#39;s Here.</h2>
<p>I didn&#39;t choose to become a manager. The architecture of modern systems — especially in regulated industries like banking — made it inevitable.</p>
<p>The developers who resist this shift will find themselves squeezed between &quot;vibe coders&quot; who ship faster and cheaper, and &quot;AI engineers&quot; who actually understand the systems underneath. There&#39;s no middle ground anymore.</p>
<p>Your choice isn&#39;t whether to manage. It&#39;s whether to <strong>manage well</strong> — with explicit frameworks, measurable outcomes, and honest accounting of where human judgment adds value.</p>
<p>Or to manage poorly — by pretending you&#39;re still just &quot;coding&quot; while the complexity tsunami washes over you.</p>
<h2>The Real Question</h2>
<p>Twenty years in, I&#39;ve learned that <strong>disruption doesn&#39;t replace expertise. It relocates it</strong>.</p>
<p>The expertise that mattered in 2005 was algorithmic. In 2015, it was architectural. In 2025, it&#39;s <strong>orchestrational</strong> — the ability to compose reliable systems from unreliable components, human and artificial.</p>
<p>So here&#39;s my provocation: <strong>When was the last time you wrote code that no agent could have generated — not because of complexity, but because of accountability?</strong></p>
<p>If you can&#39;t answer that, you&#39;re already a manager. The only question is whether you&#39;re getting paid like one.</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/1bbd240d2a1f37905243281682290918/blog-cover-every-engineer-is-now-a-manager-whether-you-signed-up-for-it-or-not.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Your Call Logs Are the Only Training Data That Actually Matters]]></title>
            <link>https://yabasha.dev/blog/your-call-logs-are-the-only-training-data-that-actually-matters</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/your-call-logs-are-the-only-training-data-that-actually-matters</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:20 GMT</pubDate>
            <description><![CDATA[Discover why authentic call logs are the only true foundation for training resilient enterprise AI agents, bridging the gap between perfect demos and messy, real-world conversations.]]></description>
            <content:encoded><![CDATA[<h2>The Dirty Secret of Enterprise AI</h2>
<p>Every AI agent demo looks perfect.</p>
<p>Clean transcripts. Happy paths. Customers who speak in complete sentences and never interrupt themselves. I&#39;ve sat through fifty of these pitches at Alrajhi Bank. The gap between demo and production is a canyon.</p>
<p>Here&#39;s what actually happens when your banking AI agent hits real users in Riyadh, Amman, or Cairo: code-switching between Arabic dialects, background noise from busy streets, customers who don&#39;t know their own account types, edge cases that break your intent classification in ways you couldn&#39;t imagine.</p>
<p>Your synthetic training data? It&#39;s architectural fiction.</p>
<h2>Why I Started Recording Everything</h2>
<p>Two years ago we built our first voice agent for balance inquiries and transfers. Standard stuff. Laravel backend, Next.js admin panel, React Native for the mobile interface.</p>
<p>We trained on 10,000 synthetic conversations. Beautiful dataset. Grammatically perfect. Covered every feature flag in our spec.</p>
<p>Week one in production: 34% containment rate. The agent couldn&#39;t handle a customer who said &quot;habibi I need my money&quot; instead of &quot;transfer funds.&quot; Couldn&#39;t parse the Jordanian dialect &quot;shu&quot; as a question marker. Crashed on overlapping speech when someone&#39;s kid grabbed the phone.</p>
<p>We were flying blind. No recordings, no telemetry, just logs saying &quot;intent_classification_failed.&quot;</p>
<p>That failure taught me something I&#39;ve applied to every AI project since: <strong>the conversation is the infrastructure.</strong> Not the model. Not the prompt engineering. The raw, messy, recorded reality of how humans actually talk to machines.</p>
<h2>What &quot;Recording Everything&quot; Actually Means</h2>
<p>I&#39;m not talking about compliance recording for legal teams. That&#39;s table stakes in banking.</p>
<p>I mean <strong>engineering-grade capture</strong>:</p>
<ul>
<li><strong>Full audio streams</strong> with speaker diarization (who spoke when)</li>
<li><strong>Real-time transcriptions</strong> with confidence scores per word</li>
<li><strong>Latency metrics</strong> for every turn in the conversation</li>
<li><strong>Context state snapshots</strong> at each decision point</li>
<li><strong>Model outputs</strong> with their probability distributions</li>
<li><strong>Fallback triggers</strong> and the exact user utterance that caused them</li>
</ul>
<p>In our Laravel backend, we built a pipeline that treats every call as a structured event stream. Audio chunks flow through Redis streams, get processed by Whisper for transcription, then land in ClickHouse for analysis. The React Native client uploads compressed audio with session metadata. Our Next.js dashboard lets engineers replay conversations with full context reconstruction.</p>
<p>This isn&#39;t Big Brother surveillance. It&#39;s <strong>observability for conversational systems.</strong></p>
<h2>The MENA Reality Check</h2>
<p>Working from Jordan, I see how Western AI assumptions crumble in emerging markets.</p>
<p>Your average Saudi customer might start a conversation in formal Arabic, switch to Najdi dialect when frustrated, drop English technical terms, then code-mix with Urdu if they&#39;re South Asian. Try training your standard OpenAI or Anthropic model on that without real recordings.</p>
<p>Network conditions matter too. Our React Native app sees call quality ranging from pristine 5G in Riyadh financial district to degraded audio on 3G in rural Jordan. Without recorded samples across this spectrum, your noise robustness is theoretical.</p>
<p>And don&#39;t get me started on <strong>cultural friction patterns</strong>. In some MENA markets, customers repeat security information slowly because they don&#39;t trust the system understood. In others, they speak fast to test if the agent is actually listening. These behaviors don&#39;t appear in synthetic data. They only emerge when you have walls of real calls to analyze.</p>
<h2>How We Use Recordings to Build Better Agents</h2>
<p>Our current workflow at Alrajhi looks like this:</p>
<h3>Week 1-2: Shadow Mode</h3>
<p>New agent runs alongside human agents. We record everything but don&#39;t automate responses. The AI generates replies, we compare against what humans actually said, measure divergence. This catches intent misclassification before customers see it.</p>
<h3>Week 3-4: Graduated Rollout</h3>
<p>We enable the agent for 5% of traffic, but with aggressive escalation triggers. Every escalation gets tagged with the audio snippet that caused it. Our Laravel pipeline automatically surfaces clusters: &quot;agent failed on balance inquiries over 1M SAR,&quot; &quot;confusion between &#39;transfer&#39; and &#39;payment&#39; verbs.&quot;</p>
<h3>Month 2+: Continuous Mining</h3>
<p>We run weekly jobs that:</p>
<ul>
<li>Extract high-entropy utterances (low confidence, multiple intent matches)</li>
<li>Find conversation patterns that led to human takeover</li>
<li>Identify successful repair sequences (where agent recovered from misunderstanding)</li>
<li>Build synthetic training data from <em>real</em> patterns, not imagined ones</li>
</ul>
<p>This last point matters. We don&#39;t use raw recordings for training (privacy, compliance, PII). We use them to understand what to synthesize. The recordings tell us where our synthetic data was wrong.</p>
<h2>The Technical Architecture</h2>
<p>For engineers who want specifics, here&#39;s our stack:</p>
<p><strong>Ingestion Layer</strong></p>
<ul>
<li>Twilio/Media Streams for telephony audio</li>
<li>React Native SDK with custom audio compression (Opus, 24kbps)</li>
<li>Laravel queue workers for stream routing</li>
</ul>
<p><strong>Processing Pipeline</strong></p>
<ul>
<li>Whisper v3 for transcription with fine-tuned Arabic dialect support</li>
<li>Speaker diarization via pyannote.audio</li>
<li>Real-time intent classification with our own fine-tuned BERT variant</li>
</ul>
<p><strong>Storage &amp; Retrieval</strong></p>
<ul>
<li>Audio: S3 with lifecycle policies (30 days hot, 1 year glacier)</li>
<li>Metadata: ClickHouse for fast analytical queries</li>
<li>Search: Elasticsearch for transcript full-text + phonetic matching</li>
</ul>
<p><strong>Analysis Tools</strong></p>
<ul>
<li>Next.js dashboard with conversation replay</li>
<li>Jupyter notebooks for pattern mining</li>
<li>Automated alerting on containment rate drops</li>
</ul>
<p>The whole system adds about 200ms latency to calls, which is acceptable for banking use cases. For high-frequency trading or real-time payments, we&#39;d need edge deployment. Trade-offs everywhere.</p>
<h2>What You&#39;ll Actually Find</h2>
<p>After six months of recording, here&#39;s what we discovered about our &quot;simple&quot; balance inquiry agent:</p>
<ul>
<li><strong>23% of calls</strong> included at least one non-task utterance (greeting, complaint about previous service, small talk)</li>
<li><strong>Edge case explosion</strong>: 147 distinct ways customers asked for &quot;recent transactions,&quot; none appearing in our original training set</li>
<li><strong>Recovery patterns</strong>: The most successful agent recoveries used acknowledgment phrases specific to Saudi Arabic politeness norms, which our synthetic data completely missed</li>
<li><strong>Silent failures</strong>: 8% of calls where the agent gave technically correct but practically wrong answers (customer asked for &quot;last transfer,&quot; agent gave last <em>incoming</em> transfer, customer wanted last <em>outgoing</em>)</li>
</ul>
<p>These aren&#39;t bugs you find in testing. They&#39;re gravity. They&#39;re the weight of real usage pulling your system toward its actual shape.</p>
<h2>The Privacy Engineering Problem</h2>
<p>I know what you&#39;re thinking. Banking. Recordings. Compliance nightmare.</p>
<p>Yes. And it&#39;s solvable.</p>
<p>We built PII detection directly into the Laravel pipeline. Audio streams get scanned for credit card numbers, account numbers, national IDs before storage. Transcripts are redacted automatically. Access controls are granular: engineers can see patterns, not individual calls, unless specifically authorized for debugging.</p>
<p>In Jordan and Saudi Arabia, regulatory frameworks are evolving fast. We work with legal teams to ensure consent flows are clear. The React Native app explicitly asks for recording permission with specific use case explanations, not blanket &quot;improve our service&quot; language.</p>
<p>The alternative — not recording, not knowing — is worse. You ship blind. You discover failures through customer complaints and regulatory fines rather than engineering analysis.</p>
<h2>Before You Ship That Agent</h2>
<p>If you&#39;re building AI agents for enterprise — especially in regulated industries, especially in complex linguistic environments — here&#39;s my checklist:</p>
<ol>
<li><p><strong>Recording infrastructure first.</strong> Not after. First. Before you train your model, before you write your prompts, you need the pipeline to capture reality.</p>
</li>
<li><p><strong>Synthetic data is a starting point, not a substitute.</strong> Use it to bootstrap. Replace it with distributions derived from real recordings as fast as possible.</p>
</li>
<li><p><strong>Measure containment, but also measure confidence calibration.</strong> An agent that escalates appropriately is better than one that&#39;s confidently wrong.</p>
</li>
<li><p><strong>Build for dialect, noise, and interruption from day one.</strong> These aren&#39;t edge cases. They&#39;re the center of mass in emerging markets.</p>
</li>
<li><p><strong>Create feedback loops where engineers hear the failures.</strong> Not dashboards. Actual audio. The emotional weight of a frustrated customer teaches more than any metric.</p>
</li>
</ol>
<h2>The Real Question</h2>
<p>Most teams building AI agents are optimizing for demo day. Clean transcripts, happy paths, metrics that look good in pitch decks.</p>
<p>The teams that survive production are optimizing for <strong>friction visibility.</strong> They want to see where the system grinds against reality. They build recording infrastructure not despite the complexity, but because it reveals truth.</p>
<p>We&#39;re about to deploy our third-generation agent at Alrajhi. It handles complex multi-turn conversations, integrates with our core banking systems through Laravel APIs, serves customers across three countries. The difference between this generation and our first attempt isn&#39;t better models or bigger training sets.</p>
<p>It&#39;s 400,000 recorded conversations showing us exactly where we were wrong.</p>
<p><strong>So here&#39;s my question for you:</strong> What&#39;s your plan for capturing reality? Or are you still building in the clean room, hoping the world will cooperate with your assumptions?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/8b15a7fa4407fbcc4b9c215b77cb9a8a/blog-cover-your-call-logs-are-the-only-training-data-that-actually-matters.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[AI Agents Ate My Boilerplate: What Actually Works in Enterprise Banking]]></title>
            <link>https://yabasha.dev/blog/ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:14 GMT</pubDate>
            <description><![CDATA[Five AI agents I run inside a bank, what each one is allowed to decide, and the three places they still get it wrong. Every one is a queue worker, not a chat window.]]></description>
            <content:encoded><![CDATA[<p><em>I work remotely with an enterprise bank in the region, and have done for four years. I do not name it here. Where a detail would describe its security posture rather than the engineering, I have generalised it.</em></p>
<p>Three agentic AI vendors pitched us last quarter. Every pitch had the same shape. A fast demo, perfect context, and a price that assumed compute was free.</p>
<p>Then you ask about throughput. About audit trails. About what a hallucinated compliance decision costs at two in the morning while the transaction pipeline is already saturated.</p>
<!-- lift --> In a regulated system the useful question is not what the agent can do. It is what the agent is allowed to decide on its own, and the honest answer is almost nothing.

<h2>What are the five agents, and what is each one allowed to decide?</h2>
<p>Five agents run in production. None of them decide anything by themselves. Each produces a draft, a score or a suggestion, and a person signs it.</p>
<table>
<thead>
<tr>
<th>Agent</th>
<th>What it produces</th>
<th>What it may never do</th>
</tr>
</thead>
<tbody><tr>
<td>API contract enforcer</td>
<td>Laravel request classes, TypeScript types, breaking-change PRs</td>
<td>Merge anything touching a monetary amount</td>
</tr>
<tr>
<td>Compliance log archaeologist</td>
<td>Draft regulator reports with an evidence chain</td>
<td>File a report</td>
</tr>
<tr>
<td>Mobile release check</td>
<td>A submission confidence score and failing screenshots</td>
<td>Submit a build</td>
</tr>
<tr>
<td>Incident response shadow</td>
<td>Likely cause, suggested rollback, prior incidents</td>
<td>Execute a remediation</td>
</tr>
<tr>
<td>Code review agent</td>
<td>Findings on N+1 queries, authorization gaps, PII in logs</td>
<td>Approve a pull request</td>
</tr>
</tbody></table>
<p>That last column is the design. Everything else is implementation detail.</p>
<h2>How do you generate API contracts from documentation that lies?</h2>
<p>By treating the documentation as a hint and the response samples as the truth. We integrate with more than 40 third-party APIs: card networks, local switches, government identity services. Their documentation includes PDFs scanned in 2019, Swagger files that disagree with the endpoint, and services that return HTTP 200 with an HTML error page in the body.</p>
<p>The agent runs inside a GitHub Actions workflow rather than a chat window. It pulls the current spec or scrapes whatever passes for documentation, generates strict TypeScript from real response samples, writes Laravel <code>FormRequest</code> classes with validation extracted from the field descriptions, and opens a pull request whose description is the breaking-change analysis.</p>
<!-- lift --> It still invents required fields. Anything touching a monetary amount carries a HUMAN_VERIFY label and does not merge without a person reading it.

<p>The win was not speed. It was that the mobile team stopped shipping type errors into production.</p>
<h2>What does a regulator&#39;s audit request actually require?</h2>
<p>Ninety days of transaction history, delivered inside 48 hours, with an evidence chain a person can follow back to the query that produced each number. That used to take four engineers three days. It now takes one agent and one compliance officer.</p>
<p>The agent has read-only views onto our transaction warehouse, <a href="https://clickhouse.com/docs">ClickHouse</a> underneath, and retrieval over our internal compliance playbook, vectorised in Postgres with <a href="https://github.com/pgvector/pgvector">pgvector</a>. The output is a Markdown report with the SQL embedded, so the officer can rerun any figure rather than trust it.</p>
<p>Embedding the query next to the answer is the part worth copying. A number a reviewer cannot reproduce is a number that fails the audit, whatever produced it.</p>
<h2>Why does data residency force inference onto your own hardware?</h2>
<p>Because the transaction data cannot leave. That single constraint decides the model, the hardware and the budget, before anyone discusses quality.</p>
<p>So we serve an open-weight model on hardware we own, with <a href="https://docs.vllm.ai/en/latest/">vLLM</a> in front of it. It is slower than a hosted frontier model and it costs more GPUs. It is also the only version of this that a regulator will accept.</p>
<p>Latency makes the same argument from a different direction. Regional fibre to Frankfurt runs around 60 ms on a good day, before the model has read a token. Hosted AI services are priced and designed around a latency budget that assumes you are next door to the datacentre. For anything a customer waits on, local inference is not a preference.</p>
<!-- lift --> Data residency is not a compliance checkbox you satisfy at the end. It is the first architectural constraint, and it eliminates most of the vendor shortlist before the evaluation starts.

<h2>How do you stop shipping App Store rejections?</h2>
<p>By testing against your own rejection history rather than against a generic checklist. <a href="https://developer.apple.com/app-store/review/guidelines/">Guideline 4.2, Minimum Functionality</a>, is subjective, and it is applied unevenly across reviewers in our region. A rejection costs a launch window, not an afternoon.</p>
<p>The agent runs <a href="https://maestro.dev/">Maestro</a> and <a href="https://wix.github.io/Detox/">Detox</a> flows in CI. The judgement sits on top: it reads previous rejection reasons from the App Store Connect API, generates cases that target those specific patterns, flags screens that read as a wrapped web page, and produces a submission confidence score.</p>
<table>
<thead>
<tr>
<th>Measure</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>Submission to approval</td>
<td>14 days</td>
<td>4 days</td>
</tr>
</tbody></table>
<p>Apple did not get faster. We stopped submitting broken builds. The agent catches broken deep links, missing loading states and Arabic text truncating in right-to-left layouts. It cannot fix an app that is genuinely a wrapped website, and no agent can.</p>
<h2>Should an agent remediate an incident on its own?</h2>
<p>No. Not in a bank. Auto-remediation is a liability position, not an engineering one, so this agent is decision support for whoever is awake at three in the morning.</p>
<p>A PagerDuty webhook lands in a Laravel <a href="https://laravel.com/docs/12.x/queues">queue worker</a>. The agent gathers recent deploys, database metrics and error logs, retrieves over our incident runbooks, and posts one Slack thread: likely root cause, a suggested rollback command, and the closest previous incidents.</p>
<p>During an annual bulk-payment run, card processing spiked 400%. The agent named the pattern correctly, bulk payment plus third-party processor throttling, and surfaced a manual failover procedure we had documented and never automated.</p>
<p>It also suggested a failover that would have broken our active-active architecture contract. A person caught it. That is the whole argument for keeping the last column of the table above empty.</p>
<h2>What does a code review agent catch that generic tools miss?</h2>
<p>The patterns that are only dangerous in your codebase. We tried the general-purpose review tools and they were too noisy to keep, because banking code has failure modes that a tool trained on public repositories does not recognise.</p>
<p>Ours takes <a href="https://phpstan.org/">PHPStan</a> output as structured input and adds rules extracted from our own past incidents.</p>
<table>
<thead>
<tr>
<th>What it catches</th>
<th>Why a generic tool misses it</th>
</tr>
</thead>
<tbody><tr>
<td><a href="https://laravel.com/docs/12.x/eloquent-relationships">Eloquent relationships</a> loaded in a loop without <code>with()</code></td>
<td>It reads as valid code; only the query count shows it</td>
</tr>
<tr>
<td>Authorization checks bypassed through <code>DB::raw()</code></td>
<td>The gate exists, the query goes around it</td>
</tr>
<tr>
<td>Personally identifiable data in structured logs</td>
<td>A logging line looks harmless out of context</td>
</tr>
<tr>
<td>Transaction boundaries spanning an external API call</td>
<td>Correct locally, wrong once the network is involved</td>
</tr>
</tbody></table>
<p>Only the last two are really about banking. The first two are the same mistakes I write about in <a href="/blog/a-production-ready-laravel-nextjs-architecture-blueprint">what yabasha.dev actually runs</a>, at a scale where nobody gets fined for them.</p>
<h2>What does &quot;no code&quot; actually cost by month six?</h2>
<p>The pitch deck said no code. Here is the inventory six months in.</p>
<table>
<thead>
<tr>
<th>What &quot;no code&quot; turned out to be</th>
<th>Why it exists</th>
</tr>
</thead>
<tbody><tr>
<td>Around 400 lines of YAML</td>
<td>GitHub Actions orchestration for every agent</td>
</tr>
<tr>
<td>Terraform for the GPU cluster</td>
<td>vLLM does not provision itself</td>
</tr>
<tr>
<td>Laravel queue workers</td>
<td>Model calls time out, and a web request cannot wait</td>
</tr>
<tr>
<td>A custom evaluation framework</td>
<td>Every model swap has to be benchmarked before it ships</td>
</tr>
</tbody></table>
<!-- lift --> "No code" is a demo conceit. A production agent needs infrastructure, observability and fallbacks, which are the same disciplines any distributed system needs.

<p>The timeout row is the one that bit hardest, and I wrote up the general version of that failure in <a href="/blog/building-production-ai-agents-that-actually-work">building production AI agents that actually work</a>. A queued job whose timeout is shorter than the model call it makes does not fail loudly. It writes nothing.</p>
<h2>Where do these agents still get it wrong?</h2>
<p>In four places, consistently. I list them because the failures shaped the design more than the successes did.</p>
<table>
<thead>
<tr>
<th>Failure</th>
<th>What it looks like</th>
<th>The guard</th>
</tr>
</thead>
<tbody><tr>
<td>Invented required fields</td>
<td>A generated request class rejects valid traffic</td>
<td><code>HUMAN_VERIFY</code> on anything monetary</td>
</tr>
<tr>
<td>Suggested remediation that breaks an architecture contract</td>
<td>A plausible failover that violates active-active</td>
<td>No agent executes anything</td>
</tr>
<tr>
<td>Schema mappings that are valid but business-wrong</td>
<td>Two fields that match on type and mean different things</td>
<td>Domain constraints as guardrails, not prompt text</td>
</tr>
<tr>
<td>Confidence without calibration</td>
<td>A high submission score on a build that still fails</td>
<td>The score advises, a person decides</td>
</tr>
</tbody></table>
<p>The third one is current work. I am testing whether agents can negotiate schema mappings across reconciliation vendors better than people do, on the theory that agents do not get bored and do not assume an equivalence is obvious. They converge fast. They also converge on mappings that pass every type check and are wrong about the business.</p>
<h2>Why is every agent a queue worker rather than an API endpoint?</h2>
<p>Because it lets us change the model without shipping a mobile release. Every agent in this stack is Laravel-side. The React Native app and the Next.js frontend call ordinary APIs, and the model is an implementation detail behind them rather than part of the interface.</p>
<table>
<thead>
<tr>
<th>Agent as queue worker</th>
<th>Agent as API endpoint</th>
</tr>
</thead>
<tbody><tr>
<td>Swap models without an app release</td>
<td>Model choice is baked into the client contract</td>
</tr>
<tr>
<td>Add a rule-based fallback or a human step behind the same API</td>
<td>Fallback logic has to live in the client</td>
</tr>
<tr>
<td>Scale GPU spend separately from user-facing compute</td>
<td>The two are coupled</td>
</tr>
<tr>
<td>Retries and timeouts are the queue&#39;s problem</td>
<td>Every caller reimplements them</td>
</tr>
</tbody></table>
<p>This is the same shape I use at a far smaller scale on my own site, described in <a href="/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs">how I built an AI agent for my portfolio</a>, and the cost argument for it is in <a href="/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1">cutting LLM API costs 60%</a>.</p>
<!-- lift --> Nobody using the app cares whether a model wrote the response. They care whether their salary arrived. The architecture follows from that, not from the model.

<h2>What is different about building this in MENA?</h2>
<p>Three constraints that changed the architecture rather than the roadmap.</p>
<table>
<thead>
<tr>
<th>Constraint</th>
<th>What it forced</th>
</tr>
</thead>
<tbody><tr>
<td>Connectivity that shifts under you</td>
<td>Retry logic that would look paranoid in Silicon Valley: routing changes, filtering updates, seasonal traffic patterns that break a statistical model</td>
</tr>
<tr>
<td>Several regulators, different guidance</td>
<td>SAMA, the Central Bank of Jordan and CBUAE each publish their own AI governance direction, so we design for evidence extraction rather than accuracy alone</td>
</tr>
<tr>
<td>We cannot hire fifty ML engineers</td>
<td>The stack optimises for Laravel developers who can read a model card; the agent infrastructure is deliberately boring</td>
</tr>
</tbody></table>
<p>The third constraint is the one that decided the most. A design only your two best people can operate is not a production design.</p>
<h2>What would I check before trusting an agent in a regulated system?</h2>
<p>Work from the audit backwards. If a reviewer cannot reproduce the number, nothing else on this list matters.</p>
<ul>
<li>Every agent output a person signs is reproducible from the query or evidence embedded beside it.</li>
<li>No agent executes anything. It drafts, scores or suggests, and a person acts.</li>
<li>Anything touching money carries an explicit human-verification gate.</li>
<li>The data-residency constraint is settled before the model shortlist, not after.</li>
<li>Every model call runs behind a queue, so timeouts and retries belong to the queue rather than the caller.</li>
<li>Model swaps are benchmarked against your own evaluation set before they reach production.</li>
<li>The failure list is written down, and each entry has a named guard.</li>
</ul>
<!-- lift --> Trust but verify is the old banking phrase. For agents I would add: automate, but audit. Every decision an agent makes without a person seeing it is a decision you cannot defend.

<h2>Measured with</h2>
<p>Figures as stated in the original post, written from four years working remotely with an enterprise bank in the region. The bank is not named, and operational detail that would describe its security posture has been generalised.</p>
<ul>
<li>Laravel backend, Next.js web, React Native mobile</li>
<li>More than 40 third-party API integrations</li>
<li>Open-weight model served with vLLM on owned hardware; ClickHouse warehouse; Postgres with pgvector</li>
<li>Regulator audit shape: 90 days of history inside 48 hours</li>
<li>App Store submission to approval: 14 days before, 4 days after</li>
<li>Six months of production agents at the time of writing</li>
</ul>
<p><em>Questions: <a href="https://x.com/bayyash">@bayyash</a></em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/74168bddfd7320d5130e39893afce91c/blog-cover-ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[You're Already an AI Manager — Just No One Updated Your Contract]]></title>
            <link>https://yabasha.dev/blog/youre-already-an-ai-manager-just-no-one-updated-your-contract</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/youre-already-an-ai-manager-just-no-one-updated-your-contract</guid>
            <pubDate>Thu, 09 Jul 2026 09:40:07 GMT</pubDate>
            <description><![CDATA[Explore the unspoken shift to AI management in software engineering, as a staff engineer learns to direct AI agents with taste and judgment, as discussed in 'You're Already an AI Manager — Just No One Updated Your Contract'.]]></description>
            <content:encoded><![CDATA[<h2>Yesterday I merged code. Today I merged 17 agent personalities.</h2>
<p>Same chair, same <code>Ctrl+S</code>, wildly different output. Last week I spent 4 hours debugging a Laravel queue that swallowed 2,000 card-transaction jobs. Yesterday I spent 4 hours debugging Henry (our Slack AI) who swallowed 2,000 card-transaction <em>descriptions</em>. Same dopamine hit when the log turns green, different muscle memory.</p>
<p>Google VP Josh Woodward says <em>&quot;everyone is becoming a manager&quot;</em>. Cute. In our Amman office we’ve skipped the ceremony and jumped straight to the messy part: directing agents that can spin 10,000 lines of boilerplate before I finish my Turkish coffee.</p>
<h2>The org chart is now a wiring diagram</h2>
<p>Three months ago I drew this on the whiteboard for our RBI compliance meeting:</p>
<pre><code>Bashar (Staff Engineer)
├── Henry-Draft (slack bot, Node 20)
├── Henry-Security (OpenAI gpt-4-turbo)
├── Henry-OnCall (PagerDuty webhook glue)
└── Bashar-HandsOn (me, 40 % headroom)
</code></pre>
<p>CFO stared at it like I’d committed tax fraud. Then I showed the numbers: Henry-Draft wrote 62 % of last sprint’s API specs. Henry-Security caught a PCI regex that would have cost us SAR 450k in fines. I still typed the final <code>git push</code>, but only after three agents debated variable naming like junior devs fighting over tabs vs spaces.</p>
<h2>Taste is the new latency</h2>
<p>Prompt engineering? Dead end. The compounding skill is taste — knowing <em>when</em> to let Henry hallucinate a JSON schema vs when to lock him in a straitjacket of Zod validation. Last Tuesday he spat out a transaction categorizer that classified <em>“shawarma”</em> as <em>“charitable donation”</em>. Funny until the regulator asks why Alrajhi’s risk engine thinks kebab shops are NGOs.</p>
<p>Same problem we had with offshore teams in 2012: garbage in, gospel out. Except now the offshore team is a 200-token context window and it never sleeps.</p>
<p>The trick is teaching taste at scale. We built a tiny Next.js app — internal codename <em>TasteLinter</em> — that scores each agent output on three axes:</p>
<ol>
<li>Compliance (does it violate SAMA circular 12/2024?)</li>
<li>Risk (could this query expose PII?)</li>
<li>Aesthetics (is the generated Laravel resource route actually RESTful?)</li>
</ol>
<p>Fail any axis and the PR auto-blocks. Henry hates it. Our auditors love it. Same friction, new surface.</p>
<h2>Voice is not the interface — narrative is</h2>
<p>Woodward talks about Gemini Spark making voice the interface. That’s surface glitter. The real shift is <em>narrative compression</em>. I can say <em>“build a refund flow that handles partial capture, partial void, and triggers an SMS to the cardholder in Arabic”</em> and Henry spins a 120-line Laravel job, a React Native screen, and an SNS template. But if I misplace one adjective the whole thing compiles into a money-losing disaster.</p>
<p>Voice lowers the floor. Judgment raises the ceiling. There’s still no autocomplete for <em>consequences</em>.</p>
<h2>Middle management without the salary bump</h2>
<p>Here’s the dirty secret from 23 years of banking tech: middle management was always 80 % translation work. Translate business into Jira. Translate Jira into code. Translate code into outage reports. Agents just made the translation layer thinner.</p>
<p>I now spend my mornings <em>curating</em> instead of <em>coding</em>. Example: Henry generated a fraud-detection heuristic that looked brilliant until I realized it used customer email domain as a signal. In Jordan that flags every user with a <code>.edu.jo</code> address as high-risk — basically every university student in the country. One line of YAML, infinite reputational damage.</p>
<p>So I curate. I downgrade false positives the way I used to review pull requests. I leave comments like <em>“use IFSC code, not email TLD”</em> and Henry learns. The diff still shows my GitHub avatar, but the patch is 90 % machine-authored.</p>
<h2>The gravity well of legacy</h2>
<p>Nothing exposes the myth of <em>“everyone is a manager”</em> faster than a 2008 COBOL module sitting between your agents and the core banking switch. You can orchestrate Henry, Claude, and a fleet of GPT-4s, but if the mainframe only speaks 370-ASCII over MQ, you’re still stuck hand-rolling hex dumps at 2 a.m.</p>
<p>Legacy is the new middle management. It doesn’t report to you, but it approves all your vacation requests.</p>
<h2>Promotion letter pending</h2>
<p>HR hasn’t updated my title yet. Officially I’m <em>Staff Software Engineer</em>. Unofficially I manage:</p>
<ul>
<li>3 AI agents (Henry variants)  </li>
<li>2 offshore contractors (humans, for now)</li>
<li>1 legacy queue system (written in Java 6, immortal)</li>
<li>1 human intern (who thinks <code>git rebase</code> is dark magic)</li>
</ul>
<p>Same paycheck, 4x the surface area for failure.</p>
<h2>Your move</h2>
<p>Look at your last week’s calendar. How many reviews were code vs <em>agent behavior</em>? If the ratio tips past 50 %, congratulations — you’re management. The org chart just didn’t send the memo.</p>
<p>So here’s my question: when performance-review season arrives, do we file the agents under <em>direct reports</em> or <em>tools</em>? Because right now they’re both, and the ambiguity is where the next outage hides.</p>
<hr>
<p>#aiManagement #mideastFintech #agentOps</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/48eaaf40250233384fe28e3559e02fc4/blog-cover-youre-already-an-ai-manager-just-no-one-updated-your-contract.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[20 Years in the Trenches: From Banking Code to AI Agents]]></title>
            <link>https://yabasha.dev/blog/20-years-in-the-trenches-from-banking-code-to-ai-agents</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/20-years-in-the-trenches-from-banking-code-to-ai-agents</guid>
            <pubDate>Mon, 25 May 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[From banking transactions to disaster zones to AI agents — 20 years of building systems that work in production, not just in demos.]]></description>
            <content:encoded><![CDATA[<h1>20 Years in the Trenches: From Banking Code to AI Agents</h1>
<p>I started writing code in 2005. Not in a garage with a startup dream — in a bank, where &quot;move fast&quot; meant deploying quarterly and &quot;break things&quot; meant losing millions.</p>
<p>Twenty-one years later, I&#39;m building AI agents that reason, plan, and execute autonomously. The journey between those two points wasn&#39;t a straight line. It was a series of controlled explosions.</p>
<h2>The Banking Years (2005–2012)</h2>
<p>Seven years in banking taught me one thing: <strong>systems that fail cost real money.</strong> Not theoretical money. Not VC money. Money that someone&#39;s pension depends on.</p>
<p>I learned to build for fault tolerance before it was trendy. Every line of code carried weight. Every deployment was a commitment. You don&#39;t unlearn that.</p>
<p>Banking gave me discipline. It also gave me a deep skepticism of anything labeled &quot;revolutionary.&quot; I&#39;ve seen too many &quot;game-changing&quot; architectures that couldn&#39;t survive a Monday morning traffic spike.</p>
<h2>Humanitarian Tech (2012–2018)</h2>
<p>Then I went where most engineers don&#39;t: <strong>humanitarian operations.</strong></p>
<p>Imagine building systems where the users are field workers in disaster zones, connectivity is intermittent at best, and the data you process determines whether food reaches 10,000 people or 100,000.</p>
<p>This is where I learned about <strong>constraints as architecture.</strong> When your network drops every 30 minutes, you design differently. When your users speak five languages and have limited tech literacy, you simplify differently. When the stakes are measured in human lives, you QA differently.</p>
<p>Humanitarian tech taught me that the best technology is invisible. It works. It doesn&#39;t ask for attention. It just does its job while the real work happens.</p>
<h2>eCommerce &amp; Operations (2018–2022)</h2>
<p>eCommerce seems simple until you&#39;re handling inventory across multiple warehouses, processing thousands of orders a day, and your payment integration fails at 2 AM on Black Friday.</p>
<p>This phase taught me <strong>operational thinking.</strong> Not just building software — building systems that run operations. The difference is enormous.</p>
<p>An engineer builds a feature. An operations-minded engineer builds a feature that degrades gracefully, reports its own failures, and doesn&#39;t wake anyone up at 3 AM unless it actually matters.</p>
<h2>The AI Pivot (2022–Now)</h2>
<p>When I saw what was happening with large language models, I didn&#39;t see a shiny new toy. I saw a <strong>force multiplier</strong> for everything I&#39;d built over 17 years.</p>
<p>Banking discipline + humanitarian constraints + operational thinking = the perfect foundation for production AI.</p>
<p>Here&#39;s what most people get wrong about AI engineering: <strong>it&#39;s not about the model. It&#39;s about the system around the model.</strong></p>
<p>Anyone can call an API and get a response. The hard part is:</p>
<ul>
<li>Grounding outputs in real data (RAG)</li>
<li>Building agents that don&#39;t hallucinate themselves into oblivion</li>
<li>Creating fallback chains when the model gets it wrong</li>
<li>Observing, measuring, and improving — not just deploying and praying</li>
</ul>
<p>That&#39;s what I do now. I build AI systems that work in production. Not demos. Not proof-of-concepts. Systems that handle real data, real users, and real consequences.</p>
<h2>What 20 Years Actually Teaches You</h2>
<p>Here&#39;s the thing nobody tells you about a long career: <strong>the domains change, the principles don&#39;t.</strong></p>
<ul>
<li>Build for failure, not just success</li>
<li>Constraints are design inputs, not obstacles</li>
<li>If it can&#39;t degrade gracefully, it&#39;s not production-ready</li>
<li>The best architecture is the one nobody notices</li>
</ul>
<p>These principles hold whether you&#39;re processing bank transactions, coordinating disaster relief, or deploying AI agents.</p>
<h2>What I&#39;m Building Now</h2>
<p>Today I&#39;m focused on <strong>Cortex</strong> (YabashaOS) — an AI operating system that brings structure to agent workflows. Alongside that: <strong>FinTrack</strong> for financial intelligence, <strong>AutomotiveAI</strong> for the automotive sector, and <strong>Jopal</strong> for document management.</p>
<p>Twenty years of banking, humanitarian work, eCommerce, and operations — all converging into AI systems that actually ship.</p>
<p>If you&#39;re building AI and wondering why your demo works but your production system doesn&#39;t, maybe you need someone who&#39;s spent two decades learning that the hard way.</p>
<p>That&#39;s what I bring to the table. Not just AI knowledge. <strong>Systems thinking forged in the trenches.</strong></p>
<hr>
<p><em>What&#39;s the one principle from your career that carried you through every domain change? I&#39;d genuinely like to know.</em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/c270fc2c461c4c27aa961eab69e58389/blog-cover-20-years-in-the-trenches.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[The 2-Hour Claude Setup That Made Everything Click]]></title>
            <link>https://yabasha.dev/blog/the-2-hour-claude-setup-that-made-everything-click</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/the-2-hour-claude-setup-that-made-everything-click</guid>
            <pubDate>Sun, 24 May 2026 12:10:00 GMT</pubDate>
            <description><![CDATA[Seven files, 115 minutes, and prompts that drop from 800 words to twelve. The exact Claude Projects setup I use, what each file prevents, and the one step that actually compounds.]]></description>
            <content:encoded><![CDATA[<p><em>A vendor product changes faster than a codebase. Where a number would age badly, I link the page that holds the current one instead of printing it here.</em></p>
<p>I wrote a LinkedIn post last Monday in six minutes. It used to take forty.</p>
<p>Nothing got smarter. I stopped opening a blank chat and re-explaining who I am, what I am building, what I sound like and which words I will not use. I wrote that down once, in files, and the chat starts from there.</p>
<!-- lift --> The setup is not a prompt library. It is a set of files that answer, once, the questions you were re-answering in every chat.

<h2>Is this about Claude Projects or Claude Code?</h2>
<p>Projects. They are different products and the setup below does not transfer.</p>
<p><a href="https://code.claude.com/docs/en/overview">Claude Code</a> is a coding agent that runs in your terminal and your editor, reads your repository, and edits files. Its configuration lives in <code>CLAUDE.md</code> and skill files inside the repo.</p>
<p><a href="https://support.claude.com/en/articles/9517075-what-are-projects">Projects</a> are workspaces on claude.ai. Each one holds its own instructions and its own uploaded files, and every chat inside it starts with all of them loaded. That is what this post configures. If you came here for the terminal tool, this is not the post you want.</p>
<h2>What does the two hours actually consist of?</h2>
<p>Seven steps. Each one produces a file or a setting, and none of them is prompting.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Step</th>
<th>Minutes</th>
<th>What it produces</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Create the Project</td>
<td>5</td>
<td>One workspace, named after one recurring task</td>
</tr>
<tr>
<td>2</td>
<td>Interview yourself</td>
<td>30</td>
<td><code>about-me.md</code></td>
</tr>
<tr>
<td>3</td>
<td>Extract your voice</td>
<td>20</td>
<td><code>my-voice.md</code></td>
</tr>
<tr>
<td>4</td>
<td>Write the anti-AI rules</td>
<td>10</td>
<td>A banned-word and banned-pattern list</td>
</tr>
<tr>
<td>5</td>
<td>Write the Project instructions</td>
<td>15</td>
<td>The standing system prompt</td>
</tr>
<tr>
<td>6</td>
<td>Add three templates</td>
<td>20</td>
<td>Your best real example of each format</td>
</tr>
<tr>
<td>7</td>
<td>Upload the project&#39;s real context</td>
<td>15</td>
<td>Analytics, numbers, the messy reality</td>
</tr>
</tbody></table>
<p>That is 115 minutes. The two hours in the title is not a round-up for effect, which I only noticed while adding this table.</p>
<h2>Why does a Project beat a good prompt?</h2>
<p>Because the prompt has to be retyped and the Project does not. Three things make the difference.</p>
<p>Separate workspaces keep contexts from bleeding. A newsletter Project and a client-email Project do not share files, so neither inherits the other&#39;s tone.</p>
<p>Instruction adherence is the part I actually evaluate. If I write &quot;never use the word leverage&quot; and the model uses it anyway, the rules file is decoration. That is the test I would run before committing two hours to any tool, rather than taking anyone&#39;s word for which one holds up.</p>
<p>File memory removes the re-upload. Drop a folder of documents into a Project and every reply is written with them in scope.</p>
<p>Projects need a paid plan. The current figure is on the <a href="https://claude.com/pricing">pricing page</a>, and I am deliberately not printing a number here that will be wrong by the time you read it.</p>
<h2>How do you write the about-me file without writing it?</h2>
<p>Get interviewed. Writing about yourself in the abstract produces a CV. Being asked one question at a time produces the messy detail that is actually useful.</p>
<p>Open a chat in the Project and ask for a 30-minute interview, one question at a time, going deep before moving on. Cover who you are, what you are building now, your goals for the next twelve months, what you are afraid of, what you already tried that failed, who your audience is, what they are sceptical of, and what is off-limits in public.</p>
<p>One instruction matters more than the rest: never invent facts about me, only use what I tell you.</p>
<!-- lift --> Answer as what is true, not as what you wish were true. A model working from your aspirational bio writes copy that sounds like someone else.

<h2>How do you capture a voice you cannot describe?</h2>
<p>By handing over work instead of adjectives. Paste in three things you wrote that sounded most like you and that landed with real people. A post that worked. An email to a friend. A caption that got a real reply.</p>
<p>Then ask for analysis rather than praise: what you would never say, what you reach for when you are excited, what changes when you are admitting a failure, whether you use emoji, your sentence-length pattern.</p>
<p>The follow-up questions are where the file gets sharp. Words you hate. Phrases you refuse. The adjective you would never pick. Those are the things three samples cannot show and you can answer in a minute.</p>
<h2>What do the anti-AI writing rules actually stop?</h2>
<p>The tics. Every model has a house style, and it leaks into your drafts unless you name the pattern and ban it.</p>
<table>
<thead>
<tr>
<th>Pattern to ban</th>
<th>Why it shows up</th>
</tr>
</thead>
<tbody><tr>
<td>&quot;Not just X, but Y&quot;</td>
<td>A formula that fills space and asserts nothing</td>
</tr>
<tr>
<td>Em dashes everywhere</td>
<td>The default rhythm, and a visible tell</td>
</tr>
<tr>
<td>Abstract verbs: navigate, unlock, leverage, delve</td>
<td>Sound technical, carry no meaning</td>
</tr>
<tr>
<td>Rule-of-three lists</td>
<td>Ideas forced into threes whether or not there are three</td>
</tr>
<tr>
<td>Title Case Headings</td>
<td>A house convention, not yours</td>
</tr>
</tbody></table>
<p>Build this file from your own drafts rather than from a list someone else wrote. Read three pieces the model produced for you, mark every phrase that made you wince, and that is the file. It takes ten minutes and it is the single highest-yield item on the list.</p>
<h2>What goes in the Project instructions?</h2>
<p>Standing rules, not a task. This field runs before every chat in the Project, so it should hold only what is true every time.</p>
<p>Write it as a job description for someone who needs to know your standards: what you want, what you never want, how output should be formatted, and what context to always assume. The same discipline applies as to a system prompt in code, which I wrote about in <a href="/blog/building-production-ai-agents-that-actually-work">building production AI agents that actually work</a>: short, role-based, and specific about the failure you are preventing.</p>
<p>Add one line at the top that states your goal for the year. A number, not a mood.</p>
<!-- lift --> Without a stated goal, "is this good?" is a question about taste. With one, it is a question about fit, and the model can answer it.

<h2>Why must templates live in the matching Project?</h2>
<p>Because context that does not apply is noise, and noise costs you the thing you came for. A pitch-email template sitting in your newsletter Project pulls drafts toward a register you did not ask for.</p>
<table>
<thead>
<tr>
<th>Project</th>
<th>What belongs in it</th>
</tr>
</thead>
<tbody><tr>
<td>The format&#39;s best real example</td>
<td>One template file per format you write often</td>
</tr>
<tr>
<td>Performance data</td>
<td>Analytics export, a CSV of recent work and how it did</td>
</tr>
<tr>
<td>Proven work</td>
<td>Three to five pieces that actually landed</td>
</tr>
<tr>
<td>The goal line</td>
<td>One number, at the top of the instructions</td>
</tr>
</tbody></table>
<p>That last row is the step most people skip. Uploading your real numbers is what turns &quot;should I post this?&quot; from a guess into a comparison against your own baseline.</p>
<h2>What does the prompt actually look like afterwards?</h2>
<p>Twelve words. &quot;Write a LinkedIn post about yesterday&#39;s podcast&quot; is enough, because voice, audience, format, banned words and call-to-action style are already loaded.</p>
<table>
<thead>
<tr>
<th>Measure</th>
<th>Before the setup</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>Prompt length</td>
<td>~800 words</td>
<td>~12 words</td>
</tr>
<tr>
<td>Time to a usable LinkedIn post</td>
<td>40 minutes</td>
<td>6 minutes</td>
</tr>
<tr>
<td>Context re-explained per chat</td>
<td>Every time</td>
<td>Never</td>
</tr>
</tbody></table>
<p>Shorter prompts are also cheaper, which matters at volume rather than at this scale. The general version of that argument, with measured numbers behind it, is in <a href="/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1">cutting LLM API costs 60%</a>.</p>
<h2>What is the step that actually compounds?</h2>
<p>Closing the feedback loop. Everything above gets you a workspace that knows you as of today. It does not get better unless your corrections find their way back into the files.</p>
<table>
<thead>
<tr>
<th>Where feedback happens</th>
<th>Reaches the model?</th>
</tr>
</thead>
<tbody><tr>
<td>You rewrite a draft and say nothing</td>
<td>No. The same mistake returns next week</td>
</tr>
<tr>
<td>You rewrite it and say what you changed and why</td>
<td>Yes, if you add it to the rules or voice file</td>
</tr>
<tr>
<td>A written thread where you reacted to work</td>
<td>Yes, if you paste it in</td>
</tr>
<tr>
<td>A phone call</td>
<td>No. Lost unless somebody writes it down</td>
</tr>
<tr>
<td>A recorded meeting, transcribed</td>
<td>Yes. <a href="https://www.granola.ai/">Granola</a>, <a href="https://otter.ai/">Otter</a> and <a href="https://wisprflow.ai/">Wispr Flow</a> all produce a transcript you can drop in</td>
</tr>
</tbody></table>
<p>The habit is small and easy to skip: when you edit something, say what you cut and why. &quot;I removed the second paragraph, it read as motivational. I rewrote the close, it had no call to action.&quot; Two sentences, once, instead of the same edit every week.</p>
<!-- lift --> The setup stops being a shortcut and starts compounding at the moment your corrections go back into the files rather than into a draft you silently fixed.

<p>I run the same pattern on this site in code rather than in a chat, described in <a href="/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs">how I built an AI agent for my portfolio</a>. The mechanism differs. The principle does not.</p>
<h2>What does this setup not fix?</h2>
<p>Four things, and knowing them in advance saves you the disappointment.</p>
<table>
<thead>
<tr>
<th>It does not</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>Decide what is worth saying</td>
<td>The files describe how you sound, not what you think</td>
</tr>
<tr>
<td>Make a weak idea publishable</td>
<td>A clear draft of a thin argument is still thin</td>
</tr>
<tr>
<td>Remove the review step</td>
<td>You are still the last reader before anything ships</td>
</tr>
<tr>
<td>Stay accurate on its own</td>
<td>Files go stale. Goals change, audiences change, the numbers move</td>
</tr>
</tbody></table>
<p>That third row is the one I hold on to in client work too. The human stays in the loop for the same reason a regulated system keeps one, which I wrote about in <a href="/blog/ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking">what actually works in enterprise banking</a>: the value is in the draft, and the accountability stays with a person.</p>
<h2>Where should you start?</h2>
<p>With the one thing you write every week and resent writing. Build that Project, in that order, and stop.</p>
<p>The failure mode is building seven Projects on a Saturday and using none of them. One Project, used daily for a fortnight, teaches you what belongs in the files. Seven Projects teach you nothing, because you never find out which context was doing the work.</p>
<p>Two hours is the whole investment. The question is not whether it works. It is which week you actually spend them.</p>
<h2>Checked on 2026-09-21</h2>
<ul>
<li>Step times as published, summing to 115 minutes</li>
<li>Prompt length and drafting times as published: 800 words to 12, 40 minutes to 6</li>
<li>Pricing linked rather than quoted, because a figure printed here will age</li>
<li>Granola, Otter and Wispr Flow all resolve and all produce transcripts</li>
<li>Claude Code and Claude Projects are separate products; this post configures Projects</li>
</ul>
<p><em>Questions: <a href="https://x.com/bayyash">@bayyash</a></em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/4bcf3e25531d259e4f68a74082fdbd4e/blog-cover-the-2-hour-claude-setup.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Stop Typing. Start Building Your AI Brain.]]></title>
            <link>https://yabasha.dev/blog/stop-typing-start-building-your-ai-brain</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/stop-typing-start-building-your-ai-brain</guid>
            <pubDate>Sun, 24 May 2026 12:05:00 GMT</pubDate>
            <description><![CDATA[I spent the last month building a 5-level AI system that actually runs my content pipeline, audits my channels, and scores podcast guests in under 10 minutes. Here's exactly what I did — and what I'd change.]]></description>
            <content:encoded><![CDATA[<h1>Stop Typing. Start Building Your AI Brain.</h1>
<p>Last week I caught myself typing the same context into Claude for the third time in one day. Who I am, what I&#39;m building, what my voice sounds like. Three separate chats, three identical walls of text. That&#39;s when it hit me — I was using AI like a search engine instead of building it like a teammate.</p>
<p>Here&#39;s the thing most people miss: AI tools are useless without organized knowledge underneath them. You can prompt all day, but if your data lives in six different apps and your voice lives in your head, every output starts from zero.</p>
<p>I spent the last month building a five-level AI system. Not theoretical. Not &quot;excited to share.&quot; A system that actually runs my content pipeline, audits my channels, and scores my podcast guests in under 10 minutes.</p>
<p>Here&#39;s exactly what I did — and what I&#39;d change.</p>
<h2>Level 1: Stop Typing, Start Talking</h2>
<p>Allie Miller told me something that changed how I work: the best prompting is complaining to your AI. Voice gives the model 10x more context than typing. You naturally explain the background, mention edge cases, give examples — without thinking about it.</p>
<p>I use WisprFlow because it handles both Russian and English. For longer recordings — conference talks, interviews — I use Trint. Every podcast episode gets transcribed automatically.</p>
<p>If you&#39;re still typing prompts, that&#39;s the first thing I&#39;d change this week.</p>
<h2>The Part Everyone Skips: Context Files</h2>
<p>Every AI tool I use has access to a small set of files about me. They don&#39;t change often, but they transform every output from generic to personal.</p>
<ul>
<li><strong>Voice profile</strong> — how I write, what I sound like, what I hate</li>
<li><strong>Fact dossier</strong> — my background, companies, what I won&#39;t talk about publicly</li>
<li><strong>Anti-AI writing guide</strong> — patterns I refuse to publish in my voice</li>
<li><strong>Channel descriptions</strong> — who my audience is, what the cadence is</li>
</ul>
<p>The move: I asked Claude to interview me for 30 minutes. One question at a time. It built a draft voice profile from my answers. My team cleaned it up. Now anyone on my team can write a post that sounds like me — not because they&#39;re good at imitation, but because the context does the work.</p>
<p><strong>Action this week:</strong> Ask Claude to interview you for 30 minutes. Save the output. Use it across every project.</p>
<h2>The Database That Lives Outside Any One Tool</h2>
<p>Here&#39;s the mistake I made three times: locking all my data inside one AI platform. Tools change every three months. Today I love Claude. Tomorrow Codex ships something that makes me switch.</p>
<p>So my entire knowledge layer lives in an external Notion database. Organized by channel — YouTube, Newsletter, Podcast, Instagram, LinkedIn. Each one has performance data, transcripts, tone of voice, branding, a decisions log.</p>
<p>Every podcast episode gets its own page. When the transcript lands, the whole team gets a notification. That single trigger kicks off clip selection, newsletter drafting, social posts — all from one source.</p>
<h2>Level 2: Build AI on Top of Your Knowledge</h2>
<p>I use Claude Projects — one per platform. YouTube, Newsletter, Podcast, Instagram, LinkedIn. Each project is built on the context files and channel data I just described.</p>
<p>My team is testing Claude Cowork, the desktop version. It doesn&#39;t just read files — it opens them, edits documents, runs scripts on your machine. We have a master folder for YouTube production with subfolders for titles, thumbnails, scripting, distribution, and guest research. Each subfolder has its own instructions file.</p>
<p>The results are far more accurate on the first try.</p>
<h3>The Guest Evaluation System That Changed Everything</h3>
<p>When my team wants to vet a podcast guest, they paste a name into the Silicon Valley Girl Claude project. The project scores guests across 8 categories: Results, AI/Business factor, Personal story, Fame fit, Zeitgeist, Bridge factor, PR/Media presence, and Thought leadership.</p>
<p>Each one weighted. Each one benchmarked against our actual Notion data on how past episodes performed.</p>
<p>A list of 10 guests used to take 2-3 days. Now it takes under 10 minutes.</p>
<p>That&#39;s 8-12 hours back per week for the team. And we make better booking decisions because every guest gets scored against the same rubric.</p>
<h2>Level 3: Scheduled Agents That Run Without You</h2>
<p>This is where it gets real. Six agents running on timers for Silicon Valley Girl.</p>
<ul>
<li><strong>Monday 9am:</strong> Trending content research — 10 video ideas drop into a doc. By the time the team opens their laptops, the ideas are ready.</li>
<li><strong>Monday 10am:</strong> Weekly AI/tech/business news summary. Before our Monday standup, we already know what&#39;s worth talking about.</li>
<li><strong>Monday 12pm:</strong> Full channel health audit covering 12 months. What&#39;s working, what&#39;s slipping, what to double down on.</li>
<li><strong>Daily 12pm:</strong> Media mention scanner. When something good comes up, we know about it the same morning.</li>
<li><strong>Wednesday 12pm:</strong> Guest follow-up agent. Reads our declined-guests list, scores each one, finds strong news hooks from the past 7 days, and recommends who to re-approach.</li>
</ul>
<p>None of these agents replace anyone. They remove the part of the job that didn&#39;t need a human in the first place.</p>
<h2>Level 4: Vibe Code Your Own Tools</h2>
<p>Luis von Ahn, CEO of Duolingo, told me on the podcast that at Duolingo, every person has built their own dashboard. I loved that. So my team and I built one.</p>
<p>I did not write the code. I described what I wanted in Claude chat. Claude turned it into a technical prompt. Claude Code wrote and edited the actual files on my Mac. Eight to ten weeks between March and May.</p>
<p>The dashboard pulls from YouTube, Instagram, Threads, LinkedIn, X, and Beehiiv. A Python cron job monitors every video from the moment it publishes.</p>
<p>Three trigger moments:</p>
<ul>
<li><strong>Day 4:</strong> If click-through drops below 4% with 20,000+ impressions — early packaging signal.</li>
<li><strong>Day 7:</strong> If views below 12K, or 30-second retention below 50%.</li>
<li><strong>Day 14:</strong> If total views below the 30K KPI, the team gets a message.</li>
</ul>
<p>When a trigger fires, the system sends metrics to Claude and asks for a diagnostic. The output lands in our team channel. Costs fractions of a cent per video.</p>
<h3>AI Search Visibility</h3>
<p>Here&#39;s what most people don&#39;t think about yet: traffic is shifting from Google to chatbots. People ask ChatGPT, Perplexity, and Google AI Overview for podcast recommendations, tool recommendations, expert recommendations.</p>
<p>When I tested earlier this year, my podcast didn&#39;t show up at all. Zero visibility in AI search.</p>
<p>So we rebuilt the podcast site, created a Wikipedia page, updated descriptions on every platform. We track against 11 competitors with 50 prompts daily.</p>
<p>The honest before-and-after:</p>
<ul>
<li><strong>Before:</strong> SVG only appeared in branded prompts (&quot;who is Marina Mogilko?&quot;)</li>
<li><strong>Today:</strong> SVG appears in 6-8 prompts that don&#39;t mention my name at all</li>
</ul>
<p>Is that a lot? No. Is it more than zero? Yes. And every one of those positions is a slot we didn&#39;t have a month ago.</p>
<h2>Level 5: Close the Loop</h2>
<p>This is where almost no one is. It&#39;s also the level I&#39;m still building.</p>
<p>Every call we run — strategy, 1:1s, planning, guest prep — gets recorded with Granola. The notes end up in clean transcripts. The problem: those notes sit in Granola. They don&#39;t get into the Claude project that runs the rest of my business.</p>
<p>So when I ask Claude &quot;what did we decide about the editorial calendar last week?&quot; — it has no idea.</p>
<p>What we&#39;re working on: when a meeting wraps, the transcript and next-step list should move straight into the right Claude project. YouTube into the YouTube project. Podcast into the podcast project.</p>
<p>This is the gap I&#39;ve been thinking about for months. Almost no founder has solved it yet.</p>
<h2>The Uncomfortable Truth</h2>
<p>Everyone wants to skip to Level 5. Nobody wants to spend 2 hours on Level 1.</p>
<p>But here&#39;s what I&#39;ve learned after rebuilding this system three times: the boring foundation is the only thing that makes the exciting stuff work. Voice inputs are useless without organized data. Scheduled agents are useless without context files. Custom dashboards are useless without a clean knowledge layer.</p>
<p>The 2 hours you spend on Level 1 is the highest-leverage time investment you&#39;ll make this year.</p>
<p><strong>Question I&#39;m sitting with right now:</strong> Are you using AI like a search engine — or building it like a teammate that knows you?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/e921c027844a27f4cdf789493cd4701d/blog-cover-stop-typing-start-building-your-ai-brain.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[What I Actually Build — AI Engineering, RAG Systems, Agents & Full-Stack]]></title>
            <link>https://yabasha.dev/blog/what-i-actually-build-ai-engineering-rag-systems-agents-full-stack</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/what-i-actually-build-ai-engineering-rag-systems-agents-full-stack</guid>
            <pubDate>Sun, 24 May 2026 08:22:00 GMT</pubDate>
            <description><![CDATA[A first-person overview of production AI engineering work spanning RAG systems, agent architectures, full-stack Laravel/Next.js delivery, and cost optimization — built for real traffic, not tutorials.]]></description>
            <content:encoded><![CDATA[<h1>What I Actually Build — AI Engineering, RAG Systems, Agents &amp; Full-Stack</h1>
<p>I have been building software for over twenty years. For the past several, that work has centered on production AI systems — the kind that handle real traffic, real data, and real failure modes. This post is a straight inventory of what I actually ship, how I architect it, and what breaks when you move beyond tutorials.</p>
<h2>RAG Systems That Survive Contact with Reality</h2>
<p>Retrieval-Augmented Generation is the backbone of most production AI products I build. But a RAG pipeline that works on a demo PDF is not the same as one that ingests ten thousand heterogeneous documents with mixed Arabic and English content, tables, headers, and inconsistent formatting.</p>
<h3>Chunking Is Not a Solved Problem</h3>
<p>The default &quot;split every 1000 characters&quot; approach fails the moment your source material has structure. I use recursive character splitting as a baseline, but the real work happens in semantic chunking — splitting at natural boundaries like section headers, paragraph breaks, and logical transitions. For structured documents, I preserve metadata (page numbers, section titles, source filenames) and attach it to every chunk so the retrieval layer can filter and rank with context.</p>
<p>For Arabic content, chunking gets harder. Arabic morphology is rich, sentence boundaries are not always obvious, and mixed Arabic-English documents create embedding drift if you are not careful. I typically chunk Arabic text slightly smaller than English equivalents to preserve semantic coherence across the embedding model&#39;s context window.</p>
<h3>Embeddings and Vector Stores</h3>
<p>I have shipped systems on both Pinecone and pgvector. Pinecone wins when you need managed scaling and hybrid search out of the box. pgvector wins when your retrieval layer is already tightly coupled to PostgreSQL, when you need transactional consistency between your application data and your vectors, or when you are optimizing for cost at scale.</p>
<p>My default embedding stack is text-embedding-3-large for English and multilingual content, with fallback to text-embedding-3-small for high-volume, lower-precision use cases. For Arabic-specific deployments, I evaluate Arabic-specific embedding models against a held-out test set before committing — generic multilingual models are better than they used to be, but they are not always the right tool.</p>
<h3>Retrieval Patterns Beyond Naive Similarity Search</h3>
<p>Cosine similarity on a single embedding is rarely enough. I run hybrid search — combining dense vector similarity with sparse BM25 keyword matching — and then re-rank with a cross-encoder or a lightweight reranking model. The dense retrieval gets you recall; the re-ranker gets you precision. Without the second step, you get plausible-sounding but irrelevant chunks at the top of your context window.</p>
<p>I also implement query expansion and hypothetical document embedding (HyDE) where appropriate: generate a synthetic answer to the user&#39;s question, embed that, and retrieve against it. It costs an extra LLM call, but the retrieval quality improvement is measurable.</p>
<h2>AI Agents: Tool-Calling, Guardrails, and Observability</h2>
<p>Agents are not magic. They are loops — a language model, a set of tools, and a control structure that decides what to call next. The hard part is making that loop reliable enough to expose to users.</p>
<h3>Tool-Calling Patterns</h3>
<p>I build agents with explicit tool schemas, not free-form prompting. Every tool has a typed input schema, a clear description, and a deterministic output. The LLM generates structured JSON to invoke tools, and I validate that JSON against the schema before execution. If the model hallucinates a parameter or sends malformed JSON, the validation layer catches it and returns a structured error back to the agent loop.</p>
<p>Tools fall into three categories in my systems: data retrieval (search, database queries), action execution (send email, create ticket, update record), and computation (math, formatting, validation). I never give an agent unrestricted write access without human confirmation — the blast radius is too large.</p>
<h3>Guardrails and Fallback Chains</h3>
<p>Every agent has input guardrails: PII detection, prompt injection filtering, and topic boundary checks. Every agent has output guardrails: format validation, safety checks, and confidence thresholds. If the model&#39;s output confidence is below a threshold, or if validation fails, the system falls back to a simpler chain or escalates to a human.</p>
<p>I implement fallback chains as explicit state machines, not implicit prompt engineering. If the primary model fails, the system tries a smaller model with a narrower prompt. If that fails, it returns a graceful degradation message and logs the incident for review.</p>
<h3>Observability with Langfuse</h3>
<p>You cannot debug what you cannot see. I instrument every agent trace with Langfuse: input prompts, tool calls, latencies, token counts, and error states. This is not optional in production. When a user reports a bad result, I need to reconstruct the exact chain of thought, see which tool was called with what parameters, and identify where the logic diverged from the intended path.</p>
<p>Langfuse also gives me cost attribution per trace, which matters when you are running multiple models and need to know which user workflows are expensive.</p>
<h2>Full-Stack Production: Laravel 12, Next.js 16, and React Native</h2>
<p>AI does not exist in a vacuum. It sits inside applications that have authentication, billing, permissions, mobile clients, and deployment pipelines. I build the full stack, not just the model layer.</p>
<h3>Laravel 12 + Next.js 16 Monorepo</h3>
<p>My default backend is Laravel 12. It handles queues, scheduling, database migrations, API authentication, and event broadcasting without ceremony. For the frontend, I use Next.js 16 with the App Router, server components for initial data fetching, and client components for interactive AI chat interfaces.</p>
<p>The monorepo structure keeps the API contracts tight. Laravel exposes typed API resources; Next.js consumes them with generated TypeScript types. When the backend schema changes, the frontend build breaks immediately — which is exactly what I want.</p>
<h3>React Native for Mobile</h3>
<p>When the product needs a mobile presence, I ship React Native apps that share business logic with the web frontend. The AI chat interface, streaming responses, and tool-call UIs are implemented once and adapted to mobile constraints. Push notifications for async agent completions are handled through Laravel&#39;s notification channels.</p>
<h2>Production vs. Tutorial AI: What Actually Breaks</h2>
<p>Tutorials show you the happy path. Production is the other 90% of the work.</p>
<h3>Cost</h3>
<p>LLM costs scale linearly with traffic and super-linearly with context window size. A naive RAG system that dumps 8,000 tokens of retrieved context into every request will bankrupt you at scale. I implement aggressive context compression, selective retrieval (only fetch what you need), and model routing: use the cheapest model that can handle the task, and escalate to expensive models only when necessary.</p>
<h3>Latency</h3>
<p>Users will not wait five seconds for a chat response. I optimize latency at every layer: faster embedding models, cached retrieval results, streaming responses to the client, and pre-computed summaries for common queries. For high-latency operations — like multi-step agent workflows — I switch to an async pattern: acknowledge the request, process in the background, and notify the user when complete.</p>
<h3>Hallucinations</h3>
<p>Hallucinations do not go away with better prompting. You need grounding: every generated claim must be traceable to a retrieved source. I implement citation requirements in the prompt, parse citations from the model output, and verify that cited chunks actually exist in the retrieval set. If the model cannot cite a source, the answer is flagged for review.</p>
<h2>Architecture Patterns</h2>
<p>Here is a simplified view of a production RAG + Agent system I have shipped:</p>
<pre><code>┌─────────────────┐     ┌──────────────────┐     ┌─────────────────┐
│   Next.js 16    │────▶│   Laravel 12     │────▶│  Vector Store   │
│   (Frontend)    │     │   (API / Queue)  │     │  (pgvector)     │
└─────────────────┘     └──────────────────┘     └─────────────────┘
                               │
                               ▼
                        ┌──────────────────┐
                        │   AI Agent Loop  │
                        │  (Tool Calling)  │
                        └──────────────────┘
                               │
              ┌────────────────┼────────────────┐
              ▼                ▼                ▼
        ┌──────────┐    ┌──────────┐    ┌──────────┐
        │  Search  │    │  Action  │    │  LLM API │
        │  (RAG)   │    │  Tools   │    │ (Router) │
        └──────────┘    └──────────┘    └──────────┘
</code></pre>
<p>The LLM API layer routes between models based on task complexity: small model for classification and routing, large model for generation and reasoning. Every call is traced, every cost is logged, every failure triggers a fallback.</p>
<h2>Cost Optimization Strategies</h2>
<p>Model routing is the single biggest lever. I classify incoming requests by complexity and route them to the appropriate model tier. Simple queries hit a fast, cheap model. Complex reasoning escalates to a larger model. This alone can reduce costs by 60-70% without measurable quality loss.</p>
<p>Caching is the second lever. Embedding results, retrieval sets, and common LLM responses are cached with TTLs appropriate to the data freshness requirements. For batch workloads — like nightly document ingestion — I use batching APIs where available to cut per-request overhead.</p>
<h2>The MENA Context: Arabic NLP and Regional Scaling</h2>
<p>Building AI for the Middle East and North Africa introduces constraints that Western-centric tutorials ignore. Arabic NLP requires handling diacritics, dialectal variation, and right-to-left UI flows. Compliance requirements vary by country: data residency, content moderation standards, and regulatory approval for AI-generated outputs.</p>
<p>I design systems with regional modularity from the start: embedding models that handle Arabic well, UI components that support RTL layouts, and deployment architectures that can pin data to specific geographic regions when required. Scaling in MENA also means optimizing for mobile-first usage and variable network conditions — lightweight APIs, aggressive caching, and offline-capable mobile features.</p>
<h2>Evaluation Loops: Automated Quality Checks</h2>
<p>Shipping AI features without evaluation is reckless. I run automated quality checks on every deployment: retrieval accuracy (did we fetch the right chunks?), answer relevance (does the generated response address the question?), citation correctness (are the sources real and relevant?), and safety (no PII leaks, no policy violations).</p>
<p>These checks run against a held-out evaluation dataset that grows over time. When a new failure mode appears in production, I add an example to the eval set and fix the system. The eval suite runs in CI before every deploy. If scores regress, the deploy is blocked.</p>
<h2>What This Means for Your Project</h2>
<p>If you are building AI products, the gap between prototype and production is wider than it looks. I bridge that gap — architecting systems that retrieve accurately, reason reliably, scale affordably, and fail gracefully. Whether you need a RAG pipeline for internal documents, an agent that interacts with your existing APIs, or a full-stack product with mobile and web clients, the patterns above are how I ship.</p>
<p>If you want to talk architecture, model selection, or regional deployment strategy, <a href="/contact">get in touch</a>.</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/f0fc74b142de102f0b78fa1488507466/cover-ai-engineering-rag-agents-fullstack.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[20 Years in Code: The Shift from Logic to Protocols]]></title>
            <link>https://yabasha.dev/blog/20-years-in-code-the-shift-from-logic-to-protocols</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/20-years-in-code-the-shift-from-logic-to-protocols</guid>
            <pubDate>Mon, 18 May 2026 10:39:49 GMT</pubDate>
            <description><![CDATA[I started writing code in 2005. Two decades later, the rules haven't just changed—the game itself is unrecognizable. From banking monoliths to AI agents.]]></description>
            <content:encoded><![CDATA[<h1>20 Years in Code: The Shift from Logic to Protocols</h1>
<p>I started shipping code in 2005. Back then, deploying felt like launching a physical rocket. You prayed to the server gods and watched the logs.</p>
<p>Over two decades, I’ve built systems for banks, humanitarian organizations, and eCommerce giants. I’ve seen the pendulum swing from monoliths to microservices, from on-prem to cloud, from logic to machine learning.</p>
<p>But nothing prepared us for the current wave.</p>
<h2>The Reframe</h2>
<p>The last 20 years were about writing instructions. The next 20 are about building protocols.</p>
<p>We used to tell computers exactly <em>how</em> to do a job. Now, we tell them <em>what</em> the job is, and we build the guardrails to keep them from burning the house down while they do it.</p>
<p>That&#39;s not just a paradigm shift. That&#39;s a tsunami that rearranges the coastline of software engineering.</p>
<h2>Real Failures, Real Lessons</h2>
<p>In the banking sector, a missed decimal point meant a financial crisis. In the humanitarian sector, downtime meant someone didn&#39;t get help. The stakes trained me to be paranoid.</p>
<p>Now, as an AI Engineer, that paranoia is my biggest asset.</p>
<p>Everyone is racing to build agents. Most of them are just function calls with a LinkedIn profile. They lack memory, they lack context, and they lack safety.</p>
<p>Building an AI agent isn&#39;t about connecting an API key to a chat interface. It&#39;s about grounding. It&#39;s about permissions. It&#39;s about operational safety.</p>
<h2>The Next Era</h2>
<p>We are no longer just programmers. We are system architects for non-human intelligence.</p>
<p>If you are still trying to control every execution path with an <code>if/else</code> statement, you are fighting gravity.</p>
<p>Stop writing scripts. Start designing systems. The era of the prompt is over. Are you ready for the era of the protocol?</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/9d37d2385428693494f264cbd50a15d6/blog-cover-20-years-in-code.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Building Production AI Agents That Actually Work]]></title>
            <link>https://yabasha.dev/blog/building-production-ai-agents-that-actually-work</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/building-production-ai-agents-that-actually-work</guid>
            <pubDate>Thu, 05 Mar 2026 21:00:00 GMT</pubDate>
            <description><![CDATA[A model-calling job on my own site timed out three times and wrote nothing. Here are the timeout, schema, fallback and tracing rules I apply to every agent now, with the numbers from that incident.]]></description>
            <content:encoded><![CDATA[<p><em>The failure below is one of mine, on this site, with the commit that fixed it. Where a claim is a recommendation rather than a measurement, I say so.</em></p>
<p>Most writing about AI agents argues about prompts. Almost none of it is about the thing that actually takes agents down, which is the plumbing around the model call.</p>
<p>Here is a failure from my own site. It is small, it is boring, and it is exactly the shape of the ones that hurt.</p>
<!-- lift --> An agent's reliability is decided by the timeout of the thing that calls it, not by the prompt inside it.

<h2>What actually broke on my own system?</h2>
<p>A queued job that calls a language model wrote nothing, three times, and left no error a caller could see. The job was <code>GeneratePostSeoJob</code>, which asks a model to write SEO fields for a blog post.</p>
<p>The cause was two timeouts that disagreed. <a href="https://laravel.com/docs/12.x/horizon">Laravel Horizon</a> gives a worker 60 seconds. The service that makes the model call allows a single request 120. Neither number was wrong on its own. Together they guaranteed that any post long enough to take the model past a minute would be killed mid-request.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Setting</th>
<th>Where it lives</th>
</tr>
</thead>
<tbody><tr>
<td>Horizon worker timeout</td>
<td>60 seconds</td>
<td><code>config/horizon.php:210</code></td>
</tr>
<tr>
<td>Horizon retries</td>
<td>3</td>
<td><code>config/horizon.php:209</code></td>
</tr>
<tr>
<td>Single model call ceiling</td>
<td>120 seconds</td>
<td><code>OpenRouterService::HTTP_TIMEOUT_SECONDS</code></td>
</tr>
<tr>
<td>Job timeout, before the fix</td>
<td>not declared, so 60</td>
<td><code>GeneratePostSeoJob</code></td>
</tr>
<tr>
<td>Job timeout, after the fix</td>
<td>150 seconds</td>
<td><code>GeneratePostSeoJob:16</code></td>
</tr>
</tbody></table>
<p>The production <code>failed_jobs</code> table recorded it plainly: <code>Illuminate\Queue\TimeoutExceededException: App\Jobs\GeneratePostSeoJob has timed out</code>, at 10:24:47 on 19 September 2026. The fix landed at 10:47:39 the same morning.</p>
<h2>Why is a mismatched timeout worse than a crash?</h2>
<p>Because a crash tells you. This did not.</p>
<p>The worker killed the request while the model was still answering. No response arrived, so no parsing failed, no validation tripped, and no handler ran. The job simply stopped. Horizon retried it twice more, hit the same wall twice more, and only then wrote one <code>TimeoutExceededException</code> with no mention of the model, the post, or the 120-second call it interrupted.</p>
<!-- lift --> A timeout that fires inside a call you cannot see does not produce an error. It produces a gap, and gaps do not page anyone.

<p>The rule I wrote into the code as a comment, so the next person cannot miss it: any queued job that reaches the model must declare a <code>$timeout</code> larger than the model call&#39;s own ceiling. Both model-calling jobs in this repo now sit at 150 against a 120-second call.</p>
<h2>How do you size the timeout for a job that calls a model?</h2>
<p>Work outwards from the slowest thing, not inwards from the platform&#39;s ceiling. The model call is the innermost budget. Every layer wrapping it needs headroom, and the outermost layer is usually a platform limit you do not control.</p>
<table>
<thead>
<tr>
<th>Runtime</th>
<th>Documented execution limit</th>
<th>Source</th>
</tr>
</thead>
<tbody><tr>
<td>Convex runtime action</td>
<td>30 minutes</td>
<td><a href="https://docs.convex.dev/production/state/limits">Convex limits</a></td>
</tr>
<tr>
<td>Convex Node runtime action</td>
<td>10 minutes</td>
<td>same page</td>
</tr>
<tr>
<td>Laravel Horizon worker</td>
<td>whatever <code>config/horizon.php</code> says, 60 here</td>
<td>this repo</td>
</tr>
</tbody></table>
<p>An earlier version of this post claimed Convex actions time out at 30 seconds and budgeted 25 seconds against it. That is wrong, and I have corrected it. The documented limits are 30 minutes and 10 minutes. The lesson survives the correction, which is that you should never guess a platform limit. Read it, write it down next to your own budget, and make the gap between them deliberate.</p>
<h2>What should an agent do when the model returns nothing usable?</h2>
<p>Fall back, every time, on a path that cannot itself call the model. Three different failures collapse into the same answer in my own pipeline, and none of them raise to the caller.</p>
<p>The model returns null, the response does not decode as JSON, or processing it throws. In all three cases <code>PostAiService::analyzeContent()</code> logs and returns <code>fallbackAnalysis()</code>, a heuristic that reads the post and produces a summary without any model at all.</p>
<p>That is the part most agent tutorials skip. A fallback that calls a cheaper model is still a fallback that can fail. Somewhere at the bottom of the ladder there has to be a rung made of plain code.</p>
<table>
<thead>
<tr>
<th>Rung</th>
<th>What it does</th>
<th>When it runs</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Retry the same model</td>
<td>Transient errors only: 429, 5xx, network</td>
</tr>
<tr>
<td>2</td>
<td>Drop to a cheaper model</td>
<td>Budget pressure or repeated failure</td>
</tr>
<tr>
<td>3</td>
<td>Serve a cached answer</td>
<td>One exists and is fresh enough</td>
</tr>
<tr>
<td>4</td>
<td>Degrade gracefully in plain code</td>
<td>No model is reachable</td>
</tr>
<tr>
<td>5</td>
<td>Escalate to a person</td>
<td>The failure is expensive or unsafe</td>
</tr>
</tbody></table>
<p>Decide the ladder before the incident. During one you will pick whichever rung is nearest, and that is how a system ends up with no rung 4.</p>
<h2>How do you force a model to return a shape you can trust?</h2>
<p>Ask for a schema and refuse anything else. In TypeScript that is <a href="https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data"><code>generateObject</code></a> from the Vercel AI SDK with a <a href="https://zod.dev/">Zod</a> schema, so the SDK retries or fails on invalid output instead of handing you a broken object.</p>
<pre><code class="language-typescript">const ToolCallSchema = z.object({
  tool: z.enum([&#39;search_orders&#39;, &#39;create_ticket&#39;, &#39;escalate&#39;]),
  params: z.record(z.unknown()),
  confidence: z.number().min(0).max(1),
});
</code></pre>
<p>A schema is necessary and not sufficient. Valid JSON can still be wrong, and the interesting checks are the ones your schema cannot express.</p>
<p>My own pipeline has one worth copying. The model is asked for a summary of at most 60 words and overshoots that budget often enough to matter. The frontend truncates on sentence boundaries, so an overshooting summary loses its final clause, which is the part carrying the outcome and therefore the part worth quoting. So the same word clamp the heuristic fallback uses is applied to the model&#39;s output too.</p>
<!-- lift --> Schema validation asks whether the shape is right. Business validation asks whether the value is usable. A refund larger than the order passes every schema you can write.

<h2>Which retries help, and which ones just cost money?</h2>
<p>Retry the failures that are about the network and the provider. Never retry the ones that are about your own request, because they will fail identically and bill you for the privilege.</p>
<table>
<thead>
<tr>
<th>Condition</th>
<th>Retry?</th>
<th>Why</th>
</tr>
</thead>
<tbody><tr>
<td>429, rate limited</td>
<td>Yes, with backoff</td>
<td>The next window will accept it</td>
</tr>
<tr>
<td>5xx from the provider</td>
<td>Yes, with backoff</td>
<td>Transient on their side</td>
</tr>
<tr>
<td>Network timeout, <code>ETIMEDOUT</code></td>
<td>Yes</td>
<td>Blip, not a bug</td>
</tr>
<tr>
<td>4xx other than 429</td>
<td>No</td>
<td>Your request is malformed</td>
</tr>
<tr>
<td>Schema validation failed</td>
<td>No, unless you change the input</td>
<td>The same prompt yields the same shape</td>
</tr>
<tr>
<td>Worker timeout</td>
<td>No, fix the budget</td>
<td>Three retries of a doomed job is what my incident was</td>
</tr>
</tbody></table>
<p>That last row is the one I learned the hard way. Horizon retried my job twice after the first kill. All three attempts burned the same model call and none of them could have finished.</p>
<h2>What should every model call record?</h2>
<p>Input tokens, output tokens, the model name, the duration, and whether it succeeded. Without those five you are guessing, and <a href="https://langfuse.com/docs">Langfuse</a> gives you all of them as a distributed trace across a multi-step agent run.</p>
<p>The reason to trace rather than log is that agent steps are separate model calls. One user request becomes intent classification, then tool selection, then response synthesis. A log line per step tells you each step happened. A trace tells you which step spent the time and which step spent the money.</p>
<p>Cost attribution is the payoff. Tag every trace with the feature that triggered it and you can answer questions about a feature&#39;s unit economics rather than about a bill. I built the same instinct into a client&#39;s retrieval system, and the measured result is in <a href="/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1">cutting LLM API costs 60%</a>. Caching moved four times more money than model routing did, and I only know that because every call was traced first.</p>
<h2>How do you know an agent got better rather than just different?</h2>
<p>You run evals, and you accept that this is the least mature part of the stack. Three kinds are worth having, and I am describing what I recommend rather than a suite I can point you at.</p>
<p>Deterministic checks answer whether the output matched the schema and carried the required fields. A model-as-judge grades outputs against written criteria using a cheap model, which is imprecise but catches large regressions. Human spot-checks on a small sample of real traffic catch what both of the others miss, which is usually tone and context.</p>
<!-- lift --> An eval suite that only asks "did it return something" is a smoke test wearing a lab coat. The useful question is whether the answer got better, and that needs a graded dataset you maintain by hand.

<h2>What breaks first when an agent meets real users?</h2>
<p>Not the prompt. This is the order I now expect, with what each one looks like from outside.</p>
<table>
<thead>
<tr>
<th>Failure</th>
<th>What you see</th>
<th>What prevents it</th>
</tr>
</thead>
<tbody><tr>
<td>Timeout mismatch between layers</td>
<td>Silence, no error, empty column</td>
<td>Declare every budget; make the outer one larger</td>
</tr>
<tr>
<td>Malformed model output</td>
<td>A downstream service accepting garbage</td>
<td>Schema plus business-rule validation</td>
</tr>
<tr>
<td>No fallback below the model</td>
<td>Total outage when one provider wobbles</td>
<td>A rung made of plain code</td>
</tr>
<tr>
<td>Retrying an unretryable failure</td>
<td>Triple cost, same result</td>
<td>Retry on 429 and 5xx only</td>
</tr>
<tr>
<td>No per-call cost record</td>
<td>A bill you cannot attribute</td>
<td>Trace every call, tag every trace</td>
</tr>
<tr>
<td>No graded evals</td>
<td>Regressions that ship unnoticed</td>
<td>A dataset and a judge</td>
</tr>
</tbody></table>
<p>Four of those six cost nothing but attention. The expensive ones are the fallback ladder and the eval dataset, and both are cheaper than the incident that teaches you to build them.</p>
<h2>How does this fit a Laravel or Next.js codebase?</h2>
<p>The same way any other unreliable dependency does. The model call is an HTTP request to somebody else&#39;s machine, and it deserves the treatment you already give those.</p>
<p>On this site the model work runs as queued jobs behind Horizon, which is the same queue that pushes cache purges to the frontend. I wrote that protocol up in <a href="/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr">the cache handshake</a>, and the surrounding architecture, including what has no retry at all and why, in <a href="/blog/a-production-ready-laravel-nextjs-architecture-blueprint">what yabasha.dev actually runs</a>.</p>
<p>The constraints get sharper the more regulated the client. What survives contact with a bank&#39;s review is a narrower set than what survives a demo, and I wrote about that gap in <a href="/blog/ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking">what actually works in enterprise banking</a>.</p>
<h2>What does the Composable AI Stack actually give you?</h2>
<p>Less than an earlier version of this post claimed. I read the repository row by row before
republishing, and three of its eight claims were aspirational. Here is what is in the tree today.</p>
<table>
<thead>
<tr>
<th>Claim</th>
<th>State on 2026-09-21</th>
</tr>
</thead>
<tbody><tr>
<td>Tool calling with schemas</td>
<td>Real. <code>generateObject</code> from <code>ai</code> 4.3 with a Zod schema, in <code>packages/ai/src/guardrails.ts</code></td>
</tr>
<tr>
<td>Guardrails, input and output</td>
<td>Real. <code>runWithGuardrails</code> parses an input schema, runs an optional moderation hook, then parses the output schema</td>
</tr>
<tr>
<td>Tracing</td>
<td>Real. One Langfuse client in <code>packages/ai/src/tracing.ts</code>, used by both call paths and by the eval runner</td>
</tr>
<tr>
<td>Eval harness</td>
<td>Real. <code>runEval</code> and <code>summarizeResults</code> in <code>packages/evals</code>, with three string scorers</td>
</tr>
<tr>
<td>Per-call cost attribution</td>
<td>Partial. Traces exist, but no token usage is passed to <code>generation.end()</code>, so there is no cost figure to attribute</td>
</tr>
<tr>
<td>Retry logic in the model client</td>
<td>Not there. The only backoff in the repo is BullMQ&#39;s, in the worker queue</td>
</tr>
<tr>
<td>Task decomposition in <code>packages/agents</code></td>
<td>Not there. That package does not exist</td>
</tr>
<tr>
<td>An <code>AgentRunner</code> agent loop</td>
<td>Not there. No such class</td>
</tr>
</tbody></table>
<p>The import path in the older table was wrong too. The generated template uses the <code>@acme</code> scope,
so it is <code>runWithGuardrails</code> from <code>@acme/ai</code>, not from a <code>@yabasha/cas</code> subpath. The CLI on npm is
<code>@yabasha/cas</code>; the packages it scaffolds are not.</p>
<!-- lift --> The guardrail, tracing and eval core is real and worth taking. The agent loop, the retry layer and the fallback chains are a roadmap I published as if it had already shipped, which is exactly the habit this post argues against.

<pre><code class="language-bash">bun add -g @yabasha/cas
cas init my-agent-system
</code></pre>
<h2>What would I check before calling an agent production-ready?</h2>
<p>Start at the outside and work in. The prompt is the last thing on this list for a reason.</p>
<ul>
<li>Every layer that wraps a model call declares a timeout, and each one is larger than the layer inside it.</li>
<li>The platform&#39;s own execution limit is written down next to your budget, read from the vendor&#39;s docs rather than remembered.</li>
<li>Every model output passes a schema, and then passes a business rule the schema cannot express.</li>
<li>The fallback ladder ends in code that cannot call a model.</li>
<li>Retries fire on 429 and 5xx, and nowhere else.</li>
<li>Every call records tokens, model, duration and outcome, and every trace carries the feature that caused it.</li>
<li>A graded dataset exists, and something runs against it before you ship.</li>
</ul>
<!-- lift --> Demos fail on the prompt. Production fails on the timeout, the schema, and the missing rung at the bottom of the ladder.

<h2>Measured with</h2>
<p>Numbers read from this repository and from production on 2026-09-21.</p>
<ul>
<li>Laravel 12 on PHP 8.4, Horizon on Redis, one VPS</li>
<li><code>config/horizon.php</code>: <code>tries</code> 3, <code>timeout</code> 60, <code>memory</code> 128</li>
<li><code>OpenRouterService::HTTP_TIMEOUT_SECONDS</code> 120; both model-calling jobs declare <code>$timeout</code> 150</li>
<li>The incident: <code>TimeoutExceededException</code> on <code>GeneratePostSeoJob</code>, 2026-09-19 10:24:47, fixed 10:47:39</li>
<li>Convex execution limits quoted from the vendor&#39;s limits page, fetched 2026-09-21</li>
<li><code>@yabasha/cas</code> 0.2.0 on the npm registry; <code>composable-ai-stack</code> public, last pushed 2026-05-12, cloned and read on 2026-09-21</li>
</ul>
<p><em>Questions: <a href="https://x.com/bayyash">@bayyash</a></em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/b29608d78955f5c09fadc6743d920f89/ai-agents-v3-20260306-105435.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[Cutting LLM API Costs 60%: A Production RAG Post-Mortem]]></title>
            <link>https://yabasha.dev/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1</guid>
            <pubDate>Thu, 05 Mar 2026 21:00:00 GMT</pubDate>
            <description><![CDATA[A live client RAG system on Next.js, Convex, Qdrant and OpenAI billed $4,200 in one week. Five stacked changes took week six to $1,680. No rewrite, no migration, no downtime.]]></description>
            <content:encoded><![CDATA[<p><em>This post describes a client system, not my own site. Every figure below is one I recorded at the time. Where I cannot show my working, I say so.</em></p>
<p>The invoice was $4,200 for one week of LLM calls. The system was live, the users were active, and a rewrite was not on the table. The brief was narrow. Make it cheaper without making it worse.</p>
<p>Six weeks and two sprints later the same week cost $1,680.</p>
<!-- lift --> Cost per query is a product decision, not a billing line. Nobody can make it smaller without first being able to see it.

<h2>What did this RAG system cost before and after?</h2>
<p>Week 0 billed $4,200. Week 6 billed $1,680, a 60% reduction, with no database migration and no downtime. Cost per query fell from $0.08 to $0.032. Every number in this table was recorded on the same dashboard, six weeks apart.</p>
<table>
<thead>
<tr>
<th>Measure</th>
<th>Week 0</th>
<th>Week 6</th>
</tr>
</thead>
<tbody><tr>
<td>LLM spend, one week</td>
<td>$4,200</td>
<td>$1,680</td>
</tr>
<tr>
<td>Cost per query, average</td>
<td>$0.08</td>
<td>$0.032</td>
</tr>
<tr>
<td>Cache hit rate</td>
<td>0%</td>
<td>62%</td>
</tr>
<tr>
<td>Context tokens per call, average</td>
<td>8,000</td>
<td>1,200</td>
</tr>
<tr>
<td>Chunks injected per query</td>
<td>10</td>
<td>3</td>
</tr>
<tr>
<td>Average chunk relevance</td>
<td>0.65</td>
<td>0.89</td>
</tr>
<tr>
<td>Response latency, average</td>
<td>1,400 ms</td>
<td>890 ms</td>
</tr>
<tr>
<td>User satisfaction</td>
<td>baseline</td>
<td>unchanged, slightly up</td>
</tr>
</tbody></table>
<p>Latency fell alongside cost, which surprised me. It should not have. A call that never reaches the model returns in milliseconds, and a call carrying 1,200 tokens of context finishes sooner than one carrying 8,000.</p>
<h2>What was the system, and where was the money going?</h2>
<p>A retrieval system answering questions against a client&#39;s internal knowledge base. Next.js on the front, <a href="https://docs.convex.dev/home">Convex</a> as the backend, <a href="https://qdrant.tech/documentation/search/search/">Qdrant</a> for vectors, OpenAI for inference. It demoed well and held up for the first hundred users.</p>
<p>Four things were wrong at once, and none of them were visible.</p>
<p>Vector search returned 10 chunks per query, each over 800 tokens. The system prompt had grown to roughly 420 tokens of assistant boilerplate. There was no cache, so the same question cost full price every time it was asked. There was no routing, so a yes-or-no lookup paid the same rate as a multi-step reasoning task.</p>
<!-- lift --> Nothing in that list is exotic. Every one of them is invisible until somebody instruments the call and reads the number.

<h2>Which five changes produced the cut, and what did each one save?</h2>
<p>Five changes, applied in order, each one resting on the visibility the previous one created. These are the shares I attributed at the time.</p>
<table>
<thead>
<tr>
<th>#</th>
<th>Change</th>
<th>Attributed share</th>
<th>Mechanism</th>
</tr>
</thead>
<tbody><tr>
<td>1</td>
<td>Semantic cache in Redis</td>
<td>~35%</td>
<td>Repeat questions never reach the model</td>
</tr>
<tr>
<td>2</td>
<td>Prompt and context compression</td>
<td>~15%</td>
<td>Fewer input tokens per call</td>
</tr>
<tr>
<td>3</td>
<td>Model routing</td>
<td>~8%</td>
<td>Cheap queries go to a cheap model</td>
</tr>
<tr>
<td>4</td>
<td>Relevance floor on retrieval</td>
<td>~4%</td>
<td>Low-scoring chunks are dropped, not truncated</td>
</tr>
<tr>
<td>5</td>
<td>Hard output caps</td>
<td>~3%</td>
<td>No runaway responses</td>
</tr>
</tbody></table>
<p>Those five shares add to 65%, and the invoice moved 60%. They do not add, they stack. Change 2 only ever runs on the calls change 1 failed to catch, so its 15% is 15% of a smaller number by the time it lands. I am quoting the attributions as I recorded them. The 60% is the invoice, and the invoice is the number I trust.</p>
<h2>How does a semantic cache cut LLM spend?</h2>
<p>By never calling the model. An incoming query is embedded, the embedding is hashed, and the hash is looked up in Redis. A hit returns the stored answer. A miss calls the model, then writes the answer back under a <a href="https://redis.io/docs/latest/commands/setex/">SETEX</a> with a seven-day expiry.</p>
<pre><code class="language-typescript">// packages/ai/cache.ts
const CACHE_TTL = 60 * 60 * 24 * 7;

export async function getCachedResponse(
  embedding: number[]
): Promise&lt;string | null&gt; {
  return redis.get(`rag:cache:${hashEmbedding(embedding)}`);
}
</code></pre>
<p>The similarity threshold is 0.92 cosine. Above that, two phrasings of the same question are treated as one question. Below it, they are not. I set the target hit rate at 40% and it settled at 62%.</p>
<!-- lift --> A cache entry ages out on a seven-day timer and nothing invalidates it early. If the underlying answer changes on day two, the cache is wrong for five days.

<p>That is the trade I took, and it is only safe because the knowledge base changed slowly. Event-driven invalidation is the harder half of the same problem, and I wrote up how I do it for this site&#39;s own cache in <a href="/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr">the cache handshake</a>. Different cache, different system, same question. When do you evict?</p>
<h2>What did prompt and context compression actually remove?</h2>
<p>Two things: the system prompt, and the retrieved context around it. The prompt went from roughly 420 tokens of politeness instructions to 47 tokens that say what the model must do.</p>
<pre><code>Answer based on context. Say &quot;I don&#39;t know&quot; if context lacks info. Be concise.
</code></pre>
<p>The context was the larger win. Ten chunks at 800-plus tokens each meant 8,000 tokens of retrieval before the user&#39;s own question was added.</p>
<table>
<thead>
<tr>
<th>Setting</th>
<th>Before</th>
<th>After</th>
</tr>
</thead>
<tbody><tr>
<td>System prompt</td>
<td>~420 tokens</td>
<td>47 tokens</td>
</tr>
<tr>
<td>Chunk size</td>
<td>800+ tokens</td>
<td>512 tokens max</td>
</tr>
<tr>
<td>Chunk overlap</td>
<td>none recorded</td>
<td>50 tokens</td>
</tr>
<tr>
<td>Chunks per query</td>
<td>10</td>
<td>3, hard cap</td>
</tr>
<tr>
<td>Relevance floor</td>
<td>none</td>
<td>0.78 cosine</td>
</tr>
<tr>
<td>Context per call</td>
<td>8,000 tokens</td>
<td>~1,200 tokens</td>
</tr>
</tbody></table>
<p>Read your rendered prompts, not your templates. The template looks lean. The string that reaches the model is the one you are paying for, and it is usually full of things nobody chose.</p>
<h2>Why does dropping low-relevance chunks improve answers?</h2>
<p>Because low-scoring context does not sit there harmlessly. It competes. Qdrant exposes a <code>score_threshold</code> on search, so a chunk below 0.78 cosine is never returned rather than returned and then trimmed. Average relevance across the surviving chunks went from 0.65 to 0.89.</p>
<p>Hallucinations dropped when the noisy chunks stopped arriving. That was not the goal. The goal was tokens, and the quality improvement came free, which is the opposite of how cost work usually goes.</p>
<!-- lift --> A relevance floor is a quality control that happens to save money. Cutting chunk count without a floor is a cost control that happens to hurt quality. They look identical on the invoice and behave nothing alike.

<p>The pipeline also carries a <code>max_context_tokens</code> ceiling. If query plus context exceeds it, the oldest chunks are truncated first. It has never been the binding constraint since the floor went in, which is the point of a backstop.</p>
<h2>When is a cheaper model the right model?</h2>
<p>When the query does not need reasoning. A definition lookup and a multi-step analysis were paying the same rate, and a query classifier fixed that. Classification runs on a cheap embedding compared against labelled examples, so the routing decision costs close to nothing.</p>
<table>
<thead>
<tr>
<th>Query class</th>
<th>Model</th>
<th>Max output tokens</th>
</tr>
</thead>
<tbody><tr>
<td>Simple lookup, definition, boolean</td>
<td>gpt-3.5-turbo</td>
<td>150</td>
</tr>
<tr>
<td>Reasoning, creative, complex</td>
<td>gpt-4o</td>
<td>500</td>
</tr>
<tr>
<td>Everything else</td>
<td>gpt-4o-mini</td>
<td>300</td>
</tr>
</tbody></table>
<p>Roughly 40% of queries ended up on the cheap path. Routing was worth about 8% of total spend, which is smaller than it feels like it should be, and that is worth saying out loud. Caching moves four times more money than routing does.</p>
<p>The cost is a slight quality dip on edge cases where the classifier guesses wrong. I accepted that because it is measured, not assumed. Check current rates on the <a href="https://developers.openai.com/api/docs/pricing">OpenAI pricing page</a> before copying this table. The relative gap between tiers is what makes routing pay, and that gap moves.</p>
<h2>Why cap output tokens on every call?</h2>
<p>Because output tokens bill at the same rate as input tokens, and nothing about an uncapped response is predictable. One query returned 2,400 tokens because a user typed &quot;tell me everything about&quot;. Nobody chose that cost.</p>
<table>
<thead>
<tr>
<th>Intent</th>
<th>Cap</th>
</tr>
</thead>
<tbody><tr>
<td>Lookup or definition</td>
<td>150 tokens</td>
</tr>
<tr>
<td>Explanation</td>
<td>300 tokens</td>
</tr>
<tr>
<td>Complex reasoning</td>
<td>500 tokens</td>
</tr>
<tr>
<td>Creative</td>
<td>800 tokens, explicit override required</td>
</tr>
</tbody></table>
<p>Every call now carries a <code>maxTokens</code> value from that table. When the model stops early, the user gets a &quot;Continue?&quot; button rather than a truncated paragraph, so the cap is visible in the product instead of hidden in the response.</p>
<p>This was the smallest of the five changes, worth about 3%. It is also the one I would ship first in a new system, because it costs an afternoon and it removes an entire class of surprise from the bill.</p>
<h2>What do you need to see before you change anything?</h2>
<p>Cost per query, per call, with the model name attached. I spent the first day instrumenting and changed nothing, and that day paid for itself several times over. <a href="https://langfuse.com/docs">Langfuse</a> carried the tracing.</p>
<p>Each call records input tokens, output tokens, total cost, model, latency, cache hit or miss, user and session id, query classification, and the rendered prompt. The rendered prompt matters more than it sounds. It is the only way to tell whether compression actually shipped.</p>
<p>The dashboard refreshes every five minutes and shows cost per query, cache hit rate, the split across models, and latency at p50, p95 and p99.</p>
<!-- lift --> You cannot optimise what you cannot see. Every one of the five changes above started as a number on a dashboard that I did not like.

<p>The same instinct drives the agent work I wrote up in <a href="/blog/building-production-ai-agents-that-actually-work">building production AI agents that actually work</a>. An agent you cannot trace is an agent you cannot debug or bill for.</p>
<h2>What did not work?</h2>
<p>Three attempts, all rolled back. I am listing them because the failures shaped the final design more than the wins did.</p>
<table>
<thead>
<tr>
<th>Attempt</th>
<th>Idea</th>
<th>Why it was reverted</th>
</tr>
</thead>
<tbody><tr>
<td>Response summarisation</td>
<td>Cache compressed summaries, expand on read</td>
<td>The expansion step invented details the original answer never had</td>
</tr>
<tr>
<td>Dynamic chunk sizing</td>
<td>Vary chunk size by query complexity</td>
<td>Brittle, added latency, gained roughly 2%</td>
</tr>
<tr>
<td>Client-side caching</td>
<td>Cache answers in the browser</td>
<td>The client&#39;s users share workstations, so it was a privacy problem</td>
</tr>
</tbody></table>
<p>The third one is the one I would have shipped if nobody had asked who sits at those desks. It was technically sound and completely wrong for the deployment, which is the kind of mistake you only avoid by knowing the client&#39;s floor plan. I wrote about how much of that context matters in <a href="/blog/ai-agents-ate-my-boilerplate-what-actually-works-in-enterprise-banking">what actually works in enterprise banking</a>.</p>
<h2>How do you keep every LLM call on one path?</h2>
<p>One wrapper, no exceptions. Every model call in the system goes through a single function, so caching, routing, guardrails and cost tracking cannot be skipped by a new feature written in a hurry.</p>
<pre><code class="language-typescript">// packages/ai/index.ts
export async function generateRAGResponse(
  query: string,
  context: Chunk[]
): Promise&lt;AIResponse&gt; {
  const embedding = await embed(query);

  const cached = await getCachedResponse(embedding);
  if (cached) return cached;

  const modelConfig = await routeQuery(query);
  const prompt = buildCompressedPrompt(query, context);
  const validated = await applyInputGuardrails(prompt);

  const response = await callLLM({ ...modelConfig, prompt: validated });

  await trackCost(response.usage, modelConfig.model);
  await cacheResponse(embedding, response.text);

  return response;
}
</code></pre>
<p>The module splits into cache, router, guardrails, cost tracker and shared types. None of that is clever. Its whole value is that there is one place to change a threshold and one place to read a bill. I use the same shape at much smaller scale on my own site, described in <a href="/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs">how I built an AI agent for my portfolio</a>.</p>
<h2>What would I do differently?</h2>
<p>Instrument on day one of the build, not day one of the cleanup. The $4,200 week was not caused by a bad decision. It was caused by six good decisions nobody could see the price of.</p>
<!-- lift --> Demos do not show you where a system bleeds. A hundred users will not either. Cost curves bend late, and by the time the invoice is loud enough to notice, the habit is already in the codebase.

<p>Two smaller lessons held up. The &quot;no rewrite&quot; constraint forced better thinking than an open brief would have, because every idea had to survive the question of what it touched. And the boring change won. A Redis cache is not novel and it moved more money than the three clever experiments combined.</p>
<p>The system still runs. Spend flattened while usage kept climbing, and there have been no more surprise invoices.</p>
<h2>Measured with</h2>
<ul>
<li>Stack: Next.js, Convex, Qdrant, OpenAI, Redis, Langfuse</li>
<li>Figures recorded week 0 and week 6 of the optimisation, two sprints apart</li>
<li>Client system, not yabasha.dev. Figures are from that engagement&#39;s dashboard</li>
</ul>
<p><em>Questions: <a href="https://twitter.com/bayyash">@bayyash</a> · Code: <a href="https://github.com/yabasha/composable-ai-stack">composable-ai-stack</a></em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/16dcb9919920aa356ae300fda11c763a/llm-cost-v3-20260306-105210.min.webp" length="0" type="image/webp"/>
        </item>
        <item>
            <title><![CDATA[The Tailwind Tsunami: How a CSS Framework's Collapse Signals the End of Software Development as We Knew It]]></title>
            <link>https://yabasha.dev/blog/the-tailwind-tsunami-how-a-css-frameworks-collapse-signals-the-end-of-software-development-as-we-knew-it</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/the-tailwind-tsunami-how-a-css-frameworks-collapse-signals-the-end-of-software-development-as-we-knew-it</guid>
            <pubDate>Sun, 25 Jan 2026 13:00:00 GMT</pubDate>
            <description><![CDATA[The Tailwind Tsunami: AI is vaporizing software development's economic foundations. Discover the verification-first engineering blueprint to ship AI-assisted code with higher confidence than ever.]]></description>
            <content:encoded><![CDATA[<p><strong>The winds of change aren&#39;t blowing anymore. The hurricane has made landfall.</strong></p>
<p>On January 6th, 2026, Adam Wathan, founder of Tailwind CSS, did something unprecedented: he publicly confessed that his company was dying not from competition, but from success. Seventy-five percent of his engineering team—three out of four engineers—were laid off. Revenue had cratered by 80%. The framework powering over 617,000 live websites, including Shopify, GitHub, and NASA&#39;s Jet Propulsion Laboratory, had become a victim of its own popularity. (<a href="https://www.leanware.co/insights/tailwind-ai-crisis">leanware.co</a>)</p>
<p>The killer? AI coding assistants that had absorbed Tailwind&#39;s documentation into their models, serving answers directly inside VS Code and Cursor without ever sending a developer to <a href="http://tailwindcss.com/">tailwindcss.com</a>. No doc visits. No discovery funnel. No conversions to Tailwind UI. No revenue. (<a href="https://dev.to/kniraj/tailwind-css-lays-off-75-of-engineering-team-as-ai-tools-disrupt-revenue-model-1l3d">dev.to</a>)</p>
<p>This isn&#39;t a story about CSS. It&#39;s a diagnostic X-ray of how AI is rewiring developer behavior at the neurological level—and vaporizing the economic foundations of software development in the process.</p>
<h2>The Moment I Realized I Was Part of the Problem</h2>
<p>Last Tuesday, I caught myself doing something I&#39;d been doing for months without noticing: I stopped opening documentation. Completely.</p>
<p>My AI assistant answered my Tailwind question before I could even type &quot;<a href="http://tailwindcss.com/">tailwindcss.com</a>&quot; into my browser. It was convenient. It was fast. It was <em>exactly</em> the behavior that nuked Tailwind&#39;s business model. I had become a data point in their collapse, a perfect exemplar of the 40% traffic drop they&#39;d seen since early 2023. (<a href="https://www.businessinsider.com/tailwind-engineer-layoffs-ai-github-2026-1">businessinsider.com</a>)</p>
<p>But the real gut-punch came three hours later.</p>
<p>I was reviewing a critical payments module my assistant had generated in 90 seconds. The code looked perfect—clean abstractions, proper error handling, even helpful comments. But my verification pipeline screamed bloody murder. Buried in an async function was a timezone edge case that would have double-charged users in daylight saving transitions.</p>
<p>I caught it. But only because I&#39;d built a verification-first pipeline that treated AI-generated code as radioactive ☢️ until proven safe.</p>
<p>That&#39;s when the proverb clicked: <em>&quot;When the winds of change blow, some people build walls, others build windmills.&quot;</em></p>
<p>The winds aren&#39;t blowing anymore. This is the hurricane. And I&#39;m definitely in the windmill camp.</p>
<h2>The Three Shocks Reshaping Our Reality</h2>
<p>Tailwind&#39;s collapse isn&#39;t an isolated tragedy. It&#39;s the first visible symptom of a systemic shockwave that’s hitting every knowledge-based industry simultaneously. For software engineers, it manifests in three interlocking crises:</p>
<h3>1. The Economic Shock: The Death of the Attention Funnel</h3>
<p>Tailwind&#39;s business model was simple and proven: give away the framework (MIT license), monetize through documentation traffic that converts to paid products like Tailwind UI and Catalyst. It&#39;s the same playbook used by countless open-source companies, devtools startups, and even enterprise software vendors who rely on educational content to drive sales.</p>
<p>AI has broken this funnel irreparably.</p>
<p>When developers ask ChatGPT, Cursor, or GitHub Copilot &quot;how do I implement a responsive grid in Tailwind?&quot;, the AI doesn&#39;t send them to the docs. It serves the answer directly. The developer gets what they need. The framework gets usage. The company gets nothing. (<a href="https://chyshkala.com/blog/tailwindcss-layoffs-ai-impact-documentation-revenue">chyshkala.com</a>)</p>
<p>This is the counterintuitive nightmare: <strong>increased usage correlates with decreased revenue</strong>. As Wathan lamented, &quot;Right now there&#39;s just no correlation between making Tailwind easier to use and making development of the framework more sustainable.&quot; (<a href="https://www.businessinsider.com/tailwind-engineer-layoffs-ai-github-2026-1">businessinsider.com</a>)</p>
<p>Every tool that monetizes through developer attention—from API docs to video courses to Stack Overflow—is now at existential risk. The meteor has struck. We&#39;re waiting for the dust to settle.</p>
<h3>2. The Career Shock: The Great Engineering Restructuring</h3>
<p>The layoffs keep coming. Not because AI has replaced entire engineering teams, but because companies are frantically restructuring around AI-augmented workflows and haven&#39;t found the new equilibrium yet.</p>
<p>The math is brutal: if AI generates 70% of initial code, do you need 70% fewer engineers? Or do you need <em>different</em> engineers—ones who can verify, constrain, and operate AI systems at scale?</p>
<p>The answer is emerging in real-time. Wathan&#39;s team went from four engineers to one. Not because Tailwind CSS became obsolete, but because the <em>way</em> developers use it changed. The company had &quot;six months left&quot; before payroll became impossible. (<a href="https://www.leanware.co/insights/tailwind-ai-crisis">leanware.co</a>)</p>
<p>This creates a terrifying paradox for individual engineers: <strong>your productivity is rising while your job security is falling</strong>. The developers who thrive won&#39;t be the ones who type the fastest or know the most syntax. They&#39;ll be the ones who can specify constraints clearly, design verification systems, and ship AI-assisted code with <em>higher</em> confidence than traditional code.</p>
<p>The competitive moat has shifted from coding speed to engineering judgment.</p>
<h3>3. The Execution Shock: The Silent Bug Multiplier</h3>
<p>Remember that payments bug I almost shipped? It&#39;s not an edge case. It&#39;s the <em>new normal</em>.</p>
<p>AI generates code that <strong>looks correct but harbors subtle, catastrophic failures</strong>. I&#39;ve seen it produce:</p>
<ul>
<li>SQL queries with N+1 bombs hidden behind elegant <code>async</code> sugar</li>
<li>Security checks that validate token <em>shape</em> but not cryptographic signature</li>
<li>Database migrations that pass in dev but lock entire production tables for hours</li>
<li>API integrations that don&#39;t handle rate limits because the training data never showed them failing</li>
</ul>
<p>The tragedy is this: <strong>you ship 3x faster, but you debug 5x slower</strong> because you don&#39;t own the mental model. You didn&#39;t write it line by line. You can&#39;t trace the reasoning. The AI&#39;s &quot;thought process&quot; is invisible, compressed into a black box that outputs confidently wrong answers.</p>
<p>This is the fundamental asymmetry breaking traditional software delivery. Velocity accelerates exponentially. Confidence collapses linearly. And the cost of being wrong hasn&#39;t changed—but the speed of being wrong has accelerated 100x.</p>
<h2>Building Windmills: The Verification-First Revolution</h2>
<p>The old workflow—write code, review, ship—assumes humans generate every line. That assumption is dead.</p>
<p>The new architecture treats AI as a <strong>first-class actor</strong> in your engineering system, but one that must earn trust through evidence, not authority. I call it <strong>verification-first engineering</strong>: a system where AI generates at infinite speed, but production access is gated by observable proof of correctness.</p>
<p>Here&#39;s how it works:</p>
<h3>The AI-Native Architecture Stack</h3>
<p><strong>1. Prompt Assembly Layer</strong>
Version-control your prompts like code. A prompt isn&#39;t a chat message—it&#39;s a function with inputs, outputs, and invariants. Use templates, validate parameters, test edge cases. Tools like LangChain Hub or simple JSON schemas work. The key is treating prompts as <strong>formal specifications</strong> that encode business rules, not vague requests.</p>
<p><strong>2. AI Orchestrator</strong>
A thin abstraction layer (AWS Bedrock, Azure OpenAI, or a custom proxy) that:</p>
<ul>
<li>Routes prompts to appropriate models based on cost/risk</li>
<li>Enforces token budgets and timeout limits</li>
<li>Handles fallback logic when models hallucinate</li>
<li>Logs every input/output for auditability</li>
</ul>
<p>This is your circuit breaker. It prevents AI from becoming a single point of failure.</p>
<p><strong>3. Verification Pipeline</strong>
Every AI-generated diff must pass through escalating gates:</p>
<ul>
<li><strong>Static analysis</strong>: Type checks, linters, security scanners (Semgrep)</li>
<li><strong>Unit &amp; property tests</strong>: Verify logic, fuzz edge cases (pytest-hypothesis)</li>
<li><strong>Integration tests</strong>: Confirm contracts with real services</li>
<li><strong>Load testing</strong>: Catch N+1 queries and performance regressions</li>
<li><strong>Staged rollout</strong>: Feature flags → canary → production with traffic shadowing</li>
</ul>
<p><strong>4. Observability Layer</strong>
Tag AI-generated code paths in your telemetry (<code>span.set_attribute(&quot;ai_generated&quot;, True)</code>). Measure:</p>
<ul>
<li>Defect escape rate</li>
<li>Time to detect failures in AI paths</li>
<li>Cost per feature vs. traditional development</li>
<li>Revert rate</li>
</ul>
<p><strong>This is the CI/CD pipeline for AI cognition.</strong> It moves the bottleneck from generation (which AI owns) to verification (which you own).</p>
<h3>The Mindset Fix: From Coder to Engineer-Supervisor</h3>
<p>I tell my team: <em>&quot;The AI is typing. You&#39;re still responsible.&quot;</em></p>
<p>This isn&#39;t about controlling AI. It&#39;s about <strong>owning outcomes</strong>. You don&#39;t get credit for lines written. You get credit for defects prevented, incidents resolved, and systems sustained.</p>
<p>The developers who thrive will master four new skills:</p>
<ol>
<li><strong>Constraint specification</strong>: Writing prompts that encode invariants and business rules</li>
<li><strong>Behavior verification</strong>: Designing tests that catch AI hallucinations</li>
<li><strong>Safe shipping</strong>: Architecting rollouts that contain AI mistakes</li>
<li><strong>Reality observation</strong>: Debugging systems where 30% of code came from a black box</li>
</ol>
<p>This is the new moat. Non-developers using AI can&#39;t do this. Junior developers using AI without mentorship will ship disasters. Senior developers who master verification will become 10x engineers—not because they type more, but because they ship safer.</p>
<h2>The Trust Decision Matrix: When to Believe Your AI</h2>
<p>Verification-first engineering sounds expensive. It is. But not everywhere equally. The key is matching verification rigor to risk level.</p>
<p>Here&#39;s the non-obvious heuristic I use daily:</p>
<table>
<thead>
<tr>
<th>Code Characteristic</th>
<th>Risk Level</th>
<th>Verification Required</th>
<th>When to Trust</th>
</tr>
</thead>
<tbody><tr>
<td>Pure function, no I/O</td>
<td>Low</td>
<td>Unit tests + property tests ✅</td>
<td>After 1st pass</td>
</tr>
<tr>
<td>Database query</td>
<td>Medium</td>
<td>Query plan analysis + integration tests ✅✅</td>
<td>After 2nd pass</td>
</tr>
<tr>
<td>Auth/security logic</td>
<td>High</td>
<td>Manual review + security scan + pen test ✅✅✅</td>
<td>Never fully; monitor continuously</td>
</tr>
<tr>
<td>Infrastructure as Code</td>
<td>High</td>
<td>Dry-run + cost analysis + rollback plan ✅✅✅</td>
<td>After peer review</td>
</tr>
<tr>
<td>Business logic with external deps</td>
<td>Critical</td>
<td>All of above + feature flag + canary ✅✅✅✅</td>
<td>Only in production with shadow mode</td>
</tr>
</tbody></table>
<p><strong>The rule</strong>: The more context AI lacks (business rules, production constraints, legacy quirks), the more guardrails you need. <strong>Verification cost scales with the unknowns.</strong></p>
<p>This matrix becomes your compass. It tells you when to let AI ship freely and when to slow down for deep verification.</p>
<h2>From Theory to Practice: What Changed When I Made the Shift</h2>
<p>I implemented this verification-first approach across three client projects at my consultancy. The results redefined what&#39;s possible:</p>
<ul>
<li><strong>Shipped 2.3x faster</strong>: AI cut initial dev time by 70%, but verification added only 30% overhead. Net win: 2.3x velocity.</li>
<li><strong>Escaped defect rate dropped 40%</strong>: Property tests caught edge cases I&#39;d have missed reviewing code line-by-line.</li>
<li><strong>Code review transformed</strong>: Reviewers focused on <em>intent and risk</em> instead of syntax. The conversation moved from &quot;this variable name is wrong&quot; to &quot;what happens if this AI-generated payment call fails?&quot;</li>
<li><strong>Incident frequency stayed flat</strong>: Despite 3x more deployments, we didn&#39;t see incident spikes. Feature flags contained blast radius.</li>
<li><strong>Career leverage</strong>: In client interviews, I don&#39;t lead with &quot;I code fast.&quot; I lead with: <strong>&quot;I ship AI-assisted code with lower risk than traditional code.&quot;</strong> That&#39;s a differentiator in a world where everyone&#39;s using AI but most are doing it recklessly.</li>
</ul>
<p><strong>The workflow shift is fundamental:</strong></p>
<ul>
<li><strong>Old</strong>: Write code → Review → Ship</li>
<li><strong>New</strong>: Prompt → Generate → Verify → Constrain → Observe → Ship → Verify again</li>
</ul>
<p>AI accelerates the middle. Engineering discipline protects the edges.</p>
<h2>The New Reality: Assumptions for AIxc World</h2>
<p>As of January 2026, I&#39;m operating under these assumptions. If they shift, I&#39;ll adapt my guardrails. The principle remains: <strong>verify first, ship second.</strong></p>
<ol>
<li><strong>AI models will keep improving at coding tasks, but remain unreliable on context-heavy decisions.</strong> The gap between &quot;write a function&quot; and &quot;understand our business&quot; won&#39;t close soon.</li>
<li><strong>Doc-driven monetization is dying.</strong> Tailwind is the canary in the coal mine for every devtools company. Expect consolidation, sponsorship models (like Google AI Studio and Vercel stepping in for Tailwind), and direct-to-AI licensing deals.</li>
<li><strong>Regulatory pressure around AI liability is rising.</strong> Owning outcomes becomes a legal necessity, not just a best practice. Verification paper trails will be your defense.</li>
<li><strong>Economic constraints demand smaller teams ship bigger systems.</strong> Efficiency isn&#39;t a competitive advantage anymore—it&#39;s survival. The companies that figure out verification-first workflows will outcompete those that don&#39;t.</li>
</ol>
<h2>Your Verification-First Blueprint: 15 Steps to AI-Native Engineering</h2>
<p>This isn&#39;t theory. Here&#39;s the exact checklist I use on every project:</p>
<ol>
<li><strong>Version-control your prompts</strong> in the same repo as your code</li>
<li><strong>Run static analysis</strong> (type checkers, linters, security scanners) on <em>every</em> AI-generated diff</li>
<li><strong>Write property tests</strong> for functions with clear invariants; use <code>pytest-hypothesis</code> or similar</li>
<li><strong>Flag AI-generated code paths</strong> in your telemetry (<code>span.set_attribute(&quot;ai_generated&quot;, True)</code>)</li>
<li><strong>Require feature flags</strong> for any AI-generated feature touching production data</li>
<li><strong>Shadow-deploy</strong> AI changes to compare behavior against existing code before traffic cutover</li>
<li><strong>Log prompt inputs/outputs</strong> for debugging (but scrub secrets)</li>
<li><strong>Set token budgets and timeouts</strong> in your AI orchestrator to prevent runaway costs</li>
<li><strong>Review AI code for <em>failure modes</em></strong>, not style: What happens if this API call fails? What if data is malformed?</li>
<li><strong>Build a rollback plan</strong> before merging AI-generated migrations or infrastructure changes</li>
<li><strong>Measure AI-specific metrics</strong>: time-to-first-output, defect rate, revert rate, cost per feature</li>
<li><strong>Document the <em>why</em></strong>: If AI generated it, write a comment explaining the business rule it implements</li>
<li><strong>Train your team</strong> on prompt injection attacks and AI-specific security risks</li>
<li><strong>Use canned prompts</strong> for common tasks to reduce variance and improve reproducibility</li>
<li><strong>Schedule regular &quot;AI debt&quot; reviews</strong>: Refactor AI-generated code that has drifted from your evolving standards</li>
</ol>
<p>This checklist is your windmill. Each step converts AI&#39;s chaotic energy into reliable, sustainable power.</p>
<h2>The Only Viable Response</h2>
<p>The Tailwind drama and Anthropic headlines aren&#39;t warnings to panic. They&#39;re signals to <strong>level up</strong>.</p>
<p>AI won&#39;t replace software engineers in 2026, but it will create a bifurcation:</p>
<ul>
<li><strong>Downward</strong>: Engineers who treat AI as autocomplete, skip verification, and ship AI-generated code like they wrote it themselves. They&#39;ll move fast and break things—then get broken by incidents and layoffs.</li>
<li><strong>Upward</strong>: Engineers who treat AI as a junior teammate with infinite stamina. They&#39;ll build verification pipelines, specify constraints, observe production, and own outcomes. They&#39;ll ship faster with <em>higher</em> confidence.</li>
</ul>
<p>The game has changed. The player on the other side is hidden, but the rules are clear: <strong>Generate fast, verify faster, and own the outcome.</strong> The engineers who win will be the ones who build systems so robust that AI becomes a force multiplier, not a liability.</p>
<p>My advice? <strong>Don&#39;t fear the AI that writes code. Fear the day your competitors verify theirs better than you do.</strong></p>
<p>The meteor struck Tailwind. It&#39;s headed for every knowledge-based industry next. Build walls, and you&#39;ll be buried. Build windmills, and you&#39;ll harness the hurricane.</p>
<hr>
<h2>The Work Is Just Beginning</h2>
<p>If you&#39;re building AI-assisted products and want to harden your verification pipeline, I share deeper dives, real implementation templates, and war stories from the trenches at <a href="https://yabasha.dev/blog">yabasha.dev/blog</a>. Based in Amman, Jordan, I work with small teams and solo founders who want to <strong>ship fast without breaking things</strong>.</p>
<p>The winds aren&#39;t coming. They&#39;re here. Let&#39;s build something that doesn&#39;t explode.</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/9a978b3ed319aca21dd6bd69a3c95521/01KFTJ64NQ7C6JT3Z7HF2A35EW.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Leader Quick Note: How to Keep Your Team Survived in AI Era]]></title>
            <link>https://yabasha.dev/blog/leader-quick-note-how-to-keep-your-team-survived-in-ai-era</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/leader-quick-note-how-to-keep-your-team-survived-in-ai-era</guid>
            <pubDate>Mon, 19 Jan 2026 19:00:00 GMT</pubDate>
            <description><![CDATA[Discover key strategies to keep your team ahead in the AI era, focusing on AI-native development and innovative product features.]]></description>
            <content:encoded><![CDATA[<p>The pace of change in AI is brutal, but here&#39;s what stings worse: seeing even FAANG companies lagging behind the frontier for no good reason. You are guaranteed to lose if you fall behind. This isn&#39;t about keeping up with the hype—it&#39;s about avoiding unforced errors that leave <strong>money</strong> and <strong>productivity</strong> on the table.</p>
<h2>For Your Team: Building AI-Native Development</h2>
<p><strong>Give engineers their choice of AI harnesses.</strong> Every engineer should have access to coding agents with their preferred models and interfaces: Claude Code, Cursor, Devin, leveraging both closed and open models. Hearing that Meta engineers are forced to use Llama 4? That&#39;s a self-imposed limitation. Opus 4.5 is the baseline now.</p>
<p><strong>Tool access is non-negotiable.</strong> Give your agents complete access to all dev tooling: Linear, GitHub, Datadog, Sentry, and any internal tools. If your agents are held back by lack of context, that&#39;s <em>your</em> failure, not theirs.</p>
<p><strong>Invest in codebase-specific agent documentation.</strong> Stop complaining that agents &quot;don&#39;t do X well.&quot; If that&#39;s your issue, try better prompting, <a href="https://t.co/SOjpn47yxo">agents.md</a>, linting, and code rules. Tell them exactly how you want things done. Every manual edit you make is an opportunity for <a href="https://t.co/S1ZvtYQwta">agent.md</a> improvement.</p>
<p><strong>Build robust background agent infrastructure.</strong> Get a full development stack running in VMs or sandboxes. Yes, it&#39;s hard to set up, but it pays dividends when engineers can run multiple agents in parallel. Code review will be your next bottleneck—get ahead of it.</p>
<p><strong>Solve security properly.</strong> Stop being risk-averse and do what it takes to unblock tool access. Security theater that prevents AI adoption is an unforced error.</p>
<h2>For Your Product: Shipping AI-Powered Features</h2>
<p><strong>Always use the latest generation models.</strong> Move features off last-gen models immediately unless robust evals prove otherwise. This requires updating every 1-2 weeks. As <a href="https://x.com/jaredpalmer">jaredpalmer</a> noted, GitHub Copilot mobile still offers code review with GPT-4.1 and Sonnet 3.5. You&#39;re leaving money on the table by staying on Sonnet 4 or GPT-4o.</p>
<p><strong>Use embedding semantic search everywhere.</strong> Any general embedding model will outperform Levenshtein distance and fuzzy heuristics. This is table stakes.</p>
<p><strong>Leave no form unfilled.</strong> Use structured outputs and available user context for best-effort pre-filling. Reduce friction ruthlessly.</p>
<p><strong>Allow unstructured inputs on all surfaces.</strong> Forms are dead. Every product surface must accept freeform text and documents.</p>
<p><strong>Custom fine-tuning is dead.</strong> Stop wasting 8 weeks on fine-tuning. The frontier moves too fast, costs are dropping too quickly, and better prompting gets you 90% of the way there. This will only become more true as instruction-following improves.</p>
<p><strong>Build evals for rapid model-upgrade decisions.</strong> They don&#39;t need to be perfect—just good enough to compare models on a Pareto cost vs. performance plot. Most decisions become obvious.</p>
<p><strong>Empower all engineers to build with AI.</strong> Build primitives to call models from any codebase: structured output endpoints, semantic similarity APIs, sandboxed code execution. Make AI a first-class building block.</p>
<p><strong>What else belongs in this quick note?</strong> The frontier is moving—don&#39;t let unforced errors hold you back.</p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/f372d92b6202d26d7f82ca66c84d4284/01KFBSTH1J3PJYBXZC9MP7S0AP.jpg" length="0" type="image/jpg"/>
        </item>
        <item>
            <title><![CDATA[Laravel 12 and Next.js 16 in One Bun Monorepo: What yabasha.dev Runs]]></title>
            <link>https://yabasha.dev/blog/a-production-ready-laravel-nextjs-architecture-blueprint</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/a-production-ready-laravel-nextjs-architecture-blueprint</guid>
            <pubDate>Mon, 05 Jan 2026 08:10:14 GMT</pubDate>
            <description><![CDATA[What yabasha.dev actually runs — one Bun workspace, Laravel 12 behind Next.js 16, a hashed bearer token for every write, and cache purges that never retry.]]></description>
            <content:encoded><![CDATA[<p><em>Every claim below is checkable against the repo. I removed the ones that were not.</em></p>
<p>I published a version of this post in January 2026. Rereading it this week, I found it described
PostgreSQL, Sentry, Vercel Pro and Laravel Cloud. This site runs none of them. It also walked
through an <code>app/Events/ContentInvalidated.php</code> that has never existed here, and a queue job with
five retries and exponential backoff that, in the real repo, has no retries at all.</p>
<p>So this is the honest version. Every file path, version number and behaviour below comes from the
running code.</p>
<!-- lift --> A monorepo is not an architecture. The architecture is the boundary between the two apps, and on this site that boundary is one hashed token wide.

<h2>What does yabasha.dev actually run?</h2>
<p>One repository, three workspaces, built by <a href="https://turborepo.com/docs">Turborepo</a> over
<a href="https://bun.sh/docs/install/workspaces">Bun workspaces</a>.</p>
<table>
<thead>
<tr>
<th>Piece</th>
<th>What it is</th>
<th>Where it lives</th>
</tr>
</thead>
<tbody><tr>
<td>API and admin</td>
<td>Laravel 12 on PHP 8.4, Filament 4 admin</td>
<td><code>apps/backend</code></td>
</tr>
<tr>
<td>Frontend</td>
<td>Next.js 16, App Router, React 19</td>
<td><code>apps/web</code></td>
</tr>
<tr>
<td>Shared components</td>
<td><code>@yabasha/ui</code>, shadcn/ui in the new-york style</td>
<td><code>packages/ui</code></td>
</tr>
<tr>
<td>Database</td>
<td>MySQL in production</td>
<td><code>apps/backend/.env</code></td>
</tr>
<tr>
<td>Queue</td>
<td>Horizon on Redis</td>
<td><code>apps/backend/config/horizon.php</code></td>
</tr>
<tr>
<td>Process manager</td>
<td>PM2, one VPS</td>
<td><code>/var/www/yabasha.dev</code></td>
</tr>
</tbody></table>
<p>The root <code>package.json</code> declares <code>workspaces: [&quot;apps/*&quot;, &quot;packages/*&quot;]</code> and pins
<code>packageManager: &quot;bun@1.3.0&quot;</code>. <code>turbo.json</code> defines four tasks. <code>dev</code> is marked <code>persistent</code> and
uncached. <code>build</code>, <code>lint</code> and <code>typecheck</code> each declare <code>dependsOn: [&quot;^build&quot;]</code> style ordering so
<code>packages/ui</code> compiles before <code>apps/web</code> consumes it.</p>
<p>There is no Kubernetes, no edge network and no second cloud account. One box runs everything.</p>
<h2>Why one repo instead of two?</h2>
<p>Because the two apps share a contract that changes often, and nothing else enforces it.</p>
<p>When I add a field to a Laravel API resource, the TypeScript type in <code>apps/web/src/lib/api.ts</code> that
reads it has to change in the same breath. In two repositories that is two pull requests, and the
window between them is where a production 500 lives. In one repository it is one commit, and CI
either passes on both apps or fails.</p>
<p>That is the whole argument. It is not about tooling elegance.</p>
<!-- lift --> Split the repo when two teams need different release cadences. Until then, the second repo only buys you a coordination problem you did not have.

<p>The cost is real and worth naming. A monorepo makes it easy to reach across the boundary when you
should not. <code>packages/ui</code> may not import from <code>apps/web</code>, and nothing but review enforces that.</p>
<h2>How do the two apps talk to each other?</h2>
<p>Not the way most Laravel and Next.js guides describe. There is no Sanctum cookie handshake between
the frontend and the API, because the frontend never acts as a logged-in user.</p>
<table>
<thead>
<tr>
<th>Path</th>
<th>Who calls it</th>
<th>What it needs</th>
</tr>
</thead>
<tbody><tr>
<td>Public reads, <code>GET /api/v1/*</code></td>
<td>The Next.js server, during ISR</td>
<td>Nothing. Rate limited at <code>throttle:300,1</code> per IP</td>
</tr>
<tr>
<td>Unpublished content</td>
<td>Next.js, in preview mode</td>
<td>An <code>X-Preview-Secret</code> header</td>
</tr>
<tr>
<td>Writes, <code>POST /api/v1/*</code></td>
<td>My own tooling, from anywhere</td>
<td>A bearer ManageToken</td>
</tr>
<tr>
<td>Filament admin</td>
<td>Me, in a browser</td>
<td>A session, gated by <code>User::canAccessPanel()</code></td>
</tr>
</tbody></table>
<p>The shape of those endpoints follows the conventions I wrote up in
<a href="/blog/building-restful-apis-with-laravel">building RESTful APIs with Laravel</a>. What changed since
then is the authentication, which is where most of the thinking went.</p>
<p>A ManageToken is generated once, shown once, and stored as a SHA-256 hash. The plaintext is never
written to the database. Each token carries a subset of 15 abilities from the <code>TokenAbility</code> enum,
from <code>posts.write</code> through <code>newsletter.subscribe</code>, and every use writes an audit row.</p>
<p>The <code>AuthenticateManageOrSanctum</code> middleware has one rule worth stating plainly. A bearer token
must resolve to an <strong>active</strong> ManageToken or the request gets a 401. There is no anonymous
fallback, so a revoked token fails closed rather than quietly downgrading to public access.</p>
<h2>How does a published post reach the frontend?</h2>
<p><code>PostObserver::saved()</code> dispatches <code>RevalidateFrontendJob</code>, which POSTs a cache tag and a URL list
to <code>/api/revalidate</code> on the Next.js side with an <code>X-Revalidate-Token</code> header. The route calls
<code>revalidateTag()</code> and pings IndexNow.</p>
<p>I wrote that protocol up in detail, including the version I would build where staleness costs money,
in <a href="/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr">the Cache Handshake</a>. Two
details from it matter here.</p>
<p>The route accepts 12 tag names and rejects everything else. A typo does not purge a different tag,
it purges nothing, and the 400 is only reported. Separately, the <code>urls</code> array in the request body
does <strong>not</strong> purge anything. It feeds the IndexNow ping only. The <strong>tag</strong> is what clears the cache.</p>
<p>Behind all of it sits time-based revalidation, set per fetch in <code>apps/web/src/lib/api.ts</code>:</p>
<table>
<thead>
<tr>
<th>Content</th>
<th>Seconds</th>
</tr>
</thead>
<tbody><tr>
<td>Blog post detail, project detail, media detail</td>
<td>180</td>
</tr>
<tr>
<td>Blog list, categories in context, <code>/now</code></td>
<td>300</td>
</tr>
<tr>
<td>Category and tag lists, project list</td>
<td>600</td>
</tr>
<tr>
<td>Sitemap, speaking pages</td>
<td>3600</td>
</tr>
</tbody></table>
<h2>What happens when a cache purge fails?</h2>
<p>Nothing retries. This is the part the old version of this post got most wrong.</p>
<p><code>RevalidateFrontendJob</code> sets no <code>$tries</code> and no <code>$backoff</code>. It wraps an
<code>Http::timeout(8)</code> call in a try/catch, and on failure it calls <code>report()</code> and returns. The job is
marked complete. Horizon shows no failure, because there was none.</p>
<!-- lift --> The safety net is not the retry. There is no retry. The safety net is that every cached fetch also expires on a timer, so the worst case is a stale page for 180 seconds, not forever.

<p>That is a deliberate trade for a portfolio, and I would not ship it to a client. The honest framing
is that time-based ISR does the work and the purge is an optimisation that usually fires. If you
need the acknowledged, retried version, it is in the Cache Handshake post linked above.</p>
<p>One consequence catches me every time. <code>GeneratePostSeoJob</code> writes with <code>updateQuietly()</code>, which
skips the observer, so regenerating SEO fields fires no purge at all. The page catches up on its own
timer. A <code>curl</code> immediately after still shows the old value.</p>
<h2>Why does the test suite run on SQLite when production runs MySQL?</h2>
<p>Because it was fast to set up, and I have not fixed it.</p>
<p><code>apps/backend/phpunit.xml</code> pins <code>DB_CONNECTION=sqlite</code> and <code>DB_DATABASE=:memory:</code>. The CI job
installs <code>pdo_sqlite</code> only. The suite is healthy by its own measure. As of today it runs 478 passing
tests and 1 skipped, through Pest 4.</p>
<p>But every MySQL-specific behaviour in this codebase is untested everywhere it runs. A correlated
subquery that SQLite tolerates and MySQL rejects would pass CI and fail in production. I know of no
such bug today. I also have no test that would find one.</p>
<p>I have no plan to fix it this quarter, which is the useful admission. Naming an untested surface is
worth more than pretending it is covered.</p>
<h2>What does a deploy look like?</h2>
<p>Two commands and one trap.</p>
<p>The Laravel side runs <code>composer install --no-dev</code>, which has bitten me. A class that only reaches
the tree through a <code>require-dev</code> package passes CI, where dev dependencies are installed, and fatals
on the server, where they are not. That is a whole category of bug with no test that can catch it,
because the code runs in the one place the suite never does.</p>
<p>The Next.js side runs a production build and then:</p>
<pre><code class="language-bash">pm2 restart portfolio-yabasha --update-env
</code></pre>
<p><code>--update-env</code> is not optional. A plain <code>pm2 restart</code> inherits PM2&#39;s cached environment and silently
ignores every value you just changed in <code>.env.local</code>. I have debugged the same &quot;my new env var does
nothing&quot; problem more than once before remembering.</p>
<p>The box also needed 4 GB of swap added before <code>next build</code> would finish. Without it the build is
OOM-killed with no useful message.</p>
<h2>What would I change?</h2>
<table>
<thead>
<tr>
<th>Gap</th>
<th>Why it is still open</th>
</tr>
</thead>
<tbody><tr>
<td>No retry on cache purges</td>
<td>Time-based ISR covers it for a portfolio. It would not cover a shop.</td>
</tr>
<tr>
<td>Tests run on SQLite, production on MySQL</td>
<td>Needs a MySQL service in CI. An afternoon, never the urgent afternoon.</td>
</tr>
<tr>
<td>One box, no redundancy</td>
<td>A second box doubles cost to remove an outage I have not had.</td>
</tr>
<tr>
<td>No structured trace across the two apps</td>
<td>I removed Sentry and log JSON instead. Correlating a publish to a purge is still grep.</td>
</tr>
</tbody></table>
<p>The pattern in that table is the honest one. Every gap is a deliberate trade against a failure I
have not been hit by yet, and I would make a different call for a client system.</p>
<h2>Related</h2>
<p>I use this stack to run an AI pipeline over my own content, which I describe in
<a href="/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs">how I built an AI agent for my portfolio</a>.
The Laravel queue concepts above come straight from the
<a href="https://laravel.com/docs/12.x/horizon">Horizon documentation</a>, and the caching model on the Next.js
side is the one described in the
<a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag">revalidateTag reference</a>.</p>
<h2>Tested with</h2>
<p>Versions read from <code>composer.json</code> and <code>package.json</code> on 2026-09-19.</p>
<ul>
<li>Laravel 12.69, PHP 8.4</li>
<li>Laravel Horizon 5.48, Sanctum 4.3</li>
<li>Filament 4</li>
<li>Next.js 16.3, React 19.2</li>
<li>Bun 1.3.0, Turborepo 2.10</li>
<li>MySQL in production, SQLite in the test suite</li>
</ul>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/2811bb673bf5f0c475d95fa7e39623a7/01KE6JW3G5WH9PH3HDR7D2HZ4V.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[Invalidate Next.js 16 ISR from Laravel: The Cache Handshake]]></title>
            <link>https://yabasha.dev/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr</guid>
            <pubDate>Mon, 22 Dec 2025 20:59:00 GMT</pubDate>
            <description><![CDATA[What yabasha.dev actually runs — a Horizon job, a timing-safe token and revalidateTag — and the Cache Handshake: the acknowledged, retried, circuit-broken version for when a stale page costs money.]]></description>
            <content:encoded><![CDATA[<p><em>The version I run, the version I&#39;d sell, and the line between them.</em></p>
<p>I spend weeks architecting client systems with care. For my own portfolio I was invalidating
cache like a junior developer: <code>php artisan cache:clear</code> on a cron job, and hope. The irony wasn&#39;t
lost on me.</p>
<p><a href="https://yabasha.dev">Yabasha.dev</a> is a Laravel 12 API with a Filament admin behind a Next.js 16
frontend on ISR. Making the two talk without me playing telephone operator every time I published
turned into two answers, not one: the simple version this site runs, and the Cache Handshake — the
version I&#39;d ship where a stale page costs money.</p>
<!-- lift --> Cross-system cache invalidation is a distributed transaction. Fire-and-forget is fine right up until a stale page has a price.

<h2>Why did my Laravel + Next.js cache go stale for hours?</h2>
<p>A cron job running <code>cache:clear</code> invalidates on a schedule, not on a change. When the schedule
missed a publish, the old page served until the next tick. Then the first webhook I wrote had the
opposite problem — it fired instantly and nobody checked whether it landed. Four failure modes, one
root cause: no signal from the system that knows to the system that renders.</p>
<table>
<thead>
<tr>
<th>Failure mode</th>
<th>What happened</th>
<th>Why the naive approach can&#39;t fix it</th>
</tr>
</thead>
<tbody><tr>
<td>Schedule miss</td>
<td>Cron cleared on the hour; the post went out at :05</td>
<td>Time-based purging can&#39;t know a change happened</td>
</tr>
<tr>
<td>No acknowledgement</td>
<td>Webhook returned 200 on receipt; revalidation may still fail</td>
<td>HTTP 200 says &quot;received&quot;, not &quot;regenerated&quot;</td>
</tr>
<tr>
<td>Deploy window</td>
<td>Endpoint down during <code>next build</code>; the call failed</td>
<td>Nothing queued the intent</td>
</tr>
<tr>
<td>Cascade</td>
<td>A category change must hit the category page, every post in it, <code>/blog</code>, the feed and the sitemap</td>
<td>One URL per webhook can&#39;t express a dependency graph</td>
</tr>
</tbody></table>
<p>I was spending more time verifying cache state than writing. Not engineering — wishful thinking.</p>
<h2>What is ISR in Next.js 16, and why can&#39;t Laravel see inside it?</h2>
<p>Incremental Static Regeneration serves a statically rendered page and regenerates it either on a
timer (<code>revalidate: 180</code>) or on demand, when code inside the Next.js runtime calls
<code>revalidatePath()</code> or <code>revalidateTag()</code>. The cache lives in Next.js. Laravel has no native way in —
the only door is an HTTP route you write yourself.</p>
<p>That door is the design problem. Next.js&#39;s <a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag"><code>revalidateTag</code>
documentation</a> explains how to
purge a tag; it says nothing about the caller being another service on another box that may be
down, mid-deploy, or sending the same request twice. Time-based revalidation papers over that with
a window. For a portfolio that publishes twice a month a 3-minute window is fine. For the same
architecture under a client&#39;s newsroom it isn&#39;t — and this site exists to demonstrate the
architecture I sell, not the shortcut.</p>
<h2>ISR, SSR or SSG for a CMS-backed site?</h2>
<p>ISR, when content changes on a human schedule and reads dwarf writes. The table is the argument:</p>
<table>
<thead>
<tr>
<th>Strategy</th>
<th>Render happens</th>
<th>Fresh after a CMS edit</th>
<th>Origin load</th>
<th>Fits a Laravel-backed blog?</th>
</tr>
</thead>
<tbody><tr>
<td><strong>SSG</strong></td>
<td>At build</td>
<td>Only after a full rebuild</td>
<td>None</td>
<td>No — every publish is a deploy</td>
</tr>
<tr>
<td><strong>SSR</strong></td>
<td>Every request</td>
<td>Immediately</td>
<td>Every request hits the API</td>
<td>Overkill for read-heavy content</td>
</tr>
<tr>
<td><strong>ISR, time-based</strong></td>
<td>At build + every N seconds</td>
<td>Up to N seconds late</td>
<td>Low, periodic</td>
<td>Yes, if N is small and staleness is tolerable</td>
</tr>
<tr>
<td><strong>ISR, on-demand</strong></td>
<td>At build + when told</td>
<td>Next request after the signal</td>
<td>Lowest</td>
<td><strong>Yes — if the signal is reliable</strong></td>
</tr>
</tbody></table>
<p>On-demand ISR moves the whole problem into one phrase: <em>if the signal is reliable</em>. Everything
below is about how reliable it has to be. For the layer above this — how the Laravel 12 API,
Filament 4 and the Next.js 16 app sit in one Bun monorepo — see
<a href="https://yabasha.dev/blog/a-production-ready-laravel-nextjs-architecture-blueprint">Laravel 12 + Next.js 16 Monorepo Architecture: A Production Blueprint</a>.</p>
<h2>How does yabasha.dev invalidate Next.js ISR from Laravel today?</h2>
<p>yabasha.dev invalidates Next.js ISR with one queued job. <code>PostObserver::saved()</code> dispatches
<code>RevalidateFrontendJob</code> when a published post changes; the job POSTs <code>{ tag: &quot;posts&quot;, urls: [...] }</code>
to <code>/api/revalidate</code> with an <code>X-Revalidate-Token</code> header and an 8-second timeout. Next.js verifies
the token, calls <code>revalidateTag(&quot;posts&quot;)</code>, and pings IndexNow with the URLs. <a href="https://laravel.com/docs/12.x/horizon">Laravel
Horizon</a> retries a failed job up to 3 times. That&#39;s the whole system.</p>
<p>The URL list is the dependency graph, built where the change happens:</p>
<pre><code class="language-php">// app/Observers/PostObserver.php
protected function dispatchFrontendRevalidation(Post $post): void
{
    $urls = [&#39;/blog&#39;, &#39;/feed.xml&#39;, &#39;/sitemap.xml&#39;, &#39;/blog/&#39;.$post-&gt;slug];

    if ($post-&gt;wasChanged(&#39;slug&#39;)) {
        $urls[] = &#39;/blog/&#39;.$post-&gt;getOriginal(&#39;slug&#39;);
    }

    RevalidateFrontendJob::dispatch(&#39;posts&#39;, array_values(array_unique($urls)));
}
</code></pre>
<pre><code class="language-php">// app/Jobs/RevalidateFrontendJob.php
public function handle(): void
{
    $token = config(&#39;services.frontend.revalidation_token&#39;);
    if (empty($token)) {
        return; // dormant in environments without a frontend
    }

    $response = Http::timeout(8)
        -&gt;withHeaders([&#39;X-Revalidate-Token&#39; =&gt; $token])
        -&gt;acceptJson()-&gt;asJson()
        -&gt;post(rtrim(config(&#39;app.frontend_url&#39;), &#39;/&#39;).&#39;/api/revalidate&#39;, [
            &#39;tag&#39;  =&gt; $this-&gt;tag,
            &#39;urls&#39; =&gt; $this-&gt;urls,
        ]);

    if (! $response-&gt;successful()) {
        report(new \RuntimeException(&quot;Frontend revalidate failed: HTTP {$response-&gt;status()}&quot;));
    }
}
</code></pre>
<pre><code class="language-ts">// apps/web/src/app/api/revalidate/route.ts (trimmed)
const token = request.headers.get(&quot;x-revalidate-token&quot;);
if (!verifyPasswordTiming(token, env.REVALIDATION_TOKEN)) {
  return NextResponse.json({ message: &quot;Invalid token&quot; }, { status: 401 });
}

if (!ALLOWED_TAGS.has(tag)) {
  return NextResponse.json({ message: `Tag &#39;${tag}&#39; is not allowed` }, { status: 400 });
}

revalidateTag(tag, &quot;max&quot;);
after(() =&gt; pingIndexNow(parsed.urls)); // Bing, Yandex, Seznam, Naver, Yep

return NextResponse.json({ revalidated: true, tag, now: Date.now() });
</code></pre>
<p>Every fetch in <code>lib/api.ts</code> carries <code>tags: [&quot;posts&quot;]</code> and a time-based <code>revalidate</code> of 180 seconds
on a post and 300 on the listing. Purging the tag makes the next request regenerate; the timer is
the safety net if the purge never arrives. The same fetch layer feeds the assistant in
<a href="https://yabasha.dev/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs">How I Built an AI Agent for my Portfolio using Laravel &amp; Next.js</a>,
so a stale cache would mean a stale answer.</p>
<h2>How do you secure the Next.js revalidation endpoint?</h2>
<p>Secure <code>/api/revalidate</code> with a secret in a <strong>header</strong>, compared in constant time, plus an
allowlist of what the caller may purge. On yabasha.dev the token travels as <code>X-Revalidate-Token</code>;
<code>verifyPasswordTiming()</code> compares it without leaking length or prefix through timing; a request
that puts <code>?secret=</code> in the query string gets a <strong>410</strong>, because query strings land in access logs.
The tag must be one of 12 allowed names, and the URL list is capped at 50 entries that must start
with <code>/</code>.</p>
<p>That&#39;s a static shared secret, and I&#39;ll name the weakness: it lives in two <code>.env</code> files and rotates
only when a human remembers. It is adequate here because the worst a leaked token can do is force
a cache regeneration — expensive, not dangerous. The Handshake below replaces it with a short-lived
signed JWT, which is the right call the moment the endpoint can do anything more than purge.</p>
<p>The anti-pattern to avoid is the one I started with: <code>if (secret === process.env.SECRET)</code>. It&#39;s
timing-leaky, it&#39;s usually in a query string, and it has no allowlist, so a leaked token purges
everything.</p>
<h2>What happens when revalidation fails today?</h2>
<p>When the webhook fails on yabasha.dev, Horizon retries the job up to 3 times, the failure is
reported to the log, and the 180-second time-based revalidation on the post fetch quietly repairs
the page on the next tick. There is no acknowledgement and no callback. Laravel never learns
whether Next.js regenerated anything — it learns only whether the HTTP call returned 2xx.</p>
<table>
<thead>
<tr>
<th>Situation</th>
<th>yabasha.dev today</th>
<th>Cost</th>
</tr>
</thead>
<tbody><tr>
<td>Next.js 5xx</td>
<td>Horizon retry ×3, then <code>report()</code></td>
<td>Up to 180 s stale on the post, 300 s on the listing</td>
</tr>
<tr>
<td>Next.js down during deploy</td>
<td>Same; the third failure is logged</td>
<td>Same — the timer catches it</td>
</tr>
<tr>
<td>Two edits in quick succession</td>
<td>Two purges; both regenerate from the API</td>
<td>None — purges are idempotent</td>
</tr>
<tr>
<td>Token missing in env</td>
<td>Job returns early, silently</td>
<td>Everything waits for the timer</td>
</tr>
</tbody></table>
<p>For this site the cost column is acceptable: nobody loses money because a blog post is three
minutes late. Read that column again for a bank&#39;s rate page or a newsroom&#39;s homepage, and the
simple version stops being a shortcut and starts being a liability. That&#39;s the line where the
Handshake begins.</p>
<!-- lift --> A webhook that returns 200 has told you it was received, not that it worked. Those are different sentences.

<h2>When is the simple version not enough — and what does the Cache Handshake add?</h2>
<p>The Cache Handshake treats invalidation as a two-phase commit with Redis as referee. Laravel writes
the intent to a Redis list with a unique <code>revalidation_id</code>; a worker POSTs to Next.js with a signed
JWT; Next.js regenerates and <strong>calls Laravel back</strong> with the same id; only that callback marks the
record <code>completed</code>. Failures re-queue up to 3 times, and a circuit breaker opens after 10 failures
for 300 seconds so a dead deploy isn&#39;t hammered.</p>
<p>This is a design, not what the portfolio runs — the portfolio doesn&#39;t need it. It&#39;s what I&#39;d ship
where staleness has a price. The pieces:</p>
<pre><code class="language-php">// app/Listeners/QueueRevalidation.php — write the intent, never call Next.js directly
Redis::rpush(&#39;revalidation:queue&#39;, json_encode([
    &#39;revalidation_id&#39; =&gt; $event-&gt;revalidationId,
    &#39;slug&#39;            =&gt; $event-&gt;post-&gt;slug,
    &#39;dependencies&#39;    =&gt; [&#39;blog?category=&#39;.$event-&gt;post-&gt;category-&gt;slug, &#39;blog&#39;, &#39;&#39;],
]));

Redis::hmset(&quot;revalidation:{$event-&gt;revalidationId}:status&quot;, [
    &#39;state&#39; =&gt; &#39;pending&#39;, &#39;attempts&#39; =&gt; 0, &#39;created_at&#39; =&gt; now()-&gt;timestamp,
]);
</code></pre>
<pre><code class="language-php">// app/Jobs/ProcessRevalidation.php — signed request, bounded retries
$token = JWT::encode([&#39;sub&#39; =&gt; $id, &#39;exp&#39; =&gt; now()-&gt;addMinutes(5)-&gt;timestamp], $secret, &#39;HS256&#39;);

$response = Http::withToken($token)-&gt;post(&quot;{$nextjs}/api/revalidate&quot;, $payload);

if ($response-&gt;failed()) {
    $attempts &lt; 3
        ? Redis::rpush(&#39;revalidation:queue&#39;, $raw)                 // re-queue
        : Redis::hset(&quot;revalidation:{$id}:status&quot;, &#39;state&#39;, &#39;failed&#39;);
    return;
}

Redis::hset(&quot;revalidation:{$id}:status&quot;, &#39;state&#39;, &#39;acknowledged&#39;);
</code></pre>
<pre><code class="language-ts">// app/api/revalidate/route.ts — regenerate, then close the loop
revalidatePath(`/blog/${slug}`);
await Promise.all(dependencies.map((p: string) =&gt; revalidatePath(`/${p}`)));

await fetch(`${process.env.LARAVEL_URL}/api/revalidation/complete`, {
  method: &quot;POST&quot;,
  headers: { Authorization: `Bearer ${process.env.REVALIDATION_SECRET}` },
  body: JSON.stringify({ revalidation_id }),
});
</code></pre>
<pre><code class="language-php">// app/Services/RevalidationStrategy.php — not all paths deserve the same budget
return match (true) {
    $path === &#39;&#39;                    =&gt; [&#39;workers&#39; =&gt; 5, &#39;timeout&#39; =&gt; 10, &#39;retry&#39; =&gt; 5], // homepage
    str_starts_with($path, &#39;blog/&#39;) =&gt; [&#39;workers&#39; =&gt; 2, &#39;timeout&#39; =&gt; 30, &#39;retry&#39; =&gt; 3],
    default                         =&gt; [&#39;workers&#39; =&gt; 1, &#39;timeout&#39; =&gt; 60, &#39;retry&#39; =&gt; 2],
};
</code></pre>
<table>
<thead>
<tr>
<th>Concern</th>
<th>Simple version (live)</th>
<th>Cache Handshake (design)</th>
</tr>
</thead>
<tbody><tr>
<td>Knows it worked?</td>
<td>No — knows the call returned 2xx</td>
<td>Yes — callback flips <code>pending → acknowledged → completed</code></td>
</tr>
<tr>
<td>Auth</td>
<td>Static header token, timing-safe</td>
<td>HS256 JWT, 5-minute expiry, <code>sub</code> = revalidation id</td>
</tr>
<tr>
<td>Retry</td>
<td>Horizon ×3</td>
<td>×3 with re-queue, then <code>failed</code> + log</td>
</tr>
<tr>
<td>Runaway failure</td>
<td>Nothing stops it</td>
<td>Circuit opens after 10 failures for 300 s</td>
</tr>
<tr>
<td>Priority</td>
<td>All paths equal</td>
<td>Homepage 5 workers / 10 s; old post 1 worker / 60 s</td>
</tr>
<tr>
<td>Deploy windows</td>
<td>Timer catches it</td>
<td><code>APP_ENV=preview</code> dry-run logs payloads instead of sending</td>
</tr>
<tr>
<td>Moving parts</td>
<td>1 job, 1 route</td>
<td>Queue, worker, callback route, strategy, breaker</td>
</tr>
</tbody></table>
<!-- lift --> Not all revalidations are equal: a typo on the homepage and an edit to a two-year-old post should not share a retry budget.

<h2>Is there example code?</h2>
<p>The live version is four files in the yabasha.dev monorepo: <code>apps/backend/app/Observers/PostObserver.php</code>
(builds the URL list), <code>apps/backend/app/Jobs/RevalidateFrontendJob.php</code> (the call),
<code>apps/web/src/app/api/revalidate/route.ts</code> (token check, allowlist, <code>revalidateTag</code>, IndexNow) and
<code>apps/web/src/lib/api.ts</code> (the tagged fetches). The Handshake snippets above are the design in full;
the Laravel side is an event, a listener, a job and a strategy class, and the Next.js side is one
route plus one callback.</p>
<h2>What did fixing this actually change?</h2>
<table>
<thead>
<tr>
<th></th>
<th>Cron <code>cache:clear</code></th>
<th>Tagged revalidation (today)</th>
</tr>
</thead>
<tbody><tr>
<td>Publish → live</td>
<td>Next cron tick, or never if it missed</td>
<td>The next request after the purge — no waiting on a window</td>
</tr>
<tr>
<td>Deploy windows</td>
<td>Cleared cache into a half-deployed app</td>
<td>Job retried by Horizon; timer as the net</td>
</tr>
<tr>
<td>Cascades</td>
<td>Everything, every hour</td>
<td><code>/blog</code>, the feed, the sitemap and the post — nothing else</td>
</tr>
<tr>
<td>Search engines</td>
<td>Learned of changes on their own crawl</td>
<td>IndexNow pinged with the exact URLs</td>
</tr>
<tr>
<td>Where my attention goes</td>
<td>Cache state, weekly</td>
<td>Nowhere. It&#39;s boring, and boring infrastructure is good infrastructure</td>
</tr>
</tbody></table>
<p>The workflow now: write in Filament, publish, Laravel dispatches the job, Next.js purges the tag,
the next visitor gets the new page, and Bing already knows. I don&#39;t think about caching.</p>
<h2>The catch</h2>
<p>The Handshake is more machinery than a portfolio needs — that&#39;s both the point and the warning.
Ship the simple version until a stale page costs something, and know exactly which four properties
you&#39;re missing when it does: acknowledgement, bounded retries, a breaker, and priority. It&#39;s the
same discipline as the agent work in
<a href="https://yabasha.dev/blog/building-production-ai-agents-that-actually-work">Building Production AI Agents That Work: What Failed First</a>:
the demo and the production version differ in what happens when the call fails, not in what
happens when it succeeds.</p>
<p>So the question isn&#39;t whether your webhook works. It&#39;s this: when it fails at 03:00 during a
deploy, who finds out first — you, or your reader? 🎯</p>
<p><em>Building the same stack for a client? That&#39;s the architecture on
<a href="https://yabasha.dev/services/laravel-nextjs">/services/laravel-nextjs</a>.</em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/690a31ccc998b255c43425b428e9a87d/01KD3Y9CSBDMPW6YBA5HF7YQ6X.png" length="0" type="image/png"/>
        </item>
        <item>
            <title><![CDATA[How I Built an AI Agent for my Portfolio (Yabasha.dev) using Laravel & Next.js]]></title>
            <link>https://yabasha.dev/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs</link>
            <guid isPermaLink="false">https://yabasha.dev/blog/how-i-built-an-ai-agent-for-my-portfolio-yabashadev-using-laravel-nextjs</guid>
            <pubDate>Fri, 19 Dec 2025 13:45:00 GMT</pubDate>
            <description><![CDATA[One observer, one queued job, one model call, and a heuristic that runs when the model fails. This is the version that survived, and what I had to remove from the version I first wrote about.]]></description>
            <content:encoded><![CDATA[<p><em>This post is about this site, so every claim in it is checkable. I checked them all, and most of the original ones failed.</em></p>
<p>I published a version of this in early 2026 describing a five-step agent: fetch the draft, generate metadata, run an SEO pass on a second model, validate, publish. It had a benchmark table pricing five models per task, a cost ledger, and a Filament widget showing thirty days of spend.</p>
<p>Almost none of that is in the repository. There is no <code>FetchDraftJob</code>, no <code>GenerateMetadataJob</code>, no <code>AiCall</code> model, no <code>agent_runs</code> table and no spend widget. I described the system I meant to build.</p>
<!-- lift --> The agent I wrote about had five steps, per-task model routing and a cost ledger. The one that survived has one step, one model and a heuristic. The simpler one is the one that still runs.

<p>Here is the real thing.</p>
<h2>What actually triggers the agent?</h2>
<p>Saving a post. There is no review status, no queue button and no cron. An <a href="https://laravel.com/docs/12.x/eloquent#observers">Eloquent observer</a> runs after every save, checks seven columns, and dispatches one job if any of them is empty.</p>
<table>
<thead>
<tr>
<th>Column checked</th>
<th>Purpose</th>
</tr>
</thead>
<tbody><tr>
<td><code>meta_description</code></td>
<td>The search snippet</td>
</tr>
<tr>
<td><code>ai_summary_short</code></td>
<td>The TL;DR block, capped at 60 words</td>
</tr>
<tr>
<td><code>ai_summary_long</code></td>
<td>The longer abstract</td>
</tr>
<tr>
<td><code>ai_topics</code></td>
<td>Topic labels</td>
</tr>
<tr>
<td><code>meta_keywords</code></td>
<td>Keyword list</td>
</tr>
<tr>
<td><code>primary_keyword</code></td>
<td>The single target term</td>
</tr>
<tr>
<td><code>secondary_keywords</code></td>
<td>Supporting terms</td>
</tr>
</tbody></table>
<p>If every one of those is populated, nothing happens. That single condition is the whole scheduler, and it means the agent is idempotent by accident rather than by design: refill the columns and it stops firing.</p>
<!-- lift --> The trigger is not "the author asked for metadata". It is "a column is empty". That distinction is why clearing a column is how you ask for a rewrite.

<h2>What does the job actually do?</h2>
<p>One model call. <code>GeneratePostSeoJob</code> hands the title and body to <code>PostAiService::analyzeContent()</code>, which builds one prompt, sends one chat request through OpenRouter, and parses one JSON response covering all the fields above.</p>
<table>
<thead>
<tr>
<th>Stage</th>
<th>Class</th>
<th>What it owns</th>
</tr>
</thead>
<tbody><tr>
<td>Trigger</td>
<td><code>PostObserver::saved()</code></td>
<td>The seven-column check</td>
</tr>
<tr>
<td>Queue</td>
<td><code>GeneratePostSeoJob</code></td>
<td>The timeout, and which fields may be overwritten</td>
</tr>
<tr>
<td>Prompt and parsing</td>
<td><code>PostAiService</code></td>
<td>Building the prompt, decoding JSON, clamping output</td>
</tr>
<tr>
<td>Transport</td>
<td><code>OpenRouterService</code></td>
<td>Model selection, HTTP, failover</td>
</tr>
<tr>
<td>Catalogue</td>
<td><code>AiModel</code></td>
<td>Which model is active</td>
</tr>
</tbody></table>
<p>Five classes, one call. The earlier version of this post split that into five queued steps because five steps sound like an agent. They are not free: each boundary is a place the run can die halfway with some columns written and some not.</p>
<h2>Why is there only one active model?</h2>
<p>Because choosing per task means maintaining a routing table, and the routing table was the first thing I deleted. <code>AiModel::active()-&gt;first()</code> returns one row, and that row&#39;s <code>request_id</code> is the model for every call on the site.</p>
<p>The catalogue itself is synced from <a href="https://openrouter.ai/docs/quickstart">OpenRouter</a> and stores more than the id.</p>
<table>
<thead>
<tr>
<th>Field</th>
<th>What it holds</th>
</tr>
</thead>
<tbody><tr>
<td><code>request_id</code></td>
<td>The model string sent to OpenRouter</td>
</tr>
<tr>
<td><code>context_length</code></td>
<td>Token window</td>
</tr>
<tr>
<td><code>pricing_prompt</code>, <code>pricing_completion</code></td>
<td>The vendor&#39;s list price</td>
</tr>
<tr>
<td><code>estimated_speed</code>, <code>actual_speed</code></td>
<td>Expected against measured throughput</td>
</tr>
<tr>
<td><code>is_active</code></td>
<td>Exactly one row carries this</td>
</tr>
</tbody></table>
<p>Those price columns are the vendor&#39;s published rates, not what I spent. Nothing in this repo records the cost of a call. A post that claimed an average cost per post was quoting a number no table holds.</p>
<p>If you want the version with real cost attribution, it is the client system in <a href="/blog/cutting-llm-costs-60-a-production-rag-post-mortem-1">cutting LLM API costs 60%</a>, where every call was traced before anything was optimised.</p>
<h2>What happens when the active model disappears?</h2>
<p>OpenRouter retires models, and the failover for that is the one piece of real agent machinery here. It is one level deep and it fires on unavailability, not on a retry count.</p>
<p><code>OpenRouterService::chat()</code> sends the request with the active model. If the response fails and the error means the model is gone, it deactivates that row, notifies the admin, and resends with the environment fallback model. If that also fails, it logs and returns null.</p>
<!-- lift --> The failover is not a retry ladder across three vendors. It is one swap, triggered by the model being retired rather than by the request being slow.

<p>That is smaller than what I described before, and it covers the failure that has actually happened to me.</p>
<h2>What runs when the model returns nothing usable?</h2>
<p>A heuristic that calls no model at all. Three different failures collapse into the same path, and none of them reach the caller as an exception.</p>
<table>
<thead>
<tr>
<th>Failure</th>
<th>What <code>analyzeContent()</code> does</th>
</tr>
</thead>
<tbody><tr>
<td>Null response, including a missing API key</td>
<td>Logs a warning, returns <code>fallbackAnalysis()</code></td>
</tr>
<tr>
<td>Response does not decode as JSON</td>
<td>Logs the decode error, returns <code>fallbackAnalysis()</code></td>
</tr>
<tr>
<td>Anything throws while processing</td>
<td>Logs the exception, returns <code>fallbackAnalysis()</code></td>
</tr>
</tbody></table>
<p><code>fallbackAnalysis()</code> reads the post and produces a summary, an excerpt and a description by truncation and sentence splitting. It is unglamorous and it always answers.</p>
<p>There is a second fallback underneath that one. If the model answers but omits <code>meta_description</code>, the job calls <code>Post::deriveDescription()</code>, which takes the excerpt, or the body if there is no excerpt. That is why a good excerpt is the best insurance in the whole pipeline.</p>
<h2>Why is the model&#39;s output clamped before it is saved?</h2>
<p>Because the model overshoots the word budget often enough to lose the sentence worth quoting. <code>ai_summary_short</code> is capped at 60 words. The frontend truncates on sentence boundaries, so an over-long summary loses its final clause, and the final clause is usually the one carrying the outcome.</p>
<p>So the same clamp the heuristic uses is applied to the model&#39;s answer too. It is a two-line fix for a failure that is otherwise invisible: the field is populated, the type is right, and the most citable sentence is missing.</p>
<!-- lift --> Schema validation tells you the field arrived. It does not tell you the useful half of the sentence was cut off. That check has to be written by hand.

<h2>How long is the job allowed to take?</h2>
<p>Longer than the call it makes, which sounds obvious and was not. A single OpenRouter request is allowed 120 seconds. <a href="https://laravel.com/docs/12.x/horizon">Horizon</a> gives a worker 60.</p>
<table>
<thead>
<tr>
<th>Layer</th>
<th>Limit</th>
</tr>
</thead>
<tbody><tr>
<td>Horizon worker timeout</td>
<td>60 seconds</td>
</tr>
<tr>
<td>Horizon retries</td>
<td>3</td>
</tr>
<tr>
<td>Single OpenRouter call</td>
<td>120 seconds</td>
</tr>
<tr>
<td><code>GeneratePostSeoJob</code> timeout</td>
<td>150 seconds</td>
</tr>
</tbody></table>
<p>For a while the job declared no timeout of its own, so it inherited 60 and any post long enough to take the model past a minute was killed mid-request. It wrote nothing and produced no useful error. I wrote that failure up properly in <a href="/blog/building-production-ai-agents-that-actually-work">building production AI agents that actually work</a>; the short version is that the job now declares 150 and the comment above it says why.</p>
<p>The retries are Horizon&#39;s, not the job&#39;s. The published version of this post gave the job <code>$tries = 3</code> and a three-step backoff array. It has neither.</p>
<h2>How does the published page find out?</h2>
<p>Through a second job, not through this one. <code>PostObserver</code> also dispatches <code>RevalidateFrontendJob</code>, which posts a cache tag to the Next.js side, and the route calls <a href="https://nextjs.org/docs/app/api-reference/functions/revalidateTag"><code>revalidateTag</code></a>.</p>
<p>There is a catch worth knowing if you build this. The SEO job writes quietly, without firing the observer again, so regenerating metadata purges nothing. The page waits out its own timer instead. A <code>curl</code> immediately after a run still shows the old value, and I have talked myself into believing a column was dropped on exactly that evidence.</p>
<p>The full protocol, including the acknowledged version I would build if staleness cost money, is in <a href="/blog/the-cache-handshake-how-laravel-events-control-nextjs-16-isr">the cache handshake</a>.</p>
<h2>What does the admin side actually look like?</h2>
<p><a href="https://filamentphp.com/docs">Filament</a> 4, with five widgets, none of which are about AI spend.</p>
<table>
<thead>
<tr>
<th>Widget</th>
<th>What it shows</th>
</tr>
</thead>
<tbody><tr>
<td><code>ContentOverviewStats</code></td>
<td>Counts across content types</td>
</tr>
<tr>
<td><code>PublishingPipelineStats</code></td>
<td>What is drafted, scheduled and published</td>
</tr>
<tr>
<td><code>LatestPostsTable</code></td>
<td>Recent posts</td>
</tr>
<tr>
<td><code>NewsletterStats</code></td>
<td>Subscribers and issues</td>
</tr>
<tr>
<td><code>UpcomingSpeaksTable</code></td>
<td>Scheduled talks</td>
</tr>
</tbody></table>
<p>The real controls are artisan commands rather than admin pages: <code>posts:regenerate-summary</code> for one post&#39;s TL;DR, <code>posts:apply-revision</code> to write a whole post from a markdown file, and <code>posts:publish-scheduled</code>, which the scheduler runs every minute.</p>
<h2>Which versions is this running on?</h2>
<p>Read from the lockfiles on 2026-09-21, because the published version of this post was wrong about two of them.</p>
<table>
<thead>
<tr>
<th>Piece</th>
<th>Version</th>
</tr>
</thead>
<tbody><tr>
<td>Laravel</td>
<td>12.69, PHP 8.4</td>
</tr>
<tr>
<td>Filament</td>
<td>4.12, not 3</td>
</tr>
<tr>
<td>Next.js</td>
<td>16.3.4, not 15</td>
</tr>
<tr>
<td>Queue</td>
<td>Horizon on Redis</td>
</tr>
<tr>
<td>Model access</td>
<td>OpenRouter, one active model</td>
</tr>
</tbody></table>
<h2>What is still missing?</h2>
<p>Four things, and I am listing them because the previous version of this post claimed three of them were already built.</p>
<table>
<thead>
<tr>
<th>Gap</th>
<th>Why it is still open</th>
</tr>
</thead>
<tbody><tr>
<td>No per-call cost record</td>
<td>The catalogue holds list prices, not spend. I would need one table and one write per call</td>
</tr>
<tr>
<td>No evaluation set</td>
<td>Nothing checks whether a new model writes better metadata than the old one. Model swaps are judged by reading a few posts</td>
</tr>
<tr>
<td>No retry on a bad summary</td>
<td>A too-short or off-topic summary is fixed by clearing the column and saving again, by hand</td>
</tr>
<tr>
<td>No trace across the two apps</td>
<td>Correlating a publish to a purge is still grep. I removed error tracking and log JSON instead</td>
</tr>
</tbody></table>
<p>The architecture around all of this is described in <a href="/blog/a-production-ready-laravel-nextjs-architecture-blueprint">what yabasha.dev actually runs</a>, including the parts I would not ship to a client.</p>
<h2>What would I tell someone starting this?</h2>
<p>Build the trigger and one call. Nothing else, at first.</p>
<p>The instinct is to design the pipeline, because a pipeline is the interesting part. But a five-step pipeline has four places to die halfway, and on a personal site there is no traffic to justify that risk. One observer, one job, one model and a plain-code fallback covers the whole job of labelling your own writing.</p>
<!-- lift --> The hard part was never the agent. It is still thinking clearly enough to draft something worth publishing. Everything downstream of that is plumbing, and plumbing should be boring.

<p>The second mistake is never automating anything. That one I did avoid.</p>
<h2>Read from the tree on 2026-09-21</h2>
<ul>
<li><code>apps/web/package.json</code>, <code>apps/backend/composer.json</code> for every version above</li>
<li><code>PostObserver</code>, <code>GeneratePostSeoJob</code>, <code>PostAiService</code>, <code>OpenRouterService</code>, <code>AiModel</code></li>
<li><code>config/horizon.php</code> for the worker timeout and retry count</li>
<li><code>routes/console.php</code> for the scheduled commands</li>
<li><code>app/Filament/Widgets/</code> for the widget list</li>
</ul>
<p>The repository is private, so none of this is a link you can follow. The earlier version of this post said the pipeline was open source and invited readers to go and read it. That was wrong, and it is the correction I most wanted to make.</p>
<p><em>Questions: <a href="https://x.com/bayyash">@bayyash</a></em></p>
]]></content:encoded>
            <author>Bashar Ayyash</author>
            <enclosure url="https://api.yabasha.dev/media/73d1baea76a1ffd7378013e6c4f538cd/01KCVDJJGRFA2D8WNAN1H00YYC.png" length="0" type="image/png"/>
        </item>
    </channel>
</rss>