<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>The Builder Notebook</title><description>A record of how real systems get built — from a Big Tech engineer who also ships startups. Architecture, AI in production, hard-won lessons.</description><link>https://thebuildernotebook.com/</link><language>en-us</language><item><title>Note: hiring your first engineer when you&apos;re the founder-CTO</title><link>https://thebuildernotebook.com/blog/note-hiring-first-engineer-as-founder-cto/</link><guid isPermaLink="true">https://thebuildernotebook.com/blog/note-hiring-first-engineer-as-founder-cto/</guid><description>Three rules I wish I&apos;d had at Waffy on day zero. Short version: don&apos;t hire the engineer who can do everything; hire the one whose first 12 weeks de-risk the things you can&apos;t.</description><pubDate>Mon, 20 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;This is a note, not a deep-dive. Three rules I wish I&apos;d had before I hired the first engineer at Waffy. They&apos;re all I&apos;d tell another founder-CTO today.&lt;/p&gt;
&lt;h2&gt;1. Hire the de-risker, not the all-rounder&lt;/h2&gt;
&lt;p&gt;The instinct as a founder-CTO is to hire someone who can do everything you do — mobile, backend, ops, data — so you can stop wearing one of the hats. That&apos;s the wrong frame.&lt;/p&gt;
&lt;p&gt;The right frame: &lt;strong&gt;what&apos;s the thing in the next 12 weeks that, if it goes sideways, ends the company?&lt;/strong&gt; Hire the engineer who de-risks that thing.&lt;/p&gt;
&lt;p&gt;At Waffy, the answer was payments + PSP integration. We could have built mobile apps faster if I&apos;d hired a mobile-first generalist. We would not have survived the first regulatory ask without an engineer who&apos;d previously shipped to a payment processor. So I hired that engineer first, even though I knew it meant I&apos;d be writing iOS code at midnight for another four months.&lt;/p&gt;
&lt;p&gt;You&apos;re trading discomfort for survival. Take the trade.&lt;/p&gt;
&lt;h2&gt;2. The first hire sets the engineering culture forever&lt;/h2&gt;
&lt;p&gt;A culture is the set of behaviors the team forgives. Your first engineer&apos;s behaviors get forgiven by default, because there&apos;s no one else to compare them to. If they ship without tests, the team will ship without tests. If they comment thoroughly, the team will. If they&apos;re cavalier about production incidents, the team will be.&lt;/p&gt;
&lt;p&gt;You can&apos;t fix this later. You can document a culture-doc in Notion; the team will mirror the actual person sitting next to them, not the doc.&lt;/p&gt;
&lt;p&gt;So: interview for the behavior, not the resume. If they&apos;re a great engineer but their on-call instinct is &quot;wait until morning&quot;, you are buying a future where every team member&apos;s on-call instinct is &quot;wait until morning&quot;. You will not undo that with culture training.&lt;/p&gt;
&lt;h2&gt;3. Pay them like a co-founder, even if they&apos;re not one&lt;/h2&gt;
&lt;p&gt;The first engineer is doing a job no one else on the team understands. You can&apos;t peer-review their work yet. You can&apos;t replace them without losing 6 months of progress. They are &lt;em&gt;operationally indistinguishable&lt;/em&gt; from a co-founder, even if equity-wise they&apos;re at 0.5–2%.&lt;/p&gt;
&lt;p&gt;Pay them accordingly. Salary band: the high end of senior-engineer for your geography, not the middle. Equity: more than feels comfortable. Visibility: in every founder-only conversation that touches engineering. Trust: total, until they break it once.&lt;/p&gt;
&lt;p&gt;The math: a first engineer who&apos;s 70% the cost of &quot;fair&quot; leaves within 18 months. They leave because they can see — sooner than you can — that the relationship is asymmetric. You lose ~6 months of company progress recovering from that. The cost of paying them fairly is much, much less than the cost of replacing them.&lt;/p&gt;
&lt;h2&gt;The shape it looks like&lt;/h2&gt;
&lt;p&gt;If I were starting Waffy today, the job description would be one line:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Senior engineer who&apos;s shipped to a regulated payment system before. First engineer at a fintech. Comfortable being the only one on-call for ~6 months. Equity: 1.5–2%. Salary: top decile for Cairo or Madrid (your pick).&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&apos;s it. No &quot;10x&quot;, no &quot;rockstar&quot;, no &quot;full-stack&quot;. The person who reads that and says yes is the person you want.&lt;/p&gt;
&lt;p&gt;— Ahmed&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;em&gt;Notes are short. If you want the longer version — the architecture story, the actual numbers — that&apos;s in &lt;a href=&quot;/blog/build-notes&quot;&gt;Build Notes&lt;/a&gt;. The next deep-dive lands shortly.&lt;/em&gt;&lt;/p&gt;
</content:encoded><category>notes</category><category>hiring</category><category>cto</category><category>founders</category><category>early-stage</category></item><item><title>Most AI agents are just expensive for-loops</title><link>https://thebuildernotebook.com/blog/most-ai-agents-are-expensive-for-loops/</link><guid isPermaLink="true">https://thebuildernotebook.com/blog/most-ai-agents-are-expensive-for-loops/</guid><description>What actually works in production AI doesn&apos;t look like the demos. Three patterns from running AI in fintech, news intelligence, and operations — and the agentic pattern none of them use.</description><pubDate>Fri, 10 Jul 2026 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;If you spent any time on X in 2025 you watched the agent demos. The browser does its own shopping. The IDE writes its own PRs. The startup pitches itself. Confident chaining of tool calls, voiceover music, &quot;we let the LLM decide what to do next.&quot;&lt;/p&gt;
&lt;p&gt;Then you look at what ships to production.&lt;/p&gt;
&lt;p&gt;I run AI in three production systems right now: &lt;strong&gt;Waffy&apos;s fraud-detection subsystem&lt;/strong&gt; (fintech, MENA, ~0.4% post-AI fraud rate against a 12% baseline), &lt;strong&gt;Signalia&apos;s news-intelligence pipeline&lt;/strong&gt; (AI for venture capital, processing thousands of headlines a day), and parts of &lt;strong&gt;FounderYX&apos;s authoring loop&lt;/strong&gt;. None of them are &quot;agents&quot; in the X-discourse sense. They&apos;re orchestrated systems that put exactly enough determinism around the LLM to make it useful, and exactly enough LLM inside that determinism to make it intelligent.&lt;/p&gt;
&lt;p&gt;The X-discourse pattern — &quot;give the LLM the tools and let it figure out the order&quot; — works in two places: demos, and tasks where wrong-answers are cheap. Production AI in fintech, healthtech, or anywhere a wrong answer costs real money or real trust, runs differently.&lt;/p&gt;
&lt;p&gt;Three patterns that work, plus the one that doesn&apos;t.&lt;/p&gt;
&lt;h2&gt;Pattern 1 — The router (Waffy&apos;s fraud subsystem)&lt;/h2&gt;
&lt;p&gt;The simplest production pattern. The LLM doesn&apos;t &lt;em&gt;do&lt;/em&gt; the work; it routes incoming events to one of N specialized handlers, each of which is deterministic code.&lt;/p&gt;
&lt;p&gt;Waffy&apos;s fraud subsystem at v3 was structured this way. An incoming transaction event lands on the bus. A small router model — currently a fine-tuned 7B with a four-category output head — classifies it: &lt;code&gt;clean | suspicious_geo | suspicious_velocity | suspicious_pattern&lt;/code&gt;. Each non-&lt;code&gt;clean&lt;/code&gt; category hands off to a deterministic rules-engine specialist that asks N specific questions (&quot;is this card seen on 6 sellers in 4 hours?&quot;, &quot;is the device ID known-bad?&quot;) and produces a hold-extension recommendation. The recommendation goes to the wallet; the wallet decides whether to release on time or extend the hold by 48h.&lt;/p&gt;
&lt;p&gt;The router &lt;em&gt;itself&lt;/em&gt; is the only LLM call. Total cost per transaction: well under a cent. Latency: ~80 ms p50, ~180 ms p99.&lt;/p&gt;
&lt;p&gt;The pattern works because &lt;strong&gt;the LLM is good at the part of the job that&apos;s pattern-matching across noisy inputs&lt;/strong&gt; (&quot;does this transaction look weird?&quot;), and the deterministic specialists are good at the part of the job where you need to defend a decision against an auditor (&quot;we held this transaction because rule 7c fired with weight 0.83&quot;).&lt;/p&gt;
&lt;p&gt;The agentic alternative — give the LLM all the rules-engine tools and let it call them in any order — was the obvious approach in late 2024. We tried it. Three things broke: cost (each transaction now made 4–7 LLM calls instead of 1), latency (P99 ballooned to ~1.4s, killing the wallet&apos;s settlement SLA), and auditability (every regulatory ask became &quot;let me check the trace, hang on, why did it call this tool before that one?&quot;). We rewrote to router+specialists in a weekend. Production has been quieter since.&lt;/p&gt;
&lt;h2&gt;Pattern 2 — The decoder ring (Signalia&apos;s news pipeline)&lt;/h2&gt;
&lt;p&gt;Signalia turns thousands of news headlines a day into actionable insights for VC. Every headline goes through a pipeline that&apos;s roughly:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Ingest&lt;/strong&gt; — RSS, Twitter API, scraped press feeds. Deterministic, idempotent, boring.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Extract&lt;/strong&gt; — LLM call #1 (the only one per headline most days): pull structured fields from the headline + first paragraph. Company name, sector, stage signal, geography, amount-if-disclosed, named participants. Output is JSON with a schema we enforce.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Score&lt;/strong&gt; — Deterministic. Match against each subscribing VC firm&apos;s investment thesis (a vector + a few rules), produce a relevance score.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bundle&lt;/strong&gt; — Deterministic. Group scored headlines by firm and email schedule.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Send&lt;/strong&gt; — Deterministic.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The LLM is &lt;em&gt;one step&lt;/em&gt; in a five-step pipeline. It does one job well: extract structured data from unstructured text. Everything else is plumbing.&lt;/p&gt;
&lt;p&gt;This is the &lt;strong&gt;decoder ring&lt;/strong&gt; pattern. The LLM converts something messy (English text) into something tractable (JSON with a schema). Once it&apos;s in the schema, you don&apos;t need the LLM anymore — you have actual data, and you can write actual code that operates on it.&lt;/p&gt;
&lt;p&gt;The agentic alternative — &quot;give the LLM the user&apos;s thesis and the day&apos;s headlines, and let it produce the digest&quot; — also works, in the demo. It also costs ~30x more per user per month, takes 40x longer, and surfaces ~15% more &quot;soft hallucinations&quot; (paraphrased facts that lose specificity). We measured. The schema-first pattern wins on every axis a VC actually cares about.&lt;/p&gt;
&lt;h2&gt;Pattern 3 — The thin wrapper (FounderYX authoring)&lt;/h2&gt;
&lt;p&gt;FounderYX has an authoring loop where founders draft launch posts, customer interviews, and outbound emails. The LLM here is the most &quot;agentic&quot; of the three — it suggests, drafts, refines. But the agentic part is &lt;em&gt;thin&lt;/em&gt;. It&apos;s a single LLM call per user action: &quot;given this draft and this rubric, produce 3 alternative versions.&quot; The user picks one (or none) and continues. The agent has no memory between actions, no autonomous tool calls, no &quot;I&apos;ll figure out what to do next.&quot;&lt;/p&gt;
&lt;p&gt;It&apos;s the pattern Cursor uses, the pattern Linear&apos;s AI uses, the pattern that ships. It feels agentic to the user; it&apos;s a single deterministic-then-LLM-then-user loop under the hood.&lt;/p&gt;
&lt;p&gt;This is the &lt;strong&gt;thin wrapper&lt;/strong&gt; pattern. The intelligence is in the LLM call, but the &lt;em&gt;agency&lt;/em&gt; — the decision about what to do next — sits with the user. The system never autonomously chains decisions because that&apos;s where every production-AI tool I&apos;ve ever seen has had its worst failure modes.&lt;/p&gt;
&lt;h2&gt;The pattern none of them use — &quot;autonomous agent&quot;&lt;/h2&gt;
&lt;p&gt;The pattern where the LLM is given a set of tools and asked to figure out the order, the timing, the budget, the success criteria. The X demos. The YC pitches. The &quot;we&apos;re an AI-first company&quot; decks.&lt;/p&gt;
&lt;p&gt;I have never shipped this pattern to production. I have tried it three times — once at Waffy (replaced after 11 days), once on an internal Amazon experimentation tool (cancelled at the trial gate), once on a Signalia experiment we never publicly launched.&lt;/p&gt;
&lt;p&gt;Three things kill it every time:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Cost is multiplicative.&lt;/strong&gt; Each autonomous decision is an LLM call. Each LLM call is a chance to call another tool, which is another LLM call. The cost graph is not linear with task complexity; it&apos;s exponential. Production budgets are linear.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Latency is unforgiving.&lt;/strong&gt; A 1-second decision feels great in a demo. A 1-second decision that makes 6 more decisions ends up as a 12-second user-facing wait. Real users tolerate sub-second; demos don&apos;t have to.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Auditability is binary.&lt;/strong&gt; When a regulator (SAMA, CBE, GDPR, anyone) asks &quot;why did this happen to this user?&quot;, the answer needs to be defensible in plain language. &quot;Because the model decided to call tool X before tool Y based on its judgment of the task state&quot; is not defensible. &quot;Because rule 7c fired with weight 0.83 because of inputs A, B, C&quot; is. The two architectures are not the same product. They look the same in the demo, but only one of them ships to a regulated industry.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The agentic-autonomous pattern works &lt;em&gt;somewhere&lt;/em&gt; — research demos, hobby projects, anything where the cost of a wrong answer is zero. It does not work in fintech, healthtech, or anywhere money or trust is on the line. Which is most of the production AI work that needs doing.&lt;/p&gt;
&lt;h2&gt;What this means if you&apos;re shipping AI&lt;/h2&gt;
&lt;p&gt;If you&apos;re building a real product and the AI is the differentiator:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pick the pattern before you pick the model.&lt;/strong&gt; Router, decoder ring, or thin wrapper. Decide which one your problem is. If your honest answer is &quot;I don&apos;t know, I want the LLM to figure it out&quot; — your problem is the agentic-autonomous pattern, and you should reconsider whether the LLM is the right tool &lt;em&gt;or&lt;/em&gt; whether your problem is real.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Measure cost per task at the start.&lt;/strong&gt; A pattern that&apos;s 30x more expensive per task than the alternative will not survive contact with a finance team. Pick the one that&apos;s cheap enough to scale.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Architect for auditability before you architect for intelligence.&lt;/strong&gt; If you can&apos;t explain in plain English why a specific output happened for a specific input, you cannot ship to anyone who cares (regulators, enterprise customers, insurance underwriters).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Treat the LLM as one component, not the system.&lt;/strong&gt; Most of the &quot;AI product&quot; is the deterministic plumbing around the LLM call. If you spend 80% of your engineering time on the deterministic part, you&apos;re probably on the right track.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The router, the decoder ring, the thin wrapper — these don&apos;t sound like AI demos. They sound like normal engineering with one LLM call inside. That&apos;s what production AI actually looks like.&lt;/p&gt;
&lt;p&gt;The demos look like agents. The shipping code looks like for-loops with one LLM call somewhere inside the loop body.&lt;/p&gt;
&lt;p&gt;— Ahmed&lt;/p&gt;
</content:encoded><category>ai-in-production</category><category>ai</category><category>agents</category><category>production</category><category>llm</category><category>architecture</category></item><item><title>How we architected Waffy&apos;s multi-party escrow settlement engine</title><link>https://thebuildernotebook.com/blog/architecting-waffy-multi-party-escrow-settlement-engine/</link><guid isPermaLink="true">https://thebuildernotebook.com/blog/architecting-waffy-multi-party-escrow-settlement-engine/</guid><description>v1 was a single-table state machine. v3 routes 9 transaction types through a deterministic settlement core with an AI fraud subsystem. Here&apos;s what broke between them, and what survived.</description><pubDate>Mon, 15 Jun 2026 00:00:00 GMT</pubDate><content:encoded>&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note from Ahmed:&lt;/strong&gt; This is the first deep-dive in The Builder Notebook. The architecture story is true; the specific numbers below (latencies, FSM state counts, bug counts) are illustrative — I&apos;ll verify each against our internal records before this leaves draft. Treat them as the right shape, not the final receipts.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Waffy is &lt;strong&gt;MENA&apos;s first digital escrow payment platform&lt;/strong&gt;. It serves C2C buyers and sellers transacting on social platforms, B2B SMBs needing transaction security across borders, and B2B2C platforms embedding escrow as a service. We scaled it from a single commit in 2022 to &lt;strong&gt;181,000+ users and 25M SAR in GMV&lt;/strong&gt; — and along the way, the settlement engine got rewritten three times.&lt;/p&gt;
&lt;p&gt;This is the architecture story of those three rewrites. What v1 got wrong. What v2 fixed. What v3 unlocked. The receipts — fraud rate, dispute rate, settlement latency, hot-path P99 — are at the end.&lt;/p&gt;
&lt;p&gt;This is a story about &lt;strong&gt;escrow settlement at scale&lt;/strong&gt;, not a story about fraud detection (which is one &lt;em&gt;subsystem&lt;/em&gt;, not the product). The fraud engine deserves its own post.&lt;/p&gt;
&lt;h2&gt;The job to be done&lt;/h2&gt;
&lt;p&gt;When a buyer and seller transact across MENA — Instagram resellers, SMB import/export, classifieds, B2B2C marketplaces — neither party fully trusts the other. The cultural and regulatory default is &lt;strong&gt;cash on delivery&lt;/strong&gt; or &lt;strong&gt;bank transfer with hope&lt;/strong&gt;, both of which lose ~12% of transactions to fraud or refusal-to-deliver.&lt;/p&gt;
&lt;p&gt;Waffy&apos;s promise is: &lt;strong&gt;you can transact with a stranger as if you&apos;d transacted with your cousin.&lt;/strong&gt; Money is held in escrow. Goods get delivered or service rendered. Buyer confirms. Settlement releases to seller. If something breaks — dispute resolution and reverse settlement.&lt;/p&gt;
&lt;p&gt;That promise hides ~14 transaction states, &lt;strong&gt;3 distinct counterparty topologies&lt;/strong&gt; (C2C single buyer/seller, B2B SMB-to-SMB, B2B2C platform-embedded), and &lt;strong&gt;5 settlement rails&lt;/strong&gt; (Saudi SADAD, Egyptian Fawry, regional Mada/Visa/Mastercard, bank wire, regional crypto rails). And an unforgiving &lt;strong&gt;regulatory layer&lt;/strong&gt; — SAMA, CBE, and per-country licensing that varies more than the Western payments crowd appreciates.&lt;/p&gt;
&lt;p&gt;When we started, I drew the state machine on a whiteboard. It had 8 states. The first production user broke it within 4 hours.&lt;/p&gt;
&lt;h2&gt;v1 — The single-table state machine &lt;em&gt;(2022)&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;The original architecture was the most pragmatic thing that could work.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// transactions table — the entire v1 settlement engine, basically
{
  id: uuid,
  buyer_id: uuid,
  seller_id: uuid,
  amount_sar: decimal,
  state: &apos;created&apos; | &apos;funded&apos; | &apos;released&apos; | &apos;refunded&apos; | &apos;disputed&apos; | ...,
  state_history: jsonb,    // append-only audit log
  payment_intent_id: text, // PSP reference
  created_at: timestamp,
  updated_at: timestamp,
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Single Postgres table. Application-layer FSM with a &lt;code&gt;transitions&lt;/code&gt; map. Webhooks from the payment processor (Tap Payments at the time) moved transactions through states. Tap handled the actual money movement; we just tracked state.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What worked:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Shipped in &lt;strong&gt;6 weeks&lt;/strong&gt; from first commit to first paid transaction&lt;/li&gt;
&lt;li&gt;Easy to reason about — anyone on the team could read a row and know what happened&lt;/li&gt;
&lt;li&gt;The append-only &lt;code&gt;state_history&lt;/code&gt; jsonb gave us free audit trails for SAMA compliance&lt;/li&gt;
&lt;li&gt;One database, one ORM, one deploy&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What broke at scale:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The first real bug was a B2B partner who needed to settle to &lt;em&gt;three&lt;/em&gt; sellers at once (one buyer paying for goods from multiple SMBs in a coordinated shipment). Our single &lt;code&gt;seller_id&lt;/code&gt; column couldn&apos;t model it. We bolted on a &lt;code&gt;splits&lt;/code&gt; jsonb. It worked. It also made every subsequent query incomprehensible.&lt;/li&gt;
&lt;li&gt;Dispute resolution was an afterthought — the state graph needed &lt;code&gt;disputed&lt;/code&gt; as a &lt;em&gt;modifier&lt;/em&gt;, not a state. v1 modeled it as &lt;code&gt;disputed → resolved_buyer | resolved_seller&lt;/code&gt;, which meant a &quot;released and then disputed&quot; transaction needed an entirely new state path.&lt;/li&gt;
&lt;li&gt;Webhook idempotency was hand-rolled and brittle. We had &lt;strong&gt;14 duplicate-settlement bugs&lt;/strong&gt; in the first 90 days. Every one of them cost us trust + a manual reconciliation.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;state_history&lt;/code&gt; jsonb grew unbounded. Queries got slow. We hit our first N+1 disaster at month 7.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;When the first B2B2C partner signed (a Saudi classifieds platform wanting embedded escrow at their checkout), v1 was already structurally wrong. We started v2 while v1 was still live.&lt;/p&gt;
&lt;h2&gt;v2 — Split the engine from the wallet &lt;em&gt;(2023)&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;The key insight: &lt;strong&gt;escrow settlement is two distinct concerns wearing the same name&lt;/strong&gt;. There&apos;s the &lt;em&gt;settlement engine&lt;/em&gt; (the state machine — what step is this transaction in?) and there&apos;s the &lt;em&gt;wallet&lt;/em&gt; (the money — who holds it, who can release it, who owns the float). v1 had conflated them.&lt;/p&gt;
&lt;p&gt;v2 split:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌─────────────────────┐         ┌─────────────────────┐
│  settlement-engine  │         │       wallet        │
│  (Postgres + FSM)   │ ◀───▶  │ (Postgres + ledger) │
│                     │ events  │                     │
│  - transaction      │         │  - balance          │
│  - dispute          │         │  - transfer         │
│  - state machine    │         │  - hold             │
│  - regulatory state │         │  - reverse          │
└─────────────────────┘         └─────────────────────┘
         │                                │
         └──── eventbus ────┬──────────────┘
                            │
                  ┌─────────▼─────────┐
                  │  fraud-detection  │
                  │    (subsystem)    │
                  │                   │
                  │  scores each      │
                  │  state transition │
                  └───────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Settlement engine&lt;/strong&gt; owned the state machine. &lt;strong&gt;Wallet&lt;/strong&gt; owned the money (a proper double-entry ledger, audit-grade). They communicated through an event bus — we used Postgres LISTEN/NOTIFY for the first six months because it was free and good enough, then migrated to Cloud Pub/Sub when our throughput crossed ~50/sec sustained.&lt;/p&gt;
&lt;p&gt;The wallet&apos;s ledger was the most important architectural decision we made:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;// wallet_ledger — every cent&apos;s movement is a row
{
  id: uuid,
  transaction_id: uuid,        // FK to settlement-engine
  account_id: uuid,            // who&apos;s account moves
  type: &apos;hold&apos; | &apos;release&apos; | &apos;refund&apos; | &apos;fee&apos;,
  amount_sar: decimal,         // signed: + credit, - debit
  balance_after_sar: decimal,  // running balance — sanity check
  created_at: timestamp,
  // No updated_at. Append-only.
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Why double-entry, append-only:&lt;/strong&gt; every audit ever (SAMA, internal, partner SOC2) asks &quot;where is this 1 SAR right now and where has it been?&quot; The ledger answers in O(1) per row. Mutable rows are an audit liability.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What v2 unlocked:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;B2B multi-party transactions&lt;/strong&gt;: one settlement-engine transaction → N wallet ledger entries. Partial settlements are just multiple &lt;code&gt;release&lt;/code&gt; rows.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disputes as a modifier, not a state&lt;/strong&gt;: dispute became a &lt;em&gt;flag&lt;/em&gt; + &lt;em&gt;hold-extension&lt;/em&gt; in the wallet, not a state in the engine. The transaction could be &lt;code&gt;released&lt;/code&gt; &lt;em&gt;and&lt;/em&gt; &lt;code&gt;disputed&lt;/code&gt; simultaneously.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reverse settlements&lt;/strong&gt; for chargebacks: just write a negative-signed row. No state machine reverse-transitions needed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Idempotency&lt;/strong&gt; via deterministic event IDs derived from the PSP webhook payload. Duplicate-settlement bugs dropped from 14 in v1&apos;s first 90 days to &lt;strong&gt;2&lt;/strong&gt; in v2&apos;s first 90 days.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;What v2 didn&apos;t solve:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The settlement engine itself was still 1 monolithic FSM. As C2C, B2B, and B2B2C requirements diverged, the FSM grew to &lt;strong&gt;14 states&lt;/strong&gt; with ~40 valid transitions. Reasoning about it became a full-time job.&lt;/li&gt;
&lt;li&gt;Settlement latency. The P99 on the C2C &quot;release-to-seller&quot; hot path was &lt;strong&gt;~850ms&lt;/strong&gt; — fine for C2C, but B2B2C partners wanted sub-second confirmation for embedded checkout, and we couldn&apos;t deliver.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;v3 — Specialized engines + a deterministic core &lt;em&gt;(2024)&lt;/em&gt;&lt;/h2&gt;
&lt;p&gt;The third rewrite came when we realized: &lt;strong&gt;C2C and B2B don&apos;t share a settlement engine. They share a &lt;em&gt;core&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The core: a deterministic settlement primitive — &lt;code&gt;Settle(transactionId, releaseTo, amount)&lt;/code&gt; — that&apos;s sub-100ms for the happy path and writes one wallet ledger row + one event. It cannot fail in a recoverable-state way; it either settles or it errors out before touching the wallet.&lt;/p&gt;
&lt;p&gt;Around the core: &lt;strong&gt;specialized engines per topology&lt;/strong&gt;.&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;┌──────────────────┐  ┌──────────────────┐  ┌──────────────────┐
│  c2c-engine      │  │  b2b-engine      │  │  b2b2c-engine    │
│  (Instagram      │  │  (SMB import/    │  │  (platform       │
│   sellers,       │  │   export, multi- │  │   embedded —     │
│   classifieds)   │  │   party, larger  │  │   white-label    │
│                  │  │   amounts)       │  │   API)           │
└────────┬─────────┘  └────────┬─────────┘  └────────┬─────────┘
         │                     │                     │
         └─────────────────────┼─────────────────────┘
                               │
                  ┌────────────▼────────────┐
                  │  settlement-core        │
                  │  (deterministic         │
                  │   Settle primitive)     │
                  └────────────┬────────────┘
                               │
                  ┌────────────▼────────────┐
                  │       wallet ledger     │
                  └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each topology engine encapsulates its own FSM. The C2C engine knows about Instagram-DM-shared payment links, single buyer/seller, no escrow extension — &lt;strong&gt;9 states&lt;/strong&gt;. The B2B engine knows about multi-party settlement, SMB invoice integration, partial release on inspection — &lt;strong&gt;16 states&lt;/strong&gt;. The B2B2C engine exposes a smaller API surface (the platform partner doesn&apos;t &lt;em&gt;see&lt;/em&gt; the underlying state machine — they see &lt;code&gt;intent → confirmation → settlement&lt;/code&gt;) — &lt;strong&gt;7 states&lt;/strong&gt; exposed externally, more internally.&lt;/p&gt;
&lt;p&gt;All three call the same &lt;code&gt;Settle&lt;/code&gt; primitive when it&apos;s time to move money.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What v3 unlocked:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reasoning by topology&lt;/strong&gt;: a B2B engineer doesn&apos;t need to understand C2C edge cases. None of the engines has to think about the others.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Sub-200ms P99 settlement&lt;/strong&gt; on the B2B2C hot path. The core does one ledger write + one event publish, nothing else.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI fraud detection as a subsystem&lt;/strong&gt; — not part of any engine. Fraud-detection subscribes to events, scores transactions, and &lt;em&gt;recommends&lt;/em&gt; hold-extensions through the wallet. The engines stay deterministic; intelligence sits outside.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The migration was incremental&lt;/strong&gt;: v3 ran alongside v2 for ~4 months. New transactions used v3 topology engines; legacy v2 transactions kept being processed until they settled out.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;The numbers&lt;/h2&gt;
&lt;p&gt;After ~24 months in production &lt;em&gt;(illustrative — to be verified)&lt;/em&gt;:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;v1 (2022)&lt;/th&gt;
&lt;th&gt;v2 (2023)&lt;/th&gt;
&lt;th&gt;v3 (2024)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Settlement P99 (C2C release)&lt;/td&gt;
&lt;td&gt;~4,200 ms&lt;/td&gt;
&lt;td&gt;~850 ms&lt;/td&gt;
&lt;td&gt;~320 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Settlement P99 (B2B2C embedded)&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;~1,400 ms&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~180 ms&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Duplicate-settlement bugs / first 90d&lt;/td&gt;
&lt;td&gt;14&lt;/td&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FSM states in any single engine&lt;/td&gt;
&lt;td&gt;8&lt;/td&gt;
&lt;td&gt;14 (monolithic)&lt;/td&gt;
&lt;td&gt;C2C 9 / B2B 16 / B2B2C 7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Fraud rate post-AI subsystem&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;n/a&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;~0.4%&lt;/strong&gt; (vs MENA C2C baseline ~12%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Dispute rate&lt;/td&gt;
&lt;td&gt;~8%&lt;/td&gt;
&lt;td&gt;~5%&lt;/td&gt;
&lt;td&gt;~3%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PSP webhook → seller sees release&lt;/td&gt;
&lt;td&gt;~6 s&lt;/td&gt;
&lt;td&gt;~2 s&lt;/td&gt;
&lt;td&gt;~600 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2&gt;What I&apos;d do differently&lt;/h2&gt;
&lt;p&gt;If I were starting v1 today, knowing what we know now:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Double-entry ledger from day zero.&lt;/strong&gt; v1&apos;s &quot;state field&quot; approach worked for six weeks and cost us a quarter of refactoring. The cost-of-not-doing-it from day one was much higher than the cost-of-doing-it.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Disputes are a modifier, not a state.&lt;/strong&gt; Designing the state graph as if disputes are linear transitions was the single deepest architectural mistake of v1. They are a &lt;em&gt;parallel concern&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Topology engines beat one monolithic FSM&lt;/strong&gt; — but only after you understand the topologies. v2 was right to wait. Don&apos;t pre-split before you&apos;ve seen the production patterns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;AI fraud detection is a subsystem, not a step.&lt;/strong&gt; v2&apos;s mistake was almost making fraud part of the FSM (&quot;scoring&quot; state between funded and released). Then we couldn&apos;t change the model without changing the state graph. v3 made it a side-car. That decoupling was the unlock.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The PSP integration is the dirtiest code in the stack.&lt;/strong&gt; Tap&apos;s webhooks were not idempotent the way we&apos;d like, regulatory differences across MENA mean the integration matrix is N×M, and the cross-currency settlement saga remains the longest single-bug ticket in our history.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2&gt;What&apos;s next&lt;/h2&gt;
&lt;p&gt;We&apos;re now in v3.x territory — minor optimizations, more topology engines (a fourth, for B2B trade-finance escrow, where settlement waits on a goods-inspection signature from a third-party verifier), and deeper AI fraud detection.&lt;/p&gt;
&lt;p&gt;The next post in the &lt;em&gt;Build Notes&lt;/em&gt; category will be about &lt;strong&gt;how the AI fraud subsystem actually works&lt;/strong&gt; — what we tried, what surprised us, what production-AI looks like when the cost of a false positive is &quot;buyer&apos;s money held an extra 48 hours&quot; and the cost of a false negative is real money on a real card.&lt;/p&gt;
&lt;p&gt;— Ahmed&lt;/p&gt;
</content:encoded><category>build-notes</category><category>fintech</category><category>escrow</category><category>architecture</category><category>mena</category><category>payments</category></item></channel></rss>