Options API How-To

How to Build an Options Chain Scanner

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.

Quick answerLast verified June 4, 2026

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

Use listed expirations as the scanner boundary

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

Filter by tradability before scoring opportunity

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.

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.

Moneyness gate

Strike relative to underlying price controls DTE, delta bucket, and whether the row is ATM, OTM, or deep ITM.

Reject: outside strategy range.

Quote-quality gate

Quote age and spread percent protect the scanner from stale or untradable rows.

Reject: stale quote, crossed market.

Alert handoff

Inspect a contract snapshot before alerting

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.

Options scanner architecture

Separate discovery, filtering, scoring, and alert handoff so each failure mode is visible.

LayerData to requestImplementation detail
DiscoveryExpirationsFetch listed dates per underlying; label 0DTE, weekly, monthly, and quarterly cycles in the UI.
UniverseChain pagesPull each expiration, paginate fully, and normalize every contract into one scanner schema.
Quality gateQuotes and chain quote fieldsReject no-bid, stale, crossed, and wide-spread contracts before ranking.
Signal gateTrades, volume, open interest, GreeksScore volume/OI pressure, delta bucket, IV, gamma exposure, and price response.
HandoffContract snapshotRefresh the exact OCC ticker before alerting or opening a detail panel.

API example

Verify the answer with listed data

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

Implementation checklist

01

Load listed expirations

Request expirations for each underlying and build the scanner queue from listed dates instead of guessed calendar rules.

02

Fetch and paginate chains

Pull chain pages for each expiration, persist cursors, and stop only after the full chain is loaded.

03

Normalize contract rows

Create one row schema with OCC ticker, strike, side, expiration, DTE, moneyness, open interest, Greeks, quote fields, and day stats.

04

Reject weak markets first

Apply no-bid, stale quote, wide spread, low open-interest, and delta-range rejects before ranking by opportunity.

05

Snapshot before alerting

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.

Related questions

Should a chain scanner request every expiration at once?

No. Request listed expirations first, then scan one expiration at a time so pagination, rate limits, DTE labels, and failures stay explicit.

What filters matter most for options scanners?

Bid/ask spread, quote age, open interest, day volume, moneyness, delta bucket, and reject reason matter before any ranking score.

Why use a contract snapshot after a chain row?

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