OptionsData: read this first OptionsData provides US options market data for assistants and software: quotes, trades, dated open interest, counted volume, implied volatility (IV) and Greeks. It reads data; it does not place trades or give investment advice. An underlying is what an option is based on, such as an equity, ETF or index. A contract is one option: an underlying, expiration, strike, and call or put. A chain groups contracts for an underlying and expiration, possibly across roots. A snapshot gives the latest available state. Trades gives individual recorded prints in a time window. A series gives stored field values over time, where coverage exists. A series is not necessarily every original market event. First useful result Choose the user's route: MCP for a compatible assistant, REST for software. Account admission, subscriber qualification/agreement and data grants are separate requirements. Sign-in and catalog listing do not prove access to market data. These instructions describe the data contracts; they do not establish that an account, every underlying, history range or connection is available to you. Assistant (MCP) 1. Add https://api.optionsdata.ai/mcp as a remote HTTP MCP connector in the assistant's connection settings, if supported. Follow its OAuth sign-in with the approved OptionsData account. MCP uses mcp.read and an access token bound to that exact resource URL. A REST API key cannot replace this OAuth flow. A URL pasted into chat does not install a connector. If the client cannot connect, use the REST route in a local coding environment. 2. Read tools/list, then call list_underlyings with {} for symbol strings; use query for full catalog rows matching a case-insensitive symbol prefix. Choose an underlying appropriate to the user's question. Call get_chain with only underlying for underlying_price, underlying_analytics and term records. Inspect records.term for expiration, settlement and forward values, and records.underlying_price for the current price when present (some underlyings, e.g. indexes, may have none; use the term forward). Settled expirations leave term at settlement (AM and PM independently), so listed expirations are fetchable. Do not assume today's date is an expiry. 3. Call get_chain with that underlying, a discovered expiration, strike_min and strike_max around the underlying price, and fields="symbol,expiration,right,strike,currency,bid,ask,iv". If the tool returns response_too_large, it includes centre/centre_source and an example window around that centre. Follow its example call verbatim or narrow further. This selects one expiration and a strike window, not a contract count. Join records.contract, records.quote and records.option_analytics by iid. Choose one returned contract and, if useful, call get_contract using its exact symbol and fields="bid,ask,iv". 4. Report that contract's symbol, expiration, strike, right, bid/ask and IV when present, with currency, units, as_of, exchange times, calc_time and quality. Explain nulls and absent fields. Outside trading hours, latest available is not a fresh live quote. If access or data is unavailable, say where it stopped. Software (REST) Base: https://api.optionsdata.ai. Use Authorization: Bearer with a separately issued, authorized API key. The user supplies OPTIONSDATA_API_KEY through a local secret manager or a hidden terminal prompt; do not ask them to paste it into chat. Never put a key in a URL, source commit, copied prompt, analytics or logs. Disable shell tracing; do not print the environment or use verbose HTTP credential logs. This Python 3 standard-library quickstart runs locally. Save the following code as optionsdata_quickstart.py, then run python3 optionsdata_quickstart.py from a terminal. It asks for discovered identifiers, URL-encodes each path segment and query value, and saves responses only in memory. Export OPTIONSDATA_API_KEY securely before running it. No key is included in this example. Do not send its requests until access is authorized. import json, os from urllib.parse import quote, urlencode from urllib.request import Request, urlopen from urllib.error import HTTPError base = 'https://api.optionsdata.ai' key = os.environ['OPTIONSDATA_API_KEY'] def get(path, **query): url = base + path + ('?' + urlencode(query) if query else '') req = Request(url, headers={'Authorization': 'Bearer ' + key, 'Accept': 'application/json'}) try: with urlopen(req, timeout=15) as response: return json.loads(response.read()) except HTTPError as error: raise SystemExit('HTTP ' + str(error.code) + ': ' + error.read().decode('utf-8', errors='replace')) print(json.dumps(get('/v1/underlyings'), indent=2)) symbol = input('Choose a returned underlying: ') underlying = get('/v1/underlyings/' + quote(symbol, safe='')) print(json.dumps(underlying, indent=2)) expiry = input('Choose a returned records.term expiration: ') chain = get('/v1/chains/' + quote(symbol, safe=''), expiration=expiry, fields='symbol,expiration,right,strike,currency,bid,ask,iv') print(json.dumps(chain, indent=2)) osi = input('Copy one exact records.contract symbol, keeping spaces: ') contract = get('/v1/contracts/' + quote(osi, safe=''), fields='bid,ask,iv') print(json.dumps(contract, indent=2)) Follow the same interpretation in step 4 above. Keep UTC ns and large sequences as exact integers (Python integers are exact); JavaScript clients need a lossless JSON parser before conversion to BigInt. Ordinary JSON.parse can lose precision. Reference: MCP The five tools are read-only. Each successful result's text content is JSON; parse it before interpreting rows. Check isError even when the MCP HTTP request succeeds. REST failures become tool errors with status/body; admission errors can instead be fixed reasons such as grant_required. All tools have a 100000-byte successful-result budget; larger results return response_too_large with no rows, so reduce fields, limit or time range as applicable. Remote MCP is stateless Streamable HTTP with JSON responses, no SSE, no sessions and no market-data streaming. Use the client's MCP implementation for transport. Input shapes below: ? means optional; all values are strings except iid/limit (integers), fill (boolean), and keys (object of string values). Inputs reject extra properties. tools/list supplies the exact machine-readable input schemas. list_underlyings: {query?} get_chain: {underlying, expiration?, fields?, strike_min?, strike_max?, records?} get_contract: {symbol, fields?} get_trades: {symbol, start, end, limit?, cursor?} get_series: {record, iid, start, end, fields, limit?, underlying?, fill?, keys?} All static symbols, dates, IDs and numbers below are illustrative inputs or unit examples, never current-data or coverage promises. Discover replacements first. list_underlyings {"query":"SP"} get_chain {"underlying":"AAPL","expiration":"2026-10-16","strike_min":"230","strike_max":"235","fields":"symbol,strike,bid,ask,iv"} get_contract {"symbol":"AAPL 261016C00232500","fields":"bid,ask,iv"} get_trades {"symbol":"AAPL 261016C00232500","start":"2026-10-09T14:00:00Z","end":"2026-10-09T14:01:00Z","limit":100} get_series {"record":"quote","iid":42,"underlying":"AAPL","start":"2026-10-09T14:00:00Z","end":"2026-10-09T14:01:00Z","fields":"bid,ask","limit":100,"fill":false} Without expiration, get_chain defaults to underlying_price, underlying_analytics and term records unless records is given. records is a comma-separated list of record names to keep. strike_min/strike_max are inclusive decimal strings; if fields is given with a strike filter, it must include strike. get_contract and get_trades require OSI, not iid. There is no underlying-wide trades tool. iid is 0..4294967295; limit is 1..100000, default 10000. get_series record names come from descriptors, excluding print (use get_trades). Include underlying in get_series for account admission; it also filters history. For keyed records supply every descriptor key in keys, with values as REST query strings: numbers/booleans as strings, enum names, "null" for a null dimension. For example term needs keys={"expiration":"2026-10-16","settlement":"pm"}, using the underlying iid, not an option iid. Only descriptor key names are allowed. Reference: REST paths, identifiers and history GET /v1/underlyings GET /v1/underlyings/{symbol} GET /v1/chains/{symbol}?expiration=YYYY-MM-DD&fields=bid,ask GET /v1/contracts/{osi}?fields=bid,ask,iv GET /v1/contracts/{osi}/trades?start=...&end=...&limit=100&cursor=... GET /v1/{record}/{iid}/series?start=...&end=...&fields=bid,ask&limit=100&fill=false&underlying=... REST supports JSON and Accept: text/csv. CSV snapshots add a record column; metadata is in X-As_Of, X-Version, X-Fidelity, X-Next_Cursor and, when supplied, X-Complete_Through headers. Errors remain JSON. GET/HEAD are supported. Use symbols returned by the service. An option's OSI symbol is exactly 21 characters: root padded to six characters, YYMMDD, C/P, strike x 1000 in eight digits. The two spaces in "AAPL 261016C00232500" matter; its encoded segment is AAPL%20%20261016C00232500. Encode spaces as %20, not +, in paths. Encode each segment independently: BRK/B becomes BRK%2FB. Encode query values, including opaque cursors, separately. Do not unpad OSI or substitute another symbol format. The contract master maps symbol to permanent numeric iid. Join by iid; use symbols for contract snapshot/trades paths, iid for series. Requesting a known chain first also seeds contract resolution. Discover expirations from term rows; if there are none, report discovery unavailable rather than manufacturing an expiration. start/end accept RFC3339 (use Z or an explicit offset) or decimal-string UTC ns. Use short ranges first. Each request is at most 24 hours, one trade date and 100000 rows; split longer work into bounded windows. The trade date rolls at 17:00 America/New_York, following the trading calendar, not midnight UTC. Trades paginate with the opaque next_cursor: repeat the same request with cursor until it is null. Do not construct cursors or silently truncate at the first page. Series has no cursor input; reduce/split the time window when its row cap is hit. Deduplicate overlapping print pages/windows by (iid, trade_date, seq); handle status=new/correction/cancel and orig_seq when counting prints or volume. Stored history is coverage-dependent. For today's covered range, complete_through is the actual published watermark; an overlap may return only the covered prefix. It does not certify completeness through the requested end. Missing current-day coverage or a start beyond the watermark returns 409 history_unavailable; a covered interval with no trades can succeed empty. Future trade dates return 409 live_history_deferred. Explicit unavailable historical assessments also fail with history_unavailable. Partial assessments qualify fidelity and set bit 128 on flow/exposure; absence of an assessment does not certify completeness. Check version (array), fidelity (array), coverage qualifications and quality before combining responses. Do not promise a retention period or complete day. Series fields is required and uses comma-separated descriptor names; bucket_end may also be requested for the stored cadence boundary. fill defaults to false; true forward-fills stored state, not evidence of a new observation. Do not infer event frequency from series rows. Keyed REST series require each descriptor key as its own query parameter: e.g. term needs expiration=2026-10-16&settlement=pm. Resolve field names, types, units, enums and key dimensions from the schema, not from a guessed table layout. Match aggregate rows on iid plus all key fields. Reference: reading a response correctly Snapshots contain a records map (record name -> row arrays); catalog/history use rows. as_of is UTC ns. Snapshot as_of is the frame tick bounding row receive times, not a promise that every quote changed then; catalog as_of is its response time. seq is the snapshot's stream sequence. bid_time/ask_time and last_time/trade_time are exchange times; calc_time is calculation time; stored recv_time is ingest time. Live snapshots omit per-row recv_time. Encoded ns does not mean ns source resolution: exchange/receive times have millisecond resolution. Preserve the returned timestamps and distinguish them in the answer. A missing value is not zero. JSON null means absent; a field may also be omitted. fields projection retains iid, key dimensions and available exchange timestamps, calc_time, method and quality. It does not insert missing fields. Check each record separately; never fill absent IV, OI or quotes with invented numbers. Contract flow has a specific exception: a contract that has never printed has no flow row, meaning zero counted flow. This does not override coverage flags. Prices are in the instrument's currency, sizes/OI/option volume in contracts; use contract multiplier/deliverable metadata for totals, never assume 100. Decimal fields are exact JSON strings (strike "232.500" is already 232.5 currency units); quote prices are numeric currency values, not integer cents to rescale. OSI strike encoding is separate from JSON field encoding. IV is annual decimal volatility: 0.2531 means 25.31%. delta/gamma reference the implied forward F, per forward point and point squared; theta is price change per calendar day, vega per 0.01 volatility and rho per 0.01 continuous rate. These are model values. Black-76 uses quote mid and chain-implied forward/discount. american=true marks its approximation for American exercise; non-standard contracts lack analytics. Spot variants can be absent. Contract time to settlement uses seconds without a floor; do not substitute the whole-minute, minimum-30-minute term/index clock. Derived method versions and quality travel with available analytics. IV/delta quanta are 1e-4, gamma 1e-10, theta/vega/rho 1e-3; term/index volatility is 1e-6. REST snapshots carry wire precision, not extra calculation precision. Read field quanta/increments from descriptors; do not apply one decimal scale to all fields. OI is daily with oi_date, not a live counter. Headline volume counts regular, non-spread prints; vendor_day_volume is a separate reference value. quality is a bit mask: few_strikes=1, poor_fit=2, crossed=4, stale_input=8, no_spot=16, extrapolated=32 (reserved, not set), non_standard=64, partial_coverage/incomplete=128, unconfirmed_strip=256 (index strip confirmation), rate_unstable=512 (term implied_rate uncertain; common on short expiries; value kept). Explain relevant set bits. iv_flattened and american are separate fields. An old quote or calculation does not become fresh when a heartbeat advances. Reference: WebSocket for coding agents Endpoint: wss://api.optionsdata.ai/v1/stream. It supplies initial state snapshots then changes for subscribed underlying/expiration streams, including prints. It requires a binary PSF1 decoder; these are not JSON quote events. MCP does not expose this feed. Use REST first unless the user's task needs ongoing updates. In account mode send Authorization: Bearer with the API key on upgrade. If the client cannot set headers, its first message within five seconds must be {"type":"auth","bearer":""}, populated from secure local configuration. Never use query credentials or credential cookies. A browser Origin must be explicitly allowed. Realm tokens with the API audience and stream.read are a separate supported identity path; an MCP-scoped OAuth token is not a substitute. Wait for {"type":"hello","version":1,"schema":...}. Build the decoder from schema.version, records and their fields: id, name, wire_type, unit, quantum, exact_increment, enum_values and key. Hello alone proves no data grant. Send {"op":"list"} for names/iids, then, using a returned underlying, {"op":"list","underlying":"AAPL"} for expirations. Subscribe narrowly with {"op":"sub","underlying":"AAPL","expirations":["2026-10-16"]} using discovered values. expiration="" selects underlying aggregates; expirations="all" includes those plus every expiration. Unsubscribe with op="unsub" and the same shape. Each list/sub can be denied independently of catalog or hello success. PSF1 essentials: header is magic PSF1, underlying iid, expiration, sequence, tick UTC ns, snapshot byte, section count. Integers use unsigned LEB128; signed values use zigzag; strings are byte length then UTF-8. Sections contain record ID, row count, then iid gaps, a u64 field mask and values in field-ID order. iid gaps restart from zero per section; bit 0 is forbidden. Tags: 0 null, 1 little-endian literal float (4 bytes f32, 8 f64), 2 descriptor-typed value. Numeric tag 2 uses zigzag integers, decimal coefficients use descriptor scale, f64 uses quantum or exact_increment; enum is a zero-based descriptor index, bool one byte, text/date length-prefixed UTF-8. Unkeyed numeric values use per-stream/record/iid/field delta bases; keyed records use absolute values. Null/literal resets the numeric base. Absent mask bits retain state; explicit null clears it. Key by iid plus descriptor keys. Consume unknown fields using the received schema, retaining their unkeyed bases. Reject invalid frames without changing state. Do not guess record IDs or precision from this summary. Live hello supplies schema, not decoder code; obtain a compatible, validated full PSF1 protocol/decoder before implementing streaming. This Readme does not link to a public full wire reference or downloadable SDK. A snapshot resets state and decoder bases only for its stream and establishes a new sequence base, which may move backwards. A delta must follow seq+1; otherwise discard that stream's state, send unsub then sub for a fresh snapshot. Reconnect requires fresh discovery, subscriptions and snapshots. Ignore unknown control types. On gap, mark the stream degraded and wait for the automatic snapshot; refetch missing prints over REST if needed. A snapshot restores state, not all missed prints. gap from/to and refetch start/end are UTC milliseconds, unlike REST ns inputs: multiply by 1000000 exactly or convert to RFC3339 first. For source_disconnected with streams, invalidate only those streams and wait for fresh snapshots; without scope treat all streams as unavailable. Honor authorization/lease errors: delivery can stop when grants expire or renewal fails. Stream volume: enable WebSocket compression (permessage-deflate) in the client. Frames are roughly four to six times smaller; browsers negotiate it by default, and so do many libraries, but check yours. There is no per-connection rate limit. Each stream sends at most one bundle per 100 ms carrying every change since the previous one, so quiet streams send fewer. Each connection has a bounded queue (currently 2 MiB). A client that reads slower than delivery fills it and gets a gap plus a fresh snapshot for the affected streams; with several expirations uncompressed this can happen within a second. Read continuously, decode off the socket reader, subscribe narrowly, or split heavy subscriptions across connections. When a request fails Inspect REST error.code/error.message, MCP isError/content or WebSocket error code. Never substitute illustrative values for an error or empty real result. 401: check the chosen credential type, expiry/revocation and local configuration; reconnect OAuth when applicable. Never ask for the credential in chat. 403 or grant_required/agreement_required/classification_required/credential_scope: complete the account's required review/setup or obtain the appropriate grant. Do not treat a retry, another endpoint or a successful catalog list as permission. 404/unknown_resource: rediscover identifiers and preserve OSI spaces/encoding. 400: correct inputs/fields/keys; 413: narrow the expiry, fields or history window. 409 history_unavailable: report unavailable coverage; do not relabel it no trades. 429: honor Retry-After if supplied, otherwise use bounded exponential backoff with jitter. Stop concurrent retry loops. No fixed rate limit is promised here. 503/unavailable or a transport timeout: retry read requests with bounded backoff; after a small retry budget report unavailable. Preserve coverage/quality warnings on recovery. Sign-in, data grants and service readiness still require the actual account and service checks; this page cannot establish them.