ROME (RFQ)
A Request-for-Quote workflow for AX — participants negotiate block-sized trades off the lit book, and an accepted quote is booked once to EP3 by the block-trade protocol. ROME owns the negotiation; the order-gateway block-trade booking core owns settlement. The separate rome service was dropped.
#Principles
- Negotiation on top, settlement below#ROME runs the RFQ negotiation entirely AX-side — public/targeted request stream, quote book, anonymity, maker UX. When a requester accepts a quote, ROME does not call EP3: it produces a consummated two-sided ticket and hands it to the order-gateway block-trade booking core (A-3622), which owns the single
InsertTwoSidedBlockTradecall. - One EP3 caller, one settlement discipline#The block-trade work made the order-gateway the sole caller of
InsertTwoSidedBlockTradewith hardened book-exactly-once semantics (no-dedup classifier, never-retry latch, Postgres CAS, reconciliation). ROME reuses it; it never opens a second booking path. This is the load-bearing invariant inherited from A-3622 §2h. - Born-affirmed handoff#An accepted RFQ has already completed an allege/affirm-equivalent handshake — the maker quoted a firm price, the requester accepted it. So ROME hands the booking core an already-consummated ticket that enters past
pending_counterparty; the RFQ accept is the affirmation.version_hashis computed over the accepted quote's terms. - Margin is checked at accept#Unlike a directly-alleged ticket that pends for hours and reserves at submit, an RFQ quote is short-lived, so both legs are checked-and-reserved in one phase at accept, reusing the booking core's
can_send_ordergating. ROME supplies both legs at once. - Negotiation in-memory; settlement is the block-trade SoR#Live RFQ state is in-memory in the gateway process; the ClickHouse
rfq_logis the negotiation audit trail. Settlement durability is the block-tradeblock_tradesPostgres table — ROME introduces no new store. Redis is reused only for cross-replica directed-event pub/sub.
#TODO 1 / 10 done
-
Re-baseline on the block-trade protocol#
The block-trade protocol landed first and independently (A-3622) — the "two-sided trade booking thing" subset of ROME — and dropped the separate
romeservice. This phase re-baselines the design: the RFC is rewritten so ROME is the negotiation layer on top of the booking core, settlement delegates downward, and the dead standalone-service framing is removed. -
1
Protocol types in sdk-internal#
RFQ wire types (
RfqRequest/RfqEvent, optional-sidesRfqQuote, prefixed-ULIDR-/Q-ids) following the #2087 conventions shared with the block-tradebtfamily. Per the #2068 revert policy, the public SDK ships RFQ types only with the integrated feature — they live insdk-internaland the gateway dispatches them as a fallback until RFQ ships.#2068(public types) merged and was reverted within hours. -
2
Negotiation engine in the order-gateway#
The in-memory request/quote book and the negotiation lifecycle (
Active → {Accepted, Cancelled, Expired}), expiration heap, public and targeted RFQ fanout, and therfq_logaudit writer — built into the order-gateway, following the block-trade precedent (a negotiation needs only what the gateway already owns). NoSettling/Settledstate and no EP3 call: those belong to the booking core. Supersedes the standaloneromeengine stack (#2070,#1843) and the closed minimal service. -
3
Settlement handoff — born-affirmed ticket into the booking core#
The one new seam ROME adds: an internal (non-public) born-affirmed entry point on the block-trade booking core that ROME alone calls with both legs, the locked price, and a
version_hash. The ticket enters ataccepted → bookingand from there settlement is the block-trade protocol verbatim (fresh-ULIDcross_id, snapshot-before-call, definitive-vs- ambiguous classifier, never-retry latch, Postgres SoR). The earlier ROME-owned EP3 booking PRs were adopted by / folded into A-3622. -
4
Visibility & anonymity#
target_makers: Vec<UserId>for directed RFQs that skip the public stream;disclose_identityfor protocol-level anonymity (per-request pseudonym on outgoing events, realuser_idin ClickHouse and on the produced ticket); maker discovery + per-user favorites. Competing quotes on an accept emitQuoteRemovedwith aDONE_AWAYreason, mirroring EP3's native RFQ semantics. Epic A-3301. -
5
Fair-market controls (from EP3's RFQ model)#
Sourced from EP3's native Quotes service: a configurable minimum unaffiliated firms floor on a targeted RFQ (the SEF/CFTC fair-market knob), a self-RFQ restriction policy flag (EP3's
restrictFirmAcceptOwnRfqs), and reciprocal firm-association visibility. V1 surfaces these as config knobs (not enforced), mirroring how the block-trade prototype stubs min-block-size; enforcement lands at the AIEX/SEF tier. -
6
Pre-rollout hardening#
Per-instrument RFQ minimum block size, per-user submission rate limit, public trade-tape
blockcondition print (rides the block-trade tape work), the quote-side enforcement matrix, forward-compatlegsshim, and connection-state hardening — cancel-on-disconnect (gateway-crash recovery, server-side heartbeat) and the counterparty mid-flow disconnect policy (Q1). Epics A-3294 (P0) + A-3295 (P1/P2). The earlier cancel-on-disconnect PR is superseded.superseded / related#1915 feat(rome): cancel-on-disconnect -
7
GUI — book, quote, accept#
Requester "Create Strategy" modal + maker-selection sidebar (with the minimum-unaffiliated-firms warning), confirm-then-send for submit and accept, anonymity toggle, and the responder surface (RFQ inbox, quote composer, block-fill view). Shares components with the block-trade GUI prototype. Epic A-3211; milestones G1–G7 → A-3316…A-3322. The earlier RFQ GUI PR is superseded, to be re-done post-integration.
superseded / related#1903 feat(gui): RFQ -
8
Observability & testing#
Prometheus metrics for the negotiation path (
rome_active_requests,rome_accept_to_ticket_latency_seconds,rome_tickets_produced_total,rome_log_drops_total, public-RFQ subscriber/lag gauges), Grafana dashboards, incident.io alerts (A-3219), and the batteries-included test matrix (A-3260) — happy paths, concurrent-accept exactly-one-ticket, targeted/anonymous visibility, and the connection-state scenarios the project rule requires before rollout. -
9
Feature flag & rollout#
Gate RFQ behind a feature flag, graduate the RFQ types to the public SDK (per the #2068 revert policy, only with the integrated feature), and roll out to demo. Epic A-3296.
awaiting sign-off connection-state matrix green (client/responder disconnect, gateway restart/recovery) at every negotiation stateawaiting sign-off RFQ booked end-to-end on demo — accepted quote settles as a two-sided block print, margin verified at accept
#Design Questions
-
Born-affirmed seam — does the booking core enter at
accepted → booking, or does ROME drive a real allege+affirm round-trip?#YesBorn-affirmed internal entry point (RFC §2.2 option B): the RFQ accept is the affirmation, so ROME hands the core an already-consummated ticket and ROME alone calls that seam. Avoids a redundant affirm round-trip and its loss window. -
Does ROME ever call EP3 directly?#NoNo — exactly one EP3 caller (the order-gateway block-trade booking core). ROME produces consummated tickets into it and never books itself; this is the invariant inherited from A-3622 §2h.
-
When are RFQ types in the public SDK?#YesOnly with the integrated feature — Michael's #2068 revert sets the policy. RFQ types live in
sdk-internaland graduate when RFQ ships (phase 9), same policy that governs the block-tradebttag. -
Counterparty disconnect between SubmitQuote and AcceptQuote — cancel the quote or leave it live?#OpenDefault: cancel the quote when the responder's WS drops, mirroring order behavior. Decision shapes cancel-on-disconnect (phase 6). A-3295 item 5.
-
Enforce the minimum-unaffiliated-firms floor and self-RFQ restriction in v1?#NoStub as config knobs (phase 5), mirroring min-block-size; enforce at the AIEX/SEF tier. Sourced from EP3's native RFQ admin model.
-
Do we adopt EP3's native Quotes service for settlement?#NoNo for v1 — accepted RFQs settle as block trades via the booking core (block prints, one EP3 caller). Revisit only if AIEX requires native SEF-regulated RFQ-to-trade semantics with EP3-side
DONE_AWAYaudit. -
Multi-leg RFQs in v1?#NoSingle-leg — the booking core is single-leg and the EP3 atomic multi-leg block question is unasked in #ext-architect-connamara (block-trade plan §2f). The earlier RFC claim that
InsertTwoSidedBlockTradeaccepts a multi-leg form is wrong; the vendored proto carries one symbol/price/qty. Multi-leg is v2 (A-3297 F1).
#Scratchpad
Long-form reference and working notes — mostly write-only, for LLM/agent use. Collapsed by default.
›Scratchpad — reference design & raw notes
#Architecture & data flow
Every client message goes through order_gateway; ROME negotiation and
the block-trade booking core both live in-process there. Two state
machines, one handoff:
- ROME negotiation (in-memory, per request):
Active → {Cancelled, Expired, Accepted}.Acceptedis terminal for ROME — it produces a ticket and records the producedticket_idfor correlation. - Block-trade ticket (Postgres SoR, booking core): the born-affirmed
ticket enters at
accepted → booking, thencleared(orneeds_manual_reconciliationon an ambiguous EP3 outcome). ROME observes the terminal state to emit the requester/makerFilledevent; it does not drive settlement.
The per-entry lock can be parking_lot::Mutex (not tokio::Mutex): ROME
no longer holds a lock across the EP3 await — that call lives in the
booking core, after ROME has produced the ticket and released its entry.
#The accept handoff — the one new seam
AcceptQuote: gateway risk-checks and reserves both legs at the locked
price (single margin phase) → take the entry mutex, verify Active and the
quote present/unexpired → flip Active → Accepted, mark competing quotes
DONE_AWAY, build the born-affirmed two-sided ticket → drop the mutex →
call the booking core's internal born-affirmed entry point. Concurrent
AcceptQuote/Cancel on the same request see Accepted and reject
cleanly — exactly-one-winner depends on the mutex-then-flip ordering, and
exactly one ticket is produced, so EP3 is never double-called.
#EP3 booking & the no-retry contract — owned below ROME
All of it belongs to the block-trade protocol now (A-3622), reproduced here only for legibility:
- One
InsertTwoSidedBlockTradecall, two-sided and atomic, after consummation. - EP3 does not dedup (vendor-confirmed 2026-05-26) and exposes no
idempotency_key. Never auto-retry; ambiguous outcomes latch toneeds_manual_reconciliation. Definitive 4xx → roll the ticket back / reject. A 1s sweeper janitor drives the latch. cross_idis a fresh ULID minted by the booking core (not ROME), snapshotted to Postgres before the call, propagated to both order legs and every drop-copyExecution(vendor-confirmed 2026-05-27).- Positions/fees/PnL flow through existing drop-copy machinery.
Earlier drafts of the ROME RFC owned a Settling/Settled state machine,
cross_id minting, a Redis durable_state module, and a layered
retry-safety analysis. All of that moved to the block-trade layer.
#Fair-market controls from EP3's native RFQ
EP3 ships a native Quotes/RFQ service (RequestForQuote/Quote,
DONE_AWAY, PassQuote, accept-via-InsertOrder-with-quote-ref, FIX
QuoteResponse <AJ>). AX does not adopt it for settlement — accepted
RFQs settle as block trades (block prints, one EP3 caller). But its
market-structure model is the source for ROME's fair-market controls:
- Minimum unaffiliated firms — the minimum distinct unaffiliated firms an RFQ must reach for a fair market (SEF/CFTC); an EP3 admin knob.
- Self-RFQ restriction —
quotesServer.behavior.restrictFirmAcceptOwnRfqs. - Reciprocal firm associations — RFQ visibility governed by opt-in,
reciprocal "RFQ Participant Firm" associations; anonymity is therefore a
presentation choice (
disclose_identity), not a requirement.
EP3 native RFQ stays a future option if AIEX later needs native RFQ-to-trade semantics (Q8).
#Relationship to block trades — the decided carve
Brett's framing (2026-06-09): block trades "use a subset of ROME which is the two-sided trade booking thing." That subset landed first (A-3622), folded into the order-gateway with Postgres SoR. ROME builds on top:
- Block-trade plan owns: the directed two-sided ticket lifecycle,
version_hash, the single EP3 call, book-exactly-once, two-phase margin, Postgres SoR, sweeper, reconciliation. - ROME owns: RFQ negotiation, public/targeted stream, anonymity, maker UX, expiration, and the accept→born-affirmed-ticket handoff. An accepted RFQ is another producer of consummated tickets into the one booking core — additive, not a rewrite.
Much of the original standalone-rome stack (engine #2070/#1843, EP3
booking #2102, mock #2096, minimal service #2370) was superseded when
the booking core folded into the order-gateway; ROME's remaining work is
the negotiation layer above it.