Built over 30 crypto platforms across 12 countries But it's the kind of bug that only shows up once real traffic hits a system you built to mirror someone else's liquidity instead of your own — and it's the reason this build ended up being about a lot more than white-label customization.
I'm Yuriy Musiienko, co-founder and backend developer at Merehead. I've been writing backend code since 2008, and this is a straight account of one exchange build — a white-label centralized platform with spot, margin, futures, options, and P2P — where the client's constraint reshaped our architecture twice before we shipped.
The client came to us wanting a mid-tier CEX — something in the functional range of a BingX or a Kraken, minus the years of build time and the capital required to seed order books across dozens of pairs. We split the work into nine milestones with fixed payments per stage: platform installation and UI adaptation first ($6,000-$8,000, paid within the first six weeks), then advanced user management plus OKX liquidity integration and logging, then overdrafts, new markets and fiat deposits, public API, overnight staking, A-book futures with PnL/ROI, final testing, and a P2P module. Total budget landed around $50,000-$70,000 across roughly eight months, with a base installation window of two to four months and a six-month warranty period after launch.
The constraint that shaped everything: the client didn't have the capital or the time to build its own market — no market makers on payroll, no seed liquidity to place across BTC/USDT and ETH/USDT order books. Building that kind of depth from zero is a multi-month, sometimes multi-year problem even before you touch UI. So instead of a from-scratch matching engine on day one, we proposed something else: mirror liquidity from an established exchange and let the client's platform act as the user-facing layer on top of it.
We laid the choice out for the client as a straight trade-off, not a sales pitch. Building your own liquidity gives you full independence but costs months and real capital before the first trade executes. Mirroring an existing exchange's liquidity provider relationship gets a deep order book on day one, at the cost of an ongoing dependency you have to manage actively.
| Approach | Time to market | Upfront capital | Ongoing dependency |
|---|---|---|---|
| Build own liquidity from zero | Months to years | High (market-making capital) | None |
| Mirror liquidity via margin borrow (our approach) | Weeks | Minimal | High — clearing, borrow limits, funding rates |
We went with the second option, knowing full well what it would cost us later in operational complexity. That decision is the spine of everything that follows in this build.
The mechanics work like this. The client's platform holds all user balances in its own hot wallet — nothing custodial happens on the external exchange from the user's point of view. When a user places an order, the platform mirrors the external exchange's order book on the frontend, so the user sees real market depth. When the user actually executes a trade, the platform opens a mirrored position on the external exchange using margin borrow, executes the trade there, and settles the user instantly on our side.
Here's a real example from the flow we built: a user deposits 1 BTC and places a sell order. Our backend borrows 1 BTC on the external exchange under isolated margin, places a mirrored market sell order, and the trade executes at, say, 65,000 USDT. The user sees 65,000 USDT credited to their account within seconds. Meanwhile, our platform now owes 1 BTC to the external exchange — a debt position that has to be settled.
That debt is where the real engineering lives. We built a clearing job that runs every three hours, or immediately if the imbalance between our hot wallet and the external exchange's balance crosses roughly 65%. The clearing logic pulls our real BTC and USDT positions, compares them against what we owe externally, and transfers the minimum amount needed to close the gap — either repaying BTC debt directly from the hot wallet, or pulling USDT back the other way.
We deliberately built the imbalance check to use "total controllable balance" (available plus frozen funds) rather than just available balance, because early in design we realized that funds temporarily locked under a limit order would otherwise trigger unnecessary clearing runs — a false alarm that costs transfer fees for no reason.
This pattern — bridging depth from a bigger venue while keeping your own custody and UX — is a common shortcut for teams that can't yet justify the cost of a full exchange architecture built entirely in-house. It buys time. It doesn't buy you out of operational risk.
Milestone two shipped on schedule with the liquidity bridge live. Milestone three onward is where things got interesting, in the sense engineers mean when they say that.
The first crack was small: a calculation bug on spot orders for the USDT/RUB pair. We'd left part of the pricing math on the frontend instead of centralizing it on the backend, and a rounding error slipped through. We fixed it and pushed it to pre-prod, but the incident forced a bigger conversation: any financial calculation split between frontend and backend is a synchronization risk waiting to surface again in a different pair, under different conditions.
Then came a database incident that actually worried me. During a routine migration, not all containers got moved — a partial migration that nobody caught until transactions started arriving late or, in a handful of cases, not at all. We synced the containers, recovered the missing data, and — because a partial DB migration in a financial system is not something you patch and move on from — we isolated the database onto its own dedicated server and added caching for frequently requested data to reduce load on it going forward.
Around the same time, we started seeing 500 errors under load that made no sense from the application logs alone. It took a DevOps deep-dive to find the actual cause: our CronJobs weren't closing database connections on SIGTERM, and separately, our Spot, Wallet, and Admin workers were crashing from Out-of-Memory conditions in Kubernetes — and in both cases, the dying connections weren't being released cleanly. The pool filled up, and legitimate requests started failing.
I added a termination handler to the CronJob so it closes DB connections properly on shutdown, and we temporarily set `wait_timeout = 300` and `interactive_timeout = 300` at the database level to force-kill connections stuck in a sleep state. We're still deciding between optimizing worker memory consumption directly or simply increasing pod resources as a fallback — that decision isn't closed yet, and I'm not going to pretend it is.
Then there was the gap between pre-prod and production that cost us a release day. Everything worked on pre-prod. On production, requests from our KYC provider just weren't arriving. The cause was a misconfigured whitelist and trusted-proxies setup at the API gateway — pre-prod and production weren't network-identical, which is exactly the kind of thing that looks fine until it isn't. We aligned the whitelist across gateway and backend and re-ran QA before the next release.
KYC brought its own edge case. Our provider, when a user abandoned a verification session partway through — say, right after choosing which document to upload — didn't send a webhook for either completion or decline. The provider's own API kept returning "active" for a session that, on their side, no longer existed. Our internal status sat in "pending" for up to two hours before timing out. We considered polling the verification ID directly, contacting the provider's support, and ultimately looked at removing the "Continue Verification" button from the UX entirely rather than trust a lifecycle the provider wouldn't guarantee.
AML integration was worse, honestly. We had four providers wired in. One worked correctly. One had a roughly one-hour timeout under certain conditions. Two returned incomplete payloads — no risk category data at all. Rather than block users on a provider fluke, we made a call: when all AML providers are unavailable, the user gets an "AML on hold" status, not a "KYC fail". Those are two different things — fail is a business decision about the user, hold is an infrastructure state about our vendors — and conflating them would have cost us real users over provider flakiness that had nothing to do with them.
Smaller things piled up too: our Twilio integration was live on production but sitting on a balance of roughly $0.08, which meant we couldn't run full end-to-end SMS verification tests without topping it up first. Our referral system correctly calculated 10% of platform profit as commission, but didn't surface it as a discrete, itemized transaction — which meant users had no way to verify their own payout was correct, a trust problem dressed up as a UX ticket.
All of this — the clearing complexity, the debt management, the dependency on an external exchange's uptime and API behavior — fed into the decision that mattered most. We proposed moving away from the bot-mirrored liquidity model entirely and building a genuine order-to-order matching engine: users placing real orders that match directly against each other, with no external exchange in the loop and no synthetic borrow position to manage.
That's not a small ask. It's more development time, more testing surface, and it means the platform stops being an integration and starts being an actual exchange with its own core logic. But it removes the single biggest operational risk in the whole system — a dependency on someone else's API, funding rates, and margin rules. This is usually the point in a build where we tell a client to price out an in-house matching engine as its own line item, separate from the base white-label platform, because the two are genuinely different products with genuinely different risk profiles.
Fixing each incident individually mattered less than what we changed structurally afterward. The database moved to isolated infrastructure with caching in front of it. The CronJob termination handler and connection timeout settings closed the OOM-to-connection-exhaustion pipeline. Gateway whitelist configuration got documented and version-controlled so pre-prod and production couldn't silently drift apart again. We added centralized logging at both the database and application layers, brought in Grafana for visualization — though our WebSocket connection for real-time chart updates was still not refreshing correctly at the time of writing, and that's a root-cause investigation we haven't closed out yet either.
None of this stabilization work is glamorous, and none of it shows up on a landing page. It's the difference between a platform that survives its first real trading volume and one that doesn't. This is the layer of work Merehead typically folds into ongoing DevOps and QA support after a build like this ships, rather than treating it as a one-time delivery.
We defined pre-production load targets before final testing, split into three tiers. At target load — 50 to 100 concurrent users — the system needs to hold 20-50 orders per second (create plus cancel combined) and 250-600 total API requests per second, with P95 latency under 200ms for order creation, under 150ms for cancellation, and under 100ms for order book reads, with a 5xx error rate under 1% and zero data loss. Stress load, roughly double that, is allowed to show rising latency and occasional errors without full failure. Crash load, three to four times target, is expected to degrade badly — the pass condition there isn't "stays fast", it's "recovers cleanly once load drops".
| Metric | Target load | Stress load (~2x) | Crash load (~3-4x) |
|---|---|---|---|
| Concurrent users | 50–100 | 150–200 | 250–300 |
| Orders/sec | 20–50 | 80–150 | 200–300 |
| Total API RPS | 250–600 | 800–1,200 | 1,800–2,500 |
| Create order latency (P95) | ≤200ms | ≤500ms | degraded, timeouts expected |
| 5xx error rate | ≤1% | rising, acceptable | mass degradation expected |
On the business side, all nine milestones stayed on their original budget lines — $6,000 for the base platform, $7,500 for OKX liquidity and user management, down through $7,500 for the final P2P module — with the client paying stage by stage against a defined scope rather than one lump-sum contract. That structure is what let us renegotiate the matching-engine pivot mid-build without blowing up the whole timeline: it was priced and scoped as its own addition, not an emergency change order.
Yes, but you lose the ability to settle the user instantly with your own funds. Margin borrow lets the platform take on the position itself and pay the external exchange back later; without it, you'd need to hold enough of every asset on hand to cover every possible trade, which defeats the point of mirroring liquidity in the first place.
It depends on order types and throughput targets, but plan for it as a separate scope item, not an add-on. A liquidity bridge reuses another exchange's execution infrastructure; a matching engine means building and load-testing your own, including the clearing and risk logic that used to live outside your system.
We don't let a vendor outage become a user-facing failure. We separate "the user failed verification" from "our provider is temporarily unavailable" as two distinct system states, so users aren't wrongly blocked while we work the vendor issue on our end.
No — it's tunable per platform based on trading volume, borrow costs, and how much operational risk the client is willing to carry between clearing cycles. We started at 65% for this build and adjusted the calculation itself once we saw locked order funds triggering false clearing runs.