Prompt
You are adding EasyPPP to this codebase. EasyPPP localizes Stripe prices by purchasing power: for each Stripe Price the merchant selects, it creates sibling Prices in the merchant's own Stripe account, one per market tier (for example 70%, 45%, 35% and 130% of the base price for a USD Price). A pricing page asks EasyPPP's API which Price a visitor should pay and shows an amount in the visitor's currency; the server then creates the Checkout Session with that Price ID exactly as it does today. Nothing else about charging changes. Docs: https://easyppp.com/docs
Work through the steps below in order. Explain what you are doing in plain language. Ask before editing files. Do not invent API fields or behavior that is not described here.
STEP 1. ACCOUNT SETUP (the user does this in a browser; you guide them)
Ask the user to do the following at https://app.easyppp.com and tell you when each is done:
a. Sign up and create a team.
b. Connect Stripe. Recommend connecting in Stripe test mode or a sandbox first so nothing live is created while you integrate. Live, test mode and sandboxes are separate connections with separate keys.
c. Choose products: tick the Stripe Prices to localize. EasyPPP shows a dialog listing the sibling Prices it will create, then creates them. Only active, fixed-amount-per-unit Prices in the account's default currency can be selected.
d. Review markets (read only; tiers are the same for every merchant and cannot be changed).
e. Copy the public key from the last step. It starts with ep_test_, ep_sbx_ or ep_live_. It is public and safe to put in client code. Live keys require a card on file because EasyPPP bills 10% of what localized Prices collect; test and sandbox keys are free.
Then ask the user to paste: the key, and the Stripe Price IDs (price_…) that the pricing page currently uses. If the page's Price IDs are already in the code, find them and confirm with the user.
STEP 2. SMOKE TEST
Run this with the user's key and one of their Price IDs:
curl -s "https://api.easyppp.com/v1/price?key=KEY&prices=PRICE_ID"
Expected: HTTP 200 and JSON of the form
{"data":[{"price":"price_…","base_price":"price_…","localized":false,"known":true,"country":"US","pay":100,"discount_percent":0,"currency":"USD","amount":4900,"display":"$49.00","estimated":false,"base_currency":"USD","base_amount":4900,"rates_as_of":"2026-10-03T22:50:29Z"}],"meta":{"request_id":"…","paused":false}}
Interpret it for the user:
- known:true means EasyPPP localizes this Price for this key. known:false means the ID was not selected in the dashboard (or belongs to a different environment than the key); ask the user to check the products page.
- localized is usually false from a developer's machine, because the country is taken from whoever makes the request and most developers are in a base-bucket country. That is expected. A localized response has localized:true, pay below 100 (or above 100 in a premium market), and price set to a sibling ID.
- display is the amount in the requester's currency, already formatted.
Errors come back as {"error":"code"}: 400 invalid_key (key malformed), 401 unknown_key (key not active, or wrong environment), 400 missing_prices / too_many_prices (max 50) / invalid_price, 405 method_not_allowed, 503 unavailable. If you get 401, the key was probably mistyped or the account was disconnected.
STEP 3. THE API, IN FULL
GET https://api.easyppp.com/v1/price?key=KEY&prices=ID1,ID2 (1 to 50 IDs, comma separated)
Browser calls only. No headers. CORS is open. Responses are not cacheable (Cache-Control: no-store); fetch once per page load and keep the result in memory.
The country cannot be passed in; it comes from the request. Never call this from the server to localize a visitor, because the server's location would be used.
Each entry in data (same order as requested):
price Stripe Price ID to charge. The only field Checkout needs.
base_price The base Price this entry belongs to. Use it as the lookup key.
localized true when a market sibling was chosen.
known false when EasyPPP does not localize this ID; pricing fields are then null.
country ISO alpha-2 or null.
pay percent of base the visitor pays (100 = base; can exceed 100).
discount_percent 100 - pay, or 0. Never negative.
currency, amount amount in the smallest unit of currency (Stripe rules: 0 decimals for JPY, 3 for KWD, etc). Do not divide by 100; use display.
display formatted string, e.g. "₹3,353.00". Render this.
estimated true when amount was converted into the visitor's currency for display.
base_currency, base_amount what Stripe actually charges (the selected Price's own currency and amount). Absent when known is false.
rates_as_of timestamp of the exchange rates used. Absent when known is false.
meta.paused true when the merchant has paused localization; entries then hold the base Price.
STEP 4. FRONTEND
Find the pricing page or component that shows prices and starts checkout. Then:
- Keep the existing hard-coded prices and Price IDs. They are the fallback and the initial render.
- On page load, in the browser, fetch /v1/price once with every Price ID on the page. Use a 2 second timeout via AbortController. On any failure (network error, timeout, non-200, JSON error) return null and change nothing.
- Build a map from base_price to entry, including only entries with known:true.
- For each plan: show entry.display if present, otherwise the hard-coded price. Pass entry.price (or the hard-coded base ID when there is no entry) to the checkout call.
- When entry.localized is true and entry.discount_percent > 0, add a short line such as "Pricing for {country}, {discount_percent}% off". Optionally show the base price struck through. When localized is false, or pay is above 100, show nothing extra.
- Never block rendering on the request. Render the base price immediately and swap when the response arrives (or keep it if nothing arrives).
- Put the key in a constant or a public environment variable (NEXT_PUBLIC_…, PUBLIC_…, VITE_…). It is not a secret.
- In server-rendered frameworks (Next.js app router, Astro, Remix, SvelteKit), the fetch must run in a client component or client-side script, not during server rendering.
Framework notes:
- Next.js / React: a small hook (useEffect + useState) that returns the map; pricing cards read from it.
- Astro: an inline <script> on the pricing page that updates data-price elements by base Price ID.
- Plain HTML: same as Astro, with elements marked data-easyppp-price="price_…".
STEP 5. BACKEND
Find where the Checkout Session, Subscription or Payment Intent is created. Accept the Price ID from the client request and use it in line_items (or items) instead of the hard-coded one. Siblings are real Prices in the merchant's account, so Stripe needs nothing else. If the code validates the Price ID against a fixed list, extend that list to accept siblings or remove the check, and tell the user why.
STEP 6. VERIFY
- Load the pricing page: base prices render immediately, then display values appear (they may equal the base price when the developer is in a base-bucket country).
- Temporarily use a wrong key: the page must look and behave exactly as before. Restore the key.
- Start a checkout and confirm the Session line item uses the Price ID returned by the API.
- If the user wants to see a localized result, they need a request from a localized country (for example a teammate in India, Brazil or Germany for a USD base).
- Remind the user: when deploying live, connect Stripe in live mode (a card is required), select the same products, and switch the key to the ep_live_ one. Test keys only return test Price IDs.
Finish by summarizing the files you changed and anything the user still has to do.
What the agent will ask you for
- Your EasyPPP public key.
- The Stripe Price IDs your pricing page uses, if it cannot find them in the code.
- Confirmation before it edits files.
After it finishes
Switch the key to your live one when you deploy. Live Prices need a card on file. If the agent’s test showedlocalized: false, that is normal from most developer locations; see How it works for which countries are localized.