Every trading day we work out four levels from the options chain of a handful of major index and ETF symbols — the call wall, the put wall, the gamma flip and max pain. This page documents the endpoint that hands them to anyone who asks. It is free, it needs no key and no account, and it is open to browsers from any domain, so you can call it from a blog post, a notebook, a dashboard or your own tool.
Four levels, plus the spot price they were measured against.
| Level | What it is |
|---|---|
| Call Wall | The largest positive-gamma strike above spot — dealers sell into rallies here, so it often caps upside (resistance). |
| Put Wall | The largest positive-gamma strike below spot — dealer buying tends to cushion selloffs here (support). |
| Gamma Flip | The gamma-flip / zero-gamma level: above it dealers dampen moves (mean-reversion), below it they amplify them (trend). |
| Max Pain | The strike where the most option value expires worthless — a weak magnet into monthly/weekly expiry. |
| Spot | The current underlying price the levels are measured against. |
GET https://volstrata.com/embed/levels.json?ticker=SPX
Returns one JSON object. No key, no account, no sign-up. It answers with
Access-Control-Allow-Origin: *, so you can call it straight from a
browser on any domain. Responses carry Cache-Control: public, max-age=30.
Send a User-Agent that names your project. A bare default
library user-agent can be refused at the edge with a 403 — if a
script gets one and a browser does not, that is why. The Python example below
shows it.
| Name | Required | What it does |
|---|---|---|
| ticker | no | The symbol you want.
Currently published: SPX, NDX, QQQ, SPY. Defaults to SPX. Anything
else is answered with SPX and clamped: true rather than an error
— this endpoint does not return a 4xx for a symbol it does not carry, so
do not use the status code to test whether a symbol exists. Read
clamped. |
This is not a mock. It is an actual response for SPX,
captured as this page was rendered (Sep 15, 2026 · 12:06 PM ET), pretty-printed (the wire
response is a single line).
{
"ticker": "SPX",
"requested_ticker": "SPX",
"clamped": false,
"spot": 7583.58,
"call_wall": 7675.0,
"put_wall": 7580.0,
"flip": 7592.97,
"max_pain": 7650.0,
"regime": null,
"net_gex_bn": null,
"as_of_epoch": 1789488387,
"as_of_label": "Sep 15, 2026 · 12:06 PM ET",
"delayed": false,
"delay_secs": 0,
"stale": false,
"has_data": true,
"source": "live",
"status": "ok",
"reason": null
}
Read straight off the response above, so this table cannot describe a field the endpoint no longer sends or miss one it has started sending. Anything marked null now was null in that exact response.
| Field | Type | What it is |
|---|---|---|
| ticker | string | The symbol these levels belong to. This is the symbol we answered with, which is not always the one you asked for — see clamped. |
| requested_ticker | string or null | Exactly what you passed in ?ticker=, echoed back. Null when you passed nothing at all. |
| clamped | boolean | True when you asked for a symbol this feed does not publish and we answered with SPX instead of an error. |
| spot | number or null | The price of the underlying that the levels were measured against. |
| call_wall | number or null | The call wall strike — the level that most often acts as resistance. |
| put_wall | number or null | The put wall strike — the level that most often acts as support. |
| flip | number or null | The gamma flip (zero-gamma) level. Above it moves tend to be dampened, below it they tend to be amplified. |
| max_pain | number or null | The max pain strike — where the most option value expires worthless. |
| regimenull now | string or null | A one-word summary of the gamma regime. Frequently null on this feed; always check before you rely on it. |
| net_gex_bnnull now | number or null | Net gamma exposure in billions. Frequently null on this feed; always check before you rely on it. |
| as_of_epoch | integer or null | Unix time, in seconds, of the market data these levels were computed from. This is the age of the NUMBERS, not the time you called us — subtract it from your own clock to find out how old they are. |
| as_of_label | string | The same instant already formatted in US Eastern time, for display. The string “—” when we do not have a timestamp. |
| delayed | boolean | True when a deliberate delay was applied to this symbol before serving it to you. See How fresh is it, really. |
| delay_secs | integer | The size of that delay in seconds, or 0. It is a FLOOR, not a promise of precision — read the note under the freshness table. |
| stale | boolean | True when we could not build a level set at the point you were entitled to and are showing you the most recent one we do have instead. as_of_epoch still tells you its real age. |
| has_data | boolean | False before the first level set of the session exists. Every price field is null when this is false, and the response is still a 200. |
| source | string | How this record was arrived at: “live”, “delayed”, “last-known”, or “none” when there is nothing to serve yet. |
| status | string | What kind of answer this is: “ok” when the levels below are a real reading, “no_data” when the session has genuinely not published any yet (nothing is wrong — it is pre-open, or a sparsely-polled symbol), and “unavailable” when WE failed and this record is standing in for a reading we could not take. Branch on this rather than inferring health from the absence of numbers: the last two look identical otherwise. |
| reasonnull now | string or null | A short machine-readable tag naming the fault when status is “unavailable”, and null otherwise. Log it; do not parse it for meaning — the set of tags is not part of this contract. |
Any price field can be null. Before the first level set of a session exists,
has_data is false, every price is null, and the
response is still a 200 — handle that case rather than
assuming a number is always there.
There is no single answer, so here are the actual numbers, read off the feed as this page rendered (Sep 15, 2026 · 12:06 PM ET). Some symbols are served with no delay applied and others are held back; do not assume either.
| Symbol | Delay applied | delayed |
delay_secs | Age of the data right now |
|---|---|---|---|---|
| SPX | No delay applied — served as soon as it exists. | false no clamp | 0 | 27 s |
| NDX | No delay applied — served as soon as it exists. | false no clamp | 0 | 3 min 12 s |
| QQQ | Held back by at least 30 minutes before it reaches you. | true | 1800 | 31 min 22 s |
| SPY | Held back by at least 30 minutes before it reaches you. | true | 1800 | 32 min 6 s |
Do not take this page's word for it. Every response carries
delayed, delay_secs and as_of_epoch.
Those three are the authoritative answer for that request; the table
above is the same three values read at one moment. If we ever change what we
hold back, your response changes with it and this table changes with it. The
fields cannot go out of date; a sentence can.
delay_secs is a floor, not a precision. When a delay
applies we serve the most recent level set at or before that edge, so the real
age is the delay plus however long ago that set was computed. Expect the
observed age to run somewhat past delay_secs, not to sit on it.as_of_epoch is the age of the market data, not of the
HTTP response. Subtract it from your own clock; that is the honest number to
show your users.stale: true means we had nothing to build from at the
point you were entitled to and are showing the most recent set we have
instead. It is still stamped with its own real timestamp.has_data: false.Use the feed for anything you like, including commercial work. There is one condition and a few requests.
volstrata.com. A credit your readers cannot see or cannot click
does not count.<a href="https://volstrata.com/">Gamma levels by VolStrata</a>as_of_label through, or render
as_of_epoch yourself.No rate limit is enforced on this endpoint today, and there is no key to ask for. We would rather you used it than wrote to us for permission. So this is a request, not a gate:
max-age=30 we send is
enough.User-Agent that names you. This is the one thing
that can actually stop you: the edge in front of this feed refuses some
default library user-agents, so a bare scripted request can come back
403 where the identical request with a name attached returns
200. A project name and a contact URL is ideal — if your traffic
ever became a problem we would far rather email you than start returning
errors.If a limit ever becomes necessary, it will be written on this page before it is enforced.
curl -s 'https://volstrata.com/embed/levels.json?ticker=SPX'
// Runs in a browser from any origin — no key, no proxy.
const r = await fetch('https://volstrata.com/embed/levels.json?ticker=SPX');
const levels = await r.json();
console.log(levels.call_wall, levels.put_wall, levels.as_of_label);
if (levels.delayed) console.log('delayed by', levels.delay_secs, 's');
import json, time, urllib.request
url = "https://volstrata.com/embed/levels.json?ticker=SPX"
req = urllib.request.Request(url, headers={
# Name your project. A bare default library user-agent
# can be refused at the edge with a 403.
"User-Agent": "my-project/1.0 (+https://example.com)",
})
lv = json.load(urllib.request.urlopen(req))
print(lv["ticker"], lv["call_wall"], lv["put_wall"],
lv["flip"], lv["max_pain"])
if lv["as_of_epoch"]: # can be null — check it
age = int(time.time() - lv["as_of_epoch"])
print(f"data is {age}s old ({lv['as_of_label']})")
if lv["delayed"]:
print(f"held back by at least {lv['delay_secs']}s")