<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Lora Henley</title>
    <description>The latest articles on DEV Community by Lora Henley (@lora_henley_dekh).</description>
    <link>https://dev.to/lora_henley_dekh</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3913525%2F53694270-f7b8-47a0-9504-941996e07c79.jpg</url>
      <title>DEV Community: Lora Henley</title>
      <link>https://dev.to/lora_henley_dekh</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/lora_henley_dekh"/>
    <language>en</language>
    <item>
      <title>How to Fact-Check a Payment Provider's Sales Deck Before You Sign</title>
      <dc:creator>Lora Henley</dc:creator>
      <pubDate>Tue, 11 Aug 2026 09:10:41 +0000</pubDate>
      <link>https://dev.to/lora_henley_dekh/how-to-fact-check-a-payment-providers-sales-deck-before-you-sign-1fa0</link>
      <guid>https://dev.to/lora_henley_dekh/how-to-fact-check-a-payment-providers-sales-deck-before-you-sign-1fa0</guid>
      <description>&lt;p&gt;A high-risk PSP pitch is a well-rehearsed document. It has a rate card with a headline percentage, a map with a lot of countries shaded in, a line about settlement, and a licence badge somewhere near the footer. Every element is technically a claim, and almost none of them are written in a way that survives contact with a contract.&lt;/p&gt;

&lt;p&gt;This is not usually fraud. It is a sales artifact produced by a sales team, describing best-case terms for an ideal merchant in the friendliest of the provider's acquiring relationships. The gap between that and what you get shows up three months in, when your effective rate is a point higher than quoted, your rolling reserve is 10% instead of 5%, and settlement moved from T+3 to T+7 "temporarily" after a chargeback spike.&lt;/p&gt;

&lt;p&gt;The good news is that most of the deck can be checked in an afternoon, using public records and a handful of carefully worded emails. Here is how to do it, line by line.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start by converting the deck into a list of testable claims
&lt;/h2&gt;

&lt;p&gt;Open the deck and write down every factual assertion in one column. Not the adjectives — the assertions. Typical output:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;"1.9% + €0.20 for EU cards"&lt;/li&gt;
&lt;li&gt;"Coverage in 40+ countries"&lt;/li&gt;
&lt;li&gt;"T+3 settlement"&lt;/li&gt;
&lt;li&gt;"Licensed EMI"&lt;/li&gt;
&lt;li&gt;"5% rolling reserve, 180 days"&lt;/li&gt;
&lt;li&gt;"Direct acquiring relationships"&lt;/li&gt;
&lt;li&gt;"No hidden fees"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now add three columns: &lt;em&gt;how I verify this&lt;/em&gt;, &lt;em&gt;what the evidence says&lt;/em&gt;, and &lt;em&gt;is it in the contract&lt;/em&gt;. That third column is the one that matters at signature. A claim that cannot make it into the agreement is a claim the provider does not intend to be held to.&lt;/p&gt;

&lt;p&gt;Most of what follows is about filling in the middle column.&lt;/p&gt;

&lt;h2&gt;
  
  
  The rate card: find the effective rate, not the headline rate
&lt;/h2&gt;

&lt;p&gt;The headline percentage is the cheapest card type in the cheapest region under the friendliest interchange assumption. It is real, in the sense that some transaction somewhere will price at that number. It is not what you will pay.&lt;/p&gt;

&lt;p&gt;Ask for the full fee schedule in writing, and specifically for these items, because they are the ones that go missing from decks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rate by card type and region — consumer vs commercial, domestic vs cross-border, EEA vs non-EEA&lt;/li&gt;
&lt;li&gt;The scheme fee and interchange pass-through treatment (blended or IC++, and if blended, what happens when interchange rises)&lt;/li&gt;
&lt;li&gt;Chargeback fee, representment fee, and whether you are charged for chargebacks you win&lt;/li&gt;
&lt;li&gt;Refund fee, and whether the original processing fee is returned on refund&lt;/li&gt;
&lt;li&gt;Monthly minimum, gateway fee, per-transaction fee, PCI fee, setup fee, account maintenance fee&lt;/li&gt;
&lt;li&gt;FX markup on settlement into your currency&lt;/li&gt;
&lt;li&gt;Payout fee per settlement batch and per bank account&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then do the arithmetic on your own last 90 days of transaction data, not on a hypothetical basket. Take your actual card mix, your actual refund rate, your actual chargeback count, and price it under their schedule. That number is your effective rate. Compare it against your current provider's effective rate calculated the same way. In high-risk, the difference between headline and effective is frequently large enough to reverse the ranking of two providers.&lt;/p&gt;

&lt;p&gt;If the provider will not give you a full fee schedule before you sign, that is itself a finding. Write it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  The licence badge: check which entity holds it, and which entity signs your contract
&lt;/h2&gt;

&lt;p&gt;This is the single highest-value check in the whole process, and the one merchants skip most often.&lt;/p&gt;

&lt;p&gt;PSP licensing verification is a two-step process. First, take the licence number or the entity name from the deck and look it up in the register of the regulator that issued it — the FCA register in the UK, the relevant national register for EEA EMIs and PIs, MAS, and so on. Confirm four things: the entity exists on the register, the permission covers what you are buying (payment services, e-money issuance, safeguarding), the status is active rather than lapsed or restricted, and the registered address matches what the provider tells you.&lt;/p&gt;

&lt;p&gt;Second — and this is the step that matters — get a draft of the merchant agreement and read the counterparty name on page one. Very often it is not the licensed entity. It is a sales or technology company in another jurisdiction, sometimes an offshore holding entity, contracting with you for "services" while the regulated entity sits behind it holding the funds and the permission you were shown.&lt;/p&gt;

&lt;p&gt;That structure is not automatically bad; layered corporate groups are normal in payments. But it changes your position materially. If the entity you contract with is not the regulated one, then safeguarding rules, complaints procedures and regulatory recourse may not attach to your relationship at all. When funds are held and you escalate, you need to know which company is actually holding them and which regulator, if any, will take your complaint.&lt;/p&gt;

&lt;p&gt;So write down both names. Then pull corporate records for each: incorporation date, registered office, directors, shareholders, filed accounts if the jurisdiction requires them. A processor pitching you on a decade of experience while its contracting entity was incorporated eight months ago is telling you something the deck did not.&lt;/p&gt;

&lt;h2&gt;
  
  
  Coverage claims: ask which acquirer, in which country, for which MCC
&lt;/h2&gt;

&lt;p&gt;"Coverage in 40+ countries" usually means the group can, in principle, route transactions to acquirers in those countries. It does not mean your MCC is approved in those countries, and in high-risk verticals the MCC is the whole question.&lt;/p&gt;

&lt;p&gt;The precise question to ask is: for MCC [yours], in [country], which acquiring bank will my traffic route to, and is my business type already approved there or does it require a new underwriting submission? Ask for the same answer for each of your top three markets.&lt;/p&gt;

&lt;p&gt;You will get one of three responses. A specific acquirer name and a confirmation of existing approval — good, and now verifiable. A vague answer about "our banking partners" — this means the relationship does not yet exist for your vertical. Or a promise to "get you approved" — which means you are the underwriting submission, and the timeline is theirs, not yours.&lt;/p&gt;

&lt;p&gt;Also ask what happens when an acquirer drops your MCC. Every high-risk merchant experiences this eventually. The answer you want describes redundancy: a second approved acquirer, and a stated migration process. The answer you do not want is silence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Settlement terms, reserves, and the fund-hold question
&lt;/h2&gt;

&lt;p&gt;Settlement terms in a deck are almost always stated as a single figure — T+3, T+7 — with no conditions attached. The contract will attach conditions. Find them.&lt;/p&gt;

&lt;p&gt;The four questions to put in writing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Under what circumstances can settlement be delayed or suspended unilaterally, and with what notice?&lt;/li&gt;
&lt;li&gt;Can the rolling reserve percentage or the hold period be changed during the term, and if so, by whose decision and with what notice?&lt;/li&gt;
&lt;li&gt;What happens to reserved funds on termination — what is the release schedule, and does it start at termination or at the end of the chargeback window?&lt;/li&gt;
&lt;li&gt;In the last 24 months, has this provider extended holds or raised reserves across a merchant category?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Question four will rarely be answered honestly, which is why the fund-hold track record has to come from outside the provider. Merchants who have had funds held tend to talk about it; the difficulty is that the accounts are scattered across forums, chat groups and payment service provider reviews of wildly varying reliability, and a lot of the directory-style sites that aggregate them are ranked by who paid for placement. &lt;a href="https://psptrust.org/" rel="noopener noreferrer"&gt;PSP Trust&lt;/a&gt; exists to put that in one place — an independent directory that publishes what a provider states alongside what verification shows, covering licensing, corporate records, live site vitals and merchant-reported incidents that are classified and published only after human review, across 54 providers and 133 countries.&lt;/p&gt;

&lt;h2&gt;
  
  
  Live vitals as a proxy for operational care
&lt;/h2&gt;

&lt;p&gt;You cannot audit a PSP's internal engineering from the outside. You can look at the surfaces they do expose, and treat them as a signal.&lt;/p&gt;

&lt;p&gt;Check the SSL certificate on the merchant dashboard and the API endpoint: issuer, expiry, whether the chain is complete, whether TLS versions and ciphers are current. Check the domain: registration date, registrar, expiry, whether it renews annually at the last minute, whether the WHOIS record is consistent with the corporate entity you looked up. Check whether the status page exists and has real incident history, or whether it is a green square that has never changed.&lt;/p&gt;

&lt;p&gt;None of these prove anything on its own. A lapsed certificate on a marketing site is trivial. A lapsed certificate on the endpoint your checkout posts to, on a company asking you to trust it with settlement, is a statement about internal process. Companies that let those things slip are usually letting other things slip too, and the other things are the ones that hold your money.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the claims back in writing
&lt;/h2&gt;

&lt;p&gt;The mechanism that converts a sales claim into an obligation is email. Not a call, not a demo, not a Slack channel with your account manager. Email, from you, restating the claim in their words, asking for confirmation.&lt;/p&gt;

&lt;p&gt;Use language like this:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Following our call, I want to confirm my understanding before we proceed to contract.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Pricing for EU consumer cards is 1.9% + €0.20, on an IC++ basis, with no additional scheme fee markup. Please confirm, and send the full fee schedule including chargeback, refund, FX and payout fees.&lt;/li&gt;
&lt;li&gt;Settlement is T+3 from capture. Please confirm the conditions under which settlement may be delayed, and the notice period.&lt;/li&gt;
&lt;li&gt;Rolling reserve is 5% over 180 days and is fixed for the initial term. Please confirm whether this can be varied during the term, and by what process.&lt;/li&gt;
&lt;li&gt;Our contracting counterparty is [Entity A]. The licence referenced in your materials is held by [Entity B]. Please confirm the relationship between the two entities and which one holds and safeguards merchant funds.&lt;/li&gt;
&lt;li&gt;For MCC [xxxx] in [country], traffic routes to [acquirer]. Please confirm this approval is already in place.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If any of the above is inaccurate, please correct it in reply.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last line does the work. A sales representative who will not correct an inaccuracy in writing has told you the claim was not meant literally. And when you later end up in a dispute, a thread where the provider confirmed specific terms is worth considerably more than your recollection of a call.&lt;/p&gt;

&lt;h2&gt;
  
  
  The one-page pre-signature audit
&lt;/h2&gt;

&lt;p&gt;Run this before you sign anything. It takes an afternoon.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Entity and licence&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Licence number located on the issuing regulator's public register&lt;/li&gt;
&lt;li&gt;[ ] Permissions cover the service you are buying; status active&lt;/li&gt;
&lt;li&gt;[ ] Contracting entity on the draft agreement identified by name&lt;/li&gt;
&lt;li&gt;[ ] Relationship between contracting entity and licensed entity confirmed in writing&lt;/li&gt;
&lt;li&gt;[ ] Corporate records pulled for both: incorporation date, directors, registered office&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Money&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Full written fee schedule received&lt;/li&gt;
&lt;li&gt;[ ] Effective rate calculated against your own last 90 days of transactions&lt;/li&gt;
&lt;li&gt;[ ] Reserve percentage, hold period and release schedule confirmed in writing&lt;/li&gt;
&lt;li&gt;[ ] Settlement timing plus the conditions under which it can change, in writing&lt;/li&gt;
&lt;li&gt;[ ] Termination clause read: notice period, reserve release, data export&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Coverage&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Named acquirer per key market, with MCC approval status confirmed&lt;/li&gt;
&lt;li&gt;[ ] Redundancy answer received for acquirer loss&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Operational signals&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] SSL and domain vitals checked on the dashboard and API endpoints&lt;/li&gt;
&lt;li&gt;[ ] Status page and incident history reviewed&lt;/li&gt;
&lt;li&gt;[ ] Independent merchant reports and fund-hold history reviewed from a source that does not sell placement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Paper trail&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Confirmation email sent restating every material claim&lt;/li&gt;
&lt;li&gt;[ ] Reply received and archived&lt;/li&gt;
&lt;li&gt;[ ] Every confirmed term traced to a clause in the agreement&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Any unchecked box is not a dealbreaker by itself. Three unchecked boxes in the money section, on a provider that will be holding six figures of your revenue in reserve, is a decision you are making without information you could have had.&lt;/p&gt;

</description>
      <category>analysis</category>
      <category>fintech</category>
      <category>startup</category>
    </item>
    <item>
      <title>Why Most iGaming Platforms Have Slow Withdrawals (And How We Architected Around It)</title>
      <dc:creator>Lora Henley</dc:creator>
      <pubDate>Tue, 05 May 2026 08:44:48 +0000</pubDate>
      <link>https://dev.to/lora_henley_dekh/why-most-igaming-platforms-have-slow-withdrawals-and-how-we-architected-around-it-2ghf</link>
      <guid>https://dev.to/lora_henley_dekh/why-most-igaming-platforms-have-slow-withdrawals-and-how-we-architected-around-it-2ghf</guid>
      <description>&lt;p&gt;If you've ever wondered why most online gaming platforms take 3–7 business days to process a withdrawal while your bank app moves money in seconds, the answer isn't regulation. It isn't even a fraud risk, mostly.&lt;/p&gt;

&lt;p&gt;It's that the average iGaming backend is a stack of legacy services duct-taped to payment processors from 2014, running synchronous flows with manual review queues bolted onto them. The technology to do this in minutes has existed for years. The architect will haven't.&lt;/p&gt;

&lt;p&gt;I've been working on the payments layer at &lt;a href="https://6ense.vip/slots" rel="noopener noreferrer"&gt;6ense&lt;/a&gt; - a new iGaming platform licensed by the Curaçao GCB - and I want to walk through how we approached the withdrawal pipeline, because it's a more interesting problem than it gets credit for in most engineering discussions.&lt;/p&gt;

&lt;p&gt;This post is about the architecture, not the product. If you build payment systems, fintech, or anything where "fast and safe" pull in opposite directions, the patterns here generalize.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Core Tension
&lt;/h2&gt;

&lt;p&gt;Withdrawals in iGaming have to satisfy three things that fight each other:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Speed&lt;/strong&gt; - players expect their money quickly. Slow payouts are the single biggest trust signal in the category.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compliance&lt;/strong&gt; - KYC checks, AML thresholds, sanctions screening, jurisdiction rules.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fraud prevention&lt;/strong&gt; - multi-accounting, bonus abuse, stolen-card top-ups followed by withdrawal to a clean wallet.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The legacy approach optimizes for #2 and #3 by making everything synchronous and human-reviewed. Player requests withdrawal → sits in queue → operator reviews → payment processor called → funds eventually leave. Total elapsed time: days.&lt;/p&gt;

&lt;p&gt;The modern approach decomposes the problem. Each concern runs in parallel, scores independently, and converges on a decision. If the decision is "approve," money moves immediately. If it's "review," it goes to a human. If it's "decline," the player gets a clear reason.&lt;/p&gt;

&lt;p&gt;The architecture looks roughly like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                 ┌─────────────────┐
                 │ Withdrawal      │
                 │ Request (API)   │
                 └────────┬────────┘
                          │
                          ▼
                 ┌─────────────────┐
                 │ State Machine   │  ← single source of truth
                 │ (PENDING)       │
                 └────────┬────────┘
                          │
          ┌───────────────┼───────────────┐
          ▼               ▼               ▼
    ┌──────────┐    ┌──────────┐    ┌──────────┐
    │ Risk     │    │ Compliance│   │ Liquidity│
    │ Scoring  │    │ Engine    │   │ Check    │
    └────┬─────┘    └────┬──────┘   └────┬─────┘
         │               │               │
         └───────────────┼───────────────┘
                         ▼
                ┌─────────────────┐
                │ Decision Engine │
                │ APPROVE/REVIEW/ │
                │ DECLINE         │
                └────────┬────────┘
                         │
                         ▼
                ┌─────────────────┐
                │ Payment Router  │ ← picks rail by amount/region
                └─────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's walk through the parts that matter.&lt;/p&gt;

&lt;h2&gt;
  
  
  The State Machine
&lt;/h2&gt;

&lt;p&gt;Anyone who has built a payments system the second time knows that the first thing you build the second time is a proper state machine. Withdrawals can't be modeled as Boolean flags on a row.&lt;/p&gt;

&lt;p&gt;Our states:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;WithdrawalState&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;PENDING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;          &lt;span class="c1"&gt;// initial&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RISK_SCORING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;     &lt;span class="c1"&gt;// parallel checks running&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWAITING_REVIEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;  &lt;span class="c1"&gt;// flagged for human review&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APPROVED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;         &lt;span class="c1"&gt;// ready to dispatch to payment rail&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DISPATCHED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;       &lt;span class="c1"&gt;// sent to processor, awaiting confirmation&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SETTLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;          &lt;span class="c1"&gt;// money has left our system&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;           &lt;span class="c1"&gt;// processor rejected&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;         &lt;span class="c1"&gt;// we declined (fraud/compliance)&lt;/span&gt;
  &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;       &lt;span class="c1"&gt;// player cancelled before dispatch&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;validTransitions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Record&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;WithdrawalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;WithdrawalState&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;PENDING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;RISK_SCORING&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;RISK_SCORING&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APPROVED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWAITING_REVIEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;AWAITING_REVIEW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APPROVED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;APPROVED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DISPATCHED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;CANCELLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;DISPATCHED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;      &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SETTLED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;FAILED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="na"&gt;SETTLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;         &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;FAILED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;          &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APPROVED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="c1"&gt;// retryable on a different rail&lt;/span&gt;
  &lt;span class="na"&gt;DECLINED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;        &lt;span class="p"&gt;[],&lt;/span&gt;
  &lt;span class="na"&gt;CANCELLED&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;       &lt;span class="p"&gt;[],&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;transition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WithdrawalState&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;WithdrawalState&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;validTransitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;InvalidTransitionError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;current&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;next&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The transitions are persisted as an event log, not just current-state mutations on the row. This matters because:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can replay a withdrawal's history for debugging.&lt;/li&gt;
&lt;li&gt;Compliance audits become trivial.&lt;/li&gt;
&lt;li&gt;Recovery from partial failures is bounded - you know exactly where you were.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A surprising number of iGaming platforms still model this as &lt;code&gt;status: string&lt;/code&gt; on a withdrawals table and update it in place. That's how you end up with money "already sent," but the row still shows &lt;code&gt;pending&lt;/code&gt; because two services raced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Idempotency or Death
&lt;/h2&gt;

&lt;p&gt;The single most important property of the entire pipeline is that &lt;strong&gt;every external call is idempotent&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;When you're moving money, network failures are not edge cases. They are the case. Your payment processor will time out. Your fraud-scoring service will return 502. Your queue will redeliver. If any of these can cause double-spending, you don't have a payment system; you have a lawsuit waiting.&lt;/p&gt;

&lt;p&gt;Every external operation gets an idempotency key derived deterministically from the withdrawal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;withdrawalId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s2"&gt;`wd:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;withdrawalId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;operation&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Usage with a payment processor&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;dispatchToProcessor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Withdrawal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;idempotencyKey&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dispatch&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;attempt&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;processor&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createPayout&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;destination&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;idempotency_key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// processor dedupes server-side&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;attempt&lt;/code&gt; field is critical. If a payout legitimately fails on rail A and you retry on rail B, you want a &lt;em&gt;new&lt;/em&gt; idempotency key - not a replay of the failed attempt. Most processors will return the cached failure if you reuse the key.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parallel Risk Scoring
&lt;/h2&gt;

&lt;p&gt;The legacy iGaming pattern is sequential: KYC check, then AML check, then fraud check, then liquidity check, each waiting on the last. Total time: minutes.&lt;/p&gt;

&lt;p&gt;We run them in parallel and combine the scores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;scoreWithdrawal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Withdrawal&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;RiskDecision&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;liquidity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;all&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="nx"&gt;riskEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;score&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;       &lt;span class="c1"&gt;// ML model on player behavior&lt;/span&gt;
    &lt;span class="nx"&gt;complianceEngine&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;check&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="c1"&gt;// sanctions, AML thresholds&lt;/span&gt;
    &lt;span class="nf"&gt;liquidityCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;         &lt;span class="c1"&gt;// do we have funds on this rail?&lt;/span&gt;
    &lt;span class="nf"&gt;velocityCheck&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;          &lt;span class="c1"&gt;// recent withdrawal patterns&lt;/span&gt;
  &lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// Hard fails short-circuit immediately&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;BLOCKED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;reason&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;liquidity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;sufficient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSUFFICIENT_LIQUIDITY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Combine soft signals&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;compositeScore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nx"&gt;risk&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;score&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
    &lt;span class="nx"&gt;compliance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;softScore&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compositeScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD_AUTO_APPROVE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;APPROVED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;compositeScore&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;THRESHOLD_REVIEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;AWAITING_REVIEW&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;score&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;compositeScore&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DECLINED&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;reason&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;HIGH_RISK_SCORE&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The thresholds are tunable per jurisdiction and per player tier - a verified player with a year of clean history gets different thresholds than a brand-new account. This is where you spend most of your time as the system matures.&lt;/p&gt;

&lt;h2&gt;
  
  
  Payment Rail Routing
&lt;/h2&gt;

&lt;p&gt;The other thing legacy platforms do badly: they pick a single payment processor and route everything through it. When the processor degrades (and they all do, regularly), withdrawals back up.&lt;/p&gt;

&lt;p&gt;Modern routing is multi-rail with active health checking:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;Rail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;supportsCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Currency&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;supportsRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Region&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;boolean&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;feeFor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Amount&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;estimatedSettlement&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nx"&gt;Duration&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nf"&gt;healthScore&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 0-1, updated by background prober&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;selectRail&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Withdrawal&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;rails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;Rail&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Rail&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;eligible&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;rails&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supportsCurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;currency&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;supportsRegion&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;region&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;healthScore&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;NoRailsAvailableError&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;

  &lt;span class="c1"&gt;// Score by cost + speed + health&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;eligible&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scoreA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;railScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;scoreB&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;railScore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;withdrawal&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;scoreB&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;scoreA&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;})[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The health score is updated by a background process that does small probe transactions and watches for elevated error rates. When a rail starts degrading, traffic shifts away from it automatically - players don't experience the outage, they just get routed to a healthier rail.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Surprised Me
&lt;/h2&gt;

&lt;p&gt;A few things I didn't expect when we started building this:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Most slow withdrawals are not fraud reviews. They're queuing.&lt;/strong&gt; When you instrument the legacy flow, the actual time spent on risk and compliance checks is small - usually seconds. The hours and days come from work, sitting in queues waiting for human attention. The fix isn't "faster fraud detection," it's "stop putting things in human queues that don't need to be there."&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Idempotency bugs are silent.&lt;/strong&gt; A double-payout doesn't crash your system. It just quietly costs you money and shows up days later in reconciliation. Test your idempotency by deliberately replaying every external call in your dev environment. If your numbers don't match exactly, you have a bug in production right now.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Compliance is faster than you think when modeled correctly.&lt;/strong&gt; Most operators treat compliance as a black box that returns "yes/no" after some time. In practice, compliance is a deterministic function of (player profile, transaction details, jurisdiction rules). It can run in milliseconds if you've structured the rules correctly. The slow part is when humans get involved - and humans should only be involved on the edges, not the median case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Players notice the speed before they notice anything else.&lt;/strong&gt; This is the part that surprised me as an engineer. We obsessed over the architecture, but the player feedback we got was almost entirely about how fast the payouts felt. Not the games. Not the UI. The thing they cared about most was the thing the industry has historically given them least of.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond iGaming
&lt;/h2&gt;

&lt;p&gt;The patterns above - state-machine modeling, idempotency-by-default, parallel scoring, multi-rail routing - generalize to basically any system that moves money. iGaming is just an industry where the gap between what the technology can do and what operators actually deliver is unusually wide, which makes it interesting to work in if you like building a better version.&lt;/p&gt;

&lt;p&gt;If you're working on payments anywhere - fintech, e-commerce, marketplaces - and your withdrawal/payout flow looks like a synchronous queue with manual review, you have low-hanging fruit. The architectural change isn't large. The user-experience change is enormous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;The "slow withdrawal" problem in online gaming is fundamentally an architectural debt problem dressed up as a regulatory one. The platforms that figure this out are going to define the next decade of the category. The ones that don't will keep losing players to the ones that do.&lt;/p&gt;

&lt;p&gt;If you're curious what this looks like as a finished product, &lt;a href="https://6ense.vip/slots" rel="noopener noreferrer"&gt;6ense&lt;/a&gt; is the platform we're building it on. Curaçao GCB licensed (OGL/2024/431/0231), built around the patterns above. Adults 18+ only, please play responsibly.&lt;/p&gt;

&lt;p&gt;Happy to discuss any of the architecture in the comments. I'm particularly interested in how other payment-engineering teams handle the rail-health-scoring problem - there's a lot of room for better approaches there.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Edit: a few people asked about the fraud-scoring model - it's a separate post, will write one up if there's interest. The short version is gradient-boosted trees on behavioral features, retrained weekly, with hand-coded rules layered on top for known fraud patterns.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>backend</category>
      <category>architecture</category>
      <category>fintech</category>
    </item>
  </channel>
</rss>
