<?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: Roland HUON</title>
    <description>The latest articles on DEV Community by Roland HUON (@imprion).</description>
    <link>https://dev.to/imprion</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%2F4140002%2F5135412c-1720-47f7-944a-375ed07f2717.png</url>
      <title>DEV Community: Roland HUON</title>
      <link>https://dev.to/imprion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/imprion"/>
    <language>en</language>
    <item>
      <title>An AI wrote my trading app's currency converter. When it doesn't know a rate, it says 1.0</title>
      <dc:creator>Roland HUON</dc:creator>
      <pubDate>Fri, 23 Oct 2026 07:10:32 +0000</pubDate>
      <link>https://dev.to/imprion/an-ai-wrote-my-trading-apps-currency-converter-when-it-doesnt-know-a-rate-it-says-10-3pan</link>
      <guid>https://dev.to/imprion/an-ai-wrote-my-trading-apps-currency-converter-when-it-doesnt-know-a-rate-it-says-10-3pan</guid>
      <description>&lt;p&gt;On Monday I posted a function Claude Opus 5.5 wrote for my personal trading dashboard and asked whether you'd merge it. Here's the answer.&lt;/p&gt;

&lt;p&gt;The function converts every position into my portfolio currency before the dashboard adds them up:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fx_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="c1"&gt;# Les cotations londoniennes sont en pence (GBp)
&lt;/span&gt;    &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It reads well. It knows that London stocks are quoted in pence. It caches rates so I don't hit the API limits. It short-circuits when there's nothing to convert.&lt;/p&gt;

&lt;p&gt;It shouldn't be merged.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is a fallback that looks like caution
&lt;/h2&gt;

&lt;p&gt;When the quote provider fails or doesn't know the symbol, &lt;code&gt;quotes()&lt;/code&gt; returns an empty dict. Follow it through:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;({}.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# → None
&lt;/span&gt;&lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;                          &lt;span class="c1"&gt;# → 1.0
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No exception. No log. The function answers with a perfectly plausible number.&lt;/p&gt;

&lt;p&gt;Line 3 does the same thing in a different place: if a position has no currency, it's treated as if it were already in the target currency.&lt;/p&gt;

&lt;p&gt;And then &lt;code&gt;@cached(ttl=3600)&lt;/code&gt; makes it worse. If your decorator stores whatever comes back — and most do — that &lt;code&gt;1.0&lt;/code&gt; stays there for an hour. A quote provider that hiccups for one second gives you sixty minutes of wrong valuations.&lt;/p&gt;

&lt;h2&gt;
  
  
  What that looks like in a portfolio
&lt;/h2&gt;

&lt;p&gt;Say I hold ¥1,000,000 of a Japanese stock, and my dashboard reports in dollars.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;correct:   1,000,000 JPY × 0.0067  ≈  $6,700
fallback:  1,000,000 JPY × 1.0     =  $1,000,000
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The position appears about 150 times larger than it is, inside a total that still looks like a real number. On a page whose job is to help me decide what to buy and sell, that's the worst kind of failure: silent, confident, and wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Nobody asked for this, and nobody forbade it
&lt;/h2&gt;

&lt;p&gt;The whole project started from this prompt (my translation; I wrote it in French):&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to build a platform to organise and plan my trading. I need to know about the news instantly, see what I can invest in, see the confidence index, how many people bought shares in it, the returns, etc. Plan what you need to set up for this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It says nothing about what to do when data is missing. So the model filled that silence with the most comfortable choice for the code itself: never crash, always return something.&lt;/p&gt;

&lt;p&gt;That's the part I keep coming back to. The missing requirement — &lt;em&gt;in a trading tool, a wrong number is worse than no number&lt;/em&gt; — is obvious once you say it. I never said it. A better model won't fix that; it will follow my incomplete prompt even more faithfully.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Fail loudly, and let nothing wrong reach the cache. This is the version I'm moving to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;SUBUNITS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ZAc&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ZAR&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ILA&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ILS&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="nd"&gt;@cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fx_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Missing currency&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_from&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUBUNITS&lt;/span&gt;&lt;span class="p"&gt;.&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;from_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;f_to&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SUBUNITS&lt;/span&gt;&lt;span class="p"&gt;.&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;to_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&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;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;f_from&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f_to&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&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;rate&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;LookupError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;No quote for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;/&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;f_from&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;f_to&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Caching decorators generally store return values, not exceptions, so a failed lookup is retried on the next call instead of being frozen for an hour. It's worth checking that yours behaves that way.&lt;/p&gt;

&lt;p&gt;The caller changes too: when a rate is unavailable, the dashboard shows "rate unavailable" for that position and marks the total as incomplete. It never adds up a total that's quietly wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  A smaller one, with the same ending
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Pence only worked in one direction.&lt;/strong&gt; The original handled &lt;code&gt;GBp&lt;/code&gt; as a source currency, but &lt;code&gt;fx_rate("USD", "GBp")&lt;/code&gt; returned the USD→GBP rate, 100 times too small. Johannesburg (&lt;code&gt;ZAc&lt;/code&gt;) and Tel Aviv (&lt;code&gt;ILA&lt;/code&gt;) also quote in sub-units; for those, the symbol most likely doesn't exist, the quote comes back empty, and you're back to &lt;code&gt;1.0&lt;/code&gt;. The fix handles both directions and all three.&lt;/p&gt;

&lt;h2&gt;
  
  
  What looked wrong and wasn't
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;"The factor should be 100, not 0.01."&lt;/strong&gt; It shouldn't. A penny is 0.01 pounds; converting from pence means multiplying by 0.01, then by the pound's rate.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"The second &lt;code&gt;if from_ccy == to_ccy&lt;/code&gt; is dead code."&lt;/strong&gt; It isn't. After &lt;code&gt;GBp&lt;/code&gt; becomes &lt;code&gt;GBP&lt;/code&gt;, it correctly handles &lt;code&gt;fx_rate("GBp", "GBP")&lt;/code&gt;, which should return 0.01.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"Use &lt;code&gt;Decimal&lt;/code&gt; instead of &lt;code&gt;float&lt;/code&gt;."&lt;/strong&gt; Reasonable for accounting. For a displayed exchange rate, it doesn't change the outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;"A one-hour cache means stale rates."&lt;/strong&gt; That's what a TTL is for. The problem isn't stale rates, it's invented ones.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;Every line here is defensible on its own. The bug is a default: a value the code reaches for when it doesn't know, chosen to keep things running rather than to keep them true.&lt;/p&gt;

&lt;p&gt;Worth searching your own code for: &lt;code&gt;or 1.0&lt;/code&gt;, &lt;code&gt;or 0&lt;/code&gt;, &lt;code&gt;.get(key, 0)&lt;/code&gt;, an &lt;code&gt;except&lt;/code&gt; that returns something plausible. Each one is a question about what your program should do when it doesn't know — and whether anyone ever answered it.&lt;/p&gt;

&lt;p&gt;I asked Claude Code to search the rest of my project for the same pattern. That list will probably be next week's article.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I write one of these every week from code an agent wrote for my own projects. If you'd rather catch the next one before the answer: &lt;a href="https://discord.com/invite/WsYyZk5sWj" rel="noopener noreferrer"&gt;https://discord.com/invite/WsYyZk5sWj&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>codereview</category>
      <category>fintech</category>
      <category>debugging</category>
    </item>
    <item>
      <title>An AI wrote this currency converter for my trading app. Would you merge it?</title>
      <dc:creator>Roland HUON</dc:creator>
      <pubDate>Mon, 19 Oct 2026 07:34:50 +0000</pubDate>
      <link>https://dev.to/imprion/an-ai-wrote-this-currency-converter-for-my-trading-app-would-you-merge-it-4cf2</link>
      <guid>https://dev.to/imprion/an-ai-wrote-this-currency-converter-for-my-trading-app-would-you-merge-it-4cf2</guid>
      <description>&lt;p&gt;I asked Claude Opus 5.5 to build a personal trading dashboard: news, what to invest in, a confidence index, returns. Along the way it wrote the function that converts every position into my portfolio currency before computing totals.&lt;/p&gt;

&lt;p&gt;Here it is, unedited:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nd"&gt;@cached&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ttl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;fx_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="c1"&gt;# Les cotations londoniennes sont en pence (GBp)
&lt;/span&gt;    &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBp&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GBP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;from_ccy&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt;
    &lt;span class="n"&gt;q&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;from_ccy&lt;/span&gt;&lt;span class="si"&gt;}{&lt;/span&gt;&lt;span class="n"&gt;to_ccy&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;=X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;q&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;values&lt;/span&gt;&lt;span class="p"&gt;()),&lt;/span&gt; &lt;span class="p"&gt;{}).&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;price&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rate&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;factor&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;(The comment is in French because I am. It says London quotes are in pence.)&lt;/p&gt;

&lt;h2&gt;
  
  
  What you need to know
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;quotes()&lt;/code&gt; is the market data helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EURUSD=X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# → {"EURUSD=X": {"price": 1.0874}}
&lt;/span&gt;&lt;span class="nf"&gt;quotes&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;XXXUSD=X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;   &lt;span class="c1"&gt;# → {}  when the provider fails or the symbol is unknown
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And this is roughly how it's used (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;positions&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;qty&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;
    &lt;span class="n"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nf"&gt;fx_rate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ccy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;base_ccy&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The session started from this prompt, which I wrote in French:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;I want to build a platform to organise and plan my trading. I need to know about the news instantly, see what I can invest in, see the confidence index, how many people bought shares in it, the returns, etc. Plan what you need to set up for this.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Your call
&lt;/h2&gt;

&lt;p&gt;You're the reviewer. &lt;strong&gt;Merge, request changes, or reject&lt;/strong&gt; — and leave the one comment you'd actually post on the PR.&lt;/p&gt;

&lt;p&gt;Start your comment with your decision in capitals: &lt;code&gt;MERGE&lt;/code&gt;, &lt;code&gt;CHANGES&lt;/code&gt; or &lt;code&gt;REJECT&lt;/code&gt;. If you don't want to spoil it for others, you can hide the reasoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight markdown"&gt;&lt;code&gt;CHANGES
{% details my comment %}
your review comment here
{% enddetails %}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One thing to keep in mind: flagging something that isn't actually wrong counts against you. Blocking a good PR has a cost too.&lt;/p&gt;

&lt;p&gt;I'll post the answer on Friday, and go through every comment: the right call, the right reason, and the comments that looked convincing but weren't.&lt;/p&gt;

</description>
      <category>python</category>
      <category>codereview</category>
      <category>ai</category>
      <category>discuss</category>
    </item>
    <item>
      <title>It built on Windows, failed on Mac and Linux, and the Dockerfile was fine</title>
      <dc:creator>Roland HUON</dc:creator>
      <pubDate>Sat, 03 Oct 2026 13:26:20 +0000</pubDate>
      <link>https://dev.to/imprion/it-built-on-windows-failed-on-mac-and-linux-and-the-dockerfile-was-fine-e8o</link>
      <guid>https://dev.to/imprion/it-built-on-windows-failed-on-mac-and-linux-and-the-dockerfile-was-fine-e8o</guid>
      <description>&lt;p&gt;I asked Claude Opus 5 for a Dockerfile and a Compose file for a Spring Boot project. Multi-stage build, dependencies cached in their own layer, a healthcheck on Postgres, the app waiting for the database before starting. I read it, it looked right, I ran it on my Windows machine. It built. It ran. I merged it.&lt;/p&gt;

&lt;p&gt;On Mac and Linux, the build died on the first &lt;code&gt;RUN&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/bin/sh: 1: ./mvnw: Permission denied
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Here are the lines involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .mvn/ .mvn/&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; mvnw pom.xml ./&lt;/span&gt;
&lt;span class="k"&gt;RUN &lt;/span&gt;./mvnw dependency:go-offline &lt;span class="nt"&gt;-B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is nothing wrong with them. That's the interesting part.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where the bug actually was
&lt;/h2&gt;

&lt;p&gt;Not in the Dockerfile. In git:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ git ls-files -s mvnw
100644 bd8896bf2217b46faa0291585e01ac1a3441a958 0   mvnw
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;100644&lt;/code&gt; means a regular, non-executable file. A script you're meant to run should be &lt;code&gt;100755&lt;/code&gt;. The Maven wrapper had been committed without its executable bit — most likely because the first commit came from Windows, where git doesn't track that bit by default.&lt;/p&gt;

&lt;p&gt;On Mac and Linux, checking out the repo gives you an &lt;code&gt;mvnw&lt;/code&gt; with mode &lt;code&gt;644&lt;/code&gt;. &lt;code&gt;COPY&lt;/code&gt; preserves that mode inside the image. &lt;code&gt;./mvnw&lt;/code&gt; refuses to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why Windows hid it
&lt;/h2&gt;

&lt;p&gt;Windows files don't carry Unix permissions. When you build from a Windows client against a Linux engine, Docker has nothing to copy over, so every file in the build context arrives in the image as &lt;code&gt;rwxr-xr-x&lt;/code&gt;. Docker has long printed a warning about exactly this when building from Windows.&lt;/p&gt;

&lt;p&gt;So on Windows, &lt;code&gt;mvnw&lt;/code&gt; became executable by accident, and the build passed. The one test I ran was structurally incapable of catching the bug.&lt;/p&gt;

&lt;p&gt;It wasn't just Docker, either: &lt;code&gt;./mvnw spring-boot:run&lt;/code&gt; failed on Mac and Linux for the same reason. The whole project only worked on Windows.&lt;/p&gt;

&lt;h2&gt;
  
  
  The red herring
&lt;/h2&gt;

&lt;p&gt;The repo had this &lt;code&gt;.gitattributes&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;/&lt;span class="n"&gt;mvnw&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="n"&gt;eol&lt;/span&gt;=&lt;span class="n"&gt;lf&lt;/span&gt;
*.&lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="n"&gt;eol&lt;/span&gt;=&lt;span class="n"&gt;crlf&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It's easy to read that and conclude &lt;code&gt;mvnw&lt;/code&gt; is taken care of. It isn't. That rule handles line endings. File mode is a completely separate thing, and nothing in the repo was handling it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;At the source, so every clone gets it right:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git update-index &lt;span class="nt"&gt;--chmod&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;+x mvnw
git commit &lt;span class="nt"&gt;-m&lt;/span&gt; &lt;span class="s2"&gt;"Make mvnw executable"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And belt and braces in the Dockerfile, so a copy that lost the bit — a downloaded zip, say — still builds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;&lt;span class="nb"&gt;chmod&lt;/span&gt; +x mvnw &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; ./mvnw dependency:go-offline &lt;span class="nt"&gt;-B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or skip the question entirely, since &lt;code&gt;sh&lt;/code&gt; doesn't need the executable bit:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;RUN &lt;/span&gt;sh mvnw dependency:go-offline &lt;span class="nt"&gt;-B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same applies to &lt;code&gt;gradlew&lt;/code&gt; and any shell script you expect people to run.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check your own repos
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git ls-files &lt;span class="nt"&gt;-s&lt;/span&gt; | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-E&lt;/span&gt; &lt;span class="s1"&gt;'(mvnw|gradlew|\.sh)$'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Anything in that list showing &lt;code&gt;100644&lt;/code&gt; is waiting for someone on another OS.&lt;/p&gt;

&lt;p&gt;This isn't a rare edge case. Out of curiosity I checked another public Spring Boot project from a different author: same wrapper file, byte for byte, same &lt;code&gt;100644&lt;/code&gt;. Its Docker build was fine, because it used the official Maven image instead of the wrapper. Its local build was not.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I take from it
&lt;/h2&gt;

&lt;p&gt;The generated Dockerfile was correct. The diff was correct. The review, had there been one, would have looked at those lines and approved them. The bug lived in the relationship between the file and the repository's metadata, which a diff doesn't show — and it only appeared on machines the author never used.&lt;/p&gt;

&lt;p&gt;When the code in front of you is right, the questions worth asking are about everything around it: which platform was this tested on, what does the file mode say, what happens on the second machine?&lt;/p&gt;




&lt;p&gt;&lt;em&gt;I run a small weekly code-review challenge built around bugs like this one, where every line is correct and something is still broken. This was the first challenge of the season: &lt;a href="https://www.linkedin.com/feed/update/urn:li:activity:7510232443304714240" rel="noopener noreferrer"&gt;https://www.linkedin.com/feed/update/urn:li:activity:7510232443304714240&lt;/a&gt;. Would you have merged it?&lt;/em&gt;&lt;/p&gt;

</description>
      <category>docker</category>
      <category>java</category>
      <category>git</category>
      <category>devops</category>
    </item>
    <item>
      <title>Looking for beta testers: we're building a tool that bundles your trip, hotel and event into one plan</title>
      <dc:creator>Roland HUON</dc:creator>
      <pubDate>Wed, 30 Sep 2026 20:29:14 +0000</pubDate>
      <link>https://dev.to/imprion/looking-for-beta-testers-we-building-a-tool-that-bundles-your-trip-hotel-and-event-into-one-plan-450k</link>
      <guid>https://dev.to/imprion/looking-for-beta-testers-we-building-a-tool-that-bundles-your-trip-hotel-and-event-into-one-plan-450k</guid>
      <description>&lt;p&gt;Hey DEV community 👋&lt;/p&gt;

&lt;p&gt;My team and I are a team of students working on &lt;strong&gt;Bundle Events&lt;/strong&gt; as part of an entrepreneurship project at my school, and we're looking for beta testers to help me shape it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try it here:&lt;/strong&gt; &lt;a href="https://bundle-event.vercel.app/" rel="noopener noreferrer"&gt;https://bundle-event.vercel.app/&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem
&lt;/h2&gt;

&lt;p&gt;Every time we go to a concert or festival or somehere else in another city, it's the same routine: one tab for train tickets, one for flights, one for car options, another for hotels, and a map to figure out which hotel is actually close to the venue. Then we realize the last train leaves before the show ends. 🙃&lt;/p&gt;

&lt;h2&gt;
  
  
  What Bundle Events does
&lt;/h2&gt;

&lt;p&gt;You tell it where you're leaving from and where you're going. It then:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;compares transport options (train, plane, car)&lt;/li&gt;
&lt;li&gt;finds accommodation close to the venue&lt;/li&gt;
&lt;li&gt;assembles everything into a single "bundle" you can book&lt;/li&gt;
&lt;li&gt;shows the indication of the &lt;strong&gt;carbon footprint&lt;/strong&gt; of each trip, so you can pick the greener option if you want&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  🇫🇷 France only (for now)
&lt;/h2&gt;

&lt;p&gt;Right now, Bundle Events only covers &lt;strong&gt;trips and events in France&lt;/strong&gt;. That's a deliberate choice: we'd rather make it work really well in one country before expanding.&lt;/p&gt;

&lt;p&gt;But if enough of you tell me you'd use it where you live, going international is definitely on the table. So if you're outside France, I still want to hear from you: &lt;strong&gt;tell me in the comments which country or city you'd want it in&lt;/strong&gt;, and which events you usually travel to. That's exactly the kind of signal that will decide where Bundle goes next.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where we need your help
&lt;/h2&gt;

&lt;p&gt;The project is still early, and we want to iterate based on real feedback rather than my own assumptions. We'd love it if you could:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a bundle for a real or imaginary event in France (5 minutes max)&lt;/li&gt;
&lt;li&gt;Tell us what confused you, what broke, or what you'd expect to find&lt;/li&gt;
&lt;li&gt;Answer a few quick questions here: &lt;a href="https://forms.gle/sciKEqaKDpZxtxgQA" rel="noopener noreferrer"&gt;https://forms.gle/sciKEqaKDpZxtxgQA&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Any kind of feedback is welcome: UX, performance, design, the concept itself, or even "I would never use this, here's why". The honest stuff is the most useful.&lt;/p&gt;

&lt;h2&gt;
  
  
  Tech side
&lt;/h2&gt;

&lt;p&gt;It's built with Next.js, Supabase and deployed on Vercel. If you're curious about how the bundle logic works or want to discuss the architecture, ask away in the comments. Happy to share what I've learned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Thanks!
&lt;/h2&gt;

&lt;p&gt;Every tester will genuinely help me improve the product (and my project grade 😄). If you know someone who travels a lot for concerts or festivals, feel free to share this with them too.&lt;/p&gt;

&lt;p&gt;Drop your feedback in the comments or through the form. I'll try to reply to everyone!&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>webdev</category>
      <category>discuss</category>
      <category>startup</category>
    </item>
  </channel>
</rss>
