RFQ V2
RFQ V2 is enabled in production. The generated REST reference, WebSocket reference, and error catalog list the V2 routes, messages, and codes.
V1 RFQ does not change. /rfq/request, /rfq/accept, and the rfq WebSocket channel keep their current contract.
V2 is a separate, signed RFQ contract. The taker signs the RFQ, the maker signs the quote, and the taker signs the execution. Hypercall checks all three signatures, the remaining size, and both sides' margin when it fills.
What V2 adds over V1:
- Anonymous takers. Makers quote without seeing the taker wallet.
- Selected-maker routing. Send to one maker, a signed set of up to 128, or every eligible maker.
- Indicative quotes. Non-executable price discovery, then a signed firm-up request.
- Partial fills. Three fill policies, including repeated partial fills against one firm quote.
- Sweeps. One signed, atomic execution across up to 1,024 firm quotes.
- Maker controls. Withdraw one quote, every quote on an RFQ, or every open quote at once, plus an account-wide dead-man cancel.
- Auto execution. The taker signs a premium bound once; the first qualifying firm quote fills.
Availability
| Status | What you see |
|---|---|
| Disabled | Every V2 route returns 403 {"code":"rfq_v2_disabled"}, even for a malformed request. On the socket, AuthenticateRfqV2 answers an Error frame |
| Limited rollout | V2 is open to listed accounts only. An unlisted account gets 403 rfq_v2_not_allowlisted when it creates or replaces an RFQ, publishes or replaces a quote, bulk-withdraws, or sets the cancel schedule, and 401 rfq_v2_session_required on reads. A selected_makers RFQ that names an unlisted maker also gets 403 rfq_v2_not_allowlisted. Execution, sweep, cancellation, quote withdrawal, firm-up, and decline act on an RFQ or quote that already passed the check, so they stay available. all_eligible routing reaches listed makers only, and create returns 422 rfq_v2_no_eligible_makers if none remain |
| Enabled | No gate. Production has been enabled since October 16, 2026 |
Detect availability by the code, not the status. 403 rfq_v2_disabled means V2 is off. 403 rfq_v2_unauthorized means your signer may not act for the account.
Conventions
Base URLs: https://api.hypercall.xyz and wss://api.hypercall.xyz/ws. V2 lives under /private/.... There is no /v2 path prefix and no public V2 route.
Amounts are decimal strings. Every package, capacity, price, premium, and fee value is an unsigned integer in a JSON string. target, quantity_micros_per_unit, firm quote price_micros, and the auto execution premium_micros are capped at 2^96 - 1. Timestamps (*_ms) and nonce are JSON numbers.
| Quantity | Unit | Example |
|---|---|---|
Package units (target, capacity, package_units, minimum, step) | 1,000,000 = one package | "5000000" is 5 packages |
quantity_micros_per_unit | Contracts per package, times 1,000,000 | "1000000" is 1 contract per package |
price_micros, *_premium_micros, fees | USD times 1,000,000 | "830000000" is $830 |
Leg size in contracts is package_units * quantity_micros_per_unit / 10^12. Positions are held in whole millionths of a contract, so package_units * quantity_micros_per_unit must divide by 1,000,000 for every leg. A size that fails this is refused at execution.
Enums are lowercase snake_case: buy, sell, firm_only, repeated_partial. Addresses, bytes32 values, and signatures are lowercase 0x hex. Signatures are 65 bytes, 0x plus 130 hex characters, with v of 0, 1, 27, or 28.
Errors are {"code": "...", "message": "..."}. Branch on code. Every V2 code starts with rfq_v2_. The full list is in Error codes. Two shared codes come before V2 sees the request: a body that does not decode (bad JSON, unknown enum, uppercase hex, a signature v outside the allowed set) returns 422 invalid_json, and a path ID that is not a UUID returns 400 bad_request.
Responses carry Cache-Control: no-store.
Identifiers, nonces, and operation IDs
Object IDs. terms.id on an RFQ is any non-nil UUID except version 8. A quote terms.id is any non-nil UUID. The ID is its own idempotency key: resending the identical signed body returns the original acknowledgement; different terms under the same ID return 409 rfq_v2_idempotency_conflict. A firm quote that firms up an indicative needs a fresh ID.
Nonces. Same rules as order nonces: a millisecond timestamp within two days before and one day after server time, unique per signer. See Nonce Management. Firm quote nonces are tracked separately from order nonces, so order flow on the same key cannot invalidate a live quote. Indicative quotes carry no nonce. Read grants sign a nonce field like any other message, but it is not checked for reuse; keep it in the signed struct.
Operation IDs must be in-window UUIDv7. These actions take a client operation_id:
- execution and sweep
- RFQ cancellation and quote withdrawal
- bulk withdrawal
- the cancel schedule
- RFQ replacement and quote replacement, firm or indicative
For these, operation_id must be an RFC 9562 version 7 UUID whose embedded timestamp is no more than 1 hour before and no more than 30 seconds after server time. Anything else returns 400 rfq_v2_invalid_operation_id; generate a fresh UUIDv7. A bad signature still reports as a signature error.
Version 8 is reserved. A client operation_id with version 8 returns 400 rfq_v2_invalid_request on every route that takes one. Firm-up and decline accept any other non-nil version. Use UUIDv7 everywhere and the rule never bites.
The window is also your retry horizon. Retrying an action with the same operation_id and the identical body returns the original result, but only while the ID is inside the 1-hour window. After that the retry returns 400 rfq_v2_invalid_operation_id and can no longer tell you the outcome, so retry unknown outcomes well inside the hour.
Signing
All V2 messages use the standard Hypercall domain from Authentication: name Hypercall, version 1, the environment's chain ID (999 in production), and verifyingContract 0x0000000000000000000000000000000000000000.
The signer is the account itself or an agent the account approved through /approve-agent. Agent approval is rechecked on every request, including reads.
| Action | Primary type | Signer |
|---|---|---|
| Create RFQ | AuthorizeRfqV2 | Taker |
| Firm quote | AuthorizeRfqQuoteV2 | Maker |
| Indicative quote | PublishRfqIndicativeQuoteV2 | Maker |
| Firm-up request | RequestFirmRfqQuoteV2 | Taker |
| Decline | DeclineRfqV2 | Maker |
| Execute or sweep | ExecuteRfqV2 | Taker |
| Cancel RFQ, withdraw quote | InvalidateRfqV2 | Taker, maker |
| Bulk withdrawal | BulkWithdrawRfqV2 | Maker |
| Cancel schedule | ScheduleRfqQuoteCancelV2 | Maker |
| Replace RFQ or quote | ReplaceRfqV2, plus the replacement's own signature | Taker, maker |
| Read session | AuthorizeRfqReadV2 | Any participant |
EIP-712 type definitions
struct RfqV2SigningLeg { bytes32 instrumentId; uint8 takerSide; uint128 quantityMicrosPerUnit; }
struct RfqV2SigningFeeLimits { uint128 perExecutionMicros; uint128 cumulativeMicros; }
struct RfqV2SigningTerms {
bytes16 rfqId; address takerAccount; RfqV2SigningLeg[] legs; uint128 targetUnits;
uint8 fillPolicy; uint128 minimumUnits; uint128 stepUnits;
uint8 executionMode; bool autoMakerReceivesPremium; uint128 autoPremiumLimitMicros;
uint8 quotePolicy; uint8 takerIdentity; uint8 routingMode; address[] selectedMakers;
uint64 quoteDeadlineMs; uint64 expiresAtMs; RfqV2SigningFeeLimits feeLimits;
address builder; bytes32 privateTermsBlinding;
}
struct AuthorizeRfqV2 { RfqV2SigningTerms terms; uint64 nonce; }
struct RfqV2SigningPriceLeg { bytes32 instrumentId; uint128 priceMicros; }
struct RfqFirmQuoteV2SigningTerms {
bytes16 quoteId; bytes16 rfqId; bytes32 rfqTermsCommitment; address makerAccount;
uint64 makerGeneration; uint64 rfqGeneration; RfqV2SigningPriceLeg[] pricedLegs;
uint128 referenceQuantity; bool makerReceivesReferencePremium; uint128 referencePremiumMicros;
uint128 capacity; uint8 fillPolicy; uint128 minimumUnits; uint128 stepUnits;
uint8 withdrawalPolicy; RfqV2SigningFeeLimits feeLimits; uint64 expiresAtMs;
bytes32 protectionScope; bool hasSourceIndicativeQuote; bytes16 sourceIndicativeQuoteId;
}
struct AuthorizeRfqQuoteV2 { RfqFirmQuoteV2SigningTerms terms; uint64 nonce; }
struct RfqIndicativeQuoteV2SigningTerms {
bytes16 quoteId; bytes16 rfqId; bytes32 rfqTermsCommitment; address makerAccount;
RfqV2SigningPriceLeg[] pricedLegs; uint128 referenceQuantity;
bool makerReceivesReferencePremium; uint128 referencePremiumMicros;
uint128 indicatedCapacity; uint64 expiresAtMs;
}
struct PublishRfqIndicativeQuoteV2 { RfqIndicativeQuoteV2SigningTerms terms; }
struct RfqV2ExecutionAllocation { bytes16 quoteId; uint128 packageUnits; }
struct ExecuteRfqV2 {
bytes16 rfqId; bytes16 operationId; RfqV2ExecutionAllocation[] allocations;
uint128 maxFeeMicros; uint64 expiresAtMs; uint64 nonce;
}
struct InvalidateRfqV2 {
bytes16 rfqId; bytes16 quoteId; uint8 scope; uint8 reason;
bytes16 operationId; uint64 expiresAtMs; uint64 nonce;
}
struct RfqV2BulkWithdrawalTarget { bytes16 rfqId; bytes16 quoteId; }
struct BulkWithdrawRfqV2 {
address makerAccount; uint8 scope; bytes16 rfqId; RfqV2BulkWithdrawalTarget[] targets;
bytes16 operationId; uint64 expiresAtMs; uint64 nonce;
}
struct ScheduleRfqQuoteCancelV2 {
bytes16 operationId; address makerAccount; uint64 cancelAtMs; uint64 expiresAtMs; uint64 nonce;
}
struct ReplaceRfqV2 {
bytes16 rfqId; bytes16 oldObjectId; bytes16 newObjectId; uint8 scope;
bytes16 operationId; uint64 expiresAtMs; uint64 nonce;
}
struct RequestFirmRfqQuoteV2 {
bytes16 operationId; bytes16 rfqId; bytes16 indicativeQuoteId;
uint128 requestedUnits; uint64 expiresAtMs; uint64 nonce;
}
struct DeclineRfqV2 { bytes16 rfqId; address makerAccount; bytes16 operationId; uint64 expiresAtMs; uint64 nonce; }
struct AuthorizeRfqReadV2 { address account; uint64 issuedAtMs; uint64 expiresAtMs; uint64 nonce; }
Field encoding:
| Signed field | Encoding |
|---|---|
UUIDs (bytes16) | The 16 raw UUID bytes |
instrumentId | keccak256("instrument" ‖ symbol), symbol as UTF-8, for example BTC-20261225-100000-C |
takerSide | buy = 0, sell = 1 |
fillPolicy | full_size_once = 0 (minimum and step signed as 0), one_smaller_package = 1, repeated_partial = 2 |
executionMode | manual = 0, auto = 1. Manual signs autoMakerReceivesPremium false and autoPremiumLimitMicros 0 |
quotePolicy | firm_only = 0, indicative_allowed = 1 |
takerIdentity | anonymous = 0, disclosed = 1 |
routingMode | all_eligible = 0 (empty selectedMakers), selected_makers = 1 |
withdrawalPolicy | not_permitted = 0, permitted = 1 |
builder, protectionScope | Zero when absent |
sourceIndicativeQuoteId | hasSourceIndicativeQuote false and zero bytes when absent |
InvalidateRfqV2 | Cancel RFQ: scope 0, reason 0, zero quoteId. Withdraw quote: scope 1, reason 1 |
BulkWithdrawRfqV2.scope | quote_ids = 0 (zero rfqId, targets set), rfq = 1 (rfqId set, empty targets), all_open = 2 (both empty) |
ReplaceRfqV2.scope | RFQ = 0, quote = 1 |
ScheduleRfqQuoteCancelV2.cancelAtMs | 0 removes the schedule. In the JSON body, omit cancel_at_ms; do not send 0 |
terms_commitment is the EIP-712 struct hash of RfqV2SigningTerms. Every quote signs it as rfqTermsCommitment, so a quote cannot be moved to another RFQ or to altered terms.
Taker: create an RFQ
POST /private/rfqs
{
"terms": {
"id": "6d3f7a52-9c1e-4b8a-a0f4-2f71c9e0b5d1",
"taker_account": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
"legs": [
{ "instrument": "BTC-20261225-100000-C", "taker_side": "buy", "quantity_micros_per_unit": "1000000" },
{ "instrument": "BTC-20261225-120000-C", "taker_side": "sell", "quantity_micros_per_unit": "1000000" }
],
"target": "5000000",
"fill_policy": { "mode": "repeated_partial", "minimum": "1000000", "step": "1000000" },
"execution_mode": "manual",
"auto_execution_limit": null,
"quote_policy": "indicative_allowed",
"taker_identity": "anonymous",
"routing": {
"mode": "selected_makers",
"makers": ["0x1111111111111111111111111111111111111111", "0x2222222222222222222222222222222222222222"]
},
"quote_deadline_ms": 1792159220000,
"expires_at_ms": 1792159260000,
"fee_limits": { "per_execution": "0", "cumulative": "0" },
"private_terms_blinding": "0x5be1c0b4d08e2f6c3a97f4e1d2b8a6c50f3e9d7a1b4c8e2f6a0d3b5c7e9f1a2b"
},
"nonce": 1792159200000,
"signature": "0x..."
}
201 response:
{
"rfq_id": "6d3f7a52-9c1e-4b8a-a0f4-2f71c9e0b5d1",
"status": "open",
"terms_commitment": "0x4ac1...",
"quote_deadline_ms": 1792159220000,
"expires_at_ms": 1792159260000
}
A 201 means the RFQ is signature-checked, stored, and sent to makers. It reserves no margin. The RFQ nonce is used on the first fill; later partial fills reuse it.
| Field | Values | Rule |
|---|---|---|
legs | 1 to 10 | No repeated instrument. Instruments must be listed and open for RFQ, else 422 rfq_v2_unknown_instrument. Quotes price legs in this order |
target | package units | Nonzero |
fill_policy.mode | full_size_once, one_smaller_package, repeated_partial | See Fill policies |
execution_mode | manual, auto | auto requires auto_execution_limit; manual forbids it |
auto_execution_limit | {maker_receives_premium, premium_micros} or null | Premium bound per package (1,000,000 units), scaled by the executed size. maker_receives_premium: true caps the taker's debit; false floors the taker's credit |
quote_policy | firm_only, indicative_allowed | Part of the signed terms. The HTTP API reads an omitted field as firm_only; the Rust SDK requires it. Gates indicative quotes and firm-ups |
taker_identity | anonymous, disclosed | Anonymous requires a nonzero private_terms_blinding |
routing.mode | all_eligible, selected_makers | Selected set: 1 to 128 addresses, sorted ascending by raw bytes, no zero or duplicate address |
quote_deadline_ms | ms | Must be before expires_at_ms. Quotes, firm-ups, and declines close at this time |
expires_at_ms | ms | Last instant any fill can land |
fee_limits | {per_execution, cumulative} | Builder fee ceilings. Nonzero values require builder; per_execution at most cumulative |
builder | {recipient} or omitted | Builder attribution address |
private_terms_blinding | bytes32 | 32 bytes from a cryptographically secure random generator (CSPRNG), fresh per anonymous RFQ. A predictable value lets makers recover the hidden taker account |
Builder fees are zero at launch. Hypercall charges no V2 builder fee yet; fee_micros in every fill is "0". Signed fee ceilings still bind.
Anonymous is not unsigned. taker_account stays in the signed terms. Maker views drop it, along with routing, builder, and private_terms_blinding.
Maker: discover, quote, decline
Read grants
Private reads and the socket use a signed read grant, not the request signature.
{ "account": "0x1111...", "issued_at_ms": 1792159200000, "expires_at_ms": 1792159500000, "nonce": 1792159200000, "signature": "0x..." }
Sign AuthorizeRfqReadV2 with the account or an agent. Encode the JSON as base64url without padding (at most 1,024 bytes encoded) and send Authorization: Bearer <token>. A grant issued in the future, already expired, or valid for more than 5 minutes (expires_at_ms - issued_at_ms <= 300000) is refused. Reuse a grant until it expires, then sign a new one.
Discovery
Makers learn about a new RFQ from an RfqV2Changed hint on the rfq_v2 channel or from the change feed. The socket carries no RFQ payload. Read the RFQ:
GET /private/rfqs/{rfq_id}
Authorization: Bearer <grant>
The maker view carries parent.view: "maker" with the economic terms, taker_account only when the RFQ is disclosed, and the fence values your next firm quote must sign:
"maker_quote_generation": { "maker_generation": 3, "rfq_generation": 0 }
Quote fences
Every firm quote signs the two fence values above, copied from your latest read of the RFQ. They let one withdrawal kill quotes you have already sent:
- A bulk withdrawal with scope
all_open, or a fired scheduled cancel, invalidates every withdrawable quote you signed before it, on every RFQ, including quotes still in flight. - A bulk withdrawal with scope
rfqinvalidates every withdrawable quote you signed before it on that RFQ only, including quotes still in flight. Your quotes on other RFQs stay live. - Invalidated quotes return
409 rfq_v2_quote_revoked. - Withdrawing quotes by ID leaves the fence values unchanged.
Never compute the values yourself. After any withdrawal or a fired schedule, re-read the RFQ and sign what it returns.
Quotes signed with withdrawal_policy: "not_permitted" ignore withdrawals and the scheduled cancel, and stay executable until they expire. Use that policy only for quotes you are prepared to see filled until then.
Firm quote
POST /private/rfqs/{rfq_id}/quotes/firm
{
"terms": {
"id": "0e9b3c1a-7f45-4d2e-9b8c-1a2d3e4f5a6b",
"rfq_id": "6d3f7a52-9c1e-4b8a-a0f4-2f71c9e0b5d1",
"rfq_terms_commitment": "0x4ac1...",
"maker_account": "0x1111111111111111111111111111111111111111",
"maker_generation": 3,
"rfq_generation": 0,
"priced_legs": [
{ "instrument": "BTC-20261225-100000-C", "price_micros": "830000000" },
{ "instrument": "BTC-20261225-120000-C", "price_micros": "320000000" }
],
"reference_quantity": "1000000",
"maker_receives_reference_premium": true,
"reference_premium_micros": "510000000",
"capacity": "5000000",
"fill_policy": { "mode": "repeated_partial", "minimum": "1000000", "step": "1000000" },
"withdrawal_policy": "permitted",
"fee_limits": { "per_execution": "0", "cumulative": "0" },
"expires_at_ms": 1792159245000,
"protection_scope": null,
"source_indicative_quote_id": null
},
"nonce": 1792159209000,
"signature": "0x..."
}
Quote rules:
priced_legsmatch the RFQ's legs one for one, in order.fill_policyequals the RFQ's exactly.capacityis at most the RFQtargetand on the step grid;reference_quantityis at mostcapacity. Afull_size_oncequote sets both to the RFQ target.expires_at_msis at most the RFQ'sexpires_at_ms. Quoting closes at the RFQ'squote_deadline_ms.fee_limitsare at most the RFQ's.- Reference premium must be exact. Sum
price_micros * quantity_micros_per_unit * reference_quantity / 10^12over legs, adding legs the taker buys and subtracting legs the taker sells. A positive result means the maker receives: signmaker_receives_reference_premium: trueand the amount. Zero signsfalse. Each step-sized fill must also come out in whole micros. In the example: (830 - 320) x 1 x 1 = $510, maker receives. source_indicative_quote_idlinks a firm-up to its indicative quote (same maker,indicative_allowedRFQ, different ID).nullfor a direct firm quote.
201 response:
{
"firmness": "firm",
"quote_id": "0e9b3c1a-7f45-4d2e-9b8c-1a2d3e4f5a6b",
"status": "open",
"executable": true,
"remaining_capacity": "5000000",
"preflight": { "status": "passed", "checked_at_ms": 1792159209120 }
}
Preflight
Before relaying a firm quote, Hypercall runs a margin check on the maker at reference_quantity.
preflight.status | Result |
|---|---|
passed | Stored and relayed |
unavailable_admitted | The check could not run. Stored and relayed; margin is checked at execution |
rejected | 422 rfq_v2_preflight_rejected. Not relayed. The RFQ stays open; send a fresh quote before the deadline |
Preflight reserves no margin and promises no fill. Margin is checked again at the executed size. A quote that loses a race with a fill, a withdrawal, or a deadline is not relayed as executable.
Indicative quote
Only on indicative_allowed RFQs. Same terms shape as a firm quote minus fence values, fill policy, withdrawal policy, fees, protection scope, and source link, with indicated_capacity in place of capacity. Body is {terms, signature}: no nonce.
POST /private/rfqs/{rfq_id}/quotes/indicative
Response: {"firmness":"indicative","quote_id":"...","status":"open","executable":false,"indicated_capacity":"5000000"}. No preflight, no reservation, never executable. Passing an indicative ID to an execution returns 400 rfq_v2_quote_not_executable.
Decline
POST /private/rfqs/{rfq_id}/declines
{ "operation_id": "01a14503-38dc-72bc-a2d1-beb3ab04b646", "maker_account": "0x1111...", "expires_at_ms": 1792159215000, "nonce": 1792159201500, "signature": "0x..." }
Returns {"operation_id":"...","status":"declined"}. Closes at the quote deadline. The taker sees every decline in declined_makers on the RFQ read; a maker sees only its own. Declines are never public.
Taker: firm-up
Ask the maker of an open indicative quote for executable terms.
POST /private/rfqs/{rfq_id}/quotes/{indicative_quote_id}/firm-up-requests
{ "operation_id": "01a14503-3ad0-76a3-b5b3-a3216cb18501", "requested_units": "3000000", "expires_at_ms": 1792159212000, "nonce": 1792159202000, "signature": "0x..." }
Returns {"operation_id":"...","status":"requested"}. The request is delivered to the maker and nothing else: it changes nothing on the indicative quote, reserves nothing, and does not oblige the maker. The maker answers with a fresh firm quote (new ID, new nonce, source_indicative_quote_id set), which may carry a different price, size, and expiry. The first linked firm quote marks the indicative firmed.
Execute
One quote
POST /private/rfqs/{rfq_id}/executions
{
"operation_id": "01a14503-61e0-7cac-9a5d-5ba4beb4fc1b",
"quote_id": "0e9b3c1a-7f45-4d2e-9b8c-1a2d3e4f5a6b",
"package_units": "2000000",
"max_fee_micros": "0",
"expires_at_ms": 1792159230000,
"nonce": 1792159212000,
"signature": "0x..."
}
200 response:
{
"operation_id": "01a14503-61e0-7cac-9a5d-5ba4beb4fc1b",
"status": "filled",
"executions": [
{
"execution_id": "9b2f...",
"quote_id": "0e9b3c1a-7f45-4d2e-9b8c-1a2d3e4f5a6b",
"maker_account": "0x1111111111111111111111111111111111111111",
"maker_provider": "0x1111111111111111111111111111111111111111",
"maker_signer": "0x3333333333333333333333333333333333333333",
"package_units": "2000000",
"fee_micros": "0"
}
],
"rfq_remaining_target": "3000000",
"quote_remaining_capacity": "3000000"
}
status: "filled" describes this execution. The RFQ can still have target left. maker_account takes the position and premium; maker_provider is the enrolled quote provider that routed it; maker_signer signed the quote.
Sweep
POST /private/rfqs/{rfq_id}/sweeps
{
"operation_id": "01a14503-6d98-71d5-a57b-33a5eb6093e6",
"allocations": [
{ "quote_id": "0e9b3c1a-7f45-4d2e-9b8c-1a2d3e4f5a6b", "package_units": "3000000" },
{ "quote_id": "c41d8e2b-5a3f-4e6d-8b9a-0f1e2d3c4b5a", "package_units": "2000000" }
],
"max_fee_micros": "0",
"expires_at_ms": 1792159230000,
"nonce": 1792159215000,
"signature": "0x..."
}
Allocation order and sizes are signed. A sweep fills every allocation or none. At most 1,024 allocations, else 400 rfq_v2_too_many_targets. An empty list or a repeated quote returns 400 rfq_v2_invalid_terms. Same response shape, one entry per allocation. quote_remaining_capacity is present only when the execution used one quote; a sweep across several quotes returns rfq_remaining_target alone, so read each quote for its capacity.
What Hypercall checks at execution
Every execution checks: all signatures and signer approval, that the quote was signed for this RFQ, that the RFQ, quote, and execution are unexpired, that the quote has not been withdrawn or cancelled, the fill policy on both documents, remaining target and capacity, the size grid, fee ceilings, and self-trade (taker and maker resolving to the same account, on-chain manager, signer, or provider: 409 rfq_v2_self_trade). Then it checks market state, MMP, and both sides' margin.
| Refusal | Code | What to do |
|---|---|---|
| Size breaks the fill policy or exceeds remaining capacity or target | 409 rfq_v2_capacity_unavailable | Re-read, resize, re-sign |
| Quote or RFQ already used up, withdrawn, cancelled, or replaced | 409 rfq_v2_authorization_terminal | Stop. Pick another quote |
| Quote signed before a maker withdrawal or fired cancel | 409 rfq_v2_quote_revoked | Stop. Pick another quote |
| Margin, funding, or position check refuses | 409 rfq_v2_risk_rejected | Nothing filled and no nonce was used. The same signed request may pass later |
| Request does not match the signed documents (reused nonce, mismatched terms, off-grid size, a fill amount over the fill size limit) | 409 rfq_v2_authority_rejected | Final for that request. Fix and re-sign |
| Outcome unknown | 503 rfq_v2_authority_unavailable | Retry the identical request with the same operation_id. See Unknown outcomes |
Fill size limit
In one fill, each leg size must be below 10^12 contracts, and each leg price, leg premium, package premium, and option leg underlying notional (size times spot) below 10^12 USD. In micros that is 10^18. A sweep applies the limit to each allocation; the sweep total is not limited. Signed terms can be larger; split the execution.
Fill policies and repeated partial fills
| Policy | Fills allowed |
|---|---|
full_size_once | One fill of exactly the full target. The quote's capacity and reference_quantity equal the target |
one_smaller_package | One fill, at least minimum, a multiple of step. Whatever is left after it is unusable |
repeated_partial | Any number of fills, each at least minimum, a multiple of step, and within what is left |
minimum and step are at most the target, and minimum, the target, and each quote's capacity are multiples of step.
Repeated partials against one quote. Capacity is tracked per quote; target is tracked per RFQ. Each fill uses both, and every response returns the RFQ remainder (and the quote remainder when one quote was used). A taker can hit the same firm quote repeatedly until its capacity, the RFQ target, or a deadline runs out, and can mix quotes from several makers across executions or within a sweep. There is no exception for a final remainder below minimum: once what is left is under minimum, the quote reads executable: false.
Auto execution. On an auto RFQ, Hypercall fills the first qualifying firm quote at the largest size both signed fill policies, the size grid, and the remainders allow, within the signed premium bound. The taker sends no execution request. An auto fill never happens twice for the same quote usage, and each later partial is a separate fill. If publication returns 503 rfq_v2_auto_execution_unavailable, the quote was accepted and the auto fill is still pending; retry the identical publication.
Cancel, withdraw, replace
All take {operation_id, expires_at_ms, nonce, signature} (UUIDv7 operation ID) unless shown.
| Action | Route | Signer | Response |
|---|---|---|---|
| Cancel RFQ | POST /private/rfqs/{rfq_id}/cancellations | Taker | 200 {operation_id, status: "cancelled"} |
| Withdraw quote | POST /private/rfqs/{rfq_id}/quotes/{quote_id}/withdrawals | Maker | 200 {operation_id, status: "withdrawn"} |
| Replace RFQ | POST /private/rfqs/{rfq_id}/replacements | Taker | 201, create response for the new RFQ |
| Replace quote | POST /private/rfqs/{rfq_id}/quotes/{quote_id}/replacements | Maker | 201, publish response for the new quote |
| Bulk withdrawal | POST /private/rfq-quote-withdrawals | Maker | 200 {operation_id, status: "withdrawn"} |
Replacement is never an in-place edit. The body is {operation_id, replacement, expires_at_ms, nonce, signature}: replacement is a complete, separately signed create or publish body with a new ID, and the outer signature is ReplaceRfqV2 binding the old and new IDs. The old object closes and the new one opens in one step.
Bulk withdrawal takes maker_account and a scope:
{ "mode": "quote_ids", "quote_ids": [{ "rfq_id": "...", "quote_id": "..." }] }
{ "mode": "rfq", "rfq_id": "..." }
{ "mode": "all_open" }
quote_ids holds at most 64 distinct pairs, else 400 rfq_v2_too_many_targets; it withdraws exactly those quotes. rfq and all_open invalidate every withdrawable quote you signed for that RFQ, or for every RFQ, including quotes still in flight (see Quote fences). The 200 arrives only once the withdrawal has taken effect.
Dead-man switch: scheduled quote cancel
A maker sets an account-wide deadline. If it is not renewed, every permitted firm quote of that maker, on every RFQ, stops being executable at that time.
POST /private/rfq-quote-cancel-schedule
{
"operation_id": "01a14503-5628-789d-b927-3a1e9ac8d854",
"maker_account": "0x1111111111111111111111111111111111111111",
"cancel_at_ms": 1792159230000,
"expires_at_ms": 1792159220000,
"nonce": 1792159209000,
"signature": "0x..."
}
Response: {"operation_id":"...","cancel_at_ms":1792159230000}. Trust the returned value; a renewal that timed out may not have taken effect.
- Lead time:
cancel_at_msat least 5,000 ms in the future, else422 rfq_v2_deadline_too_short. No maximum. - Renew by sending a later
cancel_at_msbefore the current one arrives. Each request replaces the schedule. - Clear by omitting
cancel_at_ms(signed ascancelAtMs = 0). An explicit0in the JSON is refused with400 rfq_v2_invalid_terms. If the old deadline is already due when you renew or clear, it fires first. - On fire: every withdrawable quote you signed before the deadline is dead, and the schedule is removed. A quote cannot fill after its deadline, even in the moment before the cancel shows up in reads.
- Scope: account-wide, not per socket. A dropped connection cancels nothing. RFQs and other makers' quotes are untouched.
not_permittedquotes are exempt. Indicative quotes were never executable.
Reads and the change feed
All reads need Authorization: Bearer <grant>. A missing or invalid grant returns 401 rfq_v2_session_required.
| Route | Returns |
|---|---|
GET /private/rfqs | {rfqs, page, as_of_sequence}. Query: role (taker, maker, either; default either), status, limit (default 50, max 100), and cursor or after_sequence, not both |
GET /private/rfqs/{rfq_id} | One RFQ: rfq_id, status, terms_commitment, parent (tagged by view: taker or maker), target, remaining_target, quote_policy, maker_quote_generation (maker only), declined_makers (when present) |
GET /private/rfqs/{rfq_id}/quotes | {quotes, page, as_of_sequence}. Query: firmness, status, include_terminal (default false), limit (default 50, max 100), cursor or after_sequence |
GET /private/rfqs/{rfq_id}/quotes/{quote_id} | One quote, tagged by firmness, with terms, status, executable, provider, signer, and for firm quotes remaining_capacity and preflight |
GET /private/rfqs/changes | {changes: [{rfq_id, sequence}], next_after_sequence, has_more}. Query: after_sequence, limit (1 to 100, default 100) |
GET /private/rfqs/{rfq_id}/activity | Aggregate counts only (deliveries, indicative and firm responses, firm-ups, declines, preflight rejections, withdrawals, no-response by deadline), never wallets or terms. The taker sees its RFQ's totals; a maker sees only its own. Non-participants get 403 rfq_v2_unauthorized |
page is {limit, next_cursor, has_more}. RFQ list pages (GET /private/rfqs) and quote list pages (GET /private/rfqs/{rfq_id}/quotes) each scan a bounded slice of history, so with a status filter (or, for quotes, firmness or include_terminal) a page can return fewer than limit items, or none, with has_more: true. Keep paging with next_cursor until has_more is false. RFQ statuses: open, partially_filled, filled, expired, cancelled, replaced. Quote statuses: open, partially_filled, filled, expired, closed, withdrawn, replaced, firmed.
Visibility. A taker reads every quote on its RFQ. A maker reads RFQs routed to it and only its own quotes, never a competitor's. Closed quotes stay readable, but the quote list hides them by default: it returns executable firm quotes and open indicative quotes unless you pass include_terminal=true. Old RFQs stay readable with their final remainders and statuses.
Check executable, not status. A quote can read open after its RFQ filled or was cancelled.
Expiry emits nothing. Passing a signed deadline creates no change-feed row and no socket hint. Refresh anything you display at its deadline.
Change feed. The feed lists only RFQ IDs that changed for your account. Read the named RFQs and quotes, apply them, then save next_after_sequence. Gaps in sequence are normal: other accounts' changes are omitted. Page until has_more is false.
WebSocket
Same /ws connection. The legacy Authenticate message and ?wallet= do not authorize rfq_v2.
{"type":"AuthenticateRfqV2","token":"<base64url read grant>"}
{"type":"RfqV2Authenticated","account":"0x1111111111111111111111111111111111111111"}
{"type":"Subscribe","channel":"rfq_v2"}
{"type":"Subscribed","channel":"rfq_v2"}
{"type":"RfqV2Changed","rfq_id":"6d3f7a52-9c1e-4b8a-a0f4-2f71c9e0b5d1","sequence":812045}
Subscribewithout a priorAuthenticateRfqV2fails: "RFQ V2 signed read grant required". The channel takes nosymbols,expiry, oroption_typefilters.- The grant is rechecked before each hint. Expiry or agent revocation stops delivery; sign a new grant and re-authenticate before
expires_at_ms. RfqV2Changedis a hint, not state. It carries an RFQ ID and a sequence, nothing else. Hints can repeat. Fetch the RFQ and quotes.- Who gets a hint. The taker and routed makers on creation. The taker and the quoting maker on that maker's quotes, firm-ups, declines, and quote replacements. The taker and every routed maker, traded or not, on fills, RFQ cancellation, and RFQ replacement. The maker and the affected takers on withdrawals, on setting, renewing, or clearing the cancel schedule, and on fired cancels.
Run the change feed alongside the socket. After subscribing, read GET /private/rfqs/changes from your last saved sequence before trusting local state. Keep polling it while connected, because a missed hint leaves no gap you can see. On reconnect, read from your last saved sequence and de-duplicate.
Retries and unknown outcomes
- Create and publish: resend the identical signed body. Same ID, same bytes returns the original; changed bytes return
409 rfq_v2_idempotency_conflict. - Actions with an
operation_id: resend the identical body with the sameoperation_idinside the 1-hour window to get the original result. A different body under the same operation ID conflicts. 503 rfq_v2_authority_unavailableor a timeout on an execution: the outcome is unknown and the fill may have happened. Retry the identical signed request with the sameoperation_iduntil you get a definite answer: the original result, or a definite refusal. That retry is the only answer you can rely on, and it works only inside the 1-hour window. An RFQ read is a sanity check, not proof:remaining_targetand quote capacity carry nooperation_id, can move with other fills, and can lag. Do not sign a new execution for the same intent until the retry gives a definite answer.503 rfq_v2_projection_pending: the action committed but its result is not readable yet. Retry the identical request with the sameoperation_id; it returns the original result once readable.503 rfq_v2_replacement_pending: the replacement was refused but the refusal is not recorded yet. Retry the identical request with the sameoperation_id.429 rfq_v2_rate_limited: honorRetry-After. The limit is 6,000 requests per minute per source IP.
Limits
| Limit | Value |
|---|---|
| Legs per RFQ | 10 |
| Selected makers | 128 |
| Allocations per sweep | 1,024 |
| Quote pairs per bulk withdrawal | 64 |
| Read grant lifetime | 5 minutes |
| Encoded read grant | 1,024 bytes |
| Operation ID window | 1 hour back, 30 seconds forward |
| Cancel schedule lead | 5 seconds minimum |
| Page size | 50 default, 100 max (change feed: 100 default) |
| Amounts in one fill | Below 10^12 contracts or USD (10^18 micros). See Fill size limit |
| Signed amounts | Up to 2^96 - 1 |
| V2 request rate | 6,000 requests per minute per source IP |
Error codes
| Code | HTTP | Meaning and what to do |
|---|---|---|
rfq_v2_disabled | 403 | V2 is off on this environment |
rfq_v2_not_allowlisted | 403 | V2 is limited to listed accounts during rollout |
rfq_v2_session_required | 401 | Read needs a valid signed read grant. Sign a new one |
rfq_v2_unauthorized | 403 | Signer not approved for the account, or the account may not see this RFQ |
rfq_v2_not_found | 404 | RFQ or quote does not exist for this account |
rfq_v2_invalid_signature | 400 | Signature malformed or does not recover a signer |
rfq_v2_invalid_operation_id | 400 | Operation ID not a UUIDv7 inside the window. Generate a fresh UUIDv7 |
rfq_v2_invalid_request | 400 | Request cannot be served, including a version 8 operation ID |
rfq_v2_invalid_terms | 400 | Signed terms or action fields break a rule on this page |
rfq_v2_invalid_create | 400 | Create routes to more makers than one request accepts |
rfq_v2_invalid_package_units | 400 | Package, leg, target, or fill-policy quantity zero, off grid, or out of range |
rfq_v2_invalid_maker_set | 400 | Selected makers empty, over 128, unsorted, or repeated |
rfq_v2_invalid_quote | 400 | Quote malformed or does not fit the RFQ |
rfq_v2_invalid_firm_up | 400 | Firm-up malformed or does not fit its indicative quote |
rfq_v2_invalid_decline | 400 | Decline malformed |
rfq_v2_invalid_action | 400 | Execution, sweep, cancel, or withdrawal malformed |
rfq_v2_invalid_replacement | 400 | Replacement malformed or does not fit the replaced object |
rfq_v2_too_many_targets | 400 | Sweep over 1,024 quotes or bulk withdrawal over 64 pairs |
rfq_v2_invalid_query | 400 | List or read query malformed, including a page limit out of range |
rfq_v2_invalid_cursor | 400 | Cursor malformed or out of range, or a change feed limit out of range |
rfq_v2_path_mismatch | 400 | Path ID disagrees with the signed body |
rfq_v2_deadline_passed | 400 | A signed or quote deadline has passed |
rfq_v2_quote_not_executable | 400 | Execution named an indicative quote |
rfq_v2_unknown_instrument | 422 | Instrument not listed or not open for RFQ |
rfq_v2_no_eligible_makers | 422 | No eligible maker, or a selected maker is not eligible |
rfq_v2_preflight_rejected | 422 | Firm quote failed the maker margin preflight. The RFQ stays open |
rfq_v2_deadline_too_short | 422 | Cancel deadline under the 5-second lead |
rfq_v2_parent_closed | 409 | RFQ was cancelled, replaced, or filled first |
rfq_v2_idempotency_conflict | 409 | ID already used with different signed terms |
rfq_v2_indicative_replaced | 409 | Indicative quote was replaced first |
rfq_v2_quote_revoked | 409 | Quote signed before a maker withdrawal or fired cancel |
rfq_v2_replacement_conflict | 409 | The object being replaced was already used or changed |
rfq_v2_replacement_rejected | 409 | Replacement refused. The original object stands |
rfq_v2_authorization_terminal | 409 | Quote or RFQ is no longer executable |
rfq_v2_capacity_unavailable | 409 | Size exceeds remaining capacity or breaks the fill policy. Re-read and resize |
rfq_v2_risk_rejected | 409 | Margin or risk check refused. Nothing filled; the same request may pass later |
rfq_v2_authority_rejected | 409 | Request does not match the signed documents. Final for that request |
rfq_v2_self_trade | 409 | Taker and maker resolve to the same principal |
rfq_v2_rate_limited | 429 | Rate limit exceeded. Honor Retry-After |
rfq_v2_delivery_failed | 500 | The request could not be completed. Retry the identical request |
rfq_v2_delivery_unavailable | 500, 503 | Temporarily unavailable. Retry the identical request |
rfq_v2_authority_unavailable | 503 | Outcome unknown. Retry the identical request with the same operation_id for a definite answer |
rfq_v2_auto_execution_unavailable | 503 | Quote accepted, auto fill pending. Retry the identical publication |
rfq_v2_projection_pending | 503 | Committed, result not readable yet. Retry the identical request |
rfq_v2_replacement_pending | 503 | Replacement refused, refusal not recorded yet. Retry the identical request |
Also in this release: RPI outcomes on /order
This affects POST /order with route: "best_execution", not V2. If a retail price improvement (RPI) execution against an RFQ quote has an unknown outcome, the order call returns 504, "RPI execution outcome is pending; the order may have filled, do not resubmit", and does not fall back to another quote or the book. Do not resubmit. Check fills and order status. The pending execution never fills twice.