Market Maker Integration Guide
End-to-end walkthrough for Main Market Makers — SIWE auth, REST quoting, WebSocket auction participation, and position monitoring
This guide walks through the complete integration path for a Main Market Maker (MMM) on Diffusal. You will authenticate via SIWE, verify your MMM authorization, submit quotes through both the REST and WebSocket paths, and monitor your open positions in real time.
A Main Market Maker is an address authorized by the protocol admin to sign RFQ quotes that takers fill on-chain. The RFQ (Request for Quote) model works as follows: a taker broadcasts a request for a quote on a specific option series, active MMMs respond with signed quotes, and the taker fills the best quote by submitting it to the DiffusalOptionsRFQ contract on-chain. This guide covers both the programmatic off-chain quote delivery paths and the on-chain mechanics you need to be aware of.
Prerequisites
curlandwebsocatinstalled- A wallet with MON for gas on Monad Testnet (chain ID 10143)
- MMM authorization — the protocol admin must call
DiffusalOptionsPositionManager.setMmm(your_address, true)before you can submit quotes - Collateral deposited to portfolio 0 — see the On-Chain Guide for deposit steps using
DiffusalCollateralVault
Your First Quote — Linear Walkthrough
Step 1: Authenticate (SIWE)
Diffusal uses Better Auth SIWE for programmatic authentication. The three-step flow is: call POST /api/auth/siwe/nonce to request a nonce, sign a SIWE message, then call POST /api/auth/siwe/verify to verify and create a session.
a) Request a nonce
curl -s -X POST https://api.testnet.diffusal.xyz/api/auth/siwe/nonce \
-H 'Content-Type: application/json' \
-d '{"walletAddress":"0xYOUR_ADDRESS","chainId":10143}'Expected output:
{ "nonce": "a3f2c91d8b4e5072" }b) Construct the SIWE message
Build an EIP-4361 message using the nonce from the previous step. Replace YOUR_ADDRESS and NONCE with actual values:
api.testnet.diffusal.xyz wants you to sign in with your Ethereum account:
YOUR_ADDRESS
Sign in to Diffusal
URI: https://api.testnet.diffusal.xyz
Version: 1
Chain ID: 10143
Nonce: NONCE
Issued At: 2026-01-01T00:00:00.000ZSign this message with your wallet private key and encode both the message and signature as JSON strings.
c) Verify the SIWE signature
curl -s -X POST https://api.testnet.diffusal.xyz/api/auth/siwe/verify \
-H 'Content-Type: application/json' \
-d '{
"message": "api.testnet.diffusal.xyz wants you to sign in with your Ethereum account:\nYOUR_ADDRESS\n\nSign in to Diffusal\n\nURI: https://api.testnet.diffusal.xyz\nVersion: 1\nChain ID: 10143\nNonce: NONCE\nIssued At: 2026-01-01T00:00:00.000Z",
"signature": "0xYOUR_SIGNATURE",
"walletAddress": "0xYOUR_ADDRESS",
"chainId": 10143
}'Expected output:
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"success": true,
"user": {
"id": "user_abc123",
"walletAddress": "0xYOUR_ADDRESS",
"chainId": 10143
}
}d) Save the bearer token
export TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."For the full authentication reference including nonce TTL and token format, see Authentication.
Step 2: Check Your MMM Status
Before submitting quotes, verify your address is authorized as an MMM. Use cast to call isMmm on the DiffusalOptionsPositionManager contract:
cast call 0xd5feba3e400a0197d0870495f9bba5873307208f \
"isMmm(address)(bool)" \
YOUR_ADDRESS \
--rpc-url https://testnet-rpc.monad.xyzExpected output when authorized:
trueIf the result is false, contact the protocol admin to request MMM authorization via setMmm(your_address, true).
Step 3: Submit Your First Quote (REST Path)
The REST path is useful for one-off or low-frequency quote submission. Use POST /api/mm/quotes/submit with your bearer token.
You need a pairId for the option series. Use the ETH-USDT pair (0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c) from the current testnet deployment.
First, fetch your current on-chain nonce:
cast call 0x6f18dc553a5fb66d0081a84d804a608998b784c7 \
"mmmNonce(address)(uint256)" \
YOUR_ADDRESS \
--rpc-url https://testnet-rpc.monad.xyzThen submit the quote:
QUOTE_EXPIRY=$(($(date +%s) + 25)) # 25 seconds from now
curl -s -X POST https://api.testnet.diffusal.xyz/api/mm/quotes/submit \
-H 'Content-Type: application/json' \
-H "Authorization: Bearer $TOKEN" \
-d "{
\"mmm\": \"YOUR_ADDRESS\",
\"taker\": \"0x0000000000000000000000000000000000000000\",
\"seriesId\": \"0x0000000000000000000000000000000000000000000000000000000000000000\",
\"takerIsBuying\": true,
\"price\": \"50000000000000000\",
\"size\": \"10000000000000000000\",
\"nonce\": \"0\",
\"quoteExpiry\": \"$QUOTE_EXPIRY\",
\"pairId\": \"0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c\",
\"strike\": \"2000000000000000000000\",
\"optionExpiry\": \"1780000000\",
\"isCall\": true,
\"signature\": \"0xYOUR_EIP712_SIGNATURE\"
}"Expected output:
{
"quoteId": "qte_01j...",
"status": "active",
"expiresAt": "2026-01-01T00:00:25.000Z"
}RFQ quotes expire after 30 seconds. Set quoteExpiry to at most 30 seconds from now. Takers
must fill within this window or the quote is rejected. Build your signing loop with this tight TTL
in mind.
The seriesId and signature fields require off-chain computation. Use the EIP-712 Quote Structure section below to construct the typed data and sign it with your MMM key.
Step 4: Real-Time Auction Participation (WebSocket Path)
For continuous market making, connect to the RFQ WebSocket stream. Takers broadcast requests; you respond with signed quotes in real time.
a) Connect and authenticate
websocat "wss://api.testnet.diffusal.xyz/ws/rfq/mm" \
-H "Authorization: Bearer $TOKEN"Send an AUTH message as the first frame after connecting:
{
"type": "AUTH",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Expected auth acknowledgement:
{ "type": "AUTH_SUCCESS", "userId": "user_abc123" }b) Receive an RFQ request
When a taker requests a quote, the server broadcasts an rfq_request event to all connected MMMs:
{
"type": "rfq_request",
"requestId": "rfq_01j...",
"pairId": "0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c",
"seriesId": "0x...",
"strike": "2000000000000000000000",
"optionExpiry": 1780000000,
"isCall": true,
"size": "5000000000000000000",
"takerIsBuying": true,
"expiresAt": "2026-01-01T00:00:30.000Z"
}c) Respond with a quote
Sign the RfqQuote typed data (see EIP-712 Quote Structure) and send your response:
{
"type": "rfq_response",
"requestId": "rfq_01j...",
"quote": {
"mmm": "YOUR_ADDRESS",
"taker": "0x0000000000000000000000000000000000000000",
"seriesId": "0x...",
"takerIsBuying": true,
"price": "48000000000000000",
"size": "5000000000000000000",
"nonce": "0",
"quoteExpiry": 1780000030,
"pairId": "0x7020b52841bb268cbc78137a54d4bf1f5305eed1039fb5d003ba95b8ededc46c",
"strike": "2000000000000000000000",
"optionExpiry": 1780000000,
"isCall": true
},
"signature": "0xYOUR_EIP712_SIGNATURE"
}For scripted automation, pipe mode is convenient for one-shot examples. For the full auction loop, connect once and remain connected — the server will push rfq_request events as they arrive.
Step 5: Monitor Positions
Connect to the private WebSocket stream to receive real-time fill and position updates for your MMM address.
websocat "wss://api.testnet.diffusal.xyz/ws/private"Send AUTH as the first frame:
{
"type": "AUTH",
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}Subscribe to fill and position channels:
{
"method": "SUBSCRIBE",
"params": ["account@fills", "account@positions"]
}Example fill event:
{
"channel": "account@fills",
"data": {
"tradeId": "trd_01j...",
"seriesId": "0x...",
"size": "2000000000000000000",
"price": "48000000000000000",
"side": "maker",
"timestamp": "2026-01-01T00:00:28.000Z"
}
}Example position update:
{
"channel": "account@positions",
"data": {
"portfolioId": 0,
"seriesId": "0x...",
"size": "-2000000000000000000",
"unrealizedPnl": "1200000000000000",
"timestamp": "2026-01-01T00:00:28.000Z"
}
}Low-level private stream payloads may still include portfolioId, but MMMs should treat that as
an internal routing detail. Operationally, MMMs manage cash only in portfolio 0; trading
portfolios are auto-routed by series and are not client-managed through /api/mm/*.
Nonce Management
Each RFQ quote must include the MMM's current on-chain nonce. The nonce does not auto-increment on fill — multiple quotes can share the same nonce value until you explicitly advance it.
Read the current nonce:
cast call 0x6f18dc553a5fb66d0081a84d804a608998b784c7 \
"mmmNonce(address)(uint256)" \
YOUR_ADDRESS \
--rpc-url https://testnet-rpc.monad.xyzIncrement the nonce (bulk-cancel all outstanding quotes):
cast send 0x6f18dc553a5fb66d0081a84d804a608998b784c7 \
"incrementMmmNonce()" \
--private-key YOUR_PRIVATE_KEY \
--rpc-url https://testnet-rpc.monad.xyzCalling incrementMmmNonce() is the only way to cancel outstanding quotes. There is no per-quote cancellation. After incrementing, any quote signed with the old nonce will revert with InvalidNonce when a taker tries to fill it.
When to use nonce increment:
- Market conditions have changed significantly and your outstanding quotes are no longer valid
- You want to reprice in bulk
- You need an emergency stop — increment the nonce to halt all fills immediately
Collateral Model
MMMs operate with a single cash portfolio (portfolio 0). This is enforced at the protocol level:
- Debits (premium payments, margin calls) try the trading portfolio first, then fall back to portfolio 0 if insufficient
- Credits (premium receipts, settlement proceeds) are always routed to portfolio 0, regardless of which portfolio the trade occurred in
- Order placement and RFQ fills auto-resolve the internal trading portfolio from the option series; MMM clients do not choose or fund those portfolios directly
Deposit and withdraw using the standard DiffusalCollateralVault functions with portfolioId = 0. See the On-Chain Guide for the complete deposit and withdrawal flow.
Keep portfolio 0 well-funded. If it runs dry, MMM order placement, RFQ execution, or settlement can fail even though series routing is automatic.
EIP-712 Quote Structure
For developers building custom signing flows or integrating with hardware signers.
Domain:
{
"name": "DiffusalOptionsRFQ",
"version": "1",
"chainId": 10143,
"verifyingContract": "0x6f18dc553a5fb66d0081a84d804a608998b784c7"
}Types:
{
"RfqQuote": [
{ "name": "mmm", "type": "address" },
{ "name": "taker", "type": "address" },
{ "name": "seriesId", "type": "bytes32" },
{ "name": "takerIsBuying", "type": "bool" },
{ "name": "price", "type": "uint256" },
{ "name": "size", "type": "uint256" },
{ "name": "nonce", "type": "uint256" },
{ "name": "quoteExpiry", "type": "uint256" },
{ "name": "pairId", "type": "bytes32" },
{ "name": "strike", "type": "uint256" },
{ "name": "optionExpiry", "type": "uint256" },
{ "name": "isCall", "type": "bool" }
]
}Field reference:
| Field | Type | Description |
|---|---|---|
mmm | address | Market maker address (signer) |
taker | address | Authorized taker (address(0) = anyone) |
seriesId | bytes32 | Option series identifier (from SeriesRegistry) |
takerIsBuying | bool | true = taker buys long, false = taker buys short |
price | uint256 | Premium per contract in WAD (1e18 = 1.0 USDT) |
size | uint256 | Maximum contracts available (WAD) |
nonce | uint256 | Must match current on-chain MMM nonce |
quoteExpiry | uint256 | Unix timestamp — keep within 30 seconds of now |
pairId | bytes32 | Trading pair hash from deployment.json |
strike | uint256 | Strike price in WAD |
optionExpiry | uint256 | Option expiration Unix timestamp |
isCall | bool | true = call option, false = put option |
You can pre-validate your signature before broadcasting:
verifySignature(quote, signature) returns (bool)— checks signer matchesquote.mmmgetQuoteStatus(quote, signature) returns (QuoteStatus)— returns fill status, remaining size, and validity
See Also
- On-Chain Guide — general user flows: deposit, withdraw, order placement
- Backend API Quickstart — REST and WebSocket basics for API integrators
- WebSocket Reference — full channel list and control message format
- Authentication — SIWE auth details, token format, session management