Skip to main content

Settle matched intents with your own RFQ

Use this guide when you run your own RFQ / matching system and only need Opshuns for on-chain settlement after a maker and taker have already agreed off-chain.

Settlement goes through mothership executeIntent (zero-premium inventory FillMatched) or executeFixedPackageIntent when the match has a cash package premium. The Opshuns API quote auction uses the package path. You can broker signatures yourself as long as the EIP-712 payload and on-chain args line up.

For resting book posts without a bilateral match, see Call the CLOB directly. Full mode matrix (FixedPackage, Dutch, Authority): On-chain fill modes.

Prefer @opshuns/sdk (../sdk) for digests, signRfqLegByLegOffer, takerAddressHash, and opshunsMothership: see TypeScript SDK.

When to use this​

PathUse
This guideYou match off-chain → maker signs → taker calls executeIntent or executeFixedPackageIntent
Opshuns options RFQOpshuns API hosts the auction; same on-chain settle
Call the CLOBNo bilateral match. post / take the resting book

Architecture​

Your RFQ system
│
├─ Taker opens RFQ (size, terms, polarity / directions, referrer)
├─ Maker signs either:
│ inventory OpshunIntentRequestLegByLeg (no premium fields), or
│ FixedPackageRequest (absolute A/B package premium)
├─ Your matcher binds takerAddressHash + picks the winner
│
└─ Taker wallet
├─ Approve mothership (premium and/or underlying + live fees)
│ and/or attach Permit2 takerPermits
└─ mothership.executeIntent / executeFixedPackageIntent(
request, makerSignature, maker,
directions, takerSalt, takerPermits, referrer)
└─ fillMatched at premiumTick = 0
takerAddressHash = mothership.takerAddressHash(taker, salt, directions, referrer)
= keccak256(abi.encode(taker, salt, directionsHash, referrer))

directionsHash packs each direction as a bytes32. Call the mothership helper or takerAddressHash from the SDK. Do not hand-roll it.

Makers copy takerAddressHash into the signed request. They never need the raw taker address on a public feed. The hash includes referrer. Fill must pass the same address.

SDK: bind the taker hash​

import { takerAddressHash } from "@opshuns/sdk"

const directions = [1] // Directions.Sell: taker buys the option
const takerSalt = 12345n
const referrer = "0x0000000000000000000000000000000000000000"
const hash = takerAddressHash(wallet.address, takerSalt, directions, referrer)
// put `hash` on the maker-signed request as takerAddressHash

1. Agree the shared intents​

Each leg is an OpshunIntentShared-shaped intent:

FieldRole
opshuns6909ContractPair CLOB address
optionTypeCall / put
structureDirectionless structure id (spreads use multiple legs)
startExerciseTimestamp / expiryTimestampExercise window
strikeWadStrike WAD ratio (strikeCost ≈ amount × strikeWad / 1e18)
amountSize in underlying units

All legs in one request typically share the same CLOB and expiry. Multi-leg shapes: Option spreads.

Polarity (or your own convention) maps each leg to a Directions value at fill time:

  • Directions.Buy (0): taker sells (posts underlying)
  • Directions.Sell (1): taker buys

Those directions and referrer must match what was hashed into takerAddressHash.

2. Maker signs the leg-by-leg request​

EIP-712 domain (Solady on the mothership):

  • Name: OpshunsRFQ
  • Version: 1
  • verifyingContract: mothership for that chain
  • chainId: target chain

Typed data: OpshunIntentRequestLegByLeg: intents, makerExpiry, takerAddressHash, makerNonce. No per-leg premiums and no clobOffers. fillMatched is zero-premium, so there is no option-premium fee. The underlying protocol fee is halved when the maker sells.

Practical steps​

  1. Assemble the exact request the taker will submit (normalized addresses and integers).
  2. Call mothership digestOpshunIntentRequestLegByLeg(request).
  3. Sign the 32-byte digest with the maker key (recoverable secp256k1, v = 27/28).
  4. Deliver { request, makerSignature, maker } to the taker through your channel.

Do not hand-roll typed-data hashing. Details and gotchas: EIP-712 signing.

SDK: sign the maker offer​

import {
signRfqLegByLegOffer,
digestOpshunIntentRequestLegByLeg,
legByLegRequestToApi,
} from "@opshuns/sdk"

const offer = await signRfqLegByLegOffer(wallet, mothershipAddress, chainId, request)
// offer.maker_signature, offer.request (API-shaped), offer.digest

// Or digest-only with a local key:
// signRfqLegByLegOfferDigest(privateKey, mothershipAddress, chainId, request)

Optional: verify digests match mothership digestOpshunIntentRequestLegByLeg via digestOpshunIntentRequestLegByLeg in the SDK (same typed-data path).

Premiums​

Per leg the maker does not quote a premium tick. At fill, directions[i] selects Buy or Sell. The whole size is bilateral fillMatched at premiumTick = 0.

For a cash premium, sign FixedPackageRequest instead (signRfqPackageOffer) and fill with executeFixedPackageIntent. Dual A/B amounts are absolute token units. makerPays on the filled polarity halves that protocol fee.

3. Taker approvals​

Before executeIntent, the taker must fund the pull:

DirectionTaker provides
Buy option (Directions.Sell) on a packagePackage premium (+ protocol fee; half if the maker pays)
Sell option (Directions.Buy)Underlying (+ protocol underlying fee; half if the maker sells)

Pull totals include fees: see Fees. Approve the mothership (or use Permit2 witnesses in takerPermits).

  • Empty takerPermits ⇒ classic ERC-20 allowance(taker, mothership).
  • Non-empty ⇒ Permit2 path; every required pull token must be covered or the fill reverts (MissingTakerPermit2 / transfer failures).

4. Call executeIntent​

msg.sender must be the taker (the address hashed into takerAddressHash).

mothership.executeIntent(
request, // OpshunIntentRequestLegByLeg (maker-signed body)
makerSignature,
maker,
directions, // Buy/Sell per leg. must match takerAddressHash
takerSalt,
takerPermits, // empty ⇒ allowance pulls
referrer // must equal the address inside takerAddressHash
);

Settlement steps (on-chain)​

  1. Verify maker EIP-712 signature, makerExpiry, and takerAddressHash.
  2. Consume usedNonces[maker][makerNonce].
  3. For each leg, bilateral fillMatched at premiumTick = 0 (mints ERC-6909 inventory; no ERC-20 pulls inside the clone).
  4. Emit MatchedFilled / RfqExecuted(..., isPackage=false, ...).

Book vs bilateral mint: CLOB.

SDK: submit the fill​

import { opshunsMothership, ZERO_ADDRESS } from "@opshuns/sdk"

const ms = opshunsMothership(mothershipAddress, takerSigner)
const tx = await ms.executeIntent(
request, // same struct the maker signed
makerSignature,
makerAddress,
directions,
takerSalt,
[], // takerPermits. empty ⇒ ERC-20 allowance
ZERO_ADDRESS, // referrer
)
await tx.wait()

If you still use the Opshuns API auction, OpshunsClient.createQuoteSession / placeBid / fillQuote wrap the HTTP + stream side. Settle that auction with executeFixedPackageIntent (signRfqPackageOffer). See TypeScript SDK.

5. What your RFQ system must guarantee​

InvariantWhy
Same request bytes the maker signedSignature recovery / digest must match
takerAddressHash = hash(taker, salt, directionsHash, referrer)On-chain check; wrong salt, directions, or referrer → revert
Unique makerNonce per makerReplay protection; cancel stale nonces with cancelNonce
makerExpiry still in the future at fillIntentExpired
Approvals cover pull + live feesTransferFromFailed / allowance errors

FixedPackage / Dutch (optional)​

If your desk settles a Dutch schedule instead of a fixed package premium:

ModeEntrypointSigner
FixedPackageexecuteFixedPackageIntentMaker
DutchexecuteDutchIntentTaker (maker fills)
+ Authority*WithAuthoritySame + authority approval

Those paths still end in fillMatched with premiumTick = 0 on each leg after package premium. Full reference: On-chain fill modes.

Checklist​

  1. Digests from mothership digestOpshunIntentRequestLegByLeg (or FixedPackage / Dutch digests).
  2. Directions, salt, and referrer bind the taker hash before the maker signs.
  3. Taker is msg.sender on executeIntent.
  4. Allowances / Permit2 cover every pull token including fees.
  5. Standard ERC-20 only.
  6. Index RfqExecuted / MatchedFilled for your own confirmations: you do not need the Opshuns API for settlement success.