CurrencyCraft

Features

Everything CurrencyCraft actually does

A currency app for Shopify that behaves differently depending on whether Shopify Markets is already handling your pricing. Below is the full breakdown, including what each capability costs and where you still have to do something yourself.

Display-only mode: the switcher, the converted price, and the notice that tells the shopper which currency the charge will actually be in. The illustration shows a storefront header with a currency switcher set to INR, a product priced 10,636.99 rupees, and a dismissible notice reading "You will be charged in USD at checkout". Beside it are the four steps behind that figure: a base price of 128.00 US dollars, an example rate of 1 US dollar to 83.10 rupees, charm99 rounding, and the displayed result of 10,636.99 rupees. The rate is an example, not a live quote.

What makes this one different

Seven design decisions, each answering something that goes wrong in this category. Paid capabilities are labelled in the same sentence rather than in a footnote.

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.

Two modes, detected automatically

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.

The rate engine (Mode B)

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.

Display conversion, markup and rounding (Mode B)

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.

Conversion rules (Mode B, Pro)

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.

Live simulator

Country JP · currency any · market any

  • Currency shownJPY (forced by a rule, from Force a currency for country JP, priority 10)
  • Price shown¥19,800, converted from $128.00
  • Rate used154.6900 JPY per 1 USD (the live provider rate)
  • Roundingnearest (your JPY rounding rule)
  • Switchervisible (no rule changed it)
  • Currencies this visitor can chooseUSD, EUR, GBP, JPY
The simulator resolves a visitor through the same engine that answers your storefront, and names the rule that won each field, so you can read the outcome before you publish it. Rules and the simulator are part of the Pro plan. The rate above is an example, not a live quote.

On the storefront

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.

Four presets for Mode B display conversion, set in the app. The switcher renders once at least two currencies are enabled, because a switcher with one option is not a switcher.

Which plan unlocks what

CapabilityLowest plan
Up to 5 currenciesFree
Mode B rates, rounding, markup and manual rateFree
Mode B display converter and pickerFree
Unlimited currenciesGrowth and above
Crypto and precious metalsPro and above
Mode B conversion rules and simulatorPro and above
Mode B conversion-rule schedulingPro and above

Free is $0, Growth is $4.99 a month and Pro is $9.99 a month. New paid subscriptions include a 30-day trial after Shopify approval. Paid upgrades are available only when billing is enabled and verified.

Two steps the app cannot do for you

Wrap your money format, in Mode B only

Under Settings, General, Currency formatting, your price has to be wrapped in a cc-money span, in both HTML fields. Shopify has no Admin API that writes those fields, so no app can do it for you. It is a paste that takes about a minute, the setup guide gives you the exact strings, and the app then reads your live formats back from Shopify and reports each field as done or not. In Mode A the wrapper is not merely unnecessary, it would set up a double conversion, so you are never asked for it there.

Switch on the app embed, in Mode B only

The theme app extension is enabled in the theme editor under App embeds, and the switcher block is placed in a section separately. Installing the app does neither. CurrencyCraft does not request the read_themes scope, so it cannot check this for you or turn it on: the setup guide asks you to confirm it. Skipping it is the most common reason a storefront shows no change at all.

Both are Mode B setup steps. For Mode A, use your theme's native country selector. The setup guide gives you the exact strings and where they go, and the same guide lives inside the app against your live settings.