<?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: Süleyman Emir Gergin</title>
    <description>The latest articles on DEV Community by Süleyman Emir Gergin (@plutazom).</description>
    <link>https://dev.to/plutazom</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3892438%2Fac42fdcc-f3fb-4578-a8f6-e5d8ffe32004.jpg</url>
      <title>DEV Community: Süleyman Emir Gergin</title>
      <link>https://dev.to/plutazom</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/plutazom"/>
    <language>en</language>
    <item>
      <title>"How we built Birik — group expense splitting on Stellar in 30 days"</title>
      <dc:creator>Süleyman Emir Gergin</dc:creator>
      <pubDate>Wed, 22 Apr 2026 11:59:53 +0000</pubDate>
      <link>https://dev.to/plutazom/how-we-built-birik-group-expense-splitting-on-stellar-in-30-days-1aog</link>
      <guid>https://dev.to/plutazom/how-we-built-birik-group-expense-splitting-on-stellar-in-30-days-1aog</guid>
      <description>&lt;h2&gt;
  
  
  The problem we tried to solve
&lt;/h2&gt;

&lt;p&gt;Splitwise and Tricount solved the &lt;strong&gt;tracking&lt;/strong&gt; problem a decade ago: who owes what in a group, who's even, who needs to pay up. They're great. Millions of people use them.&lt;/p&gt;

&lt;p&gt;But they only ever got you halfway. When the dust settles and the app says "Selin owes you 340 TL" — you still have to &lt;em&gt;actually move the money&lt;/em&gt;. Bank transfer, PayPal, Revolut, cash. Fees. Delays. Paragraph-long explanations in the memo field. The friction is worse for international groups: Erasmus housemates, remote startup teams, holiday crews.&lt;/p&gt;

&lt;p&gt;Crypto solved peer-to-peer transfer a long time ago. So why hasn't anyone merged the two?&lt;/p&gt;

&lt;p&gt;Partly because most chains don't make this look good:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Ethereum gas fees exceed a typical restaurant bill split&lt;/li&gt;
&lt;li&gt;Bitcoin is too slow for "pay you back for dinner"&lt;/li&gt;
&lt;li&gt;Most L2s still make you bridge, reason about fees, explain wrapped tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Stellar's fee model (~$0.00005 per tx) and sub-6-second finality make it the obvious answer. Soroban — the smart contract layer — makes the non-obvious part (programmable group logic) actually pleasant.&lt;/p&gt;

&lt;p&gt;So that's what we built.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why Stellar/Soroban specifically
&lt;/h2&gt;

&lt;p&gt;Three reasons, in order of how much they mattered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Fees small enough to stop thinking about.&lt;/strong&gt; A typical settle operation on Birik costs ~125,908 stroops. That's roughly &lt;strong&gt;1.2 cents&lt;/strong&gt;. You don't build "split the bar tab" on a chain where settling costs more than the tab.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust + good tooling.&lt;/strong&gt; Soroban contracts are Rust. The SDK is well-typed. &lt;code&gt;cargo test&lt;/code&gt; gives you proper unit tests with mocked &lt;code&gt;Env&lt;/code&gt;. &lt;code&gt;stellar-cli&lt;/code&gt; is fine. This sounds minor but matters a lot on day 20 when you're debugging an &lt;code&gt;InvokeContractError&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Stellar Asset Contract (SAC).&lt;/strong&gt; Every asset on Stellar is automatically exposed as a Soroban contract. Our &lt;code&gt;settle_group&lt;/code&gt; transfers native XLM by calling into the XLM SAC — no wrapping, no bridge, no fake asset.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The thing nobody tells you going in: &lt;strong&gt;Soroban testnet is really fast to iterate on.&lt;/strong&gt; We were redeploying the contract 3-5 times a day at peak. &lt;code&gt;friendbot&lt;/code&gt; for test funds is cozy. The dev loop is maybe the best I've used on any chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture at a glance
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────────────────────────────────────────────────────────────┐
│                          Frontend (Vercel)                          │
│   React 19 + Vite 6 + TypeScript + Tailwind + Freighter wallet     │
│   i18n (TR/EN/DE/ES) · PWA · SSE subscriber · 880 Vitest tests     │
└──────────────────┬───────────────────────────────┬──────────────────┘
                   │ SIWS / JWT                    │ Soroban RPC
                   ▼                               ▼
┌──────────────────────────────────────┐   ┌─────────────────────────┐
│      Backend (Railway, NestJS)       │   │    Stellar Testnet       │
│   Prisma · Postgres · Redis · BullMQ │   │    stellar_split.wasm    │
│   SSE event bus · 389 Jest tests     │◄──┤    24 cargo tests        │
└──────────────────┬───────────────────┘   │    5 entrypoints         │
                   │ webhook bridge          │    + companion SPLT     │
                   ▼                         │    (inter-contract)    │
        Discord / Slack / Web Push            └─────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three layers, one shared feature story. CI (GitHub Actions) gates a merge on the full 1,293-test matrix going green.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3tl5bz4kaurbd9phsu4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fy3tl5bz4kaurbd9phsu4.png" alt="Birik dashboard" width="800" height="415"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Three technical details worth reading
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. On-chain min-flow settlement
&lt;/h3&gt;

&lt;p&gt;The hardest "easy" part of a group expense app: when you know everyone's net balance, what's the &lt;em&gt;minimum&lt;/em&gt; number of transfers to zero everyone out?&lt;/p&gt;

&lt;p&gt;The naive answer is O(N²): every debtor pays every creditor they owe. For a group of 6, that's up to 15 pairwise transfers. We can do a lot better with a greedy pairing.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SuleymanEmirGergin/stellar-split/blob/master/contracts/stellar_split/src/settle.rs" rel="noopener noreferrer"&gt;&lt;code&gt;contracts/stellar_split/src/settle.rs&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;compute_optimal_settlements&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;i128&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Settlement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;settlements&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;Settlement&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="c1"&gt;// Split into debtors (negative) and creditors (positive).&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;debtors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;i128&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="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;creditors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Vec&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Address&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;i128&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="nn"&gt;Vec&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.keys&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;balances&lt;/span&gt;&lt;span class="nf"&gt;.get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="nf"&gt;.clone&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;debtors&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;creditors&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&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="c1"&gt;// Greedy pairing: match largest debtor with largest creditor,&lt;/span&gt;
    &lt;span class="c1"&gt;// transfer min(debt, credit), advance whichever is now zero.&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;d_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;debtors&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;c_idx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;creditors&lt;/span&gt;&lt;span class="nf"&gt;.len&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d_remaining&lt;/span&gt;&lt;span class="nf"&gt;.min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c_remaining&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="n"&gt;settlements&lt;/span&gt;&lt;span class="nf"&gt;.push_back&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Settlement&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;amt&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
        &lt;span class="c1"&gt;// …update remaining, advance indices&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;

    &lt;span class="n"&gt;settlements&lt;/span&gt;  &lt;span class="c1"&gt;// Guarantee: at most N-1 transfers for N people.&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Guarantee: for N non-zero-balance members, at most &lt;strong&gt;N-1&lt;/strong&gt; transfers. For our "Settle Demo" group of 4, this turns what could be 6 pairwise transfers into 2. The UI surfaces this visually:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fsettle-modal-minflow.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fsettle-modal-minflow.png" alt="Settle modal with min-flow rows" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Running it on-chain isn't just "cool" — it's the &lt;em&gt;correctness&lt;/em&gt; argument. The client can't lie about whom you owe.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inter-contract call — the SPLT reward
&lt;/h3&gt;

&lt;p&gt;When a user taps &lt;em&gt;Mark Group as Settled&lt;/em&gt;, two things happen in one transaction:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The contract loops &lt;code&gt;token_client.transfer(from, to, amount)&lt;/code&gt; through the computed settlements.&lt;/li&gt;
&lt;li&gt;The main contract then &lt;strong&gt;calls into a second contract&lt;/strong&gt; — our custom SEP-41 &lt;code&gt;stellar_split_token&lt;/code&gt; — to mint 100 SPLT to the settler as a reward.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/SuleymanEmirGergin/stellar-split/blob/master/contracts/stellar_split/src/lib.rs" rel="noopener noreferrer"&gt;&lt;code&gt;contracts/stellar_split/src/lib.rs:395-400&lt;/code&gt;&lt;/a&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;reward_amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;100_i128&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// 100 SPLT&lt;/span&gt;
&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="py"&gt;.invoke_contract&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&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="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;reward_token_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nn"&gt;soroban_sdk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nn"&gt;Symbol&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"mint"&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="nn"&gt;soroban_sdk&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nd"&gt;vec!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settler&lt;/span&gt;&lt;span class="nf"&gt;.into_val&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reward_amount&lt;/span&gt;&lt;span class="nf"&gt;.into_val&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;env&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 first time we got this working was a genuinely good moment. &lt;code&gt;env.invoke_contract&lt;/code&gt; composes very cleanly — you pass the target contract's ID, the function symbol, and a &lt;code&gt;vec!&lt;/code&gt; of arguments. The typing through &lt;code&gt;into_val&lt;/code&gt; is finicky but pleasant once you learn it.&lt;/p&gt;

&lt;p&gt;A toast confirms the reward on the client:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fsplt-reward.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fsplt-reward.png" alt="Settlement complete + reward toast" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the backend side, we emit a &lt;code&gt;reward_minted&lt;/code&gt; event so analytics and the SSE bus can pick it up.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Sign-In With Stellar (SIWS)
&lt;/h3&gt;

&lt;p&gt;You can't trust a wallet-address-only auth model for any backend feature (private groups, persistence, notifications). We implemented SIWS — a Stellar-adapted SIWE:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Client asks backend for a challenge (&lt;code&gt;GET /auth/challenge?address=G...&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Backend returns a short random string + issued-at timestamp&lt;/li&gt;
&lt;li&gt;Client has Freighter sign the challenge&lt;/li&gt;
&lt;li&gt;Client POSTs &lt;code&gt;{address, signature}&lt;/code&gt; to &lt;code&gt;/auth/verify&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Backend verifies the signature against the address's public key&lt;/li&gt;
&lt;li&gt;On success: returns a short-lived JWT (access) + sets an &lt;code&gt;HttpOnly&lt;/code&gt; refresh cookie&lt;/li&gt;
&lt;li&gt;Client silently refreshes on 401&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This shipped in ~200 lines of backend code. The only thing that hurt us was cookie &lt;code&gt;SameSite&lt;/code&gt; when the frontend and backend live on different Vercel/Railway domains. Solved with &lt;code&gt;SameSite=None; Secure&lt;/code&gt; + matching CORS.&lt;/p&gt;




&lt;h2&gt;
  
  
  The test strategy, and why we over-invested
&lt;/h2&gt;

&lt;p&gt;1,293 tests is a number we're a little sheepish about. Here's why we didn't feel like we overshot:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Contract tests (24, &lt;code&gt;cargo test&lt;/code&gt;):&lt;/strong&gt; every entrypoint, happy path + failure modes (wrong auth, already-settled, removed member). A bug in &lt;code&gt;compute_optimal_settlements&lt;/code&gt; doesn't just break one feature, it silently robs users. These tests are non-optional.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend tests (389, &lt;code&gt;jest&lt;/code&gt;):&lt;/strong&gt; NestJS controllers + services + auth guards + queue processors. Wallet-address-based authorization is easy to get wrong in subtle ways; we lean on test coverage rather than careful code review alone.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend tests (880 unit + ~60 Playwright e2e, &lt;code&gt;vitest&lt;/code&gt; + &lt;code&gt;playwright&lt;/code&gt;):&lt;/strong&gt; component rendering, &lt;code&gt;useX&lt;/code&gt; hooks, i18n translations across 4 languages, form validation, demo-mode flows. The e2e layer catches routing and state-seeding bugs that unit tests can't.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All three suites are gated in &lt;a href="https://github.com/SuleymanEmirGergin/stellar-split/blob/master/.github/workflows/ci.yml" rel="noopener noreferrer"&gt;&lt;code&gt;.github/workflows/ci.yml&lt;/code&gt;&lt;/a&gt; — nothing merges unless everything is green.&lt;/p&gt;

&lt;p&gt;A small concrete example: in the last week of the build, a contributor accidentally removed a translation key. The CI frontend test suite caught it (we compile with TypeScript strict, and the &lt;code&gt;t('key.foo')&lt;/code&gt; type derived from the translations object went red). Total fix time: ~3 minutes. Without that, we'd have shipped a broken dropdown in 2 languages.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Factivity-feed.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Factivity-feed.png" alt="Activity / insights dashboard" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Stuff that was unexpectedly hard
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Freighter in Playwright.&lt;/strong&gt; We can't make Playwright sign a transaction through a browser extension non-interactively. Solution: a hard demo mode (&lt;code&gt;localStorage.setItem('stellarsplit_demo_mode', 'true')&lt;/code&gt;) that stubs the entire contract surface with deterministic mocks. All our e2e tests run in demo mode. It's honestly better — the tests are faster and free of flake from network calls.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Horizon rate limits.&lt;/strong&gt; Horizon's public testnet endpoint throttles at ~30 req/min/IP. With 4 members rendering an activity feed on dashboard load, we blew through it in testing. Fix: a small backend &lt;code&gt;/analytics/summary&lt;/code&gt; aggregator + Redis 60s cache on the public route.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Freighter network switching.&lt;/strong&gt; Users land on the app with Freighter in Mainnet mode. Our UI now detects this and gently nudges them to switch — because otherwise "connect wallet" succeeds and &lt;em&gt;everything else&lt;/em&gt; silently fails. A wasted hour, then a lesson.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Windows dev on a mixed team.&lt;/strong&gt; &lt;code&gt;LF → CRLF&lt;/code&gt; warnings from git on every commit, &lt;code&gt;node_modules&lt;/code&gt; path-length limits, &lt;code&gt;\r\n&lt;/code&gt; leaks into snapshot tests. We added &lt;code&gt;.gitattributes&lt;/code&gt;, pinned Node, and moved on.&lt;/p&gt;




&lt;h2&gt;
  
  
  Mobile, because people pay each other from their phones
&lt;/h2&gt;

&lt;p&gt;It would've been easy to let this be a desktop dApp. But the actual use case is &lt;em&gt;"I just paid for dinner, now log it from the table"&lt;/em&gt;. We designed mobile-first from day one.&lt;/p&gt;

&lt;p&gt;A small detail we're proud of: the &lt;strong&gt;bottom sheet&lt;/strong&gt; with tab categories opens from the FAB, so the dashboard works one-handed. Every major action is reachable without scrolling.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fmobile-bottomsheet.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FSuleymanEmirGergin%2Fstellar-split%2Fmaster%2Fdocs%2Fscreenshots%2Fmobile-bottomsheet.png" alt="Mobile bottom sheet" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;We shipped the core. What we want to do in the next phase:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-currency settle.&lt;/strong&gt; Payer sent XLM, recipient wants USDC? Route through Stellar path payments. One transaction, two parties, two currencies.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Yield on idle savings pools.&lt;/strong&gt; Our &lt;code&gt;create_savings_pool&lt;/code&gt; / &lt;code&gt;contribute_pool&lt;/code&gt; / &lt;code&gt;release_pool&lt;/code&gt; entrypoints are live. Hooking the contribution step into a Blend lending pool would turn passive group savings into actual returns.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mainnet.&lt;/strong&gt; After a third-party audit and more beta users.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it + feedback
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Live demo (testnet):&lt;/strong&gt; &lt;a href="https://stellar-split.vercel.app" rel="noopener noreferrer"&gt;stellar-split.vercel.app&lt;/a&gt; — press &lt;code&gt;D&lt;/code&gt; on the landing page for demo mode, no wallet needed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Contract on Stellar Expert:&lt;/strong&gt; &lt;a href="https://stellar.expert/explorer/testnet/contract/CBQENHYCVSOK3CHZ6NRT6BI34W2ERPSRUNXHI6X5X33DTDCDWX27YN7K" rel="noopener noreferrer"&gt;&lt;code&gt;CBQENHYCVSOK3CHZ6NRT6BI34W2ERPSRUNXHI6X5X33DTDCDWX27YN7K&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/SuleymanEmirGergin/stellar-split" rel="noopener noreferrer"&gt;github.com/SuleymanEmirGergin/stellar-split&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Testnet beta feedback form (2 min):&lt;/strong&gt; &lt;a href="https://forms.gle/oFSNuU6a9NthmfJR7" rel="noopener noreferrer"&gt;forms.gle/oFSNuU6a9NthmfJR7&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you try it and find a bug, ship us an issue. If you're building on Soroban and any of this was useful, we'd love to hear what you ran into — tag us &lt;a href="https://twitter.com/StellarOrg" rel="noopener noreferrer"&gt;@StellarOrg&lt;/a&gt; on the retweet.&lt;/p&gt;

&lt;p&gt;Thanks for reading 🙏&lt;/p&gt;




</description>
      <category>stellarchallenge</category>
      <category>rust</category>
      <category>react</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
