RFC: ROME RFQ Negotiation on the Block-Trade Booking Core

Status Draft re-baselined on the block-trade protocol (A-3622)
Owner @tin
Created 2026-05-06
Last updated 2026-07-08
Scope The AX RFQ (request-for-quote) workflow: a negotiation layer that settles accepted quotes as block trades through the order-gateway booking core.

Executive summary

ROME lets participants negotiate block-sized trades off the lit book a requester asks the market for a price, makers quote, the requester accepts one and books the result atomically in EP3. The design question this RFC answers is not "how do we book a two-sided trade" (that already shipped) but "how do we run the negotiation on top of it without adding a second settlement path."

The block-trade protocol landed first and independently (A-3622): submit counterparty affirm book once to EP3 via InsertTwoSidedBlockTrade, built into the order-gateway, with Postgres as system of record. That work deliberately carved out the two-sided booking spine and dropped the separate rome service. This RFC is re-baselined on that reality: ROME is the negotiation layer, the booking core is the settlement layer, and there is exactly one EP3 caller.

Three conclusions everything else follows from:

  1. ROME never calls EP3. An accepted quote is already a consummated agreement the maker quoted firm, the requester accepted. ROME turns it into a born-affirmed two-sided ticket and hands it to the block-trade booking core, which owns the sole InsertTwoSidedBlockTrade call and the book-exactly-once discipline. No second EP3 caller, no second dedup/idempotency story, no new settlement surface.

  2. ROME is not a service. Following the block-trade precedent, RFQ negotiation builds into the order-gateway it needs only what the gateway already owns (per-user WS, margin gating, EP3 connectivity via the booking core, Redis cross- replica pub/sub). There is no rs/rome crate; the prototype rome service and its compose entry were closed the day they were built.

  3. ROME owns negotiation state; the booking core owns settlement state. ROME's state machine ends at "quote accepted ticket produced." It has no Settling/ Settled/needs_manual_reconciliation those belong to the ticket. Durability, cross_id minting, never-retry latch, reconciliation, position/fee/PnL flow-through are all inherited unchanged.

1. Decisions

Load-bearing commitments. §2§8 are elaboration.

2. Block-trade protocol integration

This is the center of the RFC. ROME is layered, not standalone.

2.1 The carve who owns what

Per Brett's framing (2026-06-09, block-trade plan §2d): block trades "use a subset of ROME which is the two-sided trade booking thing." That subset landed first; ROME builds the negotiation on top.

Concern Owner
Public/targeted RFQ stream, quote book, anonymity, targeting, maker UX ROME (this RFC)
RFQ/quote expiration, cancel-on-disconnect of live negotiation ROME (§3.4, §7)
Quote-accept handshake produce a born-affirmed ticket ROME booking core (§2.3)
Two-sided ticket lifecycle, version_hash, directed bt* events block-trade protocol
The single InsertTwoSidedBlockTrade call; book-exactly-once block-trade protocol
cross_id minting, no-dedup classifier, never-retry latch block-trade protocol
Two-phase margin, Postgres CAS SoR, sweeper, reconciliation block-trade protocol
Position / fee / PnL flow-through via drop-copy existing machinery

ROME's deliverable is the first three rows. Everything below the line ROME reuses wired, tested, live-bound under A-3622.

2.2 The one invariant: a single EP3 caller

The load-bearing constraint inherited from the block-trade work (plan §2h): there is exactly one path to InsertTwoSidedBlockTrade the order-gateway booking core. The RPC does not dedup (vendor-confirmed 2026-05-26) and exposes no idempotency_key, so the book-exactly-once discipline (snapshot-before-call, definitive-vs-ambiguous classifier, never-retry latch, Postgres CAS, reconciliation) is non-negotiable and lives in exactly one place. A second caller would mean a second, differently-behaved dedup story. ROME feeds the core consummated tickets; it never books itself.

2.3 The seam ROME adds born-affirmed entry point

Two ways to map an accepted RFQ onto the booking core:

The booking core exposes an internal, non-public born-affirmed entry point that ROME alone calls with: buyer account, seller account, symbol, price, quantity, both legs' client order ids, and a version_hash over the economic terms. The core's economic-terms binding (compute_version_hash) still applies, computed over the accepted quote. From the entry point down, settlement is the block-trade protocol verbatim.

2.4 What ROME inherits unchanged

2.5 What ROME no longer carries

Earlier drafts of this RFC owned a Settling/Settled/needs_manual_reconciliation state machine, cross_id minting, a Redis durable_state module, a layered ambiguous- outcome retry-safety analysis, and the EP3 idempotency contract. All of that moved to the block-trade protocol. ROME keeps only: produce the ticket, observe its terminal state, emit the negotiation-side Filled/reject event. If the seam or its semantics change, that is a block-trade-plan change, not a ROME change.

3. Negotiation engine the solution

In-process in the order-gateway. Owns the request/quote book and the negotiation lifecycle. Does not own settlement.

3.1 State

pub struct RomeState {
    pub requests: DashMap<RequestId, Arc<RequestEntry>>,
    pub quotes_index: DashMap<QuoteId, RequestId>,
    pub expirations: Arc<Mutex<BinaryHeap<Reverse<(i64, ExpiryKey)>>>>,
    pub log_tx: mpsc::Sender<RfqLogRow>,
    pub id_gen: IdGen,
    // directed/public RFQ event fanout reuses the gateway's per-user WS
    // channels + Redis cross-replica pub/sub (shared with bt events)
}

pub struct RequestEntry {
    pub immutable: RequestImmutable,   // requester, symbol, sides, anonymity, targets
    pub state: parking_lot::Mutex<RequestState>,
}

pub enum RequestState {
    Active   { quotes: SmallVec<[Quote; 4]>, expires_at_ns: i64 },
    Accepted { quote_id: QuoteId, ticket_id: OrderId },  // terminal for ROME
    Cancelled,
    Expired,
}

The per-entry lock is parking_lot::Mutex, not tokio::sync::Mutex: ROME never holds a lock across an await to EP3. The only cross-process call happens inside the booking core, after ROME has produced the ticket and released the entry. Accepted records the ticket_id purely so a status query can correlate the negotiation with its settlement.

3.2 Concurrency

3.3 Hot-path flows

SubmitQuoteRequest WS frame → mint request_id → DashMap insert → publish on public/targeted stream → push expiration → log → SubmitQuoteRequestResponse. No margin reserve, no EP3 hop; the requester is committed only at accept.

SubmitQuote WS frame → gateway risk-*check* (no reserve) → take entry mutex → verify Active → mint quote_id → append → release → emit QuoteReceived → push expiration → log → SubmitQuoteResponse. No EP3 hop; the maker is committed only when the requester accepts.

AcceptQuote (the handoff)

  1. WS frame gateway risk-checks and reserves both legs at the locked price (single margin phase, D3). Fail typed RfqReject, nothing produced.
  2. Take entry mutex; verify Active, quote present and unexpired.
  3. Flip Active → Accepted; mark competing quotes DONE_AWAY; build the born-affirmed ticket. Drop the mutex.
  4. Call the booking core's born-affirmed entry point (§2.3). Settlement begins here; the block-trade protocol owns correctness from this point.
  5. On the ticket reaching a terminal state, emit Filled (cleared) or a pending/reject event to both parties; log.

ROME never retries the EP3 call and never inspects EP3 status codes that is the core's job.

3.4 Expiration

One in-process task. State is BinaryHeap<Reverse<(deadline_ns, key)>> behind a parking_lot::Mutex. Loop: peek, sleep to the head deadline, pop, look up the entry, take its mutex, and if still Active transition to Expired (or drop the quote), then broadcast QuoteRequestRemoved/QuoteRemoved and log. If the entry already moved (Accepted/Cancelled), drop the heap entry silently. Once Accepted, expiration is a no-op the produced ticket has its own TTL in the booking core (end-of-day default, settable shorter). Same pattern trade-engine uses for GTD orders.

4. Protocol (SDK)

Wire types in sdk-internal/src/protocol/rfq.rs (graduating to the public SDK when RFQ ships, D7). They follow the conventions frozen in the #2087 review (block-trade plan §2g), shared with the block-trade bt family.

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "t")]
pub enum RfqRequest {
    #[serde(rename = "qr")]   SubmitQuoteRequest(SubmitQuoteRequest),
    #[serde(rename = "xqr")]  CancelQuoteRequest(CancelQuoteRequest),
    #[serde(rename = "sqr")]  SubscribeQuoteRequests(SubscribeQuoteRequests),
    #[serde(rename = "uqr")]  UnsubscribeQuoteRequests,
    #[serde(rename = "q")]    SubmitQuote(SubmitQuote),
    #[serde(rename = "xq")]   CancelQuote(CancelQuote),
    #[serde(rename = "aq")]   AcceptQuote(AcceptQuote),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "t")]
pub enum RfqEvent {
    #[serde(rename = "QR")]   QuoteRequestPosted(QuoteRequestPosted),    // public/targeted
    #[serde(rename = "XQR")]  QuoteRequestRemoved(QuoteRequestRemoved),  // public/targeted
    #[serde(rename = "Q")]    QuoteReceived(QuoteReceived),              // directed to requester
    #[serde(rename = "XQ")]   QuoteRemoved(QuoteRemoved),                // directed (incl. DONE_AWAY)
    #[serde(rename = "F")]    Filled(Filled),                            // directed to both sides
}

pub struct SubmitQuoteRequest {
    pub symbol: Symbol,
    pub quantity: Decimal,
    pub req_bids: bool,
    pub req_asks: bool,
    pub expiration: DateTime<Utc>,
    pub client_request_id: Option<u64>,   // echoed back, for sender-side dedupe
    pub disclose_identity: bool,          // false → responders see a pseudonym
    pub target_makers: Vec<UserId>,       // empty = public; non-empty = directed only
}

pub struct SubmitQuote {
    pub request_id: RequestId,
    pub quote: RfqQuote,                  // Option<bid> / Option<ask>, validated
    pub expiration: DateTime<Utc>,
    pub client_quote_id: Option<u64>,
}

pub struct AcceptQuote {
    pub quote_id: QuoteId,
    pub side: Side,                       // disambiguates two-sided quotes
}

Conventions (§2g):

Settlement identifiers (cross_id, trade_id, ticket B-…) are not ROME's to mint (§2.4). The constraint that they must not be derivable from request_id which the public stream exposes is satisfied automatically: the core mints fresh ULIDs independent of any RFQ field.

4.1 Forward-compat legs shim

A-3295 introduces a legs: Vec<Leg> representation behind the single-instrument shape so v2 multi-leg work doesn't break the wire. Single-leg v1 clients deserialize unchanged; multi-leg requests are accepted by the wire and rejected by the engine with not_implemented until A-3297 F1 lands and only once the EP3 multi-leg block question is answered (D8).

5. End-to-end flow

Every client message goes through order_gateway; ROME and the booking core both live in-process there.

   requester WS ─► gateway: ROME negotiation ──► public/targeted RFQ stream
                       │                               │
                       │ accept                        ▼ broadcast
                       ▼                         responder WS
        born-affirmed ticket
                       │
                       ▼
        block-trade booking core ──tonic gRPC──► EP3 InsertTwoSidedBlockTrade
                       │
                       ├─► Postgres block_trades (settlement SoR)
                       └─► drop-copy Execution ──► position / fee / PnL (existing)

Two state machines, one handoff. ROME negotiation (in-memory): Active → {Cancelled, Expired, Accepted}; Accepted is terminal for ROME. Block-trade ticket (Postgres SoR, owned by the core): enters at accepted → booking, then cleared (or needs_manual_reconciliation). ROME observes the outcome to emit Filled but does not drive it.

sequenceDiagram
    autonumber
    actor R as Requester
    participant Gr as order_gateway (R) — ROME
    participant BC as block-trade booking core (in gateway)
    participant PG as Postgres (block_trades SoR)
    participant Gp as order_gateway (P) — ROME
    actor P as Provider
    participant E as EP3 BlockTradesAPI

    R->>Gr: WS SubmitQuoteRequest
    Gr->>Gr: ROME: insert Active request
    Gr--)Gp: publish QuoteRequestPosted (public/targeted)
    Gr-->>R: WS SubmitQuoteRequestResponse { request_id }
    Gp-->>P: WS RfqEvent::QuoteRequestPosted

    P->>Gp: WS SubmitQuote
    Gp->>Gp: risk-check provider (offered side/price)
    Gp--)Gr: directed QuoteReceived
    Gp-->>P: WS SubmitQuoteResponse { quote_id }
    Gr-->>R: WS RfqEvent::QuoteReceived

    R->>Gr: WS AcceptQuote { quote_id, side }
    Gr->>Gr: ROME: risk-check + reserve BOTH legs (single phase)
    Gr->>Gr: ROME: Active → Accepted; competing quotes DONE_AWAY
    Gr->>BC: born-affirmed two-sided ticket (internal seam)
    BC->>PG: persist ticket + cross_id (before EP3 call)
    BC->>E: InsertTwoSidedBlockTrade { cross_id }

    alt cleared (EP3 booked)
        E-->>BC: trade_id + order_ids
        BC->>PG: ticket → cleared
        E--)BC: drop-copy Executions → position/fee/PnL (existing)
        BC--)Gr: ticket cleared
        BC--)Gp: ticket cleared
        Gr-->>R: WS RfqEvent::Filled
        Gp-->>P: WS RfqEvent::Filled
    else definitive reject (4xx, no commit)
        E-->>BC: tonic::Status (INVALID_ARGUMENT, FAILED_PRECONDITION, …)
        BC->>PG: ticket → rejected; release reservations
        BC--)Gr: ticket rejected
        Gr-->>R: WS RfqReject (quote consumed; requester may re-request)
    else ambiguous (5xx / DEADLINE_EXCEEDED / crash)
        Note over BC,E: EP3 may or may not have committed. NO auto-retry.
        BC->>PG: ticket → needs_manual_reconciliation (reservations held)
        BC--)Gr: ticket uncertain
        Gr-->>R: WS RfqEvent (pending settlement)
        Note over BC: sweeper / drop-copy self-healing / operator resolves
    end

Everything in the alt block is the block-trade protocol's behavior, reproduced only so the end-to-end path is legible. The authoritative settlement spec is A-3622; §2 is the seam.

6. EP3 native RFQ reference, not settlement path

EP3 ships a native Quotes/RFQ service (connamara/ep3/quotes/v1beta1/), distinct from the block-trade API. ROME mirrors its semantics and sources its fair-market controls but does not use it to settle (D1).

What it does. CreateRequestForQuote (requester) CreateQuote (dealer, references the RFQ) requester accepts. There is no AcceptQuote RPC: acceptance is InsertOrder with the quote field set the accepted quote becomes ACCEPTED, competitors become DONE_AWAY. Dealers may PassQuote; either side may DeleteQuote. FIX wraps the lifecycle (QuoteRequest, Quote, QuoteStatusReport, QuoteResponse <AJ>; HIT_LIFT synthesizes a LIMIT+IOC order with order.quote set). The key fact: native acceptance settles through InsertOrder a lit-book order/trade, a different primitive from InsertTwoSidedBlockTrade.

Fair-market controls ROME folds in (§3.2 knobs):

The native service remains available if AIEX later needs EP3-side RFQ-to-trade with DONE_AWAY audit on EP3 (open question Q8); D1 does not foreclose it.

7. Audit, observability

Negotiation audit ClickHouse (settlement is recorded by the block-trade protocol in Postgres; the two correlate on ticket_id/cross_id):

CREATE TABLE rfq_log (
    timestamp_ns      UInt64,
    event_type        LowCardinality(String),
    user_id           String,
    request_id        UInt128,
    quote_id          Nullable(UInt128),
    ticket_id         Nullable(String),     -- produced block-trade ticket (B-…)
    symbol            LowCardinality(String),
    quantity          Decimal128(18),
    bid               Nullable(Decimal128(18)),
    ask               Nullable(Decimal128(18)),
    accepted_side     Nullable(String),
    expiration_ns     Nullable(UInt64),
    reject_reason     Nullable(String),
    target_makers     Array(String),
    disclose_identity Bool
) ENGINE = MergeTree
ORDER BY (timestamp_ns, request_id);

event_type: request_submitted, quote_submitted, quote_accepted, quote_done_away, request_cancelled, quote_cancelled, request_expired, quote_expired, ticket_produced, trade_booked, trade_book_failed. The settlement-outcome values (trade_booked/trade_book_failed) are written by ROME on observing the ticket's terminal state, for correlation only they are not the authoritative settlement record.

Writer: bounded mpsc::Sender<RfqLogRow>, batched async inserts, drop-oldest-on-full with a rome.log_drops counter never block a hot path on logging.

Anonymity + the log (D5). ClickHouse rows always carry the real user_id for compliance; stripping happens only at RfqEvent emission. The produced ticket records real buyer/seller accounts regardless.

Trade-tape marker. RFQ prints are block trades drop-copy Executions carry block_trade_indicator = true, and the public-tape block condition marker rides with the block-trade tape work (A-3294).

Prometheus (ROME negotiation path; settlement metrics are the core's): rome_active_requests, rome_active_quotes, rome_event_total{type}, rome_accept_to_ticket_latency_seconds (accept ticket produced), rome_tickets_produced_total, rome_log_drops_total, rome_public_rfq_subscribers, rome_public_rfq_lagged_total. Tracked under A-3219.

8. Failure modes

Negotiation-layer failures are ROME's; settlement-layer failures are inherited and listed for completeness only.

Scenario Layer Behavior
Requester WS disconnect ROME Cancel all their requests + quotes (mirrors cancel_session_orders); subscribers see QuoteRequestRemoved. Hardening A-3295.
Responder WS disconnect ROME Cancel their open quotes. Disconnect between SubmitQuote and AcceptQuote is Q1 default: cancel the quote.
Slow public RFQ subscriber ROME Lagged consumer re-snapshots and resumes; client never sees the lag.
Crash before cancel-on-disconnect ROME RFQs/quotes leak until their natural deadline; server-side TTL/heartbeat needed (A-3295 item 4).
Accept margin check fails ROME Typed RfqReject; no ticket produced; quote may still be acceptable if margin recovers before expiry.
Born-affirmed ticket: definitive EP3 reject (4xx) booking core Ticket rejected, reservations released; ROME emits RfqReject. Quote was consumed; requester re-requests.
Born-affirmed ticket: ambiguous EP3 (5xx / DEADLINE_EXCEEDED / crash) booking core Ticket latches needs_manual_reconciliation, reservations held, no auto-retry. Sweeper / drop-copy self-healing / operator resolves. ROME emits pending-settlement, not Filled.
Gateway restart with in-flight ticket booking core Recovery rebuilds ticket state from Postgres; ROME's in-memory negotiation state for non-accepted requests is lost by design.

The detailed retry-safety-by-layer analysis (double-click, reconnect-resubmit, gateway retry, coreEP3 retry, restart) is the block-trade protocol's. ROME's only contribution is accept-idempotency (Q5): a reconnecting client that re-issues AcceptQuote after the quote was consumed gets a clean reject, but cannot yet distinguish "trade booking" from "quote expired" a recovery-UX gap, tracked, not a correctness gap.

9. Testing

Integration tests under rs/order-gateway/tests/, using ax_test_utils containers (Postgres, ClickHouse, Redis) and Ep3Mock (whose InsertTwoSidedBlockTrade validates parties, preserves cross_id, emits drop-copy fills with block_trade_indicator = true). Each drives real WS clients against the gateway. Tracked under A-3260.

Required scenarios:

  1. Happy-path bid: two responders quote, requester accepts, a born-affirmed ticket books and clears, both see Filled, rfq_log and the block_trades row correlate on ticket_id.
  2. Happy-path two-sided: requester asks bid+ask, accepts the bid; ask side discarded.
  3. Quote after request expires reject.
  4. Accept after quote expires reject.
  5. Concurrent AcceptQuote same request exactly one wins; exactly one ticket produced no double-book.
  6. Definitive EP3 4xx ticket rejected, reservations released, both notified, trade_book_failed logged. 6a. Ambiguous EP3 (5xx / DEADLINE_EXCEEDED / crash mid-call) latch needs_manual_reconciliation, no auto-retry, operator/sweeper path (primarily a block-trade test, asserted through the RFQ entry point). 6b. Client-retry-after-accept clean reject, no second ticket, no duplicate core call.
  7. Requester WS disconnect requests cancelled; subscribers see QuoteRequestRemoved.
  8. Responder WS disconnect quotes cancelled.
  9. Gateway restart with in-flight ticket settlement recovers from Postgres; non-accepted negotiation state gone by design.
  10. Margin moves between SubmitQuote and AcceptQuote AcceptQuote rejected, no ticket.
  11. Snapshot tests (insta, inline) for every RfqRequest/RfqEvent, incl. optional-sides RfqQuote and the submit_quote_ignores_unknown_keys degradation guard.
  12. Side-enforcement matrix quote-includes-disallowed-side, accept-on-missing-side, two-sided-on-one-sided, empty-quote.
  13. Targeted-RFQ visibility: a non-targeted maker on the public stream must not see a targeted QuoteRequestPosted.
  14. Anonymous RFQ: outgoing QuoteRequestPosted must not contain the requester's user_id; the rfq_log row and the ticket carry real accounts.
  15. Competing-quote DONE_AWAY: on accept, other quotes emit QuoteRemoved with a done-away reason.
  16. position_cache flow-through: after clear, a tight opposite-side follow-on order reflects the new position (inherited block-trade behavior; regression guard).

Per the project rule, scenarios 710 (client disconnect, responder disconnect, restart/recovery) must exist and pass before rollout.

10. GUI

Tracked under A-3211 (full surface spec + milestone list). Reuse policy: check @architect-xyz/ui-components and @architect/ui before writing any new util/hook/component (CLAUDE.md); the block-trade GUI prototype (book/affirm/blotter) is the nearest surface the RFQ responder fills view and the ticket blotter should share components.

Milestones G1G7 map to A-3316A-3322; G4 (wire to WS) depends on A-3295.

11. Alternatives considered

12. Non-goals

13. Open questions

Each carries a default so we can ship without sign-off; a decision replaces the placeholder.

# Question Default Owner
Q1 Counterparty disconnect between SubmitQuote and AcceptQuote cancel or leave live? Cancel the quote (mirror order behavior) tin (A-3295 item 5)
Q2 Two-sided requests partial acceptance? No accepting closes the request tin
Q3 Quote replacement amend or cancel+resubmit? Cancel+resubmit v1; AmendQuote v2 (F4) tin
Q4 Born-affirmed seam enter at accepted → booking, or real allege+affirm round-trip? Born-affirmed internal entry (D2); ROME alone calls it tin (A-3622)
Q5 Accept idempotency does a reconnecting client need client_accept_id + a status query? Reuse the ticket blotter (bt query) keyed by produced ticket_id; add ROMEticket correlation tin
Q6 Fair-market gating enforce in v1 or stub? Stub as config knobs (§6); enforce at AIEX/SEF tier product
Q7 Fee schedule for RFQ (block) fills Standard block-trade fees v1; rebates v2 (F7) commercial
Q8 Ever adopt EP3-native Quotes (SEF DONE_AWAY audit on EP3)? No for v1 settle as block trades (D1); revisit for AIEX tin / product
Q9 EP3 multi-leg block trades native, N-call, or spread-as-instrument? Single-leg v1; vendor question unasked (block-trade plan §2f) tin (F1)

Appendix A AX vs Bybit vs Deribit vs EP3-native

# Feature AX (v1) EP3 native RFQ Bybit RFQ Deribit Block RFQ
1 Instruments Single, perp-style Single Spot+perp+future+option Option+perp+future
2 Multi-leg (v2: F1) single 20 legs
3 Settlement primitive InsertTwoSidedBlockTrade (block print) InsertOrder w/ quote (order/trade) exchange book block print
4 Quote sides Bid / Ask / Both one-way one/two-way one/two-way
5 Targeted vs public target_makers firms whitelist
6 Anonymity disclose_identity (associated firms) blind
7 Competing-quote DONE_AWAY partial
8 Quote pass / decline (cancel/no-accept) PassQuote
9 Quote aggregation (v2: F3) partial
10 Quote replacement cancel+resubmit (v2: F4)
11 Min block size stub config (§6) BlockTradeThreshold
12 Min unaffiliated firms stub config (§6) admin knob n/a n/a
13 Self-RFQ restriction policy flag (§6) restrictFirmAcceptOwnRfqs n/a n/a
14 Expiration caller sets, server enforces
15 Maker discovery favorites + targeting firm associations
16 Public tape print block condition (rides tape work) product-dependent
17 Fee rebate standard block fees (v2: F7) n/a
18 Per-user rate limit (A-3294)
19 History / analytics rfq_log + block_trades ListRfqHistory
20 Book-exactly-once on no-dedup RPC (inherited) n/a n/a n/a
21 Cancel-on-disconnect (A-3295)
22 FIX entrypoint (v2) QuoteResponse <AJ>
23 WS streaming subscription

Appendix B Glossary

Term Meaning
RFQ Request for Quote a participant asks the market for a price, makers respond, requester accepts one
ROME The AX RFQ negotiation layer (in the order-gateway, not a separate service)
Block-trade protocol The AX two-sided ticket layer (bt WS family, block_trades Postgres SoR) that books once to EP3 (A-3622); ROME's settlement layer
Born-affirmed ticket A consummated two-sided ticket ROME produces on quote-accept, entering the booking core past the allege/affirm hop (both parties already agreed via the RFQ handshake)
EP3 The Connamara matching engine; the block-trade core books into it
InsertTwoSidedBlockTrade The two-sided, atomic, single-leg EP3 block-trade RPC; no dedup, no idempotency_key
EP3 native RFQ EP3's separate Quotes service (accept-via-InsertOrder); the reference workflow and fair-market-config source, not AX's settlement path (§6)
DONE_AWAY A losing quote when another quote on the same RFQ is accepted
Min unaffiliated firms The minimum distinct unaffiliated firms an RFQ must reach for a fair market (SEF/CFTC); an EP3 admin knob ROME mirrors as config
cross_id Fresh-ULID correlation key minted by the booking core (not ROME); propagates to both order legs and drop-copy executions