Skip to main content

RFQ, FBA & utility actions

Actions on POST /exchange. The request envelope, the EIP-712 signing rules, the number planes and the response shape are on that page and apply to every action here.

RFQ block trading, the FBA frequent-batch-auction entry, and the deliberate no-op. All five return the 202 Accepted admission envelope, so a commit-time refusal comes back as a 200 with an error body — read accepted is not committed.

RFQ is the option trade path. It clears option series and nothing else. The market a request names is the signing_id of a live series, from option_series.

The session is read by polling, not by a feed

rfq_open lists every open session and its quotes, and rfq_user lists the sessions one account requested or quoted on. Both are public. No WS channel carries an RFQ event, so a taker polls for its quotes and a maker polls for requests to answer.

A fill itemises nothing of its own: the balance change on account_state is the public trace of the premium and the escrow.

Wire planes. The RFQ / FBA numeric fields (size, price, max_size, limit_px) are unsigned fixed-point u64 JSON numbers on the wire — the same 1e8 price plane / raw-lot size plane as submit_order, widened to u128 / i128 internally. They are not decimal strings: the strings-on-the-wire policy covers the whole-USDC decimal plane, not the fixed-point book plane. side here uses the core "Bid" / "Ask" tokens (capitalized — unlike the perp order body's lowercase "bid" / "ask").

Acting as a vault / master (owner). Each RFQ action takes an optional owner (0x hex): an approved agent may act as the master / vault it is approved for. Unlike the order actions, the RFQ owner is bound into the EIP-712 digest (a distinct type string with address owner right after metafluxChain): the signer cryptographically commits which account requests / quotes / accepts, because an RFQ session is gated to its requester. A signer that is not an approved agent of owner is rejected 401. Omitting owner keeps the plain sender-authorized digest. fba_submit's owner follows the order convention instead — resolved at admission, not digest-bound.

Open an RFQ session

danger
market is an option series, and nothing else

market takes the signing_id of a live option series, from option_series. Every other market is refused, on all three actions:

precondition failed: rfq is options-only: market <n> is not an option series

A series that has already expired is refused too, with precondition failed: option series expired.

Do not compute the number. signing_id is served whole because the encoding behind it is internal. There is no public formula and no base to add.

Why the lane is options-only. A request-for-quote lane beside a public order book is not fair to that book: it lets size trade away from the price everyone else is posting against. MetaFlux offers RFQ only where there is no continuous book to undercut, and options have none. See options and MIP-4.

Taker opens a request-for-quote session: size on market, optionally bounded by limit_px, open for maker quotes until expiry_ms.

{
"type": "rfq_request",
"params": {
"market": 0,
"side": "Bid",
"size": 100000000,
"limit_px": 10050000000,
"expiry_ms": 1735689605000,
"stp_group": 42
}
}
FieldTypeRange / valuesDescription
ownerhex address | omitted40 hex charsOptional: open the RFQ as this master / vault (approved agents only). Digest-bound — see above
marketuint32a live option seriesThe option_series signing_id. Any other market is refused
sideenum"Bid" / "Ask"Side the requester wants to take. "Bid" BUYS the option and pays the premium; "Ask" WRITES it and locks the escrow
sizeuint64> 0Requested size, on the series' 10^sz_decimals plane (widened to u128)
limit_pxuint64 | nullOptional taker limit price, 1e8 plane; null / omitted = none
expiry_msuint64Session expiry timestamp (consensus ms)
stp_groupuint64 | nullIGNORED. The chain resolves your self-trade group from committed state; a value sent here has no effect. The field stays on the wire because it is inside the signed digest, so keep signing exactly what you sign today. See self-trade prevention

Typed-data primary type (owner absent / present):

MetaFluxTransaction:RfqRequest(string metafluxChain,uint32 market,uint8 side,uint64 size,bool hasLimitPx,uint64 limitPx,uint64 expiryMs,bool hasStpGroup,uint64 stpGroup,uint64 nonce)
MetaFluxTransaction:RfqRequest(string metafluxChain,address owner,uint32 market,uint8 side,uint64 size,bool hasLimitPx,uint64 limitPx,uint64 expiryMs,bool hasStpGroup,uint64 stpGroup,uint64 nonce)

In the digest, side encodes as a uint8 (0 = bid, 1 = ask) and each optional flattens to a presence bool + value (0 when absent).

The assigned rfq_id is a committed effect — read it back from rfq_user. No WS channel carries it, so poll. The session is requester-gated: only the account that opened it can rfq_accept on it.

A bounded request is collateral-checked at once. With limit_px present the chain proves the taker can carry the worst case now — the premium on a "Bid", or the escrow the premium does not fund on an "Ask". It refuses with precondition failed: insufficient free collateral for the request. Without limit_px the worst case is unbounded, so the binding check waits for the accept, which gates both sides at the real price.

On a coin-settled series that check reads a COIN balance. A call escrows one unit of the underlying per whole unit, out of the writer's spot balance, so an "Ask" with a limit_px on such a series is refused with precondition failed: insufficient underlying balance for the escrow when the sender does not hold the coin. Read settle_asset on the series row to know which asset the request will be measured in.

expiry_ms is an absolute consensus-ms stamp, not a duration. 0 takes the governed default window. There is no expiry sweep: an expired request is refused by every later action, but it stays in the book until the open-request cap evicts it.


Quote onto an open RFQ

Maker posts a quote onto an open RFQ session: a price and the maximum size the maker will fill, valid until valid_until_ms.

{
"type": "rfq_quote",
"params": {
"rfq_id": 9,
"price": 2500000000,
"max_size": 100000000,
"valid_until_ms": 1735689604000,
"stp_group": 7
}
}
FieldTypeRange / valuesDescription
ownerhex address | omitted40 hex charsOptional: quote as this master / vault (approved agents only). Digest-bound — see above
rfq_iduint64an open sessionThe RFQ session id, from rfq_user
priceuint64> 0Quoted premium per whole unit, 1e8 plane (widened to i128)
max_sizeuint64> 0Maximum size the maker will fill, on the series' 10^sz_decimals plane (widened to u128)
valid_until_msuint64Quote validity deadline (consensus ms)
stp_groupuint64 | nullIGNORED. The chain resolves your self-trade group from committed state; a value sent here has no effect. The field stays on the wire because it is inside the signed digest, so keep signing exactly what you sign today. See self-trade prevention

Typed-data primary type (owner absent / present):

MetaFluxTransaction:RfqQuote(string metafluxChain,uint64 rfqId,uint64 price,uint64 maxSize,uint64 validUntilMs,bool hasStpGroup,uint64 stpGroup,uint64 nonce)
MetaFluxTransaction:RfqQuote(string metafluxChain,address owner,uint64 rfqId,uint64 price,uint64 maxSize,uint64 validUntilMs,bool hasStpGroup,uint64 stpGroup,uint64 nonce)

The optional stp_group flattens to a presence bool + value in the digest. The quote is recorded under the acting account as its maker — the digest-bound owner when quoting as a vault, else the signer — and the taker sees it on the session (quotes[*] in the rfq_open / rfq_user reads).


Accept an RFQ quote

Taker accepts one specific quote (quote_idx) on their session for a fill of size, settling off-book at the quoted price. The remaining quotes expire with the session.

{
"type": "rfq_accept",
"params": { "rfq_id": 9, "quote_idx": 0, "size": 100000000 }
}
FieldTypeRange / valuesDescription
ownerhex address | omitted40 hex charsOptional: accept as this master / vault (approved agents only). Digest-bound. Both legs must carry owner for an operator to open and accept as the vault
rfq_iduint64own open sessionThe RFQ session id
quote_idxuint32a quote on the sessionIndex of the accepted quote, from the rfq_open / rfq_user reads
sizeuint64> 0Fill size, on the series' 10^sz_decimals plane (widened to u128)

Typed-data primary type (owner absent / present):

MetaFluxTransaction:RfqAccept(string metafluxChain,uint64 rfqId,uint32 quoteIdx,uint64 size,uint64 nonce)
MetaFluxTransaction:RfqAccept(string metafluxChain,address owner,uint64 rfqId,uint32 quoteIdx,uint64 size,uint64 nonce)

Requester-gated. The accept is only honored for the account that opened the session — this binding is why the RFQ owner is part of the signed digest.

What the fill moves

The accept settles one option fill. It moves three amounts and nothing else.

  1. The premium goes from the buyer to the writer. Premium in USDC = quoted price × whole units, truncated toward zero to micro-USDC. The premium is USDC on both kinds.
  2. The escrow goes from the writer's balance into the series pot. Escrow = escrow_per_unit × whole units, denominated in the row's settle_asset. It is the strike in USDC for a put, and ONE COIN of the underlying for a call.
  3. A closing writer's escrow comes out of the pot, exactly, in the same asset. The chain nets each account's own legs first, so a round trip returns what it locked.

The fill charges the TAKER a fee, in USDC — the account that sent the request, whichever leg it took. The quoting maker pays none. The fee is the smaller of a rate on the option's strike face and a fraction of the premium, and both rates start unset, which charges nothing. See the option fee.

The affordability check covers the premium AND the fee, so a taker that can fund the premium alone is refused before anything moves. The request is checked the same way, so an rfq_request you could not afford to accept is refused up front.

On a coin-settled series the writer is checked TWICE, once per asset. A USDC escrow nets the premium it receives, so one number covers it. A coin escrow cannot net a USDC premium, so the chain tests the coin balance for the escrow and the USDC balance for the fee, separately. That is why a call writer holding every coin it needs can still be refused for the fee.

The response does not carry the fee, so compute it. Read option_taker_bps and option_premium_cap_ppm from the option row of products on /info fee_schedule, then:

strike_face = strike x size # BOTH kinds, in USDC
premium = price x size # USDC
fee = min( strike_face x option_taker_bps / 10000 ,
premium x option_premium_cap_ppm / 1000000 )

The notional is the strike face on a call too, not the coin escrow. The chain would need a price to value one coin in dollars, and it never prices an option, so the strike is the notional it can read.

Both terms truncate toward zero and the smaller wins. Your balance moves by the premium plus this fee if you are the taker on a buy, or by the escrow plus this fee less the premium if you are the taker on a sell — and on a call the escrow leg moves your COIN balance, not your USDC.

The fill opens no perpetual position and reserves no margin. An option position can never be liquidated. See options.

Refusals

Every check runs before anything moves, so a refused accept changes no state.

The column below is error.message. Its code is PRECONDITION_FAILED, AUTH_UNAUTHORIZED for unauthorized, or INVALID_REQUEST for an invalid parameters sentence. Match on the code, not on this text — the text is prose and it can change. It is listed so you can read a log.

error.messageCause
precondition failed: rfq is options-only: market <n> is not an option seriesThe session's market is not a live series
precondition failed: option series expiredThe series is at or past its expiry
precondition failed: request expired / quote expiredThe session or the quote is past its own deadline
precondition failed: quote idx <n> not found on rfq RfqId(<n>)No quote at that index
precondition failed: quote price violates taker limitThe quote is worse than the request's limit_px
precondition failed: self-trade blockedBuyer and writer are the same account, or share an STP group
precondition failed: premium truncates to zeroprice × units is below one micro-USDC. Raise the size or the price
precondition failed: insufficient free collateral for premiumThe buyer cannot pay the premium
precondition failed: insufficient free collateral for escrowUSDC series only. The writer cannot fund the escrow the premium does not cover
precondition failed: insufficient underlying balance for the escrowCoin series only. The writer does not hold the coin the escrow takes — one unit of the underlying per whole unit
precondition failed: insufficient free collateral for the feeCoin series only. The writer holds the coin but cannot pay the USDC fee. The two assets are gated separately
precondition failed: option series holds the maximum number of positionsThe series holds 2,048 position rows. Closing an existing row is still allowed
precondition failed: option position registry is fullThe chain holds 32,768 option position rows
precondition failed: series escrow would exceed the ceilingThe series pot is at its ceiling
unauthorizedThe accepter is not the account that opened the session
invalid parameters: accepted size exceeds quote max_size / ... exceeds request sizeThe fill size is above one of the two bounds

Submit into a frequent-batch auction

Submit an order into the market's live FBA window; it clears at the batch's uniform price on the next settle boundary.

{
"type": "fba_submit",
"params": {
"market": 0,
"side": "Bid",
"size": 100000000,
"price": 10050000000
}
}
FieldTypeRange / valuesDescription
ownerhex address | omitted40 hex charsOptional: submit as this master / vault (approved agents only). Not digest-bound — resolved at admission, mirroring the order actions
marketuint32an FBA-enabled marketThe market's signing_id; check fba_enabled on the same markets_meta row
sideenum"Bid" / "Ask"Order side
sizeuint64> 0Order size, fixed-point size plane (widened to u128)
priceuint64> 0Order price, 1e8 plane (widened to i128)
stp_groupuint64 | nullIGNORED. A value sent here has no effect. Self-trade prevention that asks the restrained party to describe itself restrains nobody, so a caller-declared group is never trusted — the chain resolves your self-trade group from committed state at the moment the order parks, and stores that. The field stays on the wire because it is inside the signed digest, so keep signing exactly what you sign today. fba_batch_state does not echo it back. The batch auction DOES apply self-trade prevention: see self-trade prevention in a batch. See self-trade prevention

Typed-data primary type:

MetaFluxTransaction:FbaSubmit(string metafluxChain,uint32 market,uint8 side,uint64 size,uint64 price,bool hasStpGroup,uint64 stpGroup,uint64 nonce)

Observe the pooled order and the indicative uniform clearing via the operator-lane fba_batch_state read.


Deliberate no-op

A deliberate no-op: the handler touches no state — the action's only effect is burning the envelope nonce. Use it as a keepalive or for nonce-gap management (committing a noop at nonce N invalidates any other in-flight action signed with nonce N, since replay protection enforces per-account nonce uniqueness at commit). Sender-authorized; the action carries no params.

{ "type": "noop" }

Typed-data primary type — the chain tag and the envelope nonce are the only signed fields:

MetaFluxTransaction:Noop(string metafluxChain,uint64 nonce)

Response. Non-order action → 202 Accepted admission envelope.