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

Preview: RFQ V2, enabled October 16, 2026

Preview

RFQ V2 is enabled in production on October 16, 2026. Until the switch flips, every V2 route returns 403 rfq_v2_disabled. This page is the reference until the release ships. The generated REST reference, WebSocket reference, and error catalog pick up the V2 routes, messages, and codes with the release.

V1 RFQ does not change. /rfq/request, /rfq/accept, and the rfq WebSocket channel keep their current contract. The V2 switch never gates them.

V2 is a separate, signed RFQ contract. Every object a counterparty can act on (the parent RFQ, a firm quote, an execution) is an EIP-712 document the engine re-verifies when it fills. The API relays and stores; the engine decides.

What V2 adds over V1:

  • Anonymous takers. Makers quote without seeing the taker wallet.
  • Selected-maker routing. Send to one maker or a signed set of up to 128, or to 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 fences. Generation counters, bulk withdrawal, and 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
Store terms_commitment
Maker
2. Discover
RfqV2Changed hint on rfq_v2
GET /private/rfqs/{rfq_id}
Read maker_quote_generation
Maker
3. Quote
Indicative (optional, never executable)
Firm: signed price, capacity, generations
Preflight before relay
Taker
4. Execute
Sign ExecuteRfqV2 with a fresh UUIDv7
One quote or an ordered sweep
Engine checks and fills atomically
Firm-only RFQs skip the indicative step. Partial-fill RFQs repeat step 4 until the target or the quote capacity runs out.

Go-live switch: api.rfq.v2_mode​

V2 availability is one backend setting, api.rfq.v2_mode, with three values. The default is disabled.

v2_mode: disabled
Default. Production until October 16, 2026.
Every V2 route: 403 rfq_v2_disabled
Checked before parsing, so bad bodies get 403 too
AuthenticateRfqV2 answers Error
v2_mode: allowlist
Listed accounts only.
Unlisted writer: 403 rfq_v2_not_allowlisted
Unlisted reader: 401 rfq_v2_session_required
all_eligible routes to listed makers only
v2_mode: enabled
Production from October 16, 2026.
No gate
Normal admission and engine checks apply
V1 /rfq routes unchanged in every mode
api.rfq.v2_modedisabled | allowlist | enabled
ModeWritesReads and AuthenticateRfqV2
disabledEvery V2 route returns 403 {"code":"rfq_v2_disabled","message":"RFQ V2 is not enabled in this environment"}. The gate runs before path, query, and body parsing, before authentication, and before the rate limiter, so a malformed request also gets 403.Read routes return the same 403. On the socket, AuthenticateRfqV2 answers {"type":"Error","message":"Invalid or expired RFQ V2 read grant"} with no code field.
allowlistA request whose signed account is not listed returns 403 rfq_v2_not_allowlisted. The checked account is terms.taker_account on create and parent replacement, terms.maker_account on quotes and quote replacement, and maker_account on bulk withdrawal and the cancel schedule. Every address in a selected_makers set must be listed. all_eligible routing reaches listed makers only; if none remain, create returns 422 rfq_v2_no_eligible_makers.An unlisted account gets no read session: 401 rfq_v2_session_required over HTTP and the Error frame above on the socket.
enabledNo gate. Normal admission and engine checks apply.No gate.

Execution, sweep, cancel, withdrawal, firm-up, and decline carry no account check of their own under allowlist. They act on a parent or quote that already passed it.

Detect availability by the code, not the status. 403 rfq_v2_disabled means the environment 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, up to 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. The engine books legs 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 and bytes32 values 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.

Responses carry Cache-Control: no-store.

Identifiers, nonces, and operation IDs​

Object IDs. terms.id on a parent or quote is any non-nil UUID except version 8. The object 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 live in a separate per-signer set, so order flow on the same key cannot evict a live quote nonce. Indicative quotes and read grants carry no consumable nonce.

Operation IDs must be in-window UUIDv7. Every action that the engine journals takes a client operation_id:

  • execution and sweep
  • parent cancellation and quote withdrawal
  • bulk withdrawal
  • the cancel schedule
  • parent replacement and firm-quote replacement

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 the API's clock at admission. Anything else returns 400 rfq_v2_invalid_operation_id ("generate a fresh UUIDv7"). The check runs after signer authorization, so a bad signature still reports as a signature error.

Version 8 is reserved. The server derives its own IDs (delivery records, auto-execution operations) as version 8 UUIDs. Any client operation_id with version 8 returns 400 rfq_v2_invalid_request on every route that takes one, before the signature is checked. Firm-up, decline, and indicative replacement accept any other version. Use UUIDv7 everywhere and the rule never bites.

The window is also your retry horizon. An exact retry of a journaled action returns the committed result, but only while its operation ID is inside the 1-hour window. After that the retry returns 400 rfq_v2_invalid_operation_id, not the original outcome. Reconcile through reads instead.

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 authority 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.scopeParent = 0, quote = 1
ScheduleRfqQuoteCancelV2.cancelAtMs0 removes the schedule

terms_commitment is the EIP-712 struct hash of RfqV2SigningTerms. Every quote signs it as rfqTermsCommitment, so a quote cannot move 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 parent is signature-checked, stored, and routed. It is not an engine admission and reserves nothing. The parent nonce is consumed on the first fill; later partial fills reuse that binding.

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 (default when omitted), indicative_allowedGates 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_blindingbytes32Fresh CSPRNG output per anonymous RFQ

Builder fees are zero at launch. The engine 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>. The server refuses a grant issued in the future, already expired, or valid for more than 5 minutes (expires_at_ms - issued_at_ms <= 300000). 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. There is no RFQ payload on the socket. Read the parent:

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 current fences:

"maker_quote_generation": { "maker_generation": 3, "rfq_generation": 0 }

Maker generations​

Every firm quote signs two fence counters, maker_generation (account-wide) and rfq_generation (this maker on this RFQ). Both must equal the engine's current values at publication and at execution. A stale counter returns 409 rfq_v2_quote_revoked.

EventEffect
Bulk withdrawal, all_openmaker_generation + 1
Bulk withdrawal, rfqrfq_generation moves to a new value above every prior value for this maker. It can jump by more than 1
Scheduled cancel firesmaker_generation + 1
Withdrawal by quote IDNo generation change

Never increment locally. After any withdrawal or a fired schedule, re-read the parent and sign the counters it returns. A quote signed against the old generation is dead even if it arrives after the withdrawal.

Quotes signed with withdrawal_policy: "not_permitted" ignore both fences and the scheduled cancel, and cannot be withdrawn. Use that policy only for quotes you are prepared to see filled until they expire.

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 parent's legs one for one, in order.
  • fill_policy equals the parent's exactly. capacity is at most the parent target and on the step grid; reference_quantity is at most capacity. A full_size_once quote sets both to the parent target.
  • expires_at_ms is at most the parent's expires_at_ms. Publication closes at the parent's quote_deadline_ms.
  • fee_limits are at most the parent'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 parent, 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
Generations read from the parent GET
STEP 2
Preflight
passed, unavailable_admitted, or rejected (422)
STEP 3
Relayed
No margin reserved, no fill promised
STEP 4
Executed
Engine rechecks current state at the requested size

Before relay, the API runs the maker's risk check at reference_quantity against the latest published risk snapshot.

preflight.statusResult
passedStored and relayed
unavailable_admittedSnapshot missing or stale. Stored and relayed; execution does the real check
rejected422 rfq_v2_preflight_rejected. Not relayed. The parent stays open; send a fresh quote before the deadline

Preflight reserves no margin and promises no fill. The engine checks current state at the executed size. Before relay the API also rechecks the parent, your generations, and deadlines against authority, so a quote that raced a fill, a withdrawal, or a deadline is not published as executable.

Indicative quote​

Only on indicative_allowed parents. Same terms shape as a firm quote minus generations, 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 parent 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"}. Delivery only: 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 successful linked firm quote marks the indicative firmed.

Execute​

RFQ API
Delivery plane
Signature and signer checks at admission
Durable relay to routed makers
Indicative quotes, firm-up requests, declines
Best-effort maker preflight
Anonymous redaction in maker views
Read grants, change feed, socket hints
A 201 here means stored and relayed. It reserves nothing.
Engine, journaled
Execution authority
Nonces and operation IDs
Remaining target and quote capacity
Fill policy and leg grid
Maker generations and scheduled cancel
Current margin, MMP, market state
Positions, premium, and fills
If it can permit, prevent, resize, or account for a fill, it is decided here.

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 operation. The parent 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. The engine applies all allocations against one candidate state; if any fails, nothing fills. At most 1,024 allocations, no repeated quote, else 400 rfq_v2_too_many_targets. Same response shape, one entry per allocation.

What the engine checks​

1. Resolve
Parent and quote signatures
Signer authority
Terms commitment match
Operation ID window
2. Prove
Parent and quote unexpired
Generations current
Fill policy and grid
No self-trade
3. Price risk
Target and capacity left
Market and MMP
Taker margin
Maker margin
4. Commit
Every leg of every allocation
Premium
Target and capacity
Nonces
Either every allocation fills, or nothing changes.

In one journaled command: both signatures and signer authority, the commitment link, unexpired parent, quote, and action, current generations and cancel schedule, the fill policy on both documents, remaining target and capacity, the leg grid, fee ceilings, self-trade (taker and maker resolving to the same account, on-chain manager, signer, or provider: 409 rfq_v2_self_trade), then current market, MMP, and both sides' margin.

RefusalCodeRetry
Size breaks fill policy or exceeds remaining capacity or target409 rfq_v2_capacity_unavailableRe-read, resize, re-sign
Quote or parent consumed, withdrawn, cancelled, or replaced409 rfq_v2_authorization_terminalNo
Quote signed against an old generation409 rfq_v2_quote_revokedNo
Current margin, funding, or position refuses409 rfq_v2_risk_rejectedSame signed request may pass once state changes; no nonce was used
Inconsistent with the signed documents (reused nonce, mismatched terms, leg grid, any amount at or above 10^12 in a fill record)409 rfq_v2_authority_rejectedNot unchanged
Engine not progressing or outcome unknown503 rfq_v2_authority_unavailableResend the identical signed bytes

Fill record range. Each leg size, leg price, leg premium, package premium, and sweep total in a fill must be below 10^12 in magnitude. Larger executions are refused before they are journaled. 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 both minimum and 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 parent. Each fill consumes both, and every response returns the authoritative remainders. A taker can hit the same firm quote repeatedly until its capacity, the parent 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 parent the API fills the first qualifying firm quote at the largest size both signed policies, the leg grid, and the remainders allow, within the signed premium bound. No taker execution request is needed. Each auto fill's operation ID is derived from the parent, the quote, and the quote's usage so far, so a retried trigger never double-fills, and each later partial is a new operation. If auto execution is pending, publication returns 503 rfq_v2_auto_execution_unavailable; 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 parent
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 old and new IDs. The old object goes terminal and the new one is admitted 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 those quotes and leaves generations alone. rfq and all_open advance a generation (see Maker generations), which kills every withdrawable quote signed against the old value, including any still in flight. The acknowledgement follows the engine commit.

Dead-man switch: scheduled quote cancel​

A maker arms 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 timed-out renewal may not have landed.

  • Lead time: cancel_at_ms at least 5,000 ms after admission, 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 is refused. If the old deadline is already due when you renew or clear, it fires first.
  • On fire: maker_generation + 1 and the schedule is removed. The deadline is also checked at publication and execution, so a due quote cannot fill in the gap before the fire is journaled.
  • Scope: account-wide, not per socket. A dropped connection cancels nothing. Parents 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>. Missing or invalid grant: 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 parent: 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, limit (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}. Parent statuses: open, partially_filled, filled, expired, cancelled, replaced. Quote statuses add closed, withdrawn, and firmed.

Visibility. A taker reads every quote on its RFQ. A maker reads parents routed to it and only its own quotes, never a competitor's. History is kept: terminal quotes stay readable, and include_terminal=false is a filter, not deletion. Old RFQs remain readable after the engine retires their usage records, with projected remainders and terminal statuses.

Check executable, not status. A quote can read open after its parent 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 recovery. The feed lists only RFQ IDs that changed for your account. Read the named parents and quotes, apply them, then persist 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 global sequence, nothing else. Hints can repeat. Fetch the parent and quotes.
  • Who gets a hint. Routed makers on creation. The taker and the quoting maker on that maker's quotes, firm-ups, and declines. The taker and every routed maker, traded or not, on fills and parent cancellation. The maker and the affected takers on withdrawals and fired cancels.

Run the feed alongside the socket. After subscribing, replay GET /private/rfqs/changes from your last applied sequence before trusting local state. Poll it while connected too, because a missing private hint is invisible in the global sequence. On reconnect, replay from the last applied cursor and de-duplicate.

Retries and ambiguous outcomes​

  • Create and publish: resend the identical signed body. Same ID, same bytes returns the original; changed bytes return 409 rfq_v2_idempotency_conflict.
  • Journaled actions: resend the identical body with the same operation_id inside the 1-hour window. A different body under the same operation ID conflicts.
  • 503 rfq_v2_authority_unavailable or a timeout on an execution: the fill may have committed. Resend the same bytes until you get the committed result or a definite refusal, then confirm with a parent read. Do not sign a new execution for the same intent until then.
  • 503 rfq_v2_projection_pending: the command committed; the result is not readable yet. Retry the read.
  • 429 rfq_v2_rate_limited: honor Retry-After. The ingress 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)
Fill record amountsBelow 10^12
Signed amountsUp to 2^96 - 1
V2 ingress rate6,000 requests per minute per source IP

Error codes​

CodeHTTPMeaning
rfq_v2_disabled403V2 is off in this environment
rfq_v2_not_allowlisted403V2 is limited to allowlisted accounts
rfq_v2_not_enabled501Route not enabled on this host
rfq_v2_session_required401Read needs a valid signed read grant
rfq_v2_unauthorized403Signer not authorized for the account, or the account may not see this RFQ
rfq_v2_not_found404Parent 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 violate protocol policy
rfq_v2_invalid_create400Create routes to more makers than one atomic delivery 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 parent
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
rfq_v2_invalid_cursor400Cursor malformed or out of range
rfq_v2_invalid_limit400Page 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 maker preflight; parent stays open
rfq_v2_deadline_too_short422Cancel deadline under the 5-second lead
rfq_v2_parent_closed409Parent cancelled, replaced, or filled first
rfq_v2_idempotency_conflict409ID already bound to different signed terms
rfq_v2_indicative_replaced409Indicative quote replaced first
rfq_v2_quote_revoked409Quote signed before a maker withdrawal or fired schedule
rfq_v2_replacement_conflict409Replacement target already used or changed
rfq_v2_replacement_rejected409Engine refused the replacement
rfq_v2_authorization_terminal409Quote or parent no longer executable
rfq_v2_capacity_unavailable409Size exceeds remaining capacity or breaks the fill policy; refresh and resize
rfq_v2_risk_rejected409Current risk refused; nothing applied, same request may pass later
rfq_v2_authority_rejected409Inconsistent with the signed documents; final for that request
rfq_v2_self_trade409Taker and maker resolve to the same principal
rfq_v2_rate_limited429Rate limit exceeded
rfq_v2_delivery_failed500Delivery store could not complete the operation
rfq_v2_delivery_unavailable500, 503Delivery store unreachable; retry
rfq_v2_authority_unavailable503Engine unavailable or outcome unknown; retry the same signed request
rfq_v2_auto_execution_unavailable503Quote accepted, auto execution pending; retry the publication
rfq_v2_projection_pending503Committed, result not yet readable; retry

Rust SDK​

V2 support is in the public hypercall-client crate (see Rust Crates for installation), in the rfq_v2 and rfq_v2_recovery modules, with wire types in hypercall-sdk-types. The public mirror picks up these modules with the V2 release; pin the release tag or commit once it is published and read signatures there.

HypercallClient has typed V2 calls: create_rfq_v2, publish_rfq_v2_firm_quote, publish_rfq_v2_indicative_quote, request_rfq_v2_firm_quote, decline_rfq_v2, execute_rfq_v2, sweep_rfq_v2, cancel_rfq_v2, withdraw_rfq_v2_quote, withdraw_rfq_v2_quotes, replace_rfq_v2, replace_rfq_v2_quote, schedule_rfq_v2_quote_cancel, and the reads get_rfq_v2, list_rfqs_v2, list_rfq_v2_changes, list_rfq_v2_quotes, get_rfq_v2_quote.

  • Read grants: HypercallWallet::sign_rfq_v2_read_grant and encode_rfq_v2_read_grant.
  • Socket: connect_rfq_v2 authenticates before subscribing and restores that order on reconnect. subscribe_rfq_v2 and unsubscribe_rfq_v2 manage the channel.
  • Change feed: watch_rfq_v2_changes polls from a cursor, drains has_more pages, and stops on authorization failure or grant expiry.
  • Selection: select_best_rfq_v2_quote with RfqV2BestQuoteCriteria::for_parent applies the parent's fill policy, the leg grid, remainders, and expiry before ranking by direction-aware premium.
  • Recovery (rfq_v2_recovery): sync_rfq_v2_mirror replays the feed and rereads RFQs past their deadlines; reconcile_rfq_v2_execution and reconcile_rfq_v2_sweep resend identical bytes after an ambiguous outcome (inside the 1-hour window); RfqV2MakerHold tracks a maker's open exposure until fill, competitor exhaustion, or terminal evidence.

The SDK sends signed bodies. Build and sign the V2 action messages with your own EIP-712 signer using the type definitions above.

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 is now stored before it is sent and survives an API restart. A restarted server resubmits the exact command under its original journal ID; the engine answers a repeat from its committed record and never fills twice. The record is cleared once the outcome is read.