Diese Seite wurde maschinell übersetzt. Das englische Original ist maßgeblich. Auf Englisch lesen →
Zum Hauptinhalt springen

RFQ V2

Live since October 16, 2026

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.
Taker
1. Create
Sign AuthorizeRfqV2 over legs, target, fill policy, routing
POST /private/rfqs
Keep the returned terms hash
Maker
2. Discover
RfqV2Changed hint on rfq_v2
GET /private/rfqs/{rfq_id}
Copy the fence values to sign
Maker
3. Quote
Indicative (optional, never executable)
Firm: signed price, capacity, fence values
Margin preflight before relay
Taker
4. Execute
Sign ExecuteRfqV2 with a fresh UUIDv7
One quote or an ordered sweep
All allocations fill, or none
Firm-only RFQs skip the indicative step. Partial-fill RFQs repeat step 4 until the target or the quote capacity runs out.

Availability​

StatusWhat you see
DisabledEvery V2 route returns 403 {"code":"rfq_v2_disabled"}, even for a malformed request. On the socket, AuthenticateRfqV2 answers an Error frame
Limited rolloutV2 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
EnabledNo 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.

QuantityUnitExample
Package units (target, capacity, package_units, minimum, step)1,000,000 = one package"5000000" is 5 packages
quantity_micros_per_unitContracts per package, times 1,000,000"1000000" is 1 contract per package
price_micros, *_premium_micros, feesUSD 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.

ActionPrimary typeSigner
Create RFQAuthorizeRfqV2Taker
Firm quoteAuthorizeRfqQuoteV2Maker
Indicative quotePublishRfqIndicativeQuoteV2Maker
Firm-up requestRequestFirmRfqQuoteV2Taker
DeclineDeclineRfqV2Maker
Execute or sweepExecuteRfqV2Taker
Cancel RFQ, withdraw quoteInvalidateRfqV2Taker, maker
Bulk withdrawalBulkWithdrawRfqV2Maker
Cancel scheduleScheduleRfqQuoteCancelV2Maker
Replace RFQ or quoteReplaceRfqV2, plus the replacement's own signatureTaker, maker
Read sessionAuthorizeRfqReadV2Any 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 fieldEncoding
UUIDs (bytes16)The 16 raw UUID bytes
instrumentIdkeccak256("instrument" ‖ symbol), symbol as UTF-8, for example BTC-20261225-100000-C
takerSidebuy = 0, sell = 1
fillPolicyfull_size_once = 0 (minimum and step signed as 0), one_smaller_package = 1, repeated_partial = 2
executionModemanual = 0, auto = 1. Manual signs autoMakerReceivesPremium false and autoPremiumLimitMicros 0
quotePolicyfirm_only = 0, indicative_allowed = 1
takerIdentityanonymous = 0, disclosed = 1
routingModeall_eligible = 0 (empty selectedMakers), selected_makers = 1
withdrawalPolicynot_permitted = 0, permitted = 1
builder, protectionScopeZero when absent
sourceIndicativeQuoteIdhasSourceIndicativeQuote false and zero bytes when absent
InvalidateRfqV2Cancel RFQ: scope 0, reason 0, zero quoteId. Withdraw quote: scope 1, reason 1
BulkWithdrawRfqV2.scopequote_ids = 0 (zero rfqId, targets set), rfq = 1 (rfqId set, empty targets), all_open = 2 (both empty)
ReplaceRfqV2.scopeRFQ = 0, quote = 1
ScheduleRfqQuoteCancelV2.cancelAtMs0 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.

FieldValuesRule
legs1 to 10No repeated instrument. Instruments must be listed and open for RFQ, else 422 rfq_v2_unknown_instrument. Quotes price legs in this order
targetpackage unitsNonzero
fill_policy.modefull_size_once, one_smaller_package, repeated_partialSee Fill policies
execution_modemanual, autoauto requires auto_execution_limit; manual forbids it
auto_execution_limit{maker_receives_premium, premium_micros} or nullPremium 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_policyfirm_only, indicative_allowedPart 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_identityanonymous, disclosedAnonymous requires a nonzero private_terms_blinding
routing.modeall_eligible, selected_makersSelected set: 1 to 128 addresses, sorted ascending by raw bytes, no zero or duplicate address
quote_deadline_msmsMust be before expires_at_ms. Quotes, firm-ups, and declines close at this time
expires_at_msmsLast 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 omittedBuilder attribution address
private_terms_blindingbytes3232 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 rfq invalidates 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_legs match the RFQ's legs one for one, in order.
  • fill_policy equals the RFQ's exactly. capacity is at most the RFQ target and on the step grid; reference_quantity is at most capacity. A full_size_once quote sets both to the RFQ target.
  • expires_at_ms is at most the RFQ's expires_at_ms. Quoting closes at the RFQ's quote_deadline_ms.
  • fee_limits are at most the RFQ's.
  • Reference premium must be exact. Sum price_micros * quantity_micros_per_unit * reference_quantity / 10^12 over legs, adding legs the taker buys and subtracting legs the taker sells. A positive result means the maker receives: sign maker_receives_reference_premium: true and the amount. Zero signs false. 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_id links a firm-up to its indicative quote (same maker, indicative_allowed RFQ, different ID). null for 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​

STEP 1
Firm quote signed
Fence values copied from the parent read
STEP 2
Preflight
passed, unavailable_admitted, or rejected (422)
STEP 3
Relayed
No margin reserved, no fill promised
STEP 4
Executed
Margin checked again at the executed size

Before relaying a firm quote, Hypercall runs a margin check on the maker at reference_quantity.

preflight.statusResult
passedStored and relayed
unavailable_admittedThe check could not run. Stored and relayed; margin is checked at execution
rejected422 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​

1. Signatures
Parent, quote, and execution signatures
Signer approved for the account
Quote signed for this RFQ
Operation ID in window
2. Terms
Parent and quote unexpired
Quote not withdrawn or cancelled
Fill policy and size grid
No self-trade
3. Risk
Target and capacity left
Market state and MMP
Taker margin
Maker margin
4. Fill
Every leg of every allocation
Premium settled
Remainders returned
Either every allocation fills, or nothing changes.

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.

RefusalCodeWhat to do
Size breaks the fill policy or exceeds remaining capacity or target409 rfq_v2_capacity_unavailableRe-read, resize, re-sign
Quote or RFQ already used up, withdrawn, cancelled, or replaced409 rfq_v2_authorization_terminalStop. Pick another quote
Quote signed before a maker withdrawal or fired cancel409 rfq_v2_quote_revokedStop. Pick another quote
Margin, funding, or position check refuses409 rfq_v2_risk_rejectedNothing 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_rejectedFinal for that request. Fix and re-sign
Outcome unknown503 rfq_v2_authority_unavailableRetry 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​

PolicyFills allowed
full_size_onceOne fill of exactly the full target. The quote's capacity and reference_quantity equal the target
one_smaller_packageOne fill, at least minimum, a multiple of step. Whatever is left after it is unusable
repeated_partialAny 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.

ActionRouteSignerResponse
Cancel RFQPOST /private/rfqs/{rfq_id}/cancellationsTaker200 {operation_id, status: "cancelled"}
Withdraw quotePOST /private/rfqs/{rfq_id}/quotes/{quote_id}/withdrawalsMaker200 {operation_id, status: "withdrawn"}
Replace RFQPOST /private/rfqs/{rfq_id}/replacementsTaker201, create response for the new RFQ
Replace quotePOST /private/rfqs/{rfq_id}/quotes/{quote_id}/replacementsMaker201, publish response for the new quote
Bulk withdrawalPOST /private/rfq-quote-withdrawalsMaker200 {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_ms at least 5,000 ms in the future, else 422 rfq_v2_deadline_too_short. No maximum.
  • Renew by sending a later cancel_at_ms before the current one arrives. Each request replaces the schedule.
  • Clear by omitting cancel_at_ms (signed as cancelAtMs = 0). An explicit 0 in the JSON is refused with 400 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_permitted quotes 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.

RouteReturns
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}/activityAggregate 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}
  • Subscribe without a prior AuthenticateRfqV2 fails: "RFQ V2 signed read grant required". The channel takes no symbols, expiry, or option_type filters.
  • The grant is rechecked before each hint. Expiry or agent revocation stops delivery; sign a new grant and re-authenticate before expires_at_ms.
  • RfqV2Changed is 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 same operation_id inside the 1-hour window to get the original result. A different body under the same operation ID conflicts.
  • 503 rfq_v2_authority_unavailable or a timeout on an execution: the outcome is unknown and the fill may have happened. Retry the identical signed request with the same operation_id until 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_target and quote capacity carry no operation_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 same operation_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 same operation_id.
  • 429 rfq_v2_rate_limited: honor Retry-After. The limit is 6,000 requests per minute per source IP.

Limits​

LimitValue
Legs per RFQ10
Selected makers128
Allocations per sweep1,024
Quote pairs per bulk withdrawal64
Read grant lifetime5 minutes
Encoded read grant1,024 bytes
Operation ID window1 hour back, 30 seconds forward
Cancel schedule lead5 seconds minimum
Page size50 default, 100 max (change feed: 100 default)
Amounts in one fillBelow 10^12 contracts or USD (10^18 micros). See Fill size limit
Signed amountsUp to 2^96 - 1
V2 request rate6,000 requests per minute per source IP

Error codes​

CodeHTTPMeaning and what to do
rfq_v2_disabled403V2 is off on this environment
rfq_v2_not_allowlisted403V2 is limited to listed accounts during rollout
rfq_v2_session_required401Read needs a valid signed read grant. Sign a new one
rfq_v2_unauthorized403Signer not approved for the account, or the account may not see this RFQ
rfq_v2_not_found404RFQ or quote does not exist for this account
rfq_v2_invalid_signature400Signature malformed or does not recover a signer
rfq_v2_invalid_operation_id400Operation ID not a UUIDv7 inside the window. Generate a fresh UUIDv7
rfq_v2_invalid_request400Request cannot be served, including a version 8 operation ID
rfq_v2_invalid_terms400Signed terms or action fields break a rule on this page
rfq_v2_invalid_create400Create routes to more makers than one request accepts
rfq_v2_invalid_package_units400Package, leg, target, or fill-policy quantity zero, off grid, or out of range
rfq_v2_invalid_maker_set400Selected makers empty, over 128, unsorted, or repeated
rfq_v2_invalid_quote400Quote malformed or does not fit the RFQ
rfq_v2_invalid_firm_up400Firm-up malformed or does not fit its indicative quote
rfq_v2_invalid_decline400Decline malformed
rfq_v2_invalid_action400Execution, sweep, cancel, or withdrawal malformed
rfq_v2_invalid_replacement400Replacement malformed or does not fit the replaced object
rfq_v2_too_many_targets400Sweep over 1,024 quotes or bulk withdrawal over 64 pairs
rfq_v2_invalid_query400List or read query malformed, including a page limit out of range
rfq_v2_invalid_cursor400Cursor malformed or out of range, or a change feed limit out of range
rfq_v2_path_mismatch400Path ID disagrees with the signed body
rfq_v2_deadline_passed400A signed or quote deadline has passed
rfq_v2_quote_not_executable400Execution named an indicative quote
rfq_v2_unknown_instrument422Instrument not listed or not open for RFQ
rfq_v2_no_eligible_makers422No eligible maker, or a selected maker is not eligible
rfq_v2_preflight_rejected422Firm quote failed the maker margin preflight. The RFQ stays open
rfq_v2_deadline_too_short422Cancel deadline under the 5-second lead
rfq_v2_parent_closed409RFQ was cancelled, replaced, or filled first
rfq_v2_idempotency_conflict409ID already used with different signed terms
rfq_v2_indicative_replaced409Indicative quote was replaced first
rfq_v2_quote_revoked409Quote signed before a maker withdrawal or fired cancel
rfq_v2_replacement_conflict409The object being replaced was already used or changed
rfq_v2_replacement_rejected409Replacement refused. The original object stands
rfq_v2_authorization_terminal409Quote or RFQ is no longer executable
rfq_v2_capacity_unavailable409Size exceeds remaining capacity or breaks the fill policy. Re-read and resize
rfq_v2_risk_rejected409Margin or risk check refused. Nothing filled; the same request may pass later
rfq_v2_authority_rejected409Request does not match the signed documents. Final for that request
rfq_v2_self_trade409Taker and maker resolve to the same principal
rfq_v2_rate_limited429Rate limit exceeded. Honor Retry-After
rfq_v2_delivery_failed500The request could not be completed. Retry the identical request
rfq_v2_delivery_unavailable500, 503Temporarily unavailable. Retry the identical request
rfq_v2_authority_unavailable503Outcome unknown. Retry the identical request with the same operation_id for a definite answer
rfq_v2_auto_execution_unavailable503Quote accepted, auto fill pending. Retry the identical publication
rfq_v2_projection_pending503Committed, result not readable yet. Retry the identical request
rfq_v2_replacement_pending503Replacement 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.