OTC Orders and RFQ
Integrate EVM PT/YT rate orders, signed quote requests and MetaVault responses.
Spectra OTC settles individual PT/YT trades through Spectra's deployment of the 1inch Limit Order Protocol. The API validates and stores signed orders; it does not hold funds or execute fills. RFQ adds a non-binding request and response workflow around those same orders.
For the public order book, partial fills and AMM/book batch execution, use the EVM limit-order reference. Its canonical fee-order profile and API are separate from the OTC/RFQ flow below.
This is the EVM OTC integration. Stellar limit orders use a different engine and API. OTC is currently enabled on Base (8453), Katana (747474) and Flare (14).
Resolve the deployment
See the deployed contracts for Base, Katana and Flare.
Resolve current addresses at runtime with
GET https://api.spectra.finance/v1/{network}/otc/config, where
network is a supported chain ID or network slug. A successful response is
{ success: true, data: { limitOrderProtocol, rateAmountGetter, authorizedTakersGuard, orderSigner } }.
These addresses come from the execution chain's global MetaVaultsRegistryV2 keys:
| Response field | Registry key |
|---|---|
limitOrderProtocol | spectra.otc.settlement |
rateAmountGetter | spectra.otc.rate_getter |
authorizedTakersGuard | spectra.otc.takers_guard |
orderSigner | spectra.otc.order_signer |
The endpoint is uncached. A 503 can mean that OTC is disabled or a complete
deployment cannot be resolved. Treat that as unavailable; do not substitute
test addresses. Keep each stored order's original settlement for history
and cancellation when the current configuration changes.
Rate-order model
The maker signs an eight-field 1inch v4 order with Spectra's getter installed on both amount-getter legs. The extension encodes the PT address, derivative kind, quote kind and implied APY. The salt binds the extension to the order.
| Term | Meaning |
|---|---|
derivativeKind | 0 = PT, 1 = YT. |
quoteKind | 0 = underlying, 1 = IBT, from the same market. |
makingAmount | Fixed total quantity sent by the maker, in that token's base units. |
takingAmount | Creation-time baseline; use the getter for the current payment. |
impliedApyWad | Annually compounded rate: 5% is 50000000000000000. |
| Expiry | Deadline for filling, distinct from the PT/YT maturity. |
The getter recalculates the counter-amount at execution. Do not price a fill from the stored baseline or floating-point display values. Use each token's decimals. The supported pairs are PT ↔ underlying, PT ↔ IBT, YT ↔ underlying and YT ↔ IBT. No AMM pool is required.
An order signs one of two fill policies in makerTraits, and nothing else is
accepted (isSignableOtcMakerTraits):
| Policy | Traits | Behavior |
|---|---|---|
| Partial fills (the apps' default) | ALLOW_MULTIPLE_FILLS set, NO_PARTIAL_FILLS clear | Any number of fills, each at most what is left. The settlement tracks the remainder by order hash. |
| No partial fills | NO_PARTIAL_FILLS set, ALLOW_MULTIPLE_FILLS clear | One fill of the whole makingAmount, closed through the maker's nonce bit. |
Both flags set, or neither, is rejected. A partial order still carries a
nonce, which the API keeps unique per maker. Orders also require
receiver = 0 and allowedSender = 0. Private orders use
SpectraAuthorizedTakersGuard with up to 64 authorized addresses. It checks
the immediate caller of the fill: for a MetaVault taker, authorize the Safe.
The list is signed data, not confidential data.
Private OTC orders: use direct settlement.
Do not authorize the BatchFiller as a taker for private OTC orders or use it
to fill them. Fill private OTC orders directly through LimitOrderProtocol.
ERC-4626 compliance required.
Spectra assumes its interest-bearing tokens (IBTs) conform to the ERC-4626 standard. Deviations from its exchange-rate, conversion and preview behavior can cause incorrect pricing, accounting discrepancies or failed protocol operations.
Publish and fill
Use the primitives in @perspective/spectra-sdk/otc/codec, otc/types,
otc/validation and otc/abi.
- Resolve configuration and verify the market's PT, YT, IBT and underlying addresses onchain.
- Build the canonical extension, salt and traits, choosing the fill policy
with
buildMakerTraits({ ..., allowPartialFills }). Allocate a unique nonce per maker and settlement domain; reusing it can invalidate multiple orders. - Call the getter's
previewOrder, set the creation baseline and cross-check the local hash with the settlement'shashOrder. - Approve the maker asset to the settlement and sign the order. Its EIP-712
domain is
1inch Limit Order Protocol, version4, with the chain ID and settlement asverifyingContract. - POST
{ envelope }to/v1/{network}/otc/create.
The OtcOrderEnvelope uses schema: "spectra-otc", schemaVersion: 1 and
orderType: "RATE". It carries chainId, settlementAddress, orderHash,
order, extension, signature, createdAt, createdAtBlock and, for
private orders, authorizedTakers. Serialize uints as decimal strings.
metadata is untrusted display assistance.
validateOtcEnvelope(envelope, chainId, instance) checks signed structure,
configuration, traits, expiry, hash and extension binding. It is a pure check:
it does not verify signatures, balances, allowances or current fillability.
Refresh those from the chain, including the bit invalidator and a fresh getter
preview. Failed reads must remain unknown.
Read what an order has left by its policy:
- Partial fills:
rawRemainingInvalidatorForOrder(maker, orderHash)returns0while untouched (the wholemakingAmountis left), otherwise~remaining;~0(nothing left) once filled or cancelled.otcRemainingFromRaw(raw, makingAmount)decodes it. - No partial fills: pass the full nonce to
bitInvalidatorForOrder(maker, nonce); the contract shifts it internally. Test bitnonce & 255in the returned bitmap.
A closed order takes precedence over low balance or allowance when
displaying it. Compare the maker's balance and allowance with what is left,
not with the whole makingAmount.
For a fill, require an explicit maximum payment, approve the payment token,
then simulate in maker-amount mode with the extension and threshold packed by
buildTakerTraits. The amount argument is the making amount to take: the
whole makingAmount without partial fills, at most what is left with them
(otcFillMakingAmount sizes it from a PT/YT quantity). A part is priced pro
rata of the whole order's live price: otcTakingAmountFor(preview.takingAmount, amount, makingAmount), rounded up; add your buffer to set the threshold.
The settlement fills at most what is left, so read the remainder right before
filling: a competing fill can otherwise make yours smaller than intended. Choose the entrypoint by the maker's deployed code:
| Maker | Entrypoint |
|---|---|
| EOA | fillOrderArgs, using the compact EIP-2098 signature. |
| Contract wallet | fillContractOrderArgs, passing the original ERC-1271 signature bytes. |
A 65-byte signature does not prove that the maker is an EOA. Contract signature
validity can change, so verify it again before execution. For non-ECDSA contract
signatures, including a Safe's empty 0x, call isValidSignature(orderHash, signature) directly and require 0x1626ba7e; reject failed or reverting reads.
A successful preview or simulation does not reserve the order.
Cancel with cancelOrder(makerTraits, orderHash) from the maker on the order's
original settlement; on a partially filled order it withdraws what is left.
Delisting, API shutdown and allowance revocation do not invalidate the
signature. A set bit, or nothing left, only says filled or cancelled:
every fill emits OrderFilled(orderHash, remainingAmount), a cancel does not.
resolveOtcOrderClose (otc/outcome) reads which, and what a cancel withdrew.
API reference
All paths below follow /v1/{network}/otc. Successful list calls return
{ success: true, data }; order creation returns { success: true, orderHash }.
| Method and path | Request |
|---|---|
GET /orders | Optional market (PT address), makerAddress, derivativeKind, includeInactive, limit, offset. |
POST /create | { envelope: OtcOrderEnvelope }. |
GET /rfq/requests | Exactly one of requester or vault; requester history can use includeClosed=true. |
POST /rfq/create | { envelope: QuoteRequestEnvelope }. |
POST /rfq/respond | { requestHash, vault, orderHash } for an already published response. |
POST /rfq/decline | { requestHash, vault, signer, signature }. |
POST /rfq/cancel | { requestHash, signature }. |
Each listed envelope's metadata restates, for display, the last
revalidation: invalidated, outcome (FILLED or CANCELLED, once read),
unfillableReason, allowsPartialFills and remainingMakingAmount (what the
order has left; once closed, what it left unfilled). A partially filled order
stays listed while some is left. Signed terms must still come from the order
and extension, and fillability from the chain.
Load maker history separately with makerAddress, includeInactive=true
and pagination. A page of the public book is not a complete list of a maker's
outstanding signatures. Validate historical identity with
validateOtcOrderIdentity; do not require old orders to match today's deployment
just to show or cancel them.
Write endpoints distinguish malformed input (400), semantic validation
failure (422) and unavailable service (503). An identical accepted order
can be published again without creating a new order; this does not reactivate
an expired or invalidated signature.
RFQ lifecycle
Use @perspective/spectra-sdk/otc/rfq for the canonical types, typed-data
definitions, hashing and wire conversion. The Spectra OTC RFQ EIP-712 domain
has version 1, a chain ID and no verifying contract: these signatures
authenticate API operations only.
- The requester signs
QuoteRequest: requester, derivative/quote kinds, requester-sideBUYorSELL, underlying, IBT, PT, maturity, derivative amount, recipient Safe addresses, request expiry and salt. - POST its
QuoteRequestEnvelope. The API allows up to 16 recipient vaults and 20 open requests per requester per chain. The expiry cannot exceed maturity. - Each vault either signs
DeclineQuoteRequestwith an authorized operator, or publishes a private order on the opposite side and links it with/rfq/respond. Linking requires no extra signature: the stored vault-signed order proves the offer's authorization. - The requester view includes per-vault
PENDING,DECLINEDorRESPONDEDstates and response order envelopes. Validate and fill a response through the normal order flow.
The response must match the market and derivative/quote kinds and authorize the requester. Linking does not enforce that its live quantity equals the requested amount; display the actual offer terms before acceptance.
For a new market, set request pt to the zero address. The responding curator
deploys a PT for the requested IBT. The API accepts maturity from the requested
timestamp through 24 hours later to accommodate the factory's relative-duration
deployment. Show the resulting maturity to the requester.
CancelQuoteRequest withdraws the request. Cancellation, expiry or filling
another response does not invalidate any response order. A declined recipient
cannot become responded; an existing response can only be relinked to the
same order.
Safe makers
Use buildOtcSignOrderIntent from otc/signing to validate the envelope and
build a structured signOrder(Order) delegatecall to the configured signer.
Plan approval and signing separately; a queued proposal is not a valid signature.
Publish only after confirmed execution and live ERC-1271 verification.
The per-market curator-otc-order template constrains the Safe maker, receiver,
asset pairs, traits and signer target. It grants signing once per fill policy.
A vault that applied the template before partial fills existed can sign orders
without partial fills only, until the template is applied again. The signer does not inspect the extension;
shared validation remains necessary. Generic message-signing permission is not
required. Generated approval amounts are not permission-level spending caps.
The current curator interface fills EOA-made orders only. Safe-made offers can be taken through the retail app's contract-maker path. A Safe-made partial order checks its ERC-1271 signature on the first fill only: cancel it to stop further fills. Batch fills and tokenization during a fill are outside this OTC flow.