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.

Implementation detail

How to Build an Options Chain Scanner in practice

Treat this page as an implementation boundary, more than a short answer. For How to Build an Options Chain Scanner, the practical record should include the request inputs, timestamp context, selected object, freshness state, and the reason a row was accepted or rejected. That is what keeps a concise answer usable when it becomes a scanner, dashboard, backtest, alert, or support note.

The core page facts are Primary endpoint: Options chain; Core filters: Liquidity + quote quality; Final check: Snapshot. Those values should map to fields in code or to visible labels in the interface. If a developer cannot point to the endpoint family, market-data object, date window, entitlement state, or review artifact behind the answer, the workflow is still too vague for production use.

A reliable implementation should store source URLs or endpoint paths, query parameters, pagination state, market timestamps, application timestamps, and any reject reason beside the result. That evidence makes it possible to rerun the answer later, compare it with another provider, and explain why a value changed after a calendar update, feed repair, plan change, or data-quality review.

In the checklist, the handoff starts with Load listed expirations and ends with Snapshot before alerting. Keep those steps connected with stable identifiers rather than display labels, especially when the workflow moves from stocks into options, from chains into exact contracts, or from current snapshots into historical replay.

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.

Operational usage

How to use How to Build an Options Chain Scanner in a real workflow

Treat this page as a decision boundary for the surrounding API workflow. Before a value from How to Build an Options Chain Scannerenters a scanner, dashboard, calendar, backtest, or support answer, store the source route, request parameters, relevant timestamp, freshness label, and the reason the value is suitable for the next step.

The important implementation habit is to keep display labels separate from stable identifiers. Dates should remain tied to the calendar rule or listed-expiration source that produced them. Option rows should keep the OCC symbol, expiration, strike, side, quote state, and pagination context that created the row. Provider or product answers should keep the entitlement, licensing, and support assumptions visible.

When the workflow changes, rerun the page against one concrete example instead of trusting a general claim. Pick the ticker, date window, endpoint family, and expected output artifact. Then verify that the same terminology appears in the API request, UI label, log entry, and review checklist.

Related pages