Cryptocurrency Exchange Requirements: The Spec I Wish We Had Written First

Nearly every requirement below exists because something in production made it necessary. This is the checklist I would hand a team on day one, written as obligations you can test rather than a box diagram you can admire.

By Oleksii Vasylenko, Exchange Systems Architect & Hands-on Technical Lead · · 18 min read

Where this comes from. Shipping Bitsten’s order path, matching engine, live market data, liquidity integrations, and candle generation — then rebuilding the consistency, duplicate-protection, durability, and recovery layers after production showed which requirements had been left implicit.

The first artifact on an exchange project is usually an architecture diagram. It is the wrong first artifact. A diagram tells you where the boxes are; it tells you nothing about whether the system is allowed to fill an order twice, and that is the question that decides whether the business survives its first bad week.

“Build a fast matching engine” is not a requirement. “Under the approved peak workload, every accepted command produces exactly one authoritative outcome, price-time priority holds, and p99 acknowledgement stays under the agreed limit” is a requirement, because it can fail. The first sentence invites a demo. The second can reject a system that is fast and financially wrong.

Write down the venue model, jurisdictions, asset classes, custody boundary, customer types, order types, matching policy, settlement model, operating hours, and launch markets — and name the person who approves each one. Leave unresolved decisions visible as TBD with an owner and a date. The alternative is that an engineer quietly picks a behaviour at 2am and it becomes policy by accident.

  • Define the boundary: trading venue, broker, custodian, wallet operator, clearing function, or some combination.
  • Classify each requirement as functional, integrity, performance, security, compliance, operational, or reporting.
  • Say what is out of scope — products, jurisdictions, order types, failure scenarios — as explicitly as what is in.
  • Give every critical requirement an accountable business owner and a technical verification owner. Two different people.

A continuous order book, an RFQ venue, an auction, and an internal conversion service do not share priority rules. For an order book you need price priority, the tie-break at equal price, maker/taker determination, execution-price policy, self-trade prevention, time-in-force, post-only behaviour, minimum quantity, tick size, lot size, minimum notional, price bands, market status transitions, and what happens to an unfilled remainder. Coinbase publishes price-time priority and explicit self-trade prevention behaviour. Your venue needs the same precision about its own rules, whatever they are.

Arithmetic is where this gets real. Every rounding decision needs a direction, and the direction is not arbitrary. In Bitsten’s matching domain, rounding always favours the book rather than the aggressor: asks round up, bids round down, so a rounding error can never manufacture value out of nothing.

function quoteRounding(order: Pick<Order, 'side'>): Decimal.Rounding {
  return order.side === OrderSide.Ask
    ? Decimal.ROUND_UP
    : Decimal.ROUND_DOWN;
}
apps/conductor/src/matching.domain.ts — the entire rounding policy, deliberately small enough to audit.

Every pair carries its own priceDecimals, amountDecimals, and quoteDecimals, and every calculation runs through decimal.js rather than IEEE 754 doubles. That costs throughput — and the benchmarks below show it costs far less than people assume. The test that matters is simple: if two engineers can read a rule and compute different balances, the rule is not finished.

  • Model every order type as a state machine: valid inputs, transitions, terminal states, rejection reasons.
  • Say whether an amendment keeps or loses time priority, separately for price and for quantity changes.
  • Define cancel-versus-fill precedence at the ordering boundary, not in the UI.
  • Cover halts, maintenance, auctions, suspension, delisting, and reopening.
  • Version the rulebook and store the version that applied to each accepted order.
  • Ban binary floating point anywhere a number becomes money.

Received, validated, accepted, rejected, partially filled, filled, cancelled, expired, suspended — every one of these needs an exact meaning, and “accepted” needs the most care. It is the moment after which the venue owes the customer a durable outcome. Before that point a timeout means “nothing happened.” After it, a timeout means “ask again, do not resubmit.”

Order lifecycle states on an exchangeReceived, then Rejected or Accepted. Acceptance is the point after which the venue owes a durable outcome, so a timeout after it means ask again rather than resubmit.validation failsdurable outcome owedReceivedRejectedAcceptedPartiallyFilledFilledRestingCancelledExpiredafter this point a timeout means"ask again", never "resubmit"
Every arrow here is a rule somebody has to write down. The one that matters most is the transition into Accepted — it is where the venue starts owing the customer an answer.

That distinction is what idempotency buys you. Clients send a stable command identity; the venue returns the same authoritative outcome if that identity repeats. Straightforward — until you meet the case that command-level idempotency does not cover.

  • Define the acknowledgement contract for submit, cancel, replace, deposit-credit, withdrawal, and admin actions.
  • Specify idempotency scope, retention, conflict behaviour, and what a client gets back after a lost response.
  • Use monotonic order versions so a stale update cannot overwrite a newer state.
  • Make rejection codes machine-readable and stable: malformed input, authorization failure, insufficient funds, risk breach, invalid market state, duplicate identity, stale version, temporary unavailability. Human text can change; codes cannot.
  • Say which operations fail closed when a dependency or durability condition is unavailable.

Consistency is not a global setting you turn up. Assign a contract to each fact. Trade priority, order state, available and reserved balance, executed quantity, fees, deposits, withdrawals, and administrative adjustments need authoritative ordering and protection against duplicate application. Market summaries, charts, analytics, notifications, and search indexes may lag — but you have to say how much lag is acceptable and how a gap is recovered.

Assigning a consistency contract per fact rather than per systemFacts that money or priority depend on need authoritative ordering and duplicate protection. Everything else may lag, provided the permitted lag and gap recovery are stated.yesnoa fact the system storesdoes money orpriority depend on it?authoritative ordering+ duplicate protectionmay lag — but statethe permitted lagtrade priority, order state,balances, fees, transferssummaries, charts, analytics,notifications, searchmust detect sequence gaps,never present stale as complete
One question, asked per fact rather than per system. Getting the left branch wrong loses money; getting the right branch wrong just annoys people, provided the lag is stated.

The core invariant is blunt: one accepted quantity can be executed at most once. Everything else follows from it. These are the invariants the Bitsten engine asserts rather than hopes for, and several of them abort the command outright when violated:

  1. Executed buy quantity equals executed sell quantity for every trade, within the configured rounding policy.
  2. No order fills beyond its accepted quantity, and remaining quantity is never negative.
  3. An open maker exists both in its order record and exactly once in the correct side of the book.
  4. A closed order is absent from the active book and never returns without a new approved identity.
  5. A market order is never promoted to a resting maker.
  6. An order ID is accepted at most once per partition, even when a retry arrives under a new command ID.
  7. Replaying the same accepted history produces the same orders, trades, fees, and closing balances.

Repeated delivery will happen at system boundaries — that is a property of distributed messaging, not a bug you can eliminate. What must not happen is a repeated financial effect. Design for at-least-once transport with once-only application, and give every downstream consumer a transactional inbox keyed by a stable event ID.

The ledger is the authoritative explanation for every balance change. Each entry needs an immutable identity, asset, amount, direction, account, business reason, related object, effective time, recording time, and the actor or process that caused it. Whether you use double-entry postings is an accounting choice; conserving value after declared fees and adjustments is not. Direct balance edits with no auditable counter-record should be impossible, not merely discouraged.

Deposits and withdrawals need asset-specific finality rules: supported networks, address and memo validation, confirmation thresholds, reorg handling, minimums, fees, batching, screening, manual review, approval limits, signing authority, cancellation, and customer-visible status. A credited deposit later reversed by a chain reorganisation, and a withdrawal broadcast after the client saw a timeout, are normal Tuesday events. Specify them as such.

  • Reconcile customer liabilities, internal ledger totals, hot wallets, cold storage, pending transfers, and external custodian records.
  • Set reconciliation frequency, discrepancy tolerance, escalation owner, and the conditions that halt withdrawals or trading.
  • Require dual control for privileged financial operations above approved limits.
  • Retain the complete approval, signing, broadcast, confirmation, replacement, and failure history.
  • State the negative-balance and insolvency-prevention invariants independently of any UI.

A throughput number without a workload attached is decoration. The capacity model needs sustained and burst commands per second, burst duration, concurrent sessions, active markets, hot-market concentration, open orders, occupied price levels, cancel ratio, share of aggressive orders, average and maximum fills per command, market-data fan-out, transfer rates, and admin traffic. An order that sweeps twenty price levels does roughly twenty times the downstream work of one that rests quietly.

It also needs boundaries. On Bitsten the same engine measures anywhere between 130,000 and 106 operations per second depending on which boundary you put the stopwatch at — and every one of those numbers is correct. Edge acknowledgement, authoritative acceptance, match completion, balance visibility, market-data publication, and client receipt are six different measurements. Approve p50, p95, p99, and p99.9 targets for each critical path at a named offered load.

  • Define a normal profile, an approved peak profile, a launch-event profile, and an abusive-traffic profile.
  • Set maximum queue age and backlog at steady state, during bursts, and during recovery.
  • Require headroom above forecast peak, and name the forecast horizon.
  • Measure one dominant market separately from evenly spread traffic. Averages hide hot pairs.
  • Count downstream amplification: fills, ledger postings, notifications, surveillance records, and market-data updates per command.
  • Require zero invariant violations and zero unexplained message loss in every qualifying run. A fast run that loses a trade is a failed run.

Define availability per capability. Public market data, order entry, cancellation, account reads, deposits, withdrawals, administration, and reconciliation do not deserve the same target. More importantly, say what stops when a dependency fails. Accepting orders while balance reservation is uncertain is not graceful degradation — it is manufacturing an unknown liability and calling it uptime.

Recovery point and recovery time objectives belong to datasets, not to the system as a whole, and they change quietly when infrastructure changes. Swapping KeyDB’s every-second AOF for a snapshot every five minutes moved the worst-case unreplicated loss from one second to five minutes. Nobody had asked for that; it arrived attached to a datastore migration. Now durability is an explicit, named policy that the process refuses to start without.

MATCHING_DURABILITY=aof-replicated   # default; requires WAITAOF 1 1
MATCHING_DURABILITY_TIMEOUT_MS=2000

# Boot fails rather than accepting writes it cannot honour:
# "MATCHING_DURABILITY=aof-replicated requires a datastore
#  with WAITAOF support"
Durability as a declared contract. `memory` mode is a statement that recovery comes from somewhere else.
  • Define safe-mode behaviour and customer messaging for every capability that can go away.
  • Require deterministic reconstruction and reconciliation before a recovered market reopens.
  • Test failover during active orders, partial fills, cancellations, deposits, and withdrawal approval — not on an idle system.
  • Prove backups by restoring them into an isolated environment on a schedule.
  • Document who has authority to halt, resume, cancel, or roll back market operations during an incident.

Security requirements should be threat-driven and testable. Define assurance levels for customer identity, employee identity, service identity, privileged administration, API keys, withdrawals, recovery flows, and support actions. Authentication is the first gate, not the control. Every operation needs authorization against the customer, account, market, role, jurisdiction, session risk, and current limits, and privileged access needs to be attributable to one person and bounded in purpose and time.

OWASP ASVS 5.0 is a reasonable testable baseline for the web application layer. It is not sufficient on its own — extend the threat model to cover custody, trading abuse, insider risk, market manipulation, and irreversible external transfers, none of which a generic web checklist contemplates.

  • Apply rate and exposure limits per identity, account, network, market, API key, and global boundary.
  • Require step-up controls for sensitive actions, and emit an immutable security event for each one.
  • Separate administrative interfaces from customer interfaces and deny by default.
  • Keep credentials, private keys, raw secrets, and unnecessary personal data out of logs and telemetry.
  • Give security incidents defined severity, containment, notification, evidence-preservation, and recovery paths.
  • Block launch on unresolved critical findings or unowned high-risk exceptions.

Every external interface is a versioned contract: validation, authentication, authorization, idempotency, pagination, error codes, rate limits, time semantics, precision, ordering, compatibility, deprecation. A field called timestamp must say whether it means receipt, acceptance, execution, persistence, or publication. A field called balance must say whether pending deposits, open-order reservations, and withdrawal holds are inside it.

Internal events deserve the same discipline, because they are the contract between your own teams and the thing you will be reading during an incident at 3am. Bitsten’s matching events carry an envelope that makes ordering, causation, and deduplication explicit:

interface MatchingEventEnvelope<TData = unknown> {
  schemaVersion: 1
  eventId: string          // stable dedup key, never reused
  type: MatchingEventType  // 'deal.executed.v1', 'order.rested.v1', ...
  aggregateType: 'order' | 'deal'
  aggregateId: string
  aggregateVersion: number // stale-update detection
  pairId: number
  partition: number        // which writer owned this
  causationId: string      // the command that caused it
  correlationId: string    // the client request that started it
  occurredAt: string
  exchange: string
  routingKey: string
  data: TData
}
libs/shared-lib/src/matching/contracts.ts — every field here exists to answer a question someone asked during an incident.

Market-data consumers need a defined snapshot and incremental-update model with enough ordering information to detect loss, duplication, reordering, and stale reconnection. State the maximum publication delay, snapshot age, sequence scope, reset behaviour, and gap recovery. And declare how a candle is built: rebuilding it from the same trade history must reproduce the same open, high, low, close, and volume, or your charts and your ledger disagree in public.

You must be able to explain any order, trade, balance change, halt, withdrawal, or privileged action from retained evidence alone. Correlation identifiers should connect the client request to validation, authoritative state, fills, ledger postings, outbound notifications, and operator actions. Audit records need to be tamper-evident, access-controlled, time-synchronised, retained to policy, and exportable without touching production state.

On the operations side, decide what you measure before you need it. The matching path exposes counters that are deliberately boring and stable, because an alert that fires on a name that changed last sprint is not an alert:

matching_commands_processed_total
matching_duplicate_commands_total      # redelivery caught by dedup
matching_duplicate_orders_total        # retry under a new command ID
matching_command_failures_total
matching_events_created_total
matching_last_command_duration_ms

matching_outbox_published_total
matching_outbox_publish_retries_total
matching_outbox_dead_letter_total
matching_outbox_stream_length
matching_outbox_pending
matching_outbox_owns_lease
GET /metrics on the conductor and outbox. Duplicates are counted, not hidden.

Alerts should describe customer or integrity risk, not host business. “Oldest unacknowledged event is four minutes old” tells an operator that financial projections are stale. “CPU is at 80%” does not. Every page-worthy alert needs a named responder and a first action they have actually practised.

  • Keep clocks synchronised within a stated tolerance — while making sure authoritative ordering never depends on wall-clock precision.
  • Put release identifiers and configuration versions into operational and audit evidence.
  • Require approval and a retained before/after record for material rule, limit, permission, or configuration changes.
  • Hold support tools to the same business controls as the production API. A support console that can edit a balance is a production API with worse authorization.
  • Distinguish detected duplicates from duplicated financial effects in daily reconciliation and incident reports. The first is the system working.

Licensing, customer due diligence, transaction monitoring, sanctions, travel-rule obligations, market surveillance, retention, reporting, listing, custody, disclosure, and customer-protection duties all depend on jurisdiction and venue model. Qualified legal and compliance owners identify them before scope is approved. FATF guidance expects virtual asset service providers to be licensed or registered where applicable and to assess and mitigate ML/TF risk — a baseline for analysis, not a substitute for local law.

SEC Regulation SCI does not generally apply to cryptocurrency venues, but its subject list — capacity, integrity, resiliency, availability, security, corrective action, review, records, continuity testing — is a good completeness check for market infrastructure of any kind. Record which standards apply, which you adopt voluntarily, and which are explicitly out of scope. This article is an engineering framework, not legal advice.

  • Define verification states for customers and beneficial owners, and the trading or transfer permissions attached to each.
  • Retain the market, customer, device, order, trade, transfer, and decision data that surveillance and investigation actually need.
  • Specify alerts and case workflows for suspicious funding, wash trading, spoofing, layering, account takeover, and sanctions exposure.
  • Require independent review of critical controls, and document how findings were resolved.
  • Map regulatory notifications and customer communications to incident severity and jurisdiction.

Map each mandatory requirement to at least one acceptance test, and retain the version, configuration, workload, seed, environment, result, and approver for every run. The portfolio needs unit and model tests for trading rules, property-based invariant tests, deterministic replay, contract tests, permission tests, security verification, migration tests, sustained load, burst load, soak, dependency degradation, fault injection, restoration, reconciliation, and operator exercises.

Fault injection deserves special mention because it finds things nothing else does. Resetting the matching namespace while the outbox was running removed its Redis consumer group; the worker retried the resulting error forever and its metrics endpoint started returning HTTP 500. No unit test would have found that. A benchmark run with a hostile hand on the datastore found it in minutes.

A demo that places and fills an order proves almost nothing. The system passes when it preserves money and priority through retry, concurrency, overload, failure, recovery, operator error, and investigation — and when the team can produce the evidence without writing new code to find it.

  • Zero unresolved failures of financial, order-priority, identity, authorization, or replay invariants.
  • Capacity tests meet every declared load and latency objective with headroom and without unbounded backlog.
  • Recovery exercises meet approved RPO and RTO and reconcile against an uninterrupted reference run.
  • Security verification meets the adopted baseline; critical exceptions block launch.
  • Runbooks executed by the people who will carry the pager, not by the people who wrote them.
  • A go/no-go record listing evidence, residual risks, accountable approvers, and rollback authority.
AreaRequired decisionAcceptance evidenceFailure if omitted
Trading logicPriority, order states, precision, fees, haltsDeterministic examples and rule testsTwo users get incompatible outcomes from one rule
ConsistencyAuthority, idempotency, ordering, replayInvariant and failure-injection resultsDuplicate trades and unexplained balances
CapacityWorkload, sustained and burst load, percentile SLOsSaturation, soak, and recovery-load reportsA peak number hides backlog and tail failure
RecoveryRPO, RTO, safe modes, reconciliation gateRestore and failover exercisesTrading resumes from unknown state
SecurityIdentity, authorization, custody, abuse controlsThreat model and independent verificationAccount or asset compromise
OperationsMetrics, alerts, audit, runbooks, decision rightsIncident exercise and retained evidenceNobody can explain or contain the failure
ComplianceApplicable jurisdictions and control mappingsQualified review and traceable obligationsThe product ships outside its permitted scope

This is a record of what must be true and how it will be proved. Architecture follows from approved constraints, not the other way round.

This article defines obligations without choosing a solution. The architecture guide covers implementation boundaries, failure modes, and production evidence once those obligations are agreed.

Read the matching engine architecture guide

Need the specification before the architecture starts?

I can turn a venue model, trading policy, forecast load, custody boundary, and regulatory scope into a traceable requirements and acceptance package — kept technology-neutral until the business invariants and measurable launch gates are agreed.

Request an exchange requirements review