RevolutX, Good, Bad, Ugly
A technical and product review of Revolut’s crypto exchange, from building an automated market maker on it — the documented behaviour, the undocumented behaviour, the arithmetic that decides viability, and the rough edges around the product.
-
I should declare my position up front, because it colours everything below. I like Revolut’s products. I use them daily, I recommend them, and I spent years inside the company; I know what it takes to ship at that pace and I know what the inside of those decisions looks like. So none of the criticism here is from someone looking for reasons to complain. It is from someone who wants this exchange to be good, tried to build something real on it, and wrote down exactly where it held and where it didn’t.
-
Everything technical below is measured against the live venue or our own recorded tape. Where a number came from Revolut’s documentation rather than an experiment, I say so. Where a finding was later withdrawn — two were — I say that too, because the withdrawals turned out to be the most instructive part.
PART I — THE VENUE#
1. Scale and listing breadth#
From the venue’s own pair configuration:
| Category | Count |
|---|---|
| Total pairs | 453 |
| USD-quoted | 311 |
| GBP-quoted | 61 |
| EUR-quoted | 58 |
| USDC-quoted | 23 |
That is a genuinely broad listing — GBP and EUR quoting in particular is something most global exchanges don’t offer, and for a UK or EU retail user it removes an entire FX leg and its spread. It is a real advantage and it is under-marketed.
Depth is another matter. Revolut’s own volume_24h field puts BTC at roughly 44 BTC/day (~$3.4M). This is not a venue where your size is the constraint. Flow is. A 1,000-order daily budget (§5) against a $3.4M book tells you the design point: this is retail flow, not an institutional venue, and any strategy premised on volume will be disappointed.
2. The structural fact: there is no WebSocket#
Revolut X is REST-only. No streaming market data, no streaming order updates. You poll.
This is the most consequential thing about the venue. Our trader polls at 4 Hz — every 250 ms, about as fast as rate limits allow for two symbols. Measured book age in production: 190–500 ms.
Now place that against where price actually forms:
| Venue | Typical Staleness |
|---|---|
| Binance | ~0.1 s |
| Coinbase | 0.3–0.7 s |
| Kraken | ~2 s |
| Revolut X (4 Hz REST poll) | 0.19–0.5 s |
Revolut X is the slow leg. It follows the majors. So a resting quote here is a free option written to anyone watching Binance — exercised precisely when it has become mispriced, and never when it hasn’t.
We measured the consequence: 83% of our ETH fills were resting through external fair value at the moment they were hit. Our price was already wrong, by the standard of faster venues, before the trade happened.
For a maker this is the wall, and it is not a tuning problem. A WebSocket feed would change the economics of this venue more than any other single change Revolut could make. I would put it above new listings, above fee changes, above UI work.
3. The cent: the fact that decides viability#
Revolut X settles every leg to a whole cent and rounds against you in both directions.
It is not a fee. It appears in no fee schedule, no P&L, no statement line. It is pure arithmetic and at retail clip sizes it dominates everything else.
Over 705 fills ($3,382.88 traded) it took $1.1184 — 3.31 bps of everything traded — against a total equity decline of about $1.90. Roughly 60% of everything we lost was cent rounding.
Cost as a share of clip is 2 × $0.01 / clip:
| Clip | Settlement Cost, Round Trip |
|---|---|
| $5 | 40.00 bps |
| $25 | 8.00 bps |
| $75 | 2.67 bps |
| $90 | 2.22 bps |
| $125 | 1.60 bps |
| $500 | 0.40 bps |
Read that against the spreads in §8 and you have the venue’s entire economics in two tables. A £20 trade on a tight pair can be structurally unprofitable before you have any view at all — and nothing in the app tells a retail user this.
3a. The rule is asymmetric across prints#
“Rounds against the customer” is not expressive enough. From four multi-print orders (out of 541), joined against the venue’s own filled_amount:
Order Side Exact Running Total Per-Print VENUE CHARGED
e05442f5 sell 74.9900540160 74.99 74.98 74.98
2ad35612 sell 4.9900181760 4.99 4.98 4.98
c3be28eb sell 4.9903695149 4.99 4.98 4.98
6ed9f9ae buy 4.9995517344 5.00 5.01 5.00
Sells are floored PER PRINT. Buys are ceilinged on the ORDER’s running total. Sells 3/3 per-print; the buy 1/1 running-total — and per-print would have predicted a higher debit than Revolut actually charged. Across 262 single-print orders the rule is exact 262/262; nearest-cent misses 54, floor-both misses 131.
The tempting unifying hypothesis (per-print capped by quote_size) is dead: all three sells carry quote_size = NULL, being base-sized.
3b. Realised cost is well below worst case#
The table above is the worst case. Measured across the producer boundary, the realised cost was 13.6–22.3% of one cent per leg, asymmetric — about 2.4% on buys against 42.2% on sells. Don’t assume either extreme; measure on your own fills.
4. The grid will surprise you#
Per-pair tick and lot come from /1.0/public/configuration/pairs.
The trap: BTC/USD and ETH/USD both use quote_step = 0.01, base_step = 1e-8. Develop against the majors and you will conclude those are the venue’s grid.
-
Only 14 of 453 pairs have
quote_step == 0.01. -
Those two values together are correct for 5 of 311 USD pairs.
-
HYPE/USD:
0.0001/0.000001. NEIRO/USD’s tick is1e-8. -
PEPE/USD, DOGS/USD, MOG/USD and three more publish
base_step = "10"— a lot size greater than one.
We learned this twice. Quoting on a hardcoded grid gave away ~40 bps per quote on a coarse-tick pair. Worse, the venue silently truncated all 104 of our HYPE-USD order sizes because we sent a size the lot couldn’t express. No error. No rejection. The order simply wasn’t the size we asked for — and silent truncation instead of a 4xx is the single API behaviour I’d most like changed.
4a. A Python trap specific to coarse lots#
Decimal.quantize(step) matches the exponent, not a multiple:
Python
Decimal("15").quantize(Decimal("10"), rounding=ROUND_DOWN) # -> 15 (illegal on a lot of 10)
For every sub-1 power of ten the two coincide, which is why it hides until you touch a PEPE-shaped pair. Use (value / step).to_integral_value(ROUND_DOWN) * step.
4b. One thing that is uniform#
min_order_size_quote is "0.1" for all 453 pairs.
5. Rate limits, and which ones bite#
Documented: place 10/s and 1,000 per UTC day; replace 10/s with no per-day cap.
That second line matters enormously, and we confirmed it live: a replace does not consume a placement from the daily budget. So the correct pattern for a maker is place once, then PUT to move the order, and never cancel-and-replace when a replace will do.
A 1,000/day cap is one placement every 86 seconds, globally, across every symbol and side. If you plan more than a couple of symbols, that budget — not your strategy — is your limit.
Market-data limits are per-process and more generous, but they cap how many symbols you can poll usefully. Our trader’s steady state was 1.2 + N req/s for N symbols against ~4.9 granted, so the REST budget alone caps a 4 Hz trader at about 3 symbols.
One caution: exceeding these doesn’t necessarily give a clean 429. Our client-side pacer queued instead, and the symptom was staleness — order and balance ages creeping up until risk checks started refusing. Build your limiter to fail loudly.
6. Undocumented behaviour#
None of these appears in the documentation as far as I could find. They are the ones that cost time.
6a. PUT on a partially-filled order is a 422#
Plaintext
422 "Cannot replace an order that is not in the 'NEW' state"
The moment any quantity trades, the order leaves NEW and can’t be replaced. Our quoter didn’t know, so it re-sent the same PUT indefinitely: 95 such 422s in ~27 hours, 18 consecutive on one order. While it loops the side is deadlocked and the quote pinned at a stale price. The fix is cancel-and-re-place, which costs one of your scarce 1,000.
6b. orders/active lags the venue’s own acknowledgements#
The read path is not consistent with the write path. 328 recorded divergences where our state said terminal and orders/active still listed the order. Every one was preceded by a venue-attested event:
| Preceding Event | Count |
|---|---|
replaced (venue acked the child) |
308 |
cancel_ack (venue 2xx’d the DELETE) |
19 |
fill (venue’s own trade rows reached size) |
1 |
Alarm on it and you alarm constantly; trust it and re-cancel and you burn requests. Treat it as read lag — but only when your terminal state came from a venue-attested event, never a locally inferred one.
6c. Region-split books, and an auth endpoint that won’t tell you which one you have#
Revolut X serves different books to UK and EEA accounts, and the authenticated /1.0 book carries no region field. Which book you see cannot be read from the response — it must be proven.
We prove it hourly per symbol: fetch the auth book and public region=UK within the same second, then region=EEA, and compare best-bid. Auth matches UK and UK differs from EEA → verified. Auth matches EEA instead → ambiguous, and we refuse to trust the tape.
This is the single most under-documented thing about the venue, and it silently invalidates analysis, as the next section shows.
6d. public.trades_all is a different market#
This nearly reversed a strategy decision. Two recordings of “the same” BTC tape: one authenticated per-region, one from public.trades_all, which accepts no region parameter.
Arbitrated against Revolut’s own 1-minute candles over 860 minutes:
| Source | BTC Volume |
|---|---|
| Our authenticated recorder | 34.27832062 — matches the venue’s candles to 8 decimal places |
public.trades_all |
90.24 |
The venue’s own volume_24h |
44.28 |
And 798 of 1,456 prints appearing only in the public tape traded outside the UK book’s own high/low for their minute. Real trades — just not on the book we can access.
Backtest against trades_all and you are backtesting a market you cannot trade.
6e. The trade tape contains duplicates#
One partition held 939 rows for 518 distinct trades (1.81×); another 3.33× across three dates. Deduplicate by (symbol, trade_id). For a backtest this isn’t a rounding error — a duplicated print fills a resting order twice off one real trade, a fill that never existed.
7. Fees and order types#
-
Maker 0 bps, taker 9 bps. A genuinely good maker schedule, and why passive making looked attractive here at all.
-
Post-only supported. Use it, or a “maker” strategy will cross and pay 9.
-
Replace supported, and free of the daily cap (§5).
-
Spot only — no shorting, no margin. Every ask is an exit against inventory you hold. This constrains a maker more than it sounds: you cannot quote two-sided until you hold inventory, and inventory caps then throttle the bid.
8. What the books actually look like#
Our own L2 recording, three days, time-weighted (not print-weighted — trades cluster when spreads are wide, so print-weighting flatters):
| Symbol | Median Spread | Trades/Hour | $/Hour | Net of a 2.22 bps Floor |
|---|---|---|---|---|
| PYR-USD | 184.99 bps | 32.8 | 2,025 | +182.8 |
| SWELL-USD | 165.52 | 25.1 | 1,378 | +163.3 |
| HYPE-USD | 23.76 | 30.3 | 6,348 | +21.5 |
| BCH-USD | 20.21 | 13.3 | 25,110 | +18.0 |
| HBAR-USD | 10.03 | 13.6 | 6,672 | +7.8 |
| ETH-USD | 3.66 | 13.9 | 11,151 | +1.4 |
| BTC-USD | 2.14 | 21.9 | 18,643 | -0.1 |
BTC-USD’s median spread is below its own settlement cost at a $90 clip. No strategy fixes that. And we spent weeks market-making BTC and ETH — the two least representative pairs on the venue, on both grid and spread — because the symbol list was a config line nobody revisited.
Does anyone earn the spread here?#
We marked out every print at 60 s against consolidated external fair value, split our own fills from everyone else’s, and used episode collapse (same-side prints within 1 s → one notional-weighted event) plus a block bootstrap over non-overlapping 5-minute blocks.
Other makers, HYPE-USD: +8.479 bps, 95% CI [+1.607, +13.934], 36 blocks — about 4× the settlement floor.
So yes: Revolut X pays passive liquidity on its wide books. The venue is not the problem.
Our own fills over the same tape: BTC -1.809, ETH -3.477. We were consistently on the wrong side of the same market — which is §2, not the venue’s fault.
PART II — THE PRODUCT#
The exchange is the newest thing Revolut has shipped into a category with brutal incumbents, and it mostly works. What follows is where the polish hasn’t caught up with the engineering, written as someone who has been on the other side of exactly these tradeoffs.
9. The UI is flaky in ways the core app is not#
Revolut’s main app is one of the most reliable consumer products I use. Revolut X does not yet feel like it comes from the same team. Concretely, in normal use:
-
General flakiness. Views that need a refresh to settle, states that don’t update until you navigate away and back. Nothing catastrophic, and nothing that lost me money — but it is the kind of thing that erodes confidence when the screen is showing you your own money.
-
Changing display currency doesn’t work properly. For a product whose distinguishing feature is multi-currency quoting — 61 GBP pairs and 58 EUR pairs, a real advantage over global competitors — this is a strange place to be rough. The currency selector is close to the core value proposition, not a peripheral setting.
10. The bug reporting flow has bugs#
This is the one that made me laugh and then wince: the in-product bug report returned a 502.
I could not report the bug, because the bug reporter was broken.
I want to be precise about why this matters more than it looks. A 502 on a feedback endpoint is usually triaged as low severity — nobody’s money is affected, no trade fails. But the bug reporter is the sensor. When it fails, you don’t just lose one report; you lose the signal that tells you how many other things are broken, and you lose it silently. Every subsequent “we’re not seeing many reports” is then measuring the sensor, not the product.
It is the same class of failure I spent this project chasing on my own side: a check that cannot fail tells you nothing, and reads exactly like good news.
11. Limitations, collected#
Things the venue does not do, which you should know before designing around it:
Market structure
-
No WebSocket. REST polling only (§2). The single biggest constraint.
-
No market-cap or fundamental data. Tickers carry price, 24h volume, high/low and change — nothing else. Any screening on fundamentals needs an external source.
-
Spot only. No margin, no shorting, no derivatives.
-
Region-split books that the authenticated API won’t identify (§6c).
-
Modest depth. ~$3.4M/day on BTC. Fine for retail, thin for anything systematic.
API
-
1,000 placements/day — one per 86 seconds globally (§5).
-
Silent lot truncation rather than a 4xx (§4).
-
orders/activeread lag behind the venue’s own acks (§6b). -
Duplicate prints in the trade tape (§6e).
-
trades_allis region-less and therefore a different market (§6d). -
No documented replace-per-day cap — a genuine gift, but undocumented enough that I’d hesitate to build on it without confirming it yourself.
Product
-
UI flakiness and a currency selector that doesn’t hold (§9).
-
A bug-report path that can 502 (§10).
-
No indication anywhere that a small trade may be structurally unprofitable because of cent settlement (§3) — this is the one with real user-harm potential.
12. What I’d fix, in order#
If I were still inside and holding this backlog:
-
A WebSocket market-data feed. It changes the venue’s economics more than any listing, fee change, or UI work. Everything in §2 flows from its absence.
-
Fix the bug reporter. Not because a 502 is severe, but because it is the instrument you are using to decide what else to fix.
-
Return a 4xx on an unrepresentable order size instead of silently truncating. Silent correctness failures are the expensive kind — ours took 104 orders to notice.
-
Put the region in the authenticated book response. One field removes an entire class of invalid analysis, and today it has to be proven by triangulation.
-
Document the settlement quantum, and ideally surface a cost hint in the app for small trades. A user doing £20 trades on BTC is losing to arithmetic they can’t see.
-
The currency selector. It sits on the feature that most distinguishes this exchange.
None of these is architectural. Together they’d move Revolut X from “works, with sharp edges” to something I’d recommend without caveats.
PART III — METHOD#
13. Two findings we withdrew, and why they matter most#
Both were confident numbers with tight intervals that reversed a strategy conclusion. Both were wrong. If you take one methodological thing from this article, take this.
The +20.8 bps that was one sweep counted sixteen times. An ETH bucket read +20.811 [+14.009, +27.613], n=38 — on a book whose typical spread is 4 bps, which should have been the tell. 16 of those 38 prints were a single sweep inside two seconds, all at essentially one price. Treating each print as independent (pseudo-replication) with overlapping 60 s markout windows made the CI roughly 2.4–4× too tight. After episode collapse and a block bootstrap: +3.613, [-1.940, +10.413] — no edge.
The edge that was a post-hoc window exclusion. A later analysis had ETH earning +1.818 [+0.956, +2.659] “ex-crash”. Same data, same estimator, only the exclusion of one three-hour window differing:
| ETH Band | Full Sample | Ex-Crash |
|---|---|---|
[2, 3.5) |
+0.400 [-1.226, +1.863] no edge | +1.829 [+0.983, +2.655] EDGE |
[3, 3.5) |
-0.370 [-4.007, +2.475] no edge | +1.903 [+0.412, +3.227] EDGE |
[2, 3) |
+0.989 [-1.341, +3.329] no edge | +1.754 [+0.517, +3.181] EDGE |
Every cell flips. The excluded window was 7.6% of wall clock but 43.3% of all observations, and inside it the edge was -2.285. The exclusion was the finding.
A market maker cannot condition on this. We quote continuously; we don’t get to be absent for the three hours carrying 43% of the volume. If the edge there is negative, that is realised P&L.
The rule adopted afterwards: any conditioning applied to the tape must report the share of observations it removes and the estimate inside the removed set. Anything dropping >10%, or whose removed set has the opposite sign, is a hypothesis — not a robustness check.
14. If you’re starting on Revolut X#
-
Compute your cent cost first.
2 × $0.01 / clipin bps. If it exceeds your symbol’s median spread, stop. This one line would have saved us weeks on BTC. -
Never hardcode the grid. Read the pairs endpoint per symbol. Assume nothing from BTC/ETH.
-
Keep the settlement quantum and the price tick separate. They coincide for 14 of 453 pairs.
-
Budget placements, not replaces. Replace is free — design around it.
-
Prove your region. The auth book won’t tell you, and
trades_allis a different market. -
Deduplicate the trade tape by
(symbol, trade_id). -
Expect
orders/activeto lag, and classify it as read lag only for venue-attested events. -
Don’t develop against the majors. BTC and ETH are the least representative pairs here.
15. What I am not claiming#
In the spirit of §13:
-
Spread and edge numbers come from three days, seven symbols, our own recording. Enough for a bootstrap interval; not enough to be confident across regimes.
-
The
+8.479 bpson HYPE is what other participants earned, not what we earned or would earn. Our own fills were adverse on every symbol we traded. -
The settlement rule rests on 541 orders, of which 4 were multi-print. The asymmetry depends on those four. It has held since, but four is four.
-
Rate limits are as documented plus one live confirmation. I have not probed boundaries adversarially and would not on an account I cared about.
-
The region proof is a comparison, not a statement from Revolut. It says the auth book matches the UK book; it does not say Revolut considers us a UK account.
-
The product observations in Part II are my experience as a user, not systematic testing. I have not tried to reproduce them across devices, accounts, or app versions.
Written from the engineering notes of an automated market-making project on Revolut X, August 2026. Technical claims are measured against the live venue or our own recorded L2 and trade tape; documentation-sourced claims are marked. Product observations are first-hand user experience.
The author is a Revolut customer and a former Revolut engineer (Head of Python Engineering). This is written in a personal capacity, from public endpoints and a personal account, using no information that is not available to any customer.
Figures