Corridor revenue is down 4% on flat volume. Fees are unchanged, the spread is unchanged, and no pricing decision was made. The FX position accounts show small losses concentrated in a handful of transfers: all of them confirmed several minutes after they were quoted.
What a quote actually is
A quote looks like a display value. It is not.A quote is a short-dated option that you have written and given away for free. The customer may exercise it if the rate moves in their favour and abandon it if it moves against them. You hold the risk on both sides and are paid for neither.
That asymmetry is the whole problem, and it does not require anyone to behave badly. A customer who quotes, gets distracted, comes back four minutes later and confirms is exercising an option they do not know they hold. Aggregated across everyone who does this, the losses are systematic rather than random, because the transfers that do get confirmed late are disproportionately the ones where waiting helped.
The arithmetic
EUR/KES at 139.70, Arc’s spread 60 bp, so the quoted rate is 138.86.1
Quote issued at 10:00:00
Customer sends €995.09 net, receives KES 138,254.00. Arc’s margin is the 60 bp spread, booked as
revenue.fee.fx_spread.EUR.2
The market moves to 139.10 by 10:04:00
The euro weakened by 43 bp. The correct quoted rate is now 138.27.
3
The customer confirms the old quote
Arc must deliver KES 138,254.00 but can now only source it at 138.27, needing €999.88 of the customer’s €995.09.
4
The margin is gone, and then some
The 60 bp spread absorbed 43 bp of adverse move. Most of the transfer’s revenue is gone. A 70 bp move would put it negative.
Honouring a stale quote is an unhedged loss. The spread is compensation for taking currency risk for the duration of the transfer, not for the duration of the customer’s attention.
The fix is boring and load-bearing
assertUsable throws, it does not warn.An expired quote is not usable-with-a-caveat. The customer re-quotes at the current rate, which is mildly annoying and correct.The check is at execution, not at display.A quote validated when rendered and used when confirmed is validated at the wrong moment. The saga re-checks before
reserve.The related decision: the spread is a fee line
The interesting part of Arc’s quote engine is not the TTL. It is that the spread is computed explicitly rather than left implicit in the quoted rate.Burying the spread in the rate makes the margin invisible to reporting, which is precisely the number the business runs on. It also makes this exact failure undiagnosable, because there is no expected value to compare the outcome against.
The 4% revenue decline in the symptom above is findable only because the spread has its own account. Without it, the loss appears as ordinary FX noise.
The property test
One assertion covers the whole class of pricing bug:Across the full amount range, the customer is never given more than mid-market.
It is a property, not an example, generated across randomly chosen amounts and rates. It catches:
- Sign errors in the spread: applying it in the customer’s favour
- Rounding that favours the customer at small amounts, where a fixed fee and a rounded conversion interact badly
- Rate inversion errors, where
Rate.invert()is applied one time too many
The other check that belongs at quote time
While validating the quote, it is worth rejecting the impossible:If
Failing here costs nothing. Failing mid-saga means unwinding a transfer that could never have completed: compensating journals, a customer notification, and an operational event for something arithmetic could have ruled out before anything moved.
send − corridor fee − network fee is not positive, reject at quote time. A €0.10 transfer cannot carry a €0.35 fixed fee.The lesson
1
Any price you display and honour later is an option you have written
Bound it with a TTL. The TTL length is a risk decision: it is a function of the pair’s volatility and your spread, not of what feels convenient in the UI.
2
Validate at the moment of use, not the moment of display
A check that ran when the screen rendered has told you nothing about the state when the button was pressed.
3
Make every revenue component its own account
Blended revenue cannot be diagnosed. The spread having its own ledger entry is what turned an invisible 4% decline into a findable one.
4
Reject the impossible as early as it becomes impossible
Checks that are cheap at quote time are expensive mid-saga.
The settlement saga
Quote mechanics, fee ordering, and the FX spread derivation.
Back to the scenarios
All seven, by theme.