Liquidity gate
Open interest, day volume, bid size, ask size, and notional premium decide whether the contract belongs in the ranking set.
Reject: low OI, no bid, empty size.
A practical scanner architecture for turning listed expirations and chain snapshots into ranked option candidates with liquidity, moneyness, Greeks, quote age, and spread-aware rejects.
List expirations, pull one chain per expiration, paginate fully, normalize contract fields, reject stale or too-wide quotes, rank by liquidity and Greek constraints, and inspect a contract snapshot before showing an alert.
Primary endpoint
Options chain
Request one underlying and expiration at a time, then paginate through the full chain.
Core filters
Liquidity + quote quality
Use open interest, volume, bid/ask spread, quote age, moneyness, and delta range together.
Final check
Snapshot
Use the contract snapshot to confirm latest quote, latest trade, day stats, Greeks, and IV before alerting.
Scanner input
Begin with Expirations so the scanner only requests dates that are actually listed for the underlying. A hard-coded monthly date will miss 0DTE, weekly, quarterly, and holiday-adjusted cycles, while a guessed weekly date can produce empty chains or misleading coverage gaps.
Once the expiration list is known, request the Options Chain API one expiration at a time. Keep pagination explicit and store the page cursor, because incomplete pagination can look like low liquidity when the real problem is an unfinished chain request.
Ranking model
A scanner should reject bad markets before it ranks exciting ones. Use bid, ask, midpoint, spread percent, quote age, open interest, volume, moneyness, delta, gamma, theta, vega, and implied volatility in the body of the scoring model, not as a separate glossary pasted above the result table.
For unusual activity or momentum scans, join Trades with Quotes so a high-volume contract does not pass just because old prints are large. A useful scanner row should explain whether the signal came from volume/open-interest pressure, a fresh bid/ask update, a price response, or a stale quote that should be rejected.
Open interest, day volume, bid size, ask size, and notional premium decide whether the contract belongs in the ranking set.
Reject: low OI, no bid, empty size.
Strike relative to underlying price controls DTE, delta bucket, and whether the row is ATM, OTM, or deep ITM.
Reject: outside strategy range.
Quote age and spread percent protect the scanner from stale or untradable rows.
Reject: stale quote, crossed market.
Alert handoff
Use the Option Contract Snapshot page model before a row becomes an alert, watchlist item, or dashboard drilldown. A chain row gives scanner breadth; a contract snapshot gives the latest quote, latest trade, day bar, Greeks, IV, and open-interest context for the exact OCC ticker.
The final alert payload should include the underlying, OCC ticker, expiration, strike, side, DTE, moneyness, delta, bid, ask, midpoint, spread percent, quote age, day volume, open interest, alert score, and reject reason if the row was suppressed. That makes the scanner auditable when a user asks why one contract appeared and another did not.
Separate discovery, filtering, scoring, and alert handoff so each failure mode is visible.
| Layer | Data to request | Implementation detail |
|---|---|---|
| Discovery | Expirations | Fetch listed dates per underlying; label 0DTE, weekly, monthly, and quarterly cycles in the UI. |
| Universe | Chain pages | Pull each expiration, paginate fully, and normalize every contract into one scanner schema. |
| Quality gate | Quotes and chain quote fields | Reject no-bid, stale, crossed, and wide-spread contracts before ranking. |
| Signal gate | Trades, volume, open interest, Greeks | Score volume/OI pressure, delta bucket, IV, gamma exposure, and price response. |
| Handoff | Contract snapshot | Refresh the exact OCC ticker before alerting or opening a detail panel. |
API example
scanner request sequence
# List expirations first so the scanner does not guess valid dates.
curl -H "Authorization: Bearer YOUR_API_KEY" "https://api.cutemarkets.com/v1/tickers/expirations/SPY/"
# Pull one expiration at a time and paginate until the response has no next cursor.
curl -H "Authorization: Bearer YOUR_API_KEY" "https://api.cutemarkets.com/v1/options/chain/SPY/?expiration_date=2026-05-15&limit=250"
# Inspect a candidate contract before alerting or showing a recommendation.
curl -H "Authorization: Bearer YOUR_API_KEY" "https://api.cutemarkets.com/v1/options/snapshot/SPY/O:SPY260515C00500000/"spread-aware TypeScript filter
type ChainContract = {
ticker: string;
contract_type: "call" | "put";
strike_price: string;
expiration_date: string;
open_interest?: number;
greeks?: { delta?: number; gamma?: number; theta?: number; vega?: number };
last_quote?: { bid_price?: string; ask_price?: string; sip_timestamp?: string };
day?: { volume?: number };
};
const API = "https://api.cutemarkets.com/v1";
async function cute(path: string) {
const response = await fetch(API + path, {
headers: { Authorization: "Bearer " + process.env.CUTEMARKETS_API_KEY },
});
if (!response.ok) throw new Error(await response.text());
return response.json();
}
function scannerScore(contract: ChainContract, underlyingPrice: number, nowMs: number) {
const quote = contract.last_quote;
const bid = Number(quote?.bid_price ?? 0);
const ask = Number(quote?.ask_price ?? 0);
const mid = bid > 0 && ask >= bid ? (bid + ask) / 2 : 0;
const spreadPct = mid > 0 ? (ask - bid) / mid : Infinity;
const quoteAgeSeconds = quote?.sip_timestamp ? (nowMs - Date.parse(quote.sip_timestamp)) / 1000 : Infinity;
const moneyness = Number(contract.strike_price) / underlyingPrice;
const volume = contract.day?.volume ?? 0;
const openInterest = contract.open_interest ?? 0;
const delta = Math.abs(contract.greeks?.delta ?? 0);
if (spreadPct > 0.12) return null;
if (quoteAgeSeconds > 20) return null;
if (openInterest < 100) return null;
if (delta < 0.15 || delta > 0.75) return null;
return {
ticker: contract.ticker,
moneyness,
spreadPct,
quoteAgeSeconds,
liquidityScore: volume + openInterest,
};
}
async function scanExpiration(underlying: string, expiration: string, underlyingPrice: number) {
const chain = await cute("/options/chain/" + underlying + "/?expiration_date=" + expiration + "&limit=250");
const nowMs = Date.now();
return chain.results
.map((contract: ChainContract) => scannerScore(contract, underlyingPrice, nowMs))
.filter(Boolean)
.sort((a, b) => b.liquidityScore - a.liquidityScore);
}How to implement
Request expirations for each underlying and build the scanner queue from listed dates instead of guessed calendar rules.
Pull chain pages for each expiration, persist cursors, and stop only after the full chain is loaded.
Create one row schema with OCC ticker, strike, side, expiration, DTE, moneyness, open interest, Greeks, quote fields, and day stats.
Apply no-bid, stale quote, wide spread, low open-interest, and delta-range rejects before ranking by opportunity.
Refresh the exact contract snapshot before sending an alert or opening a dashboard detail view.
Last verified
This guide was last reviewed on June 4, 2026. Date-sensitive market calendars, provider docs, and listed contracts can change, so production workflows should verify the live source before trading or publishing an automated answer.
No. Request listed expirations first, then scan one expiration at a time so pagination, rate limits, DTE labels, and failures stay explicit.
Bid/ask spread, quote age, open interest, day volume, moneyness, delta bucket, and reject reason matter before any ranking score.
The snapshot refreshes latest quote, latest trade, Greeks, IV, day stats, and open interest for the exact OCC ticker before you alert a user.
Related pages
Options chain scanner architecture
Read the system-design doc for scanner queues, scoring, and rejects.
Options liquidity scanner
See how liquidity, open interest, spread, and quote age shape scanner output.
Option quote and trade conditions
Use quote and trade conditions to avoid false scanner positives.
Unusual options activity scanner
Compare volume-only signals with quote-aware scanner checks.