Build1 publisher3 min readPublished
One zero-dependency package sets Munchable's shown and charged price in 24 currencies
Munchable's 10-pound Premium plan takes every displayed and charged price, in 24 currencies, from one package with zero runtime dependencies. With the rounding kept in that package, the web site and the Expo paywall print the amount Checkout charges.
The Engineer · Build desk

What happened
- Munchable Premium costs 10 pounds a month and is listed in 24 currencies, under a rule that the price a customer reads is the exact amount their card is charged.
- Munchable previously charged in sterling and let Stripe's Adaptive Pricing convert on Stripe's own page, so the local amount changed whenever the exchange rate moved.
- The price now lives in packages/pricing, which has zero runtime dependencies and is imported by 13 files in the Next.js site and three in the Expo app.
- Each currency has exactly one Premium price, such as 12.99 euros in the euro area, and that figure is what both apps print and what the Checkout Session is created for.
- Contributor credit converts at the plain mid-market rate with no buffer and rounds down, the opposite of how the package rounds prices.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Any team sharing one price between a web site and a React Native paywall has to keep it out of modules that build a payment client, since the app bundle cannot import server code at all.
- cost Customers outside the UK carry the currency cost, since only the UK price is the plain 1000 pence and every other price adds the 4 percent buffer and a round-up.
- exposure Rounding credit the way prices round would have turned 20 pence of reward into 0.99 euros off, over four times the 0.23-euro mid-market figure, a promise Munchable would have to honour or explain.
- capability Because a local price only changes when the table does, support replies, emails and ads can quote a figure that matches the card charge.
The first decision was where the number lives. It used to sit next to the Stripe client, and the post gives one specific reason that was wrong: a `<Price>` component on a landing page, rendering four characters of text, would carry the Stripe SDK in its module graph just to read an integer [4]. The migration step is the part I would copy. The Stripe-facing module now re-exports the pricing package, so no existing import had to change [5].
The Expo app is the other half of that argument. The post's author wrote that "a paywall that quotes a different price from the landing page is the single most expensive bug in this category" [7]. The defence is a single constant. Its unitAmount is 1000, with the comment "integer pence, never a float, never a display string", and every other figure is derived from those 1000 pence [8].
Conversion then rounds up twice. The first step multiplies by `CONVERSION_FEE_BUFFER = 1.04` [11], so 1000 pence become 1040 pence of value before any local currency appears [1]. According to the post, the buffer covers two costs. Stripe takes a percentage when it converts the customer's payment back to sterling, and the hardcoded rates drift between refreshes [11]. The buffer sits at the top of Stripe's stated 2 to 4 percent band [11]. At a full 4 percent fee and an unchanged rate, those 1040 pence settle at about 998 [2]. The fee takes nearly all of the buffer. Any cushion against rate drift comes from the second step [2].
The second step is a retail round-up in two shapes [12]. Currencies with decimals go up to the next .99. Currencies without them go up to a round step scaled to the size of the number, because a price of 2,246 yen looks like the output of a conversion. Yen land on 2,300 and forint on 4,500 [12].
The rates are hardcoded, with a source date in a comment [13]. The post argues that a live feed would put a network dependency in the path of rendering a landing page, for precision the figure does not claim. It calls that figure explicitly approximate and never a live charge [13]. Both statements hold if the rate is used only when the table is refreshed and each Checkout Session is created for the stored, rounded price [9][13]. For one subscription at a fixed list price, I think skipping the feed is correct. The post does not say how often the table is refreshed.
Credits run the other way, and the package keeps its rounding in currency.ts [2]. The comment on discountIn opens by naming its counterpart, "The opposite rounding to priceIn, for the opposite reason," and then explains that rounding a discount up would make a promise Munchable then fails to keep [15].
The pattern transfers under a narrow condition. Munchable sells one product at one price per currency, 24 list prices in all [4]. A catalogue priced per SKU, or a price computed from a live rate at request time, would still need a single owner for display and charge. The case against a rate feed would have to be made again.
What to watch
- Whether Munchable publishes a refresh cadence for the hardcoded rates, or moves to a rate feed despite the network cost on landing pages.
- Where Munchable's actual Stripe conversion fee lands in the 2 to 4 percent band; below the top, the 1.04 buffer has room left for rate drift.
- Whether a second product or tier arrives, turning the 24-price table into a per-SKU catalogue the same package has to own.