Onchain options on every Injective market
European, cash-settled, fully collateralized call spreads and put spreads on all 286 Injective mainnet perp markets: crypto, stocks, FX, commodities and indices. Takers trade over RFQ against signed quotes from liquidity providers, and a relayer settles each trade on Injective EVM. Anyone with collateral can write.
Instruments
Every option is a vertical spread defined by five terms: underlying, kind, strike, bound and expiry. Calls carry a cap above the strike, which makes them bull call spreads. Puts carry a floor below the strike, which makes them bear put spreads; a floor of 0 is a plain put. The width between strike and bound fixes the most a contract can ever pay, and the writer locks exactly that in USDC.
S is the settlement price, K the strike, C the cap, F the floor and size the contract size in index units. Collateral rounds up to the USDC unit and payoffs round down, so the vault can always pay every claim. A series is identified on chain by keccak256(abi.encode(terms)) and is created by its first fill; nobody has to list strikes.
Structured payoffs
A series can hold up to four spreads, each a leg with a signed whole-number ratio (negative means sold), on one underlying and expiry. It trades, settles and redeems as one contract, priced by one RFQ quote and filled all or nothing. A plain spread is one leg with ratio 1.
- Butterfly
CS(K−w, K) − CS(K, K+w): pays most (w) at K, nothing beyond the wings. - Condor
CS(L−w, L) − CS(H, H+w): pays w anywhere from L to H, tapering to 0 at L−w and H+w. - Bull shark fin
CS(K, B) − n × CS(B, B+tick), n = (B − K) / tick: gains like a call up to the barrier B, then knocks out one tick above it. - Bear shark fin the mirror image with put spreads: gains as the index falls to B, knocks out one tick below.
The vault requires legs sorted by (kind, strike, bound) so each structure has one series id, a payoff that is never negative (the holder never owes) and positive somewhere, strikes and bounds below 2128, and an expiry at most a year out. It checks the payoff at 0 and at every strike and bound, since it is piecewise linear and flat beyond the outermost one. The writer locks the peak, so a structure stays fully collateralized: the buyer can lose only the premium.
Structures have no indicative book. The app builds the four presets on each market's strike grid and asks for a firm quote; the reference maker prices a structure as the sum of its legs' Black-Scholes values.
Maximum loss
Both sides know their worst case before they trade, and it never changes afterwards. There are no margin calls, no liquidations and no counterparty exposure, because the payout is already in the vault.
| Position | Max loss per contract | Max profit per contract | Capital tied up |
|---|---|---|---|
| Long (bought) | premium paid | width × size − premium | premium only |
| Short (written) | width × size − premium | premium received | width × size, until expiry or close |
Examples from testnet fills
| Series | Contract size | Collateral | Premium | Writer max loss |
|---|---|---|---|---|
| XAU 4100/4200 call · 29 Oct | 0.25 oz | $25.00 | $0.42 | $24.58 |
| WTI 100/95 put · 20 Nov | 10 bbl | $50.00 | $16.85 | $33.15 |
| SICI 115/125 call · 29 Oct | 10 units | $100.00 | $1.68 | $98.32 |
The gold calls above were settled on testnet at 4150, paying (4150 − 4100) × 0.25 = $12.50 a contract.
Markets and collateral
Every Injective mainnet perp is listed. Strikes and bounds are multiples of the strike tick, and premiums multiples of $0.01. A spread's collateral per contract is its width times the contract size, which is also the most a holder can be paid and the most a writer can lose. The app quotes spreads two strike steps wide; any width on the strike tick can be requested over RFQ.
| Market | Category | Contract size | Strike tick | Strike step | Spread width | Collateral / contract | ≈ Notional |
|---|
Notional is contract size times the current mainnet mark. SICI settles on the LO:TECH SICI oracle; every other market on its mainnet perp mark. Terms of a series are fixed when it's created.
Expiry and settlement
- Expiry is any whole hour UTC. The app lists the next 12 hourly expiries, then the next daily, the next Friday and the next two monthlies (third Fridays) at 08:00 UTC; any other whole hour can be requested over RFQ.
- Trading cutoff: fills revert in the last 60 seconds before expiry.
- Price feeds: every underlying has its own feed on Injective EVM. A keeper pushes every 5 minutes for each underlying with a live series: SICI from the LO:TECH SICI/USDC oracle on Injective mainnet, the rest from the mark price of their Injective mainnet perp. A print older than 15 minutes is rejected as stale.
- Settlement is permissionless for one hour after expiry.
settle(seriesId)uses the feed's first print published at or after the expiry hour (priceAt(expiry)), so every caller settles on the same price whenever they call. The keeper pushes that print and settles every expired series within a minute of expiry. If no print lands within the hour, the admin canforceSettle. - After settlement holders call
redeemfor the payoff and writers callclaimfor collateral minus payoff. Before expiry, anyone holding both legs canclosepairs to free collateral. - Testnet only: god mode lets the admin settle any series at any time, so demos don't wait for a real expiry.
How a trade happens
Trading runs over an off-chain RFQ stream modelled on Injective's perp RFQ streams. Quotes and orders are EIP-712 signatures; only the final fill touches the chain.
fillIntent on the vault with the ranked quotes and pays the gas.- The stream verifies every maker signature and field before forwarding, and ranks by lowest premium for taker buys, highest for sells, earliest on ties.
- The vault fills quotes in order until the quantity is done. A quote that fails is skipped with a
QuoteRejectedevent and a decoded reason, so later quotes back up earlier ones. - Fill-or-kill: an intent fills in full or not at all. If the quotes can't cover the whole quantity,
fillIntentreverts withNotFullyFilledand nothing moves. A maker quote can be used for part of its size only when the taker asked for less. - Intents live up to 60 seconds and name the relayer allowed to submit them.
What a fill does on chain
Taker buys q contracts
- The taker pays q × premium to the maker first, so the maker can use it towards collateral.
- Any long inventory the maker holds in that series moves to the taker.
- For the rest, the maker writes: the vault pulls width × size per contract and records a short for the maker and a long for the taker.
Taker sells q contracts
- The maker pays q × premium to the taker.
- The taker's long in that series moves to the maker.
- For the rest, the taker writes and locks collateral.
Netting: whoever receives a long while short in the same series has the matching pairs closed in the same transaction and gets that collateral back. A maker who sold 30 and buys 30 back ends flat with all collateral returned. mint(seriesId, qty) locks collateral for both legs at once, for makers who want inventory ready before quoting.
Anyone can write
There is no maker whitelist. The vault has no maker list, and the RFQ stream accepts any address that proves its key. A writer's obligation is fully funded at the moment of the fill, so the protocol has nothing to underwrite about who the writer is. Traders can write too: selling an option you don't hold writes it against your own collateral.
Fees
Takers pay no trading fee: the premium on the quote is the whole cost. Makers pay a fee on each fill, which they price into their quotes.
Maker fee per fill
fee = min(5 bps × qty × highest strike × contract size, 5% × qty × premium)
- Notional is measured at the structure's highest strike, which the vault knows at fill time, so it doesn't depend on a fresh price and extra legs can only raise it.
- The 5% cap keeps cheap, out-of-the-money spreads from paying more than a small share of their premium.
- A selling maker pays it from the premium it just received; a buying maker pays premium plus fee.
mint,close,redeemandclaimare free. - Each fee emits
MakerFeePaid(rfqId, seriesId, maker, fee). The admin sets the rates and recipient withsetMakerFee, capped at 1% of notional and 50% of premium; the stream'shellocarries the currentmakerFee.
Example: a SICI 110/120 call with 10 units per contract has $1,100 of notional. At a $3.40 premium the cap applies and the fee is $0.17 per contract; at $50 the 5 bps applies and it's $0.55.
Integrating as a market maker
Connect and log in
- Open
wss://options.inj.so/rfq/ws?role=maker. - The server sends a
challengewith a 32-byte nonce. Within 10 seconds, reply withauth, signingStreamAuthChallenge(uint64 evmChainId, address maker, bytes32 nonce, uint64 expiresAt)under the RFQ-Options domain. - You can sign with a hot key registered by
setDelegate(key, expiry); positions and nonces stay on the maker address. - Send
pingevery 10 seconds. A stream that stays silent for 30 seconds is closed. - Approve the vault for USDC once. Makers need no INJ to trade.
Quote rules
- Premium per contract is a multiple of $0.01 (10,000 USDC units).
- The deadline must be 2 to 60 seconds away and before expiry minus 60 seconds.
- Each nonce is single use per maker;
invalidateNoncecancels one. - Up to 4 quotes per request.
minFillQuantitysets a floor on partial fills, andtaker = 0x0makes a blind quote. - Quoting less than the requested size is fine; the taker can take the partial.
EIP-712 domain and types
EIP712Domain(string name, string version, uint256 chainId, address verifyingContract)
name "RFQ-Options", version "2", chainId 1439, verifyingContract = OptionsVault
OptionQuote(uint64 chainId, uint64 rfqId, bytes32 underlying, uint64 expiry, Leg[] legs,
uint8 takerSide, uint256 quantity, uint256 premiumPerContract, address taker,
address maker, uint64 nonce, uint64 deadline, uint256 minFillQuantity)
OptionIntent(uint64 chainId, uint64 rfqId, bytes32 underlying, uint64 expiry, Leg[] legs,
uint8 side, uint256 quantity, uint256 limitPremiumPerContract, address taker,
uint64 nonce, uint64 deadline, address allowedRelayer)
Leg(uint8 kind, uint256 strike, uint256 bound, int32 ratio)
kind: 0 call, 1 put · ratio: contracts of that spread per contract, negative = sold
takerSide / side: 0 taker buys, 1 taker sells · a plain spread is one leg with ratio 1
underlying: ASCII symbol right-padded to bytes32 · strike, bound: 1e18 fixed point
premiums and collateral: USDC units (6 decimals) · stream login keeps version "1"
Messages
// server → maker
{ "type": "request", "rfqId": 1793220407574372, "underlying": "XAU", "expiry": 1793260800,
"legs": [ { "kind": 0, "strike": "4100", "bound": "4200", "ratio": 1 } ],
"kind": "call", "strike": "4100", "bound": "4200", // also sent for plain spreads
"side": "buy", "quantity": "20", "taker": "0x5115…61f9", "takerVerified": false, "requestedAt": 1793220407580 }
// maker → server: a signed quote, or { "type": "pass", "rfqId": … }
{ "type": "quote", "rfqId": 1793220407574372, "quote": { …OptionQuote, amounts as integer strings,
strike and bound as decimals }, "signature": "0x…" }
// taker → server: the underlyings to stream chains for (replaces the set, up to 64)
{ "type": "subscribe", "underlyings": [ "XAU", "BTC" ] }
// server → maker, after login and whenever it changes: what takers are watching
{ "type": "watch", "underlyings": [ "BTC", "XAU" ] }
// maker → server, any time: indicative prices for the chain and the order book (not binding)
{ "type": "indicative", "underlying": "SICI", "series": [ { "kind": "call", "strike": "110",
"bound": "120", "expiry": 1795161600, "bid": "41.93", "ask": "42.37", "size": "10",
"levels": [ { "size": "10", "bid": "41.93", "ask": "42.37" },
{ "size": "40", "bid": "41.51", "ask": "42.79" },
{ "size": "150", "bid": "40.88", "ask": "43.42" } ] } ] }
// server → maker after settlement
{ "type": "filled", "rfqId": …, "txHash": "0x…", "fills": [ { "seriesId": "…", "takerSide": 0,
"qty": "20", "premiumPerContract": "420000" } ] }
The app draws each series' order book from the best maker's indicative levels and asks for a firm quote only when someone trades. Takers get chains only for the underlyings they subscribe to, so makers need only publish the watched ones; firm requests can come for any listed underlying. Indicative prices expire 30 seconds after a maker's last update. Rate limits: 5,000 quotes per second per maker and 10 requests per second per taker.
Reference liquidity provider
One process quotes every listed underlying so the testnet always has two-sided prices. It's a starting point, not a pricing recommendation. The first nine markets have hand-tuned terms:
| Underlying | Vol | Strike step | Spread width | Collateral / contract |
|---|---|---|---|---|
| SICI | 40% | 5 | 10 | $100 |
| US500 | 18% | 10 | 20 | $20 |
| USTECH | 22% | 10 | 20 | $20 |
| XAU | 18% | 50 | 100 | $25 |
| XAG | 30% | 1 | 2 | $40 |
| XPT | 25% | 25 | 50 | $50 |
| XPD | 35% | 20 | 40 | $40 |
| WTI | 35% | 2 | 5 | $50 |
| BRENT | 32% | 2 | 5 | $50 |
- Other markets: terms come from the mainnet price at listing: a strike step near 1.5% of spot (1, 2, 2.5 or 5 times a power of ten), spreads two steps wide, a tick of a tenth of the step, and a contract size that makes each spread pay at most about $50. Vol is 50% for BTC, 65% for ETH, 90% for other crypto, 120% for memecoins, 45% for stocks, 8% for FX and 30% for other macro markets.
- Model: Black-Scholes with zero rates, valued as the difference of two vanillas at the strike and the bound. Strikes cover spot ± 6% from a week out, narrowing with the square root of time for nearer expiries, and always at least two steps either side.
- Book: three tiers around the model value: 10 contracts at ±0.5%, 40 at ±1.5% and 150 at ±3%. Asks round up and bids down to $0.01; no ask exceeds the collateral, and bids under $0.01 are left out.
- Firm quotes price the requested size as the size-weighted average of the tiers it sweeps, with anything past the book at the last tier.
- Limits: it quotes only what its live USDC balance can collateralize, valid for 30 seconds, with no inventory or delta limits on testnet.
Addresses and endpoints
| Web app | https://options.inj.so |
| RFQ stream | wss://options.inj.so/rfq/ws?role=maker | taker |
| Stream config and indicative prices | https://options.inj.so/rfq/config · /rfq/indicative |
| OptionsVault | 0x26FcFfB7E4b6A1813769E26a2Ed7A9C02D13a52d |
| USDC (testnet, 6 decimals) | 0x0C382e685bbeeFE5d3d9C29e29E341fEE8E84C5d |
| Relayer | 0x7059B83e66057d67968A1dfef2BF21dE2c4Df611 |
| RPC | https://k8s.testnet.json-rpc.injective.network |
| Explorer | testnet.blockscout.injective.network |
| Test funds | Get test funds in the app's account menu: 500 USDC and 2 INJ per address per day. |
Price feeds
FeederPriceFeed.latest() returns (price1e18, publishTime) and reverts when the print is older than 15 minutes. Every market has its own feed: GET /api/config lists all of them, and the first nine are below.
| Underlying | Feed |
|---|---|
| SICI | 0xC7b44Cff8c6c45fc5B0d0f8fb4d7693753E473D1 |
| US500 | 0x4cF8AF80F922c90BD2F78d9aeCA0A6BE570fe565 |
| USTECH | 0xA327613E3281e22838Ad04366d20D8E309f7AA45 |
| XAU | 0xa190E8aAcd6a94B16e5224d7b70753f615591dF4 |
| XAG | 0xbffe921Ef7209DE3f940F4D5319F2C27f93F8aF5 |
| XPT | 0x51c1c42026aE6CF6aC9248787CA9945d40D124E1 |
| XPD | 0x1884951c4f88891Ab3dc50b1082699bC11F6AC4e |
| WTI | 0x12bDBAED6919Da4BeA6E97aEE8b9aF73a105B9bF |
| BRENT | 0xecD61E728f08a21Af045E02771eb371b5c8F0b2B |
Before mainnet
- Price feeds move from the keeper to Pyth or the Injective exchange precompile, so settlement doesn't depend on one feeder key.
- Collateral moves from testnet USDC to mainnet USDC, and god mode is switched off.
- Contract sizes and default spread widths are confirmed with market makers.
- The RFQ stream may move into the Injective indexer next to the perp RFQ streams; its message names already match theirs.
- Single legs (sell calls, sell puts) next to spreads, and more structure presets (seagulls, ranges) built from the same legs.
- Fee rates are confirmed with market makers.