Summary
Let customers pay Pro in EUR or USD, instead of USD-only. The product is already used from Europe (operator timezone, .dev SaaS); showing $20/mo only adds FX surprise on EU cards and looks US-centric.
This is presentment currency, not a different plan. Same Free/Pro limits (10 vs 100 / 30 days).
Current behavior
- Stripe product/price bootstrap:
scripts/setup-stripe-plans.mjs creates one recurring price, currency: "usd", unit_amount: 2000, lookup key feedback2code_pro_monthly_20_usd.
- Checkout (
app/api/billing/checkout/route.ts) always uses getProPriceId() → STRIPE_PRO_PRICE_ID. No currency, no second price, no Adaptive Pricing flags.
- Landing (
components/home/landing-view.tsx) and Account (components/account/billing-actions.tsx) hardcode $20 / $20/mo.
- JSON-LD offer is
priceCurrency: "USD" (components/seo/home-next-seo.tsx).
User.stripePriceId is stored (lib/stripe-subscription-user.ts) — we can already tell which price they subscribed to, but the app never branches on it for display.
- Webhook maps any active subscription to
planTier: PRO regardless of currency. Good — keep that.
There is no VAT/tax UI. Stripe Tax is not wired (checkout.sessions.create has no automatic_tax).
Problem
EU agency cards get USD conversion + issuer fees. Some buyers bounce when the only number is $20. Conversely, US buyers should keep USD.
We should not silently charge EUR using Stripe’s conversion of the USD price at checkout unless we have decided that is the product (Adaptive Pricing). A named EUR sticker price is clearer for agencies (“€19/mo” or “€20/mo”).
Proposed design
Price
Create a second Stripe recurring price on the same Pro product, e.g.:
- USD: existing
2000 / month (keep lookup key)
- EUR: pick one and freeze it:
- €19 — slightly cheaper sticker, common SaaS psychology, or
- €20 — parity, easier support (“twenty a month”)
Assumption for this issue: €20 / month (parity with USD). Document that FX will not be 1:1 in their bank statement and we will not monthly-adjust EUR to track USD.
Env:
STRIPE_PRO_PRICE_ID # USD (existing)
STRIPE_PRO_PRICE_ID_EUR # new
Extend scripts/setup-stripe-plans.mjs to ensureRecurringPrice(..., "eur", 2000) with lookup feedback2code_pro_monthly_20_eur.
How the user chooses
v1: explicit toggle, not geo-IP.
- Landing pricing:
USD | EUR segmented control; default from Intl / navigator.language (de/fr/nl/… → EUR, else USD) but always overridable.
- Account upgrade button uses the same preference.
- Persist
User.preferredCurrency (usd | eur) when they toggle or at checkout.
Do not infer from IP (VPNs, US laptop in Berlin, agencies billing a US parent company). Language + manual toggle is enough.
Checkout: pass the matching price id. Do not put both line items on one session.
Existing subscribers
- Already on USD: stay on USD. Portal (
app/api/billing/portal/route.ts) can change payment method; changing currency mid-subscription is a Stripe headache (cancel + resubscribe or subscription update with proration). v1: no in-app currency switch for active Pro. Show “you’re billed in USD; contact us to switch” or a later portal flow.
- Display on Account: format using
stripePriceId (if it matches EUR price → €20, else $20). Stop hardcoding $20 for Pro users who paid EUR.
Landing / SEO
- Default render still needs a price in HTML for crawlers: keep
$20 as default in SSR, hydrate the toggle. Put both amounts in visible copy: “$20 or €20 / month”.
- JSON-LD: can emit two
Offers or omit precise price (Free offer is already price: 0 USD). Follow-up: AggregateOffer low/high. Do not claim EUR-only in SoftwareApplication if the default crawl is USD.
Tax (call out, do not bury)
Charging EUR without thinking about VAT is the real EU foot-gun. This issue’s v1 can ship currency without Stripe Tax, but:
- If you are VAT-registered in the EU, turn on Stripe Tax /
automatic_tax: { enabled: true } and collect billing country in Checkout (billing_address_collection: "required" or automatic).
- If you are not: still collecting EUR does not magically create a VAT obligation, but you should confirm with an accountant.
Implementation assumption: add billing_address_collection: "auto" (or required) when introducing EUR; leave automatic_tax as a checklist item in this issue, implement if keys/tax origin are already in Stripe Dashboard.
Assumptions
- One Pro SKU, two presentment currencies. No “EU plan” with different quota.
- No GBP/CHF in v1.
- Promo codes (
allow_promotion_codes: true already on checkout) must exist for both prices or be product-level.
- Free remains 0 in both; no checkout.
- Enterprise stays “contact us” (
landing-view.tsx third card).
Acceptance criteria
Out of scope
- Annual billing.
- Purchasing power parity beyond EUR/USD.
- Invoicing / SEPA-only methods beyond what Stripe Checkout already enables for that currency (
payment_method_types: ["card"] today — consider letting Checkout auto payment methods per currency as a sub-task: drop the hard-coded card so SEPA appears for EUR).
Implementation notes
Primary files:
scripts/setup-stripe-plans.mjs
lib/billing.ts — getProPriceId(currency)
app/api/billing/checkout/route.ts — read currency from body + user preference
components/home/landing-view.tsx, components/account/billing-actions.tsx
prisma/schema.prisma — preferredCurrency
.env.example
Checkout payment_method_types: ["card"] should be revisited when adding EUR (SEPA Debit is normal for EU B2B). Prefer omitting payment_method_types so Stripe Dashboard settings apply.
See also
- Quota almost-full/full emails (upgrade CTA amount)
- Use-case landings (EUR default more relevant on
/for/agencies if we geo/language hint)
Summary
Let customers pay Pro in EUR or USD, instead of USD-only. The product is already used from Europe (operator timezone,
.devSaaS); showing $20/mo only adds FX surprise on EU cards and looks US-centric.This is presentment currency, not a different plan. Same Free/Pro limits (
10vs100/ 30 days).Current behavior
scripts/setup-stripe-plans.mjscreates one recurring price,currency: "usd",unit_amount: 2000, lookup keyfeedback2code_pro_monthly_20_usd.app/api/billing/checkout/route.ts) always usesgetProPriceId()→STRIPE_PRO_PRICE_ID. Nocurrency, no second price, no Adaptive Pricing flags.components/home/landing-view.tsx) and Account (components/account/billing-actions.tsx) hardcode $20 /$20/mo.priceCurrency: "USD"(components/seo/home-next-seo.tsx).User.stripePriceIdis stored (lib/stripe-subscription-user.ts) — we can already tell which price they subscribed to, but the app never branches on it for display.planTier: PROregardless of currency. Good — keep that.There is no VAT/tax UI. Stripe Tax is not wired (
checkout.sessions.createhas noautomatic_tax).Problem
EU agency cards get USD conversion + issuer fees. Some buyers bounce when the only number is
$20. Conversely, US buyers should keep USD.We should not silently charge EUR using Stripe’s conversion of the USD price at checkout unless we have decided that is the product (Adaptive Pricing). A named EUR sticker price is clearer for agencies (“€19/mo” or “€20/mo”).
Proposed design
Price
Create a second Stripe recurring price on the same Pro product, e.g.:
2000/ month (keep lookup key)Assumption for this issue: €20 / month (parity with USD). Document that FX will not be 1:1 in their bank statement and we will not monthly-adjust EUR to track USD.
Env:
Extend
scripts/setup-stripe-plans.mjstoensureRecurringPrice(..., "eur", 2000)with lookupfeedback2code_pro_monthly_20_eur.How the user chooses
v1: explicit toggle, not geo-IP.
USD | EURsegmented control; default fromIntl/navigator.language(de/fr/nl/… → EUR, else USD) but always overridable.User.preferredCurrency(usd|eur) when they toggle or at checkout.Do not infer from IP (VPNs, US laptop in Berlin, agencies billing a US parent company). Language + manual toggle is enough.
Checkout: pass the matching
priceid. Do not put both line items on one session.Existing subscribers
app/api/billing/portal/route.ts) can change payment method; changing currency mid-subscription is a Stripe headache (cancel + resubscribe or subscription update with proration). v1: no in-app currency switch for active Pro. Show “you’re billed in USD; contact us to switch” or a later portal flow.stripePriceId(if it matches EUR price →€20, else$20). Stop hardcoding$20for Pro users who paid EUR.Landing / SEO
$20as default in SSR, hydrate the toggle. Put both amounts in visible copy: “$20 or €20 / month”.Offers or omit precise price (Free offer is alreadyprice: 0 USD). Follow-up:AggregateOfferlow/high. Do not claim EUR-only in SoftwareApplication if the default crawl is USD.Tax (call out, do not bury)
Charging EUR without thinking about VAT is the real EU foot-gun. This issue’s v1 can ship currency without Stripe Tax, but:
automatic_tax: { enabled: true }and collect billing country in Checkout (billing_address_collection: "required"orautomatic).Implementation assumption: add
billing_address_collection: "auto"(or required) when introducing EUR; leaveautomatic_taxas a checklist item in this issue, implement if keys/tax origin are already in Stripe Dashboard.Assumptions
allow_promotion_codes: truealready on checkout) must exist for both prices or be product-level.landing-view.tsxthird card).Acceptance criteria
STRIPE_PRO_PRICE_ID_EURdocumented in.env.example.stripePriceId), not always$20.PROfor either price.$20string.Out of scope
payment_method_types: ["card"]today — consider letting Checkout auto payment methods per currency as a sub-task: drop the hard-codedcardso SEPA appears for EUR).Implementation notes
Primary files:
scripts/setup-stripe-plans.mjslib/billing.ts—getProPriceId(currency)app/api/billing/checkout/route.ts— read currency from body + user preferencecomponents/home/landing-view.tsx,components/account/billing-actions.tsxprisma/schema.prisma—preferredCurrency.env.exampleCheckout
payment_method_types: ["card"]should be revisited when adding EUR (SEPA Debit is normal for EU B2B). Prefer omittingpayment_method_typesso Stripe Dashboard settings apply.See also
/for/agenciesif we geo/language hint)