It knows when not to convert
Applying an app's own exchange rate on top of a Shopify Markets price converts an already converted number.
Detection reads your Shopify Payments account and your Markets first. Where Markets already prices locally, CurrencyCraft converts nothing and hides its picker. Use your theme's native country selector; the app does not change Markets prices or checkout currency.
Targeting, not one global setting
One store-wide rate and one store-wide rounding is fine right up to the point where you want Japan on whole yen and nobody else touched.
Five rule types across four targeting levels, resolved deterministically: market beats country beats currency beats global, then the higher priority, then the lowest rule id. Rules are on Pro and above.
You see the answer before you publish it
The usual way to test a currency rule is to save it, open the storefront in another tab and hope.
The simulator resolves a country, market and currency through the same engine that answers your storefront, prints the converted sample price, and names the rule that won each field. Pro and above.
A free plan, not a countdown
A trial is not a free tier. When it ends, a small store loses the switcher it just taught its customers to use.
Free is a real plan at $0 with no expiry: up to 5 currencies with the Mode B display picker, failover rates and rounding. Paid plans raise the cap and add Mode B conversion targeting.
Crypto and precious metals
Fiat-only conversion has nothing to say to an audience that thinks in BTC, or to a store that prices against gold.
Coinbase's board of coins, tokens and stablecoins, plus XAU, XAG, XPT and XPD from a metals feed. Pro and above, and the classifier fails closed, so an unrecognised code is gated rather than leaked to a free store.
The disclosure is not a toggle
Display-only conversion with no disclosure is how a shopper reaches checkout expecting one number and finds another.
Whenever the displayed currency differs from your base, a notice names the currency the charge will be in. While browsing, a shopper can dismiss it and one dismissal settles the whole store. On the cart page it is always shown at full size, it names the exact base-currency total, and it has no close button. You can reword the first sentence. You cannot switch it off.
The storefront budget is a build gate
A currency script heavy enough to slow a collection page costs more than the conversion earns.
One JavaScript file and one stylesheet, and the build exits non-zero if the pair passes 15 KB gzipped. It loads asynchronously, reserves the price's layout box so nothing jumps, and reveals your base prices after three seconds if the config is slow.
Detection runs itself
On install, and again on any admin page load while the store is still unclassified, the app reads your Shopify Payments account and your Markets. Payments active plus a Market with local currencies turned on means Mode A. A genuinely absent Shopify Payments account means Mode B. A missing scope is told apart from a real absence, so a permission problem is never mistaken for an answer.
Mode A: Shopify does the converting
When Shopify Markets already prices and charges in the local currency, CurrencyCraft does not convert anything. Markets prices are the source of truth, and multiplying them by our own rate would double convert. CurrencyCraft's picker is hidden in this mode. Enable your theme's built-in country selector to change Shopify's native country context. CurrencyCraft does not change Markets prices or checkout currency; its display conversion, rounding and Pro conversion rules apply only in Mode B.
Mode B: displayed prices only
Without Shopify Payments, the app converts the prices shown on the page. Checkout still runs in your store's base currency. The conversion code touches display values only, and a unit test asserts that the checkout currency is the base currency whatever the shopper has selected. That invariant is not optional in this codebase.
Manual override, always available
If detection is inconclusive it says so and asks you to choose. You can also pick a mode yourself at any point. The override is stored in its own field and is read first on every classification, so a later detection run cannot quietly flip your store back.
One provider per asset class
Fiat comes from OpenExchangeRates with Frankfurter (European Central Bank data) behind it. Crypto comes from Coinbase, which returns hundreds of codes in a single call. Gold, silver, platinum and palladium come from gold-api.com, one call per symbol. A CC0 CDN feed sits at the end of the chain as a degraded last resort.
Failover with a circuit breaker
Providers are tried in order. Each has its own breaker: repeated failures open it, it stays open for a 60 second cooldown, then a single probe decides whether to close it again. An open provider is skipped instead of being hammered, so one bad upstream does not slow the rest.
Cached, and one call per miss
A fetched rate bundle is cached in process for 60 seconds, so most storefront requests never reach a provider. Concurrent misses for the same base are coalesced into a single upstream call, so a traffic spike on a cold cache does not fan out into one provider request per shopper. The config response is then cached in the shopper's browser for five minutes.
Stale but valid, never 0 and never NaN
If every provider is down, the last cached bundle is served and flagged as stale rather than showing nothing. If even that is unusable, the price falls back to the base currency figure. Zero, null and non-numeric rates are dropped at a single choke point before they can reach the cache or the storefront, because a rate of 0 would display a product as free.
USD triangulation
Rates are kept against one base, so any pair is derived from that single table: A to B is table[B] divided by table[A]. Missing or invalid entries return nothing rather than producing NaN.
Crypto and precious metals (Pro)
XAU, XAG, XPT and XPD plus crypto codes are a paid capability. The classifier enumerates the bounded sets (metals and ISO 4217 fiat) and treats anything else as crypto, so an unknown code is gated rather than leaked to a free store. Note that gold-api quotes USD per troy ounce, which is the inverse of our internal contract, and its timestamp is a refresh stamp rather than a market tick, so the app uses its own fetch clock to judge staleness.
Markup per currency
Set a percentage on top of the provider rate for any currency, to absorb card and conversion costs. Zero is the default.
Manual rate override
Pin an exact rate for a currency and the app stops following the provider for it. A pinned rate beats a markup percentage, and a rateOverride rule beats both.
Six rounding strategies
nearest, up, down, charm99, charm95 and charm00. Charm endings floor to the whole unit and add .99 or .95, and charm00 rounds up to the whole unit. Rounding is applied after conversion, per currency.
Correct minor units
Decimal places come from the ISO 4217 table, so JPY renders with no decimals and KWD with three. Charm endings have no meaning on a zero-decimal currency, so they degrade to nearest whole rather than inventing a .99. If rounding ever collapses a positive price to zero, the conversion is refused and the base-currency figure is shown instead.
Five rule types
forceCurrency sets the display currency. enableCurrency adds a currency to the switcher without selecting it. rateOverride pins a rate. rounding chooses a strategy. switcherVisibility shows or hides the switcher.
Four targeting levels
A rule can target everyone (global), a currency, a country or a Shopify market. Leaving a targeting key empty matches any value at that level.
Deterministic resolution
Precedence is market, then country, then currency, then global. Ties break on higher priority, then on the lowest id. Every field is resolved independently, so a country rule can set the currency while a global rule still supplies the rounding.
Scheduling (Pro)
A rule can carry a start and end timestamp and is simply not applicable outside that window. Useful for a sale period or a temporary pinned rate.
Live simulator in the admin
Pick a country, market and currency and the admin shows the resolved currency, rate, rounding and switcher visibility, names the rule that won each field, and prints a sample converted price using the same code the storefront runs. It also warns when a currency is disabled but an enableCurrency rule still puts it in the switcher at the raw provider rate.
The rule engine is a paid capability
Rules require Pro and affect Mode B display conversion only. In Mode B, Free and Growth stores get the currency list, picker, rate engine and rounding without rule targeting. These controls do not change Shopify Markets prices or checkout.
A theme app extension, not injected script
The storefront ships as an app embed plus an optional switcher block, installed through the theme editor. Nothing is written into your theme files with the Asset API. The one thing that does outlive an uninstall is the cc-money wrapper you add to Shopify's currency formatting, which you remove in the same settings screen you added it in.
Inside a 15 KB gzipped budget
The runtime is one JavaScript file plus one stylesheet, held together to under 15 KB gzipped. It loads asynchronously and is not on the critical path, and if the config request is slow or fails the script reveals your base prices after three seconds rather than leaving them cloaked.
No layout shift
Prices are hidden before first paint with their layout box kept, converted in place, then revealed, so the shopper never watches a number change. A MutationObserver batched on animation frames catches prices added later by the theme, quick view modals or a cart drawer. The Mode B notice is positioned out of flow, so showing or dismissing it cannot move the page.
The shopper's choice sticks
In Mode B, the selected display currency is kept in localStorage and reapplied on the next visit. Where no choice has been made, the currency comes from the visitor's country through your country mapping, and otherwise from your base currency. A forceCurrency rule is the one thing that outranks a stored choice, since overriding is the point of that rule.
Mode B checkout notice
In Mode B, whenever the displayed currency differs from your base currency, a notice tells the shopper they will be charged in the base currency at checkout. It is announced politely to screen readers and the wording is yours to edit. While browsing, a shopper can dismiss it and that one dismissal settles the whole store, whatever they switch to next. On the cart it is always shown at full size, it names the exact base-currency total, and it has no close button. This disclosure is required, not a toggle.