Trupari

The Trupari Score — Methodology

Version 3 · Effective August 2026 · Transparent by design

Trupari is an FX rate benchmarking service. It compares a quoted or published exchange rate with a mid-market reference, naming that reference's source and observation date, and with peer providers. Its Consensus Engine reconciles an institution's own rate feeds into one defensible rate.

The Trupari Score is a single number answering one question: how competitive is this published FX rate, right now? Higher is more competitive for the end customer. It runs 0–100 when peer rates are tracked on the corridor, and 0–70 when they are not — because without peers only half the question has data behind it. The scale is always stated alongside the number.

Most scoring systems are black boxes. This one is deliberately not: the complete formula is on this page, every input is visible in your dashboard, and you can recompute your own score by hand. If we ever change the formula, the version number above changes — historical scores are never silently re-graded.

1. The inputs

The score uses exactly two measurements, both shown in your dashboard:

InputMeaning
margin_vs_mid (bps) How far your published rate sits below the mid-market reference rate. That reference draws on FRED, the ECB, Wise, treaty-fixed pegs and open reference data, but it is a priority chain, not an average: each source fills only what the earlier ones left blank, so a given corridor resolves from one source rather than a consensus of several. See where the mid comes from. Positive = margin kept; 0 = pricing exactly at mid-market. 100 basis points (bps) = 1%.
vs_best_peer (bps) How your rate compares to the best competitor rate tracked on the corridor. Positive = you beat the best peer; negative = you are being undercut.

2. Component A — mid-market proximity (0–70 points)

A = 70 × clamp(1 − margin_vs_mid / 600, 0, 1)

3. Component B — peer standing (0–30 points)

B = 15 + 15 × clamp(vs_best_peer / 100, −1, 1)

What vs_best_peer is measured on

“How far you are from the best peer” is not one quantity. It can be measured on the published rate, or on the amount received after fees, and for the same two providers those give different answers — a provider who moves margin out of the rate and into a fixed fee looks better on rate and worse on amount. Both are legitimate, and this product uses both. Which one applied is published with every score, as peer_basis.

peer_basisWhereWhy that one
rate the Trupari product — your dashboard, the API and the audit export You supply a competitor's published rate. We do not ask you for that competitor's fee and you generally do not know it, so the rate is what there is. vs_best_peer is a ratio of two rates.
amount_received the public Rate Index That document reads a comparison API and therefore holds every provider's fee. Ranking on the headline rate there would flatter any provider who moves margin into the fee, so it ranks — and scores — on what the recipient actually gets.

This is not a footnote. On a 1,000-unit quote a 5-unit difference in fee is 50 bps of amount received, which moves Component B by 7.5 of its 30 points; a 100-unit difference saturates it and moves it by the full 15. So a score measured on one basis and a score measured on the other are not comparable, even at the same version and the same mode — see section 4. Until 4 September 2026 this page defined vs_best_peer in rate terms only, while the Rate Index measured it on amount received and published the result under the same version number. Both numbers were correct for their inputs; what was missing was any way to tell which input you were holding. Nothing was recomputed and no score moved — a field was added, carrying the basis each score was already measured on.

Component A has no such choice. A mid-market reference has no fee to include, so the margin over mid is always measured on rate alone. The worked example in section 6 uses peer_basis = rate, which is the product's basis.

4. The score

Trupari Score = round(A + B)

If no peer rates are tracked on a corridor, there is no Component B to add, and the score is Component A alone:

Trupari Score (mid-only mode) = round(A)  —  out of 70

A mid-only score is reported out of 70, not out of 100 (score_max in the API, and shown beside the ring in the product). It is a partial assessment on a partial scale, because half of the question — how you compare to anyone else — has no data behind it. Reaching Market Leading requires peer evidence: without peers there is no market to lead.

Supplying your first peer cannot lower your score. That is the point of scoring mid-only out of 70 rather than stretching it to fill 0–100. Since the score with peers is A + B and B is never negative, it is always at least the mid-only score. A rate at mid-market scores 70 / 70 with no peers, and 85 / 100 once an equally-priced peer is added — it goes up.

Once you have peers, the score does move with them — that is what it is for. B measures how you compare to the best peer you track, so adding a peer who is priced better than the ones already there will lower it. The same rate at mid-market scores 85 / 100 against an equally-priced peer, 78 / 100 once a peer 50 bps better is added, and 70 / 100 against one 200 bps better. Your pricing has not changed; the market you are being measured against has. B never goes below zero, so the score cannot fall beneath the mid-only 70, but within the peer range it is a live comparison and not a ratchet.

An earlier version of this page said flatly that adding peer data can never lower your score. That is true only for the first peer, and we have corrected it.

Market Leading is capped out of mid-only mode. A perfect 70 / 70 imputes to 85 on the 0–100 column, which is the Market Leading threshold; we hold it at Strong instead. Claiming market leadership requires a market to lead, and a corridor with no tracked peers has not shown us one. This is a deliberate ceiling, not a rounding artefact — it is the one place where supplying your first peer can raise your band as well as your score.

This changed in v2 (August 2026). Version 1 rescaled the mid-only score to fill 0–100, which produced a number that looked like a peer-mode score, was not on the same scale, and moved the wrong way: the same rate at mid-market scored 100 with no peers and 85 as soon as one equally-priced peer was added. Further down the range the band fell too — 90 Market Leading became 78 Strong at 60 bps. Supplying competitor data, which is exactly what this product asks you for, looked like a downgrade. It no longer is.

Scores remain comparable within one version, one mode and one peer basis. Every stored score records all three (score_version, score_peer_adjusted, and peer_basis — see section 3) and all three travel with the audit export, so a history crossing any of those boundaries can be seen to have crossed it. Snapshots taken before August 2026 carry no mode and are marked unknown rather than assumed — whether a corridor had peers at the time cannot be recovered afterwards, since peers can be added or removed later. v1 scores are never recomputed under v2; re-grading a historical number under a formula that did not exist when it was captured would be a fabrication.

5. Bands

Score (with peers, out of 100) Score (mid-only, out of 70) Band
85–100not reachableMarket Leading
70–8455–70Strong
50–6935–54Average
30–4915–34Lagging
0–290–14Weak

Why a mid-only score has its own column (new in v3). Until v3 both modes were read off the single 0–100 column, so a mid-only 69 / 70 — 98.6% of everything that could be measured — was labelled Average, and 49 / 70 was labelled Lagging. The number was right and the word was wrong.

The mid-only column is not a rescaling. It is the band you would be in with one peer priced exactly like you: the unmeasured peer component is filled in at its neutral value (15 of 30, which is what Component B pays for matching the best peer), and the total is read off the 0–100 column as usual. So the two columns line up exactly — adding a first peer at parity leaves your band where it was, because the assumption we made turned out to be right.

We considered simply scaling the mid-only score up to 100 and rejected it. Written out, that rule fills the missing peer component with a copy of Component A — it infers your standing against competitors from your distance to mid-market, which are different measurements, and counts good mid-market pricing twice. It also has a concrete failure: it moves a band down when a first peer priced identically to you is added, across margins between roughly 133 and 184 bps — a range that contains the worked example in section 6 of this page.

Peer data that disagrees with that assumption can lower your band, and should. If the first peer you add is priced better than you, your mid-only band was standing on an assumption the evidence has just contradicted, and the label corrects. Your score still cannot fall (section 4) — what changes is the scale it is being read against. A peer at parity or worse never lowers the band.

The band is one of two labels on a corridor. The other is the verdict — a direct comparison against the best peer you have supplied, on its own scale, with its own boundaries. See section 14.

6. Worked example

You publish 55.00 on USD→PHP. Mid-market is 56.00. Your best tracked competitor offers 55.50.

margin_vs_mid = (56.00 − 55.00) / 56.00 × 10,000 ≈ 178.6 bps

vs_best_peer = (55.00 − 55.50) / 55.50 × 10,000 ≈ −90.1 bps

A = 70 × (1 − 178.5714285714/600) = 49.1666666667

B = 15 + 15 × (−90.0900900901/100) = 1.4864864865

Score = round(49.1666666667 + 1.4864864865) = round(50.6531531532) = 51 → Average

Carry A and B unrounded. The final step rounds once, at the total. Rounding A and B first puts the error on both sides of that single rounding, and it can cross it. Every figure Trupari publishes for A and B is exact for this reason.

Reported precision is limited by the reference's age. The score is computed from the exact figures above. The margin we publish is rounded to a precision the mid-market reference can actually support: a tenth of a basis point when that reference is dated the current business day, whole basis points otherwise.

The reason is arithmetic, not caution. The MATCHING band is ±5 bps wide, and one ordinary 40 bps session moves the reference by eight times that. A tenth of a basis point quoted against a two-day-old mid states a precision the input does not have.

“The current business day” is a date, not a number of hours — clarified 12 September 2026. This paragraph used to say “observed the same day”, which reads as though the reference’s age were measured in hours. It is not. Most references publish a date and no time — the ECB’s daily fixing and FRED’s series among them — so the age we can state honestly is counted in business days, by date, in UTC, and the rule uses that count. Two consequences follow, and both are deliberate. A fixing counts as the current day’s from the moment it is published until midnight UTC — about ten hours, for the ECB. And Friday’s fixing counts as current through the weekend, while the market it measures is shut, until Monday begins in UTC.

The market can move inside that window, so a tenth says the margin was computed against the current day’s reference — not that the market is still where that reference put it. How old the reference is sits beside every verdict, and that is the figure that answers the second question. We kept the rule rather than switch to an hourly one because the same rule sets the precision of the competitor comparison below, and the verdict is decided on that published figure: publishing it more or less finely would move verdicts that sit on a band edge, with no change in any input.

So in this example margin_vs_mid is 178.6 bps as arithmetic, and is reported as 179 bps unless the reference is dated the current business day. The observation age is published beside every verdict, not only when it crosses our staleness threshold.

Corrected 5 September 2026: do not round A and B either. Until today this example showed A and B to one decimal place and added those, and the product published them rounded to two. The score is round(A + B), which is a cliff, and a rounded input lands on both sides of it. Swept across 1.1 million margin and peer combinations: reconstructing the score from A and B at one decimal place differed from the published score on 5.2% of them, and changed the band on 0.3%. Worked through: a margin of 4.17 bps against a peer 0.1 bps ahead gives A = 69.5 and B = 15.0 to one decimal place, which sums to exactly 84.5 and rounds up to 85, Market Leading; the exact total is 84.4985 and the published score is 84, Strong. The example above now carries the full figures, and the components in every Trupari artefact are published unrounded, so the addition works. No score changed and the version did not move — what changed is that the numbers we hand you are the ones the formula used.

So recompute from the rates, not from the rounded margin. The score is computed from your rate and the reference rate — exactly the two numbers this example starts from, and both are published beside it, at a precision that reproduces the score exactly. margin_vs_mid is that same quantity rounded for display, so starting from it reproduces the score almost always, and not quite always. Swept across every margin from 0 to 2,000 bps: recomputing from the published rates reproduced the published score on every one; recomputing from the published margin differed by a point on 0.90% of them, and 78 of those changed the band as well. Worked through: a margin of 132.51 bps publishes as 133 bps and scores 55, Strong; 133 by hand gives 54, Average. The rounding above is honest and stays — but a display is not an input, and until 2 September 2026 this page did not say which was which.

The same rule now applies to the competitor comparison, on that rate's own age. vs_best_peer is reported to a tenth of a basis point only when we recorded the competitor's rate on the current business day — by date, in UTC, as above — and to whole basis points otherwise — including when we were never told the date, because a tenth of a basis point is a claim we cannot make about an observation we cannot date. deviation_bps_vs_competitor_precision carries which applied, exactly as it does for the mid.

This is measured on the peer's age, never on the mid's: the two are separate observations and are reported separately. A competitor rate you entered months ago sits beside a mid-market reference from this morning, and until August 2026 the product treated the second as current and said nothing at all about the first.

How old the competitor rate is

A competitor rate is one you enter, and we record when you last entered it. Every benchmark now publishes that alongside the comparison: peer_observed_at, peer_age_business_days, and peer_stale. Past three business days the rate is labelled stale on the card.

Three states, not two. peer_stale is true, false, or null — and null means we hold no date for that rate, which is not the same as knowing it is current. It is never reported as false on that basis.

Why it matters more than it looks. The peer component of the score reaches its floor and its ceiling at ±100 basis points — one percent — which a major currency pair can cover in a few days. A competitor rate left un-updated for long enough will therefore pin that part of your score at 0 or 30 through nothing but market drift since you typed it. We flag it; we do not refuse to score it, and the mid-market half of the analysis is unaffected either way.

peer_count is how many competitor rates were considered. The verdict is decided by exactly one of them — whichever offers the best rate to the end customer.

When two competitors quote exactly the same best rate, the comparison is made against the one recorded most recently, because a fresh record of that rate is the evidence that it is current; if they were recorded at the same moment, against the first by name. It matters, because that competitor’s date decides how precisely the figure is published and whether it is labelled stale, and the verdict is read off the published figure. A higher rate always wins, however old it is. Until 14 September 2026 the tie went to whichever row our database happened to return first, so the same two competitors could produce MATCHING on one reading and COMPETITIVE on the next.

How old your own rate is

Your published rate is the other number you type, and it ages on the same terms. Every benchmark publishes when we last recorded it: our_rate_observed_at, our_rate_age_business_days, and our_rate_stale — and every saved snapshot stores all three as they stood when it was taken. Past three business days the rate is labelled stale on the card, in the Slack card, in the briefing and in the export. The same three states apply: null means we hold no record of when it was entered, which is not the same as knowing it is current.

Why this matters for your history. A snapshot is recorded every day from the rate you last entered. If your published rate has genuinely not moved, the margin in those rows really did move with the market, and the history is right. If it has moved and you have not updated it here, the rows are measuring an old number against a new mid. Until 12 September 2026 nothing on a row could tell those two cases apart; the stored age now does. We flag it; we do not refuse to record it, for the reason given above for competitor rates. Rows recorded before that date carry no age, and are shown as not recorded rather than assumed.

When a day’s row is taken. At or after 16:00 UTC: the first time we benchmark the corridor after that moment, whether because you opened the dashboard or because our own sweep did, and reloading the dashboard does not retake it. 16:00 UTC is after the ECB publishes its daily reference rates (about 14:00 UTC in summer and 15:00 UTC in winter), so each row measures that day’s fixing; the rate table we price from is rebuilt at 16:00 UTC for the same reason. Before then your dashboard still shows a live benchmark, but nothing is written to the day’s history. A Saturday or Sunday row measures Friday’s fixing, which is the latest there is, and keeps its tenth of a basis point under the date rule above. If the ECB publishes late, a row can still measure the previous fixing; it then carries that fixing’s own date, and the precision that goes with it.

Corrected 14 September 2026. Until this change a day’s row was taken at the first benchmark after midnight UTC — usually within six hours of it, and hours before the ECB published. Every ECB-referenced row therefore measured the previous business day’s fixing; the Saturday, Sunday and Monday rows all measured Friday’s; and under the date rule above, weekend rows were stored at a tenth of a basis point while weekday rows were stored at whole basis points, the reverse of what the rule is for. A rate saved in the afternoon re-recorded that day on the day’s own fixing, so one history could mix the two. Rows recorded before this change are not rewritten; each still carries the observation date of the reference it measured.

What records today again. After 16:00 UTC, today’s row is recorded again, whole, at the moment you:

Changing only your fee does not. The fee is not part of the benchmark or of the row, and a save that changes only the fee does not re-date your rate either. Before 16:00 UTC there is no row for today yet, and the capture will use whatever you have saved by then. Earlier days are never changed. If a new rate cannot be scored — entered the wrong way round, say — today’s earlier row is kept rather than deleted.

Corrected 14 September 2026. A save used to record today again only when your rate or fee changed. So saving the same rate again, to confirm it, left today’s row describing that rate as stale on the day you confirmed it; changing only the fee re-recorded the row although nothing in it depends on the fee; and adding, changing or removing a competitor did not reach today’s row at all, only the next day’s. Before 12 September 2026 the first reading of the day was always kept, so a rate corrected mid-morning did not reach that day’s row.

7. Versioning

This is methodology v3, effective August 2026. Any change to the formula, its anchors (600 bps, ±100 bps), or the bands produces a new version number. Scores recorded under one version are never re-labelled or recomputed under another, so historical comparisons stay honest.

September 2026 — peer_basis published, and the version deliberately did NOT move. Component B's input can be measured on the rate or on the amount received (section 3), and until this change nothing said which a given score had used. The rule at the top of this section is the test: the formula is unchanged, both anchors are unchanged, the bands are unchanged, and no score moved by a single point — each artefact went on computing exactly what it was already computing. A version bump would have implied a recomputation that did not happen, and would have made every stored score look as though it needed re-reading. What was added is a field recording the basis each score was already measured on. Scores stored before the change are all rate: the amount-received basis has only ever existed in the Rate Index, which does not write to the history this product stores.

v2 → v3: no score changed. Component A, Component B, both anchors and the arithmetic are identical, and a score computed with peer rates carries exactly the band it carried under v2. What changed is that a mid-only score is no longer read off a table it cannot reach the top of — see section 5. The version number moves because the published label moved, and the label is what you read. Scores already stored keep the band they were given.

v1 → v2: the mid-only score is no longer stretched to fill 0–100. It is Component A alone, out of 70. Nothing else changed — the anchors, the peer component and the bands are identical, and a score computed with peer rates is unaffected. See section 4 for why.

8. What the score is not

The Trupari Score measures published rate competitiveness only. It does not account for transfer fees, delivery speed, payout options, or service quality, and it is not financial advice, a rating of any company's solvency or conduct, or a recommendation to transact with any provider. Rates change continuously; any score reflects a moment in time. See our Risk Disclaimer and Terms of Service.

It is not a measure of your profitability

Component A awards full marks at mid-market or better — that is, at zero FX margin or less. So the score's optimum is a business earning nothing on the FX leg. That is not an oversight and it is not a recommendation. The score answers “how good is this rate for the person receiving the money?”, which is the only question a published rate can answer on its own. It is deliberately silent on whether that rate is good business for you.

So we report the other axis beside it, and always have it in view. Every score is accompanied by FX margin retained — the same figure as margin_vs_mid, at the same precision, named from your side of the table. The two move in opposite directions: the score rises as the margin falls. Reading either one alone will mislead you, which is why the product does not show you either one alone.

The score stops discriminating at mid. Component A is clamped at full marks from 0 bps downward, so a business pricing exactly at mid-market and one pricing 40 bps through it both score 70 / 70. Nothing in the score distinguishes them. The margin figure does, and the product marks that region explicitly rather than leaving a flat score to imply the two are equivalent.

We deliberately did not fold margin into the score. Doing that would require us to decide what level of margin is acceptable — your business, not ours — and it would make every score recorded before the change incomparable with every score after it. Reporting a second number costs you nothing and assumes nothing.

Both components are measured on rates, and neither sees fees

In your dashboard, Component A and Component B are both computed from rates: your rate against the mid-market reference, and your rate against the best peer rate you track. Neither includes transfer fees, because we do not have your peers' fee schedules. A provider who moves margin out of the rate and into a fixed fee will therefore score better than the customer's experience warrants, and that is a limit of the score rather than a finding about the provider.

Our published Rate Index readouts are different, and say so on their face: there the peer comparison is computed on the amount actually received, fees included, because a tighter rate with a larger fee is not a better deal. Each figure in those documents is labelled with the basis it was computed on. Do not compare a dashboard peer gap with a Rate Index peer gap and expect them to agree — they are measuring different things, and we would rather say so than let the two look interchangeable.

The live provider comparison in your dashboard is a third basis again, and it moves with the amount. The panel headed “Peer rates” — sourced from Wise’s comparison data — ranks providers by the amount their recipient actually receives, fees included. Because those fees are largely fixed, the ranking depends on how much is being sent: a 5-unit fee costs 50 basis points on a 1,000-unit transfer and 0.5 on a 100,000-unit one, so the order can genuinely invert with ticket size. A provider that wins on a small transfer can lose badly on a large one. Quotes are computed on a 1,000-unit transfer unless you name another amount, the panel always states the amount it used, and you can quote your own ticket size to see the order that applies to your business. These providers are shown for context and never enter your Trupari Score; only competitor rates you add yourself are scored.

These quotes have an age too, and it is measured in trading time. Wise collects each provider independently and continuously rather than at a fixed hour, so a quote in that panel is not a daily fixing and does not age in days. Each one carries how many business hours old it is, and past 48 of them the row says so. Weekends are not counted, for the reason given in section 11: what decays is not the provider’s price but how far the market has travelled since the quote was captured, and it travels only when the market is open. Where Wise gives us no capture time the age is reported as unknown, never as current.

Corrected 4 September 2026. Until that date this was the only one of the product’s staleness rules on the customer’s side of the product that was not on this page, and it was measured on the calendar. A quote captured on a Friday afternoon and read on the following Monday morning is more than 48 calendar hours old — sixty of them — so the panel marked the entire Friday capture every Monday, for a market that had not moved in between. That is the failure section 11 describes and this product avoids everywhere else. Nothing was excluded or re-ranked on the strength of that mark; it is a label, and the label was wrong roughly one day in five.

The published Rate Index uses the same clock, with a tighter window. Its readouts rank providers inside a like-for-like subset — quotes captured close enough together that the ranking measures pricing rather than the market moving underneath it — and that subset is everything captured within 24 trading hours. Weekends are not counted there either. The window is deliberately tighter than the 48-hour staleness mark above, because the two answer different questions: 48 asks whether a quote is too old to show, 24 asks whether a set is tight enough to rank across. Both numbers, and the clock they are measured on, are published inside every issue’s downloadable capture file.

Corrected 5 September 2026. The note above said this product had three staleness rules. It had four: the Rate Index carried its own, and that one was still on the calendar. So a quote captured on a Friday counted as more than 24 hours old from Saturday morning onwards, and any issue generated over a weekend lost its entire like-for-like subset — falling back to the full table, which spans days and which we label as not like-for-like. On the August GBP/INR set that is the difference between naming ten providers with a 450 bps range and naming eighteen with a 533 bps range, from the same quotes, with nothing having moved. Neither published issue was generated at a weekend, so no issue shipped with the wider figure; what was wrong was that it depended on the day of the week somebody ran the generator. Every hour figure in a capture file is now labelled as trading hours, and the capture schema is versioned capture/2 so the two generations cannot be read as the same measurement.

9. Which corridors can be scored

A corridor can only be scored when a reliable public mid-market reference rate exists for it. That covers freely-traded currencies, plus currencies a regulated provider actually transacts at a published mid-market rate. Corridors in managed or parallel-rate currencies — for example the Zimbabwean dollar (ZWL) or Argentine peso (ARS) — often have no single reliable mid-market rate; for those, Trupari shows “no mid-market data” rather than a number it cannot stand behind. We would rather cover fewer corridors honestly than quote a figure we can’t defend.

10. Where the mid comes from

The mid-market reference is built by a priority chain, not a consensus. Each step fills only the pairs the earlier steps left blank — or left stale (see below) — so no step can overwrite a more authoritative, current one:

OrderSourceRole
1FRED, ECBOfficial daily reference rates. Averaged only where both publish the same pair.
2ECB consistency passPins every ECB-covered currency’s USD pair to the ECB fixing, removing FRED’s one-day lag (worth ~24 bps on GBP/USD).
3Treaty-fixed pegsExact constants fixed by law (e.g. XOF, XAF).
4WiseTransactable mid-market rates, including corridors the official sources do not publish.
5BISInstitutional, but ~3 days behind, so it sits below a live transactable rate.
6Open reference dataRemaining freely-traded currencies.
7TriangulationPairs neither quoted directly nor pegged are derived through USD or EUR.

What that means in practice. The sources overlap far less than the list suggests. Measured across the whole published table on 31 August 2026 — 22,053 pairs:

These figures moved between 15 and 31 August 2026, and the page said so late. The earlier measurement read 83.5% triangulated and 6.6% pegged, and it was quoted here as current for two weeks after it stopped being true. The cause is the peg change of 29 August: treaty parities are now re-derived from their anchor before cross-pairs are built, so roughly 1,400 corridors that used to be triangulated are now published as the pegs they always were. Triangulation fell by about the same number it gained. Nothing about what we publish got weaker — a peg is the more exact statement of the two — but a dated measurement quoted as current is a claim about today made on a fortnight-old table, so this figure now carries the date it was taken.

How the reference rate itself is published, and a correction of 2 September 2026. The mid is published to twelve significant figures — significant figures, not decimal places. It used to be rounded to six decimal places, which is a sensible precision for a rate near 1 and a poor one below it: on a corridor quoting around 0.0000381 that rounding moved the published reference by about 26 basis points, and a customer recomputing their margin from it could get the wrong sign — 10 bps of margin kept, recomputed as 16 bps priced through mid. No score, deviation or verdict was ever affected: all of them are computed from the unrounded value, and the corrected figure changes nothing but the digits we show you. Scores already stored keep the reference rate they recorded.

So: multiple sources feed the table, but any one corridor you look at is almost always resolved by one of them. We would rather say that plainly than let “multi-source” imply an agreement between sources that is not being computed. Where a corridor’s reference comes from a provider that also appears as a peer — Wise, most often — we disclose it at the point of comparison: every result names the reference’s source beside the mid-market figure, and that provider is marked wherever it appears as a competitor — on the corridor card, in the peer list, in the free rate check, on the Slack card, in the briefing, and in the XLSX export’s “Best competitor is our reference source?” column. Until 12 September 2026 the paid dashboard named the source only when the reference was stale or undated, which left the ordinary case undisclosed; the free rate check has always named it. Until 14 September 2026 the competitor itself was marked on the card and the peer list only, while this page already said “wherever”.

Where two routes exist, they agree — and that is by construction, not by luck. A pair reachable through USD and through EUR has two triangulated values. On our table they are the same number, because step 2 above re-derives every ECB-covered currency’s USD pair from the ECB’s own EUR fixing. Written out, the USD route between two such currencies is (EUR/Y ÷ EUR/X) and so is the EUR route: one expression, so there is nothing for them to disagree about. Measured across a table of 49 currencies, 2,070 corridors had two routes and the widest disagreement between them was 4×10−12 basis points — floating-point noise, not a price difference.

We say this plainly because the alternative would be to imply we are catching disagreements that cannot arise. A second route only becomes an independent check when its legs come from a different basis, and the consistency pass exists precisely to stop that happening — an internally inconsistent table was the defect it was built to fix. Where a corridor genuinely does resolve through more than one basis, the engine computes every available route, publishes the one built from the freshest legs — ties broken by anchor order, and recorded — and reports the gap as route_spread_bps. The published number is always one of the routes exactly; we never average two disagreeing sources into a figure neither of them quoted. Where only one route exists the spread is reported as unavailable rather than as zero: those are different statements, and neither of them is corroboration.

A directly quoted pair is checked against the cross the table implies — corrected 12 September 2026. The paragraphs above are true of triangulated routes, and they left out the comparison that can actually disagree. About one pair in eleven is quoted directly by a live source (Wise, most often) and is published as quoted, because a direct quote outranks a triangulation in the chain. The legs it would otherwise be built from come from the ECB’s daily fixing, observed at a different time. Measured on 12 September 2026, GBP/INR as quoted was 15.8 bps from GBP/USD × USD/INR out of the same table — three times the MATCHING band — and nothing measured it. Now the engine multiplies the implied cross out beside every such pair and publishes the gap as route_spread_bps, with the direct quote still the published rate. Above the same 25 bps tolerance it is flagged (mid_route_spread_exceeded) on the corridor card, in the Slack card and in the briefing. A comparison that would agree by construction — an ECB quote against legs pinned to that same ECB fixing — is not computed, because a zero there would be the corroboration the paragraph above says we will not claim.

11. Staleness — how old a rate is allowed to be

Priority assumes currency. A source that has not published for a week is not more authoritative than one that published this morning; it is just older. So a value whose upstream observation is more than three business days old loses its place in the chain to any source that can show a genuinely newer observation.

Why this exists: on 15 August 2026 the ECB feed was briefly unreachable and the chain fell through to a US Federal Reserve series whose latest observation was five business days old. On GBP/USD that value sat about 29 basis points away from the ECB fixing, while a same-day rate for the same pair was already available further down the chain. Nothing in the product said so. Now the fresher rate is used, and anything genuinely stale is labelled.

12. The consensus grid — AgreementScore and dispersion

There are two scores in this product, and until now this page documented one of them. The Trupari Score, above, grades a published rate against the market. AgreementScore grades something else entirely: how much the sources behind a rate agree with each other. It appears in the consensus grid and in every engine export, so it reaches customers, and a number that reaches customers belongs on this page. If it were not worth documenting it would not be worth exporting.

AgreementScore = dispersion × corroboration × liveness

Dispersion — how far apart the sources were

dispersion = 1 / (1 + DispersionBps / 50)

DispersionBps is a column in your export, so this is recomputable by hand from data you already hold. Read the value off the row and put it in the formula. What that column contains is defined precisely below — and it is not the gap between the highest and lowest source.

Where that cell is empty there is nothing to substitute, and the term is 1. That happens on a row built from a single source, which has nothing to be dispersed against; the doubt about such a row is carried by corroboration below, which pays 0.50 for the one source that produced the empty cell. It is not a dispersion of zero — see “An empty dispersion cell is not zero” further down.

DispersionBps on the rowDispersion term
0 bps1.000
10 bps0.833
50 bps0.500
100 bps0.333
250 bps0.167
1000 bps0.048

The 50 bps half-way point is the anchor: at a DispersionBps of 50 the term is exactly one half. The curve saturates rather than falling to zero, so it keeps discriminating between a wide disagreement and an enormous one instead of pinning every thin or exotic corridor at the same floor.

This page used to say spread_bps, and that was wrong. It named the input as the gap between sources, and headed this table “Sources apart by”. DispersionBps is a standard deviation, not a gap, and the two are not the same number — so anyone recomputing their score by hand from the old wording got an answer between 5% and 32% away from the one we published. The formula and every figure in the table are unchanged; what changed is that the input is now named correctly and is a column you can read. Corrected 30 August 2026.

This changed in AgreementScore v2 (August 2026). The term used to be 1 − spread/rate. Relative dispersion in FX is a number like 0.0003, so that term was approximately 1.000 on every row that ever ran through it: a quarter-percent disagreement — enormous, in FX — moved the published score by 0.013. The score was in practice a lookup on how many sources there were. It now responds to the disagreement it claims to measure.

Corroboration — how many sources actually observed it

corroboration = the table below, read on NumberSourcesObserved

NumberSourcesObserved is a column in your export, so this term is recomputable by hand from data you already hold, exactly as the dispersion term above is. Read the value off the row and look it up. It is not NumberSourcesUsed, which is the column beside it and a different number — see below.

NumberSourcesObserved on the rowFactor
00.00
10.50
20.75
30.90
4 or more1.00

A single source cannot corroborate itself, so it is capped well below a genuine multi-source agreement however confident it looks.

Carried-forward values do not corroborate anything, and NumberSourcesUsed is the wrong column for this term. When a source publishes nothing on a given day, Data Repair may carry its last value forward for up to three days. On a day when no source reported, those carried values are what the rate is built from — that is what makes a rate available at all across a weekend or a holiday — and they are genuinely used. What they are not is a second opinion. So the grid publishes both counts, and this term reads NumberSourcesObserved, which is 0 on such a day. Before v2 the engine itself read the wrong one, and a source with one real observation went on corroborating a rate for as long as the carry lasted, which published 20% more agreement on days containing no new information.

Carried values only fill a day with no observation

On a day when at least one source reported, carried values take no part: the consensus, every outlier test and DispersionBps are computed from the values reported that day and nothing else. The carried values stay in the grid and the export, marked (carried, not used), and the row says how many were set aside in its RepairedSourcesSetAside column. On such a day NumberSourcesUsed and NumberSourcesObserved are the same number.

The day’s own readings decide even when they are refused. If the only reading of the day falls outside the plausible range, that day publishes no consensus rather than falling back on the previous day’s carried values: the warning flag says why, and the headline rate stays dated to the last day we could price.

Corrected 14 September 2026 (AgreementScore v4). Until today a carried value counted as a full vote on every day, and it could outvote the day’s only real reading. Measured on three sources, two of which missed a day while the third moved +46 bps: that day published 1.0805 — the previous day’s rate — and the one real reading, 1.0855, was disqualified as an outlier for disagreeing with two copies of yesterday. With Data Repair switched off the same file published 1.0855, and Data Repair is on by default. The day now publishes 1.0855 either way. Exports saved before today carry AgreementVersion v3; re-run the file to get the corrected rows.

Until 2 September 2026 this page named only NumberSourcesUsed. The engine has read NumberSourcesObserved since v2, but the only count column this page named anywhere was the other one — so a reader recomputing by hand substituted it and rebuilt, on paper, the exact defect v2 removed from the code. Measured then, on three sources with one of them carried: the row published 0.6864 and this page’s own instructions produced 0.8236 — about 20% high, on every day a value was carried. No published figure moved; the input is now named. (Since v4 a carried value takes no part on a day when anything was observed, so on that row the two columns are now the same number. They still differ on a day when nothing was observed, which is exactly where the difference matters.)

Liveness — whether the agreement means anything

liveness = 0.5 if LivenessSuspect else 1

LivenessSuspect is a column in your export, and it is the third and last input this formula needs. When it is true the score is halved. Perfectly agreeing frozen feeds are the signature of an upstream outage, not of a well-corroborated rate. A legally pegged corridor and a total feed failure are indistinguishable from inside the calculation, so the score is discounted in both cases, and WarningFlag beside it says which condition fired — when that flag is the one reporting it.

Read LivenessSuspect, not WarningFlag. WarningFlag carries one string chosen by a priority chain, so a row can be discounted here while the flag beside it reports something else that outranked it. Measured on three frozen feeds: the row publishes 0.45 and WarningFlag reads GlobalFlatline/PegOrOutage. Add a fourth frozen feed that is merely out of plausible range and the same three frozen feeds still publish 0.45, but the flag now reads PlausibleRangeOutlier — and a reader recomputing from the flag gets 0.90, exactly double. The condition is recorded as its own column precisely so that whatever outranks it in the flag cannot hide it. Until 2 September 2026 this page described the condition and never named the column; no published figure moved.

Dispersion, in two columns

The grid and every export carry two dispersion figures, and they are not alternatives:

ColumnWhat it is
DispersionBps The sample standard deviation of the sources used, expressed in basis points of the consensus rate: 10,000 × sd / |rate|. Comparable across corridors — 50 bps means the same thing whether the pair trades at 1.08 or 1350 — which is what StandardDeviation is not.
StandardDeviation The same figure in rate units. Not comparable across corridors — one 50 bps disagreement between two sources reads 0.00383605428794 on EUR/USD and 4.77297077301 on USD/KRW — and kept because exports and spreadsheets already reference it. Published to twelve significant figures, never to a fixed number of decimal places: a decimal place is an absolute precision, and this is a relative quantity. Until 4 September 2026 it was rounded to six decimal places, so on a corridor priced below about 0.0001 a real disagreement was published as 0.000000 — which in a dispersion column reads as perfect agreement. No rate, score or DispersionBps was ever computed from the rounded value, so nothing else moved when this was corrected.

A standard deviation is not a gap. Two consequences.

Both columns are a sample standard deviation (ddof = 1). That is a deliberate, ordinary choice — it is the estimator for a sample rather than a whole population — but it means the number does not behave the way the phrase “how far apart the sources are” suggests, in two specific ways. Both are stated here rather than left for you to discover in a spreadsheet.

1. With two sources it is the gap divided by √2. The sample standard deviation of two values is |a − b| / √2, so two sources 50 bps apart report a DispersionBps of 35.36, not 50. If you want the raw gap from the column, multiply by √2.

Two sources apart byDispersionBpsDispersion term
10 bps7.070.876
25 bps17.680.739
50 bps35.360.586
100 bps70.710.414
250 bps176.780.220

2. It depends on how many sources there are, not only on how far apart they are. A standard deviation measures spread around the mean, so adding sources between the extremes lowers it even though the widest disagreement has not moved. Holding the highest and lowest exactly 50 bps apart and spreading the rest evenly between them:

SourcesHighest to lowestDispersionBps
250 bps35.36
350 bps25.00
450 bps21.52
850 bps17.50

So DispersionBps is comparable across corridors, and it is not comparable across source counts. Sorting the column across corridors priced at 1.08 and 1350 is exactly what it is for. Comparing a two-source row against an eight-source row is comparing two different things, and the grid publishes NumberSourcesUsed beside it so you can see which you are looking at.

An empty dispersion cell is not zero. Zero means the sources agreed exactly. Empty means there was nothing to compare — fewer than two sources, or no rate that period. The two are shown differently on purpose, because reading one as the other turns “we cannot tell” into the strongest claim the column can make, on precisely the rows with the least behind them.

An empty agreement cell is not zero either. Where there was nothing to measure the cell is empty, exactly as the dispersion cell beside it already was: no rate published that period, or a rate of zero, against which a relative dispersion is undefined. Corrected 31 August 2026 — those rows previously read 0.00, which turned “we could not tell” into the strongest negative claim the column can make.

And a published 0.00 is a statement about observation, not about disagreement. Work the formula: the dispersion term saturates and stays above zero for every finite DispersionBps, and liveness is 0.5 or 1 — so corroboration is the only factor that can be zero, and a published 0.00 says NumberSourcesObserved was 0. Every value behind that rate was carried forward and nothing was independently observed that period. Where the carried values happen to be identical such a row carries a DispersionBps of 0.00 beside it, and the two columns are not in conflict: the values used agreed exactly, because they are the same carried numbers, and nobody looked. Where the carried values differ the cell shows their dispersion as usual, and the score is still 0.00 — the term it zeroes is corroboration, not dispersion. Until 2 September 2026 this page called 0.00 “the sources did not agree at all”, which is a reading the arithmetic cannot produce.

What AgreementScore is not

It is not a probability and nothing in this product may present it as one. It does not estimate how likely the rate is to be correct, it is not calibrated against outcomes, and it carries no confidence interval. It is a bounded 0–1 summary of three things we can observe: how far the sources were apart, how many of them actually looked, and whether they were moving. This is AgreementScore v4, and the version is stored with every row — in the AgreementVersion column, beside the score — so a score can always be read under the formula that produced it.

Corrected 4 September 2026: that column now exists. The version reached the live grid but not the exported file, so a saved CSV carried the number and no way to tell which formula produced it. That is not a small gap on this particular figure: the same two sources 50 basis points apart read 0.7500 under v2 and 0.4394 under v3, so two files on one desk could differ by 41% for reasons that have nothing to do with the data in them. No stored value changed and no score was recomputed — a column was added, carrying the version each row was already computed under.

v2 → v3 (September 2026): the formula is unchanged and the input is now the column named above. The dispersion term was being computed from StandardDeviation, which was at that time published rounded to six decimal places, rather than from DispersionBps, which never was. (StandardDeviation itself was moved onto significant figures on 4 September 2026 — see the column table above. That change came after this one and did not alter any score.) On corridors priced near 1 that moved the score by a unit of its last digit. On a small-denomination corridor it moved it a great deal: at a rate near 0.0001 a genuine 50 bps disagreement rounds to a standard deviation of 0.000000, and the score read 0.7500 — the highest a two-source row can reach — while DispersionBps in the same row correctly reported 35.36 bps. Recomputing by hand from the column, exactly as this section instructs, now reproduces the published number at every price level rather than only where the rounding happened not to bite. Scores stored under v1 and v2 are never recomputed; they keep the version they were given.

v3 → v4 (14 September 2026): the formula is unchanged, and a carried value no longer votes on a day when anything was observed. See “Carried values only fill a day with no observation” above. On a row that mixes reported and carried values, DispersionBps and NumberSourcesUsed are now computed over the reported values alone, so the score on those rows moves — on the case that found it, from 0.0000 beside the previous day’s rate to 0.5000 beside the day’s own. A row where every source reported, or where none did, is computed exactly as under v3, unless the day before it was one of the rows that changed: each day is checked against the previous day’s published rate, so a corrected rate is also a corrected yardstick for the next day. Scores stored under v3 keep the version they were given.

Corrected 2 September 2026: all three inputs are now named columns. v3 named the dispersion input and left the other two described but unnamed — corroboration keyed on a quantity whose column this page never mentioned, and liveness on a condition whose only visible spelling, WarningFlag, is not the one the engine reads. Both are named above, with what each of them cost a reader who substituted the obvious column instead. No formula and no published figure changed, so this is not a new version: nothing stored moved, and a score recomputed under v3 before today is still a v3 score. What changed is that the recomputation this section invites can now be completed from the export.

13. Which day is “the day”

The consensus grid has one row per day per corridor, and every source is placed on that grid before anything is compared. Two decisions are made when your file is read, both of which can move a reading from the day you wrote it on. Neither used to be stated anywhere, so both are stated here and are now reported back to you on the upload itself.

Days are UTC days

A timestamp that carries an offset — 2026-08-14 22:00:00-05:00 — is converted to UTC before it is bucketed into a day. That reading lands on 15 August, not 14 August. This is deliberate: a consensus compares several sources against each other, and aligning them on local calendar days would put a Tokyo Monday and a New York Monday — fourteen hours apart — on the same row.

A timestamp written without an offset is read as UTC. That is an assumption, and it is the one to check. The same three evening New York readings written with and without an offset land on different calendar days, so uploading one source in local time and another in UTC can put them a day apart — and where they then overlap, the grid compares one source’s Thursday against the other’s Friday and reports how well they agree. If your file states its offset, the upload now tells you how many rows changed calendar day because of it. If it does not state one, we cannot tell, and neither can the number.

Several readings in one day become one

Where a file carries more than one reading for the same day, the consensus uses the latest reading of that day, for each source separately, and the upload reports how many rows were merged. Note that “that day” is the UTC day above: a single local trading day that straddles UTC midnight is two days here, and each keeps its own latest reading.

Per source, not per row — corrected 4 September 2026. Until today the merge kept the latest row of each day and read every source off it. On a file with one column per source that is not the same rule: a later row updating one source discarded every other source’s reading for that day, because those cells were empty on the row that won. Nothing said so — the day published as an ordinary, well-formed row with fewer sources behind it. Measured on three sources all read on one day, plus a later row updating only the first, the published rate came out 20.3 basis points from the correct one and was in fact the previous day’s figure: the two discarded sources were carried forward, and the day’s one surviving real observation was then rejected as an outlier for disagreeing with them. The consensus grid published an AgreementScore of 0.00 — which section 12 defines as “nothing was independently observed that period”, on a day when three things had been. Uploads made before today may be affected; re-upload the file to get the corrected reading. Single-source files were never affected, because for them the latest row and the latest reading are the same thing.

The second half of that mechanism — carried values outvoting the day’s own reading — did not need the merge to go wrong: any day on which some sources were simply missing could produce it. It was not fixed with the merge. It was fixed on 14 September 2026; see section 12.

One consequence is worth stating on its face: a merged day’s row can now hold readings taken at different times — one source’s afternoon value beside another’s morning one — because that is what “the latest reading of each” means. That was already true across separate files, which are aligned on the same daily grid. The upload note now says so.

14. The verdict — where each label begins

Beside the Trupari Score, every corridor carries a one-word verdict about your rate against the best peer you have given us. It is the badge on the corridor card and the headline of the Slack card. Until today this page did not say where its boundaries sit, which meant the label could be read but not checked.

Published deviation_bps_vs_competitor Verdict What it says
50 bps or more aheadLeaving Margin You sit well inside the best peer’s rate.
more than 5 bps ahead, under 50Competitive You price ahead of them for the end customer.
within 5 bps either wayMatching Level pegging on this corridor.
more than 5 bps behindUncompetitive A customer comparing rates would see the peer as better priced.

Both boundaries are inclusive, and the payload carries them. Exactly 50 bps ahead is Leaving Margin, and exactly 5 bps either way is Matching. Every benchmark response includes verdict_thresholds_bps with the two figures the verdict was actually decided against, so a consumer never has to keep a copy of this table. One did keep a copy, in words, and it had drifted: it said “more than fifty basis points” about a figure of exactly fifty.

The verdict is read off the number we publish, not the one we hid. Section 11 explains that a deviation is reported to whole basis points unless the reference behind it was observed the same day. The verdict is decided on that rounded figure, so the label and the number printed beside it can never contradict each other — a rate 5.4 bps ahead of a peer recorded yesterday publishes as “5 bps” and reads as Matching, because resolving Competitive from 5.4 would assert a precision the input cannot support.

The verdict weighs exactly one peer. It is measured against the best rate among the competitors you supply — the hardest one to beat — and peer_count tells you how many were considered. A Matching verdict against one peer and against nine are different claims, and the count is what separates them. The Trupari Score in sections 3–5 is a different measurement on a different scale; the two can and do move independently.

These thresholds are a presentation choice, not a market standard. Nobody outside Trupari defines “matching” as five basis points. They are round numbers chosen to sit either side of the band where two providers are, for practical purposes, priced the same. If they are ever moved, this table and verdict_thresholds_bps move with them — and a verdict stored before the move was decided under the old ones, which is why a saved snapshot reports verdict_thresholds_bps as null rather than restating today’s.