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.

Epic: A-3209 Settlement: A-3622 (block trades) RFC: docs/rfc/rome.md Owner: Tin Status snapshot: 2026-07-08

#Principles

  1. 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 InsertTwoSidedBlockTrade call.
  2. One EP3 caller, one settlement discipline#
    The block-trade work made the order-gateway the sole caller of InsertTwoSidedBlockTrade with 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.
  3. 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_hash is computed over the accepted quote's terms.
  4. 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_order gating. ROME supplies both legs at once.
  5. Negotiation in-memory; settlement is the block-trade SoR#
    Live RFQ state is in-memory in the gateway process; the ClickHouse rfq_log is the negotiation audit trail. Settlement durability is the block-trade block_trades Postgres table — ROME introduces no new store. Redis is reused only for cross-replica directed-event pub/sub.

#TODO 1 / 10 done

not started — no PR draft PR open PR / in progress done — merged

#Design Questions

  1. 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.
  2. 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.
  3. 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-internal and graduate when RFQ ships (phase 9), same policy that governs the block-trade bt tag.
  4. 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.
  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.
  6. 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_AWAY audit.
  7. 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 InsertTwoSidedBlockTrade accepts 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}. Accepted is terminal for ROME — it produces a ticket and records the produced ticket_id for correlation.
  • Block-trade ticket (Postgres SoR, booking core): the born-affirmed ticket enters at accepted → booking, then cleared (or needs_manual_reconciliation on an ambiguous EP3 outcome). ROME observes the terminal state to emit the requester/maker Filled event; 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 InsertTwoSidedBlockTrade call, 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 to needs_manual_reconciliation. Definitive 4xx → roll the ticket back / reject. A 1s sweeper janitor drives the latch.
  • cross_id is a fresh ULID minted by the booking core (not ROME), snapshotted to Postgres before the call, propagated to both order legs and every drop-copy Execution (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 restrictionquotesServer.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.

Link copied