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
| Path | Use |
|---|---|
| This guide | You match off-chain → maker signs → taker calls executeIntent or executeFixedPackageIntent |
| Opshuns options RFQ | Opshuns API hosts the auction; same on-chain settle |
| Call the CLOB | No 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:
| Field | Role |
|---|---|
opshuns6909Contract | Pair CLOB address |
optionType | Call / put |
structure | Directionless structure id (spreads use multiple legs) |
startExerciseTimestamp / expiryTimestamp | Exercise window |
strikeWad | Strike WAD ratio (strikeCost ≈ amount × strikeWad / 1e18) |
amount | Size 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 chainchainId: 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
- Assemble the exact request the taker will submit (normalized addresses and integers).
- Call mothership
digestOpshunIntentRequestLegByLeg(request). - Sign the 32-byte digest with the maker key (recoverable secp256k1,
v=27/28). - 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:
| Direction | Taker provides |
|---|---|
Buy option (Directions.Sell) on a package | Package 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-20allowance(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)
- Verify maker EIP-712 signature,
makerExpiry, andtakerAddressHash. - Consume
usedNonces[maker][makerNonce]. - For each leg, bilateral
fillMatchedatpremiumTick = 0(mints ERC-6909 inventory; no ERC-20 pulls inside the clone). - 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
| Invariant | Why |
|---|---|
Same request bytes the maker signed | Signature recovery / digest must match |
takerAddressHash = hash(taker, salt, directionsHash, referrer) | On-chain check; wrong salt, directions, or referrer → revert |
Unique makerNonce per maker | Replay protection; cancel stale nonces with cancelNonce |
makerExpiry still in the future at fill | IntentExpired |
| Approvals cover pull + live fees | TransferFromFailed / allowance errors |
FixedPackage / Dutch (optional)
If your desk settles a Dutch schedule instead of a fixed package premium:
| Mode | Entrypoint | Signer |
|---|---|---|
| FixedPackage | executeFixedPackageIntent | Maker |
| Dutch | executeDutchIntent | Taker (maker fills) |
| + Authority | *WithAuthority | Same + authority approval |
Those paths still end in fillMatched with premiumTick = 0 on each leg after package premium. Full reference: On-chain fill modes.
Checklist
- Digests from mothership
digestOpshunIntentRequestLegByLeg(or FixedPackage / Dutch digests). - Directions, salt, and
referrerbind the taker hash before the maker signs. - Taker is
msg.senderonexecuteIntent. - Allowances / Permit2 cover every pull token including fees.
- Standard ERC-20 only.
- Index
RfqExecuted/MatchedFilledfor your own confirmations: you do not need the Opshuns API for settlement success.
Related
- TypeScript SDK:
@opshuns/sdkfrom../sdk - Call the CLOB directly. resting book without bilateral match
- On-chain fill modes. FillMatched, FixedPackage, Dutch, Authority
- EIP-712 signing: domains and field gotchas
- Fill options RFQ quotes. same settle, Opshuns-hosted auction
- Wallet API taker flow: Opshuns API + your UI