<?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: Eva Stamatovska</title>
    <description>The latest articles on DEV Community by Eva Stamatovska (@eva_stamatovska_c94468708).</description>
    <link>https://dev.to/eva_stamatovska_c94468708</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%2F4144730%2Fe7e9622a-b979-4eb9-a5a3-0defeee3919e.png</url>
      <title>DEV Community: Eva Stamatovska</title>
      <link>https://dev.to/eva_stamatovska_c94468708</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/eva_stamatovska_c94468708"/>
    <language>en</language>
    <item>
      <title>Nobody Has to Say Their Budget Out Loud: Building Wise Piggy in 24 Hours</title>
      <dc:creator>Eva Stamatovska</dc:creator>
      <pubDate>Wed, 28 Oct 2026 13:18:58 +0000</pubDate>
      <link>https://dev.to/eva_stamatovska_c94468708/nobody-has-to-say-their-budget-out-loud-building-wise-piggy-in-24-hours-a0k</link>
      <guid>https://dev.to/eva_stamatovska_c94468708/nobody-has-to-say-their-budget-out-loud-building-wise-piggy-in-24-hours-a0k</guid>
      <description>&lt;p&gt;&lt;em&gt;This is a submission for the &lt;a href="https://dev.to/mlh-hackathon"&gt;MLH x DEV Writing Challenge&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What I Built
&lt;/h2&gt;

&lt;p&gt;"So, what's everyone's budget?"&lt;/p&gt;

&lt;p&gt;That's the question that kills group trips. The chat goes quiet. Nobody wants to be the one who can't afford it, so people say yes to things they can't really pay for, or quietly drop out. It's even harder when your friends live in different countries, pay different amounts for flights and think in different currencies.&lt;/p&gt;

&lt;p&gt;So my teammate &lt;a href="https://github.com/josie-collins" rel="noopener noreferrer"&gt;Josie Collins&lt;/a&gt; and I built &lt;strong&gt;Wise Piggy&lt;/strong&gt;, a group trip planner where nobody has to say their budget out loud. We built it for the Wise "Max Ease" track, which asked for projects that take the friction out of everyday tasks.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Before the trip:&lt;/strong&gt; each person privately enters a flight budget, a budget for shared costs and a daily allowance, in their own currency. The app only shortlists destinations that fit &lt;em&gt;everyone&lt;/em&gt;, using live flight prices from each person's own airport. The group then votes anonymously.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;During the trip:&lt;/strong&gt; you snap a receipt or just say what you paid ("I paid 42 euros for dinner, Amara had the pasta for twelve"), and the bill is split by what each person actually had.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;After the trip:&lt;/strong&gt; the app settles up in as few payments as possible, converted into each payer's home currency with live Wise rates.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Keeping budgets private
&lt;/h3&gt;

&lt;p&gt;I work in security, so this was the part I cared about most. Hiding a number in the UI doesn't make it private, because anyone can open dev tools and read the raw API response. So we put the privacy rules on the server.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Budgets go in, but never come out.&lt;/strong&gt; The budget endpoint only accepts the caller's &lt;em&gt;own&lt;/em&gt; budget, checked against the user ID in their login token. Every response is built from an explicit list of approved fields, and other people's budgets are never on that list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A hidden group ceiling.&lt;/strong&gt; To work out what the group can afford together, the server calculates a ceiling from everyone's budgets. It can stretch slightly above the lowest budget, but only if the person with that budget privately agrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// ceiling = lowest + min(10% of (highest - lowest), 10% of lowest)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;stretch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;highest&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;lowest&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt;
  &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;lowest&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;stretchCeiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;lowest&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;stretch&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;stretchCeiling&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;for &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;m&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;below&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// No answer or "no" means the ceiling drops to their budget&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;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;acceptsStretch&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;ceiling&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;ceiling&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;groupBudgetBase&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 ceiling never leaves the function that computes it. Only the resulting shortlist is saved where clients can read it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No leaks through failure messages.&lt;/strong&gt; If no destination fits, everyone sees the same neutral message. It never says whose budget is the problem. Votes come back as totals only. And in a group of two, where you could work out the other person's budget by elimination, both people get a warning before entering one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The AI never sees a budget.&lt;/strong&gt; Gemini only gets receipt photos, transcripts and member names, and &lt;code&gt;gemini.js&lt;/code&gt; doesn't even import the database. All the money maths runs in plain, deterministic code covered by 75 unit tests.&lt;/p&gt;

&lt;h3&gt;
  
  
  Splitting receipts so the pennies always add up
&lt;/h3&gt;

&lt;p&gt;Receipts never divide cleanly, because tax, tips and discounts break the maths. We work in integer minor units (pence, cents) and give the leftover units to the largest remainders, so the shares always add up to the exact total:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;splitByWeights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalMinor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;weights&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;people&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;weights&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;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;exact&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;people&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&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="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;totalMinor&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;w&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sum&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;shares&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;exact&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;e&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="na"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;userId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;shareMinor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;leftover&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;totalMinor&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;shareMinor&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[...&lt;/span&gt;&lt;span class="nx"&gt;exact&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="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&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;raw&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&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;i&lt;/span&gt; &lt;span class="o"&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;i&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;for &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;e&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;order&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;leftover&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;shares&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;shareMinor&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="nx"&gt;leftover&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="mi"&gt;1&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="nx"&gt;shares&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 rest of the stack is React, Node/Express and PostgreSQL, with the &lt;strong&gt;Wise API&lt;/strong&gt; for live exchange rates (every expense stores the rate, its source and when it was fetched) and &lt;strong&gt;ElevenLabs&lt;/strong&gt; for voice entry and spoken confirmations like "Saved €42 for Dinner. Amara owes you €12."&lt;/p&gt;

&lt;h3&gt;
  
  
  What I learned
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy is a backend problem.&lt;/strong&gt; If the server sends a value, it isn't private, whatever the UI does.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Let AI handle messy input, not money.&lt;/strong&gt; Gemini is great at reading a crumpled receipt. Deciding who owes what belongs in tested code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Read the pricing page twice.&lt;/strong&gt; More on that below.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Demo
&lt;/h2&gt;

&lt;p&gt;🔗 &lt;strong&gt;Live:&lt;/strong&gt; &lt;a href="https://wisepiggy.club" rel="noopener noreferrer"&gt;wisepiggy.club&lt;/a&gt;. Log in as &lt;code&gt;tom&lt;/code&gt;, &lt;code&gt;amara&lt;/code&gt; or &lt;code&gt;lucia&lt;/code&gt; with the password &lt;code&gt;demo1234&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;💻 &lt;strong&gt;Code:&lt;/strong&gt; &lt;a href="https://github.com/josie-collins/iku-hackathon" rel="noopener noreferrer"&gt;github.com/josie-collins/iku-hackathon&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Partner Technologies
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Vultr (winner: MLH Best Use of Vultr 🏆)
&lt;/h3&gt;

&lt;p&gt;Vultr runs our production app. We used an Ubuntu 24.04 VPS with one Node process serving both the API and the built React app on port 8080. Caddy sits in front as a reverse proxy and gets and renews HTTPS certificates automatically:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight nginx"&gt;&lt;code&gt;&lt;span class="k"&gt;wisepiggy.club,&lt;/span&gt; &lt;span class="s"&gt;www.wisepiggy.club&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kn"&gt;encode&lt;/span&gt; &lt;span class="s"&gt;gzip&lt;/span&gt;
    &lt;span class="s"&gt;reverse_proxy&lt;/span&gt; &lt;span class="nf"&gt;localhost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;8080&lt;/span&gt;
&lt;span class="err"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the app handles financial data, I wanted the server locked down, not just running. The firewall only allows SSH, HTTP and HTTPS, so port 8080 can't be reached from outside. The app runs as a systemd service under its own unprivileged user, with hardening options that limit it to its own folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ini"&gt;&lt;code&gt;&lt;span class="nn"&gt;[Service]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="py"&gt;User&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;wisepiggy&lt;/span&gt;
&lt;span class="py"&gt;ExecStart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;/usr/bin/node index.js&lt;/span&gt;
&lt;span class="py"&gt;Restart&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;on-failure&lt;/span&gt;
&lt;span class="py"&gt;NoNewPrivileges&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;ProtectSystem&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;full&lt;/span&gt;
&lt;span class="py"&gt;ProtectHome&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;PrivateTmp&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The whole setup is a &lt;code&gt;setup-server.sh&lt;/code&gt; script, a Caddyfile and the service file, and redeploying is one script.&lt;/p&gt;

&lt;h3&gt;
  
  
  Gemini API
&lt;/h3&gt;

&lt;p&gt;Gemini reads receipts line by line (total, currency, shop and each item) and turns voice transcripts into draft expenses. We used structured JSON output with a strict schema, which made it far more reliable than parsing free text. Amounts are converted to minor units in our own code, not by the model. When the main model returned "high demand" errors, a lighter Flash-Lite model took over.&lt;/p&gt;

&lt;p&gt;The lesson: we activated a Google student offer expecting free Cloud credits, then found the credits only came with a higher tier. Instead of fighting billing at 2am, we redesigned around the free tier's rate limits, and the whole app runs at no cost.&lt;/p&gt;

&lt;h3&gt;
  
  
  Snowflake
&lt;/h3&gt;

&lt;p&gt;Snowflake holds anonymised analytics only: hashed trip IDs, no names, no budgets and no trips with fewer than three people. It powers the "what past groups spent here per day" figure on each destination and a Streamlit dashboard. Cortex wasn't available on trial accounts, so the "why this trip fits" explanations fall back to templates, and we throw away any model reply that mentions money.&lt;/p&gt;

&lt;h3&gt;
  
  
  GoDaddy Registry
&lt;/h3&gt;

&lt;p&gt;Our domain, &lt;strong&gt;wisepiggy.club&lt;/strong&gt;, which also gave us a nice short URL for the demo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hackathon Experience
&lt;/h2&gt;

&lt;p&gt;We attended the &lt;strong&gt;IKU Womxn in STEM Hackathon&lt;/strong&gt; at King's College London's Bush House on 17–18 October 2026. It was organised by the women in STEM societies at KCL, UCL and Imperial for women and non-binary students, and it had a lovely atmosphere: welcoming to beginners, but with lots of people building seriously ambitious things.&lt;/p&gt;

&lt;p&gt;Josie and I were a team of two, which meant we each owned big chunks of the app. That came back to bite us at 3am, when the Gemini and voice branch and the main branch had both rewritten the expenses code. We untangled the merge conflicts together, and it still worked at the demo.&lt;/p&gt;

&lt;p&gt;Talking to the people from Wise helped shape the idea early on, and incident.io gave a brilliant presentation on their software and how they help teams handle incidents.&lt;/p&gt;

&lt;p&gt;Thank you to Wise, incident.io, Deutsche Bank, MLH, Hackathons UK and ElevenLabs, and to all the organisers for such a great weekend. 💜&lt;/p&gt;

&lt;p&gt;If you've ever sat through the "so what's everyone's budget?" silence, give Wise Piggy a try and tell me what you'd add!&lt;/p&gt;

</description>
      <category>mlhacks</category>
      <category>devchallenge</category>
      <category>hackathon</category>
      <category>privacy</category>
    </item>
  </channel>
</rss>
