How I compute landed prices for gear that crosses the Canadian border (and why daily price history needs an FX filter)
I'm Dmitry, a Canadian rider. I built ALLR (allr.io), a price-comparison site for motorcycle gear. No store pays us, and this post has no affiliate links. It is about the two parts that took longest to get right: finding
I'm Dmitry, a Canadian rider. I built ALLR (allr.io), a price-comparison site for motorcycle gear. No store pays us, and this post has no affiliate links. It is about the two parts that took longest to get right: finding the same product in every store, and working out what it really costs when the store is not in your country.
The problem
A helmet listed at 339.99 EUR in Germany, 549.99 USD in the US and 715 CAD in Ontario cannot be compared by looking at the numbers. For a Canadian buyer the price is:
item (in CAD) + shipping + import duty + customs brokerage
and the item part is itself a conversion, sometimes after removing a tax. I call that total the landed cost. Each listing goes through one function that every page on the site uses, so a product page, a search result and a deals list cannot disagree.
First you have to find it
Before any price can be compared, the same product has to be recognised in every store, and stores don't make that easy.
- Names differ. Stores add their own words ("Helmet", "Full Face", the colour, the size) in their own order, write model numbers differently (RF-1400, RF1400) and sometimes use a regional name: Shoei sells the RF-1400 in Europe as the NXR2.
- Colour names differ. For the RF-1400 in plain matte black, most stores in our catalog say "Matte Black" or "matte black", and FC-Moto says "black matt". It is the same helmet, and it has to land on the same colourway.
- One product, many pages, or many products on one page. Some stores make a separate page per colour, and one store we track made a separate page per size. Others go the other way: two Canadian stores list 49 RF-1400 colours and graphics on a single page.
- Things that look alike but aren't. A named graphic edition is its own product at its own price. On ALLR the RF-1400 Beaut and RF-1400 Yagyo are separate pages from the plain RF-1400, because folding them in would make the cheapest "RF-1400" a different helmet from the one you want.
So every listing is cleaned up first (brand spellings, model-number formats, store filler words) and grouped by brand and model. A store's colour pages fold into one product as colourways, and each colourway keeps its own sizes and stock, because a size in stock in black says nothing about red. When we get a merge wrong, the old page address is redirected to the right product instead of disappearing.
Today the plain RF-1400 page brings together 56 store offers for a Canadian buyer, from shops in Canada, the US, the UK and the EU. That one page is what makes the price table below possible, and the matching is where most of our bugs have been.
The pipeline for one listing
For a listing, the code first decides which of three cases it is.
- The store does not ship to the buyer's country. The row is kept but its total is set to infinity, so it sorts last and is never shown as buyable.
- A domestic sale (a Canadian store, for a Canadian buyer). No customs. Item price in CAD plus the store's own shipping rule for that market, for example free over a threshold.
-
A cross-border sale. This is where the work is, in this order:
- Remove foreign VAT, only if the store does. European shelf prices include VAT. Some stores take it off for non-EU buyers at checkout, and some don't. A per-store flag says which; the default is to keep VAT in, because most shops charge the full price to international buyers. Getting this wrong over-states or under-states a price by about 20%.
- Convert to the buyer's currency. Some stores quote a different price per country (they re-price at checkout). When we have captured that store's own price for Canada, we use it directly and skip conversion.
- Shipping. The store's published rate for that destination if we have one, otherwise a weight-based estimate. Every estimate carries a flag, so the page can print a "~" instead of pretending to know.
- Duty. A rate chosen by product type and destination, applied to the item value. For Canada the base excludes shipping (customs value is freight-free); for the EU and UK it includes it. There is a low-value threshold below which no duty is charged (CA$20, or CA$150 for a courier parcel shipped from the US or Mexico).
- Brokerage. A flat CA$25 allowance when the parcel is dutiable.
- No customs inside one zone. An EU store to an EU buyer pays no duty and no brokerage.
Destination sales tax (GST/HST) is deliberately not in the total, because the store or the courier collects it and adding it would double-count against what the buyer sees at checkout.
Worked examples (pulled 2 November 2026, Canada view)
These come from https://allr.io/api/product-detail?id=<slug>&country=CA. Rates used: 1 EUR = 1.5625 CAD and 1 USD = 1.4000 CAD (printed on allr.io/price-index that day).
Shoei RF-1400 helmet, three stores
| FC-Moto (Germany) | RevZilla (US) | RidingGear.ca (Canada) | |
|---|---|---|---|
| Sticker price | 317.40 EUR (that store's cheapest colourway that day) | 549.99 USD | 715.00 CAD |
| VAT removed (19%) | 317.40 / 1.19 = 266.72 EUR | no | n/a |
| Item in CAD | 416.75 | 769.99 | 715.00 |
| Shipping | 43.10 | 48.99 | 0 |
| Duty (helmet, 8.5% of item) | 35.42 | 65.45 | 0 |
| Brokerage | 25.00 | 25.00 | 0 |
| Landed | 520.28 | 909.42 | 715.00 |
I re-did each line by hand from the sticker price and the two rates above, and they match the API (the line items are rounded, so they can add up to a cent off the total). The German store wins here even after duty and brokerage, because the VAT comes off. The same helmet is CA$909.42 from the US store, because the US sticker price converts to CA$769.99 and there is no VAT to take off.
Alpinestars Supertech R boots: the Canadian store wins
FC-Moto: 479.36 EUR gives item CA$629.41, plus 43.10 shipping, plus 18% duty (113.29), plus 25 brokerage: CA$810.81. RevZilla: 649.95 USD gives CA$909.93, plus 48.99, plus 163.79 duty, plus 25: CA$1,147.70. Bayside Performance, in Canada: CA$699.95 with no shipping or duty. The cheapest "sticker" in a foreign currency is not the cheapest purchase.
Cheap items: where the fixed fee hurts
A pair of Alpinestars Radar gloves. A US store (BTO Sports) lists 16.77 USD, which is CA$23.48; the parcel is under the courier relief threshold, so no duty and no brokerage: shipping 11.19, landed CA$34.66. A Spanish store re-prices to CA$34.99 for the item, adds 17.17 shipping, 18% duty (6.30) and the CA$25 brokerage: CA$83.46. The brokerage allowance alone is more than the glove. This is why a "cheapest sticker price" ranking misleads on small items.
A re-pricing store. The Dainese Racing 5 leather jacket at Motardinn (Spain) is listed at 303.99 EUR, but the item value used is CA$501.49, the store's own Canadian price, not a currency conversion of 303.99. Duty is 13% (leather jacket) on that: CA$65.19, landed CA$615.50.
What this does not know
- Country of origin. Duty depends on where a garment was made, which stores almost never publish. I use the standard rate for the product type, which suits Asian-made gear. EU-made gear can qualify for lower or zero duty under a trade agreement, so for some items the estimate runs high.
- Shipping is sometimes a model, not a quote. The flag exists so the page can say so.
- Brokerage is one fixed allowance, not a courier's actual fee.
- Sales tax is excluded on purpose.
- The retailer's own checkout is the final word. Every price carries an observed-at time.
Daily price history, and the FX trap
Since 2026-10-24 the site keeps one snapshot per day: for each product, each store's lowest in-stock new price in the store's own currency, plus that day's exchange rates. The chart on a product page converts into the viewer's currency using each day's rates.
That conversion is the trap. A product priced in euros moves a few tenths of a percent against the Canadian dollar every day, so a chart of the converted price wobbles even though nobody changed a price. In week one the first version showed fake "price changes" on products whose store price had not moved. The fix has three rules:
- A change is real only if the store or its own-currency price changed. Each day keeps the low listing's store, its native price and its currency. Two days differ if the cheapest store switched, the currency changed, or the native price moved by half a cent or more. FX-only movement is not a change.
- Fall back to the converted price only for days that have no native data.
- A noise floor of 1%. Differences under 1% of the usual price count as "the same price" for labels like "below its usual price" (real sales are 10% or more), and the chart's y-axis never zooms tighter than +/-5% of the median, so cents of FX drift look flat.
The tests were built from week-one data. For the Shoei RF-1400 at a euro-priced store, the converted daily low ranged from CA$495.38 to CA$498.16 over eight days, about 0.6% of the price, and the series now counts 0 price changes. For a GT-Air III the cheapest store switched to another one that day (CA$809.52 to CA$844.23, about 4.3% dearer) and switched back, which counts as 2 changes, correctly.
On 2 November the live page for the RF-1400 read "Unchanged for 10 days", usual price CA$496, 0 price changes. As a cross-check: the history is of the item price before shipping and duty, and 317.40 EUR x 1.5625 = CA$495.94, which rounds to the CA$496 on the page. (The landed price of CA$520.28 above is a different number: the history shows the sticker price, not the landed one.)
Ten days is not much history, and I won't claim a seasonal pattern from it. It will be about five weeks by Black Friday.
Using it from an AI assistant
The same data is available over a public, read-only MCP endpoint at https://allr.io/mcp (five tools, no authentication; docs at allr.io/developers). It is the same landed-cost function behind it.
If you've solved this differently, for example handling country of origin properly, I'd like to hear how.
Links: landed cost explained, duty calculator, method, price index.
Originally published by Dev.to WebDev. Aggregated on AIWithGhost for educational purposes — full credit and traffic to the original publisher.