Changelog
How DQL changed, set by set.
The language reached its current shape through four change sets. They are kept because a client written against an earlier one needs to know what moved, and because a decision is easier to argue with when you can see when it was taken.
Nothing in a change set was a breaking change to a released client: the platform had no public clients while they were made. From here on, a change that would break one gets its own note here first, under “Announced breaking changes”, before it ships.
2026-11-01. Breaking: 2m and 3m timeframes are retired (owner, #780, C45-01). Candles timeframe, history interval and DQL series accept 1m, 5m, 15m, 30m, 1h, 4h, 12h, 1d. Requests for 2m or 3m return DQL_TIMEFRAME_UNSUPPORTED (400), naming the supported periods including 1m and 5m. Maintenance builds neither retired rollup; existing rows are left unchanged to expire with their class. Duration literals such as freshness 2m remain valid. Strategy examples use a denser 1m grid with duration-equivalent windows; stored Agent rules are not rewritten.
2026-11-01 (issue #781). Anchor schema version 2 excludes flagged synthetic dummy/test listings from new roots. Already submitted/anchored version-1 roots and chunks stay unchanged; their proofs use version-1 membership. Canonical rows and hash algorithms are unchanged. /v2/anchors and proofs report the stored version; clients must use it when rebuilding partition leaves.
2026-11-01. Partial bars (owner). A rolled-up candle (2m and longer) whose period has closed with some but not all of its one-minute candles is stored and served, with "partial": true on the bar in /v2/candles; before, such a period was a gap. Only a period with no minute at all is a gap now. DQL indicators read partial bars like any other, so a few missing minutes no longer make a 1h indicator unknown for its whole lookback. A partial bar is replaced by the complete one when its minutes are backfilled. Additive: a client that ignores unknown members is unaffected.
2026-11-01. The Standard trial opens on the first refusal of any of three Free caps, not only of history depth: the number of Agent rules, DQL screening requests a minute (/v2/screen, /v2/dql/parse) and requests a day. The request is admitted under Standard instead of refused, and its answer’s trial member names the cap in cap (agent_rules, dql_per_minute, requests_per_day) with Free’s number in limit; a history refusal carries no cap. A rule saved over the cap answers with trial (message: “Free allows 3 rules, this is your 4th.”). The per-second burst (429 requests_per_second) is not a cap and opens nothing. Still one trial per account, ever.
2026-10-31. The Standard trial. A Free account that has never had a trial and is refused by its plan’s history depth (403 PLAN_LIMIT) is answered instead, by the same request run under Standard, and Standard is open on the account for 4 days. That answer carries a new member trial (plan, until, and layer and asked_days of the request that opened it); no other answer does. GET /v2/account gets plan.trial_until (the end of the open trial, null when none). One trial per account, ever; staff and test accounts, the Agent key, 429 and DQL errors never open one, and neither does a request Standard refuses too (data before the venue’s first day, the hot window). At trial_until the account is Free again and its limits apply at once. Additive: a client that ignores unknown members is unaffected.
2026-10-30. Breaking: the timeframe 3m is removed (owner, issue #722). /v2/candles timeframe, the history interval and a DQL timeframe accept 1m, 2m, 5m, 15m, 30m, 1h, 4h, 12h and 1d; 3m is DQL_TIMEFRAME_UNSUPPORTED (400) with that list. The 3-minute rollups were about a third of the stored rollup rows, and no API, MCP, Studio or agent request asked for them in the call log (14 to 30 October). GET /v2/instruments lists the timeframes that remain. Nothing else changes.
2026-10-30 (shape). plan.history of GET /v2/account is an object where it was a sentence: a client that reads it as a string must change. history.layers and history.candles give, per layer and candle timeframe, hot_days (days the database holds it, from the retention classes in force: the shortest of any collected listing; hot_days_longest where listings differ), reads_days (the smaller of that and the plan’s own cap; Free and Mini keep theirs) and archive_since (the first day with a verified archive file; null for a layer the archive does not hold). history.archive.files_per_month is the plan’s files, history.not_swept lists funding_settlement and liquidation. Every plan of GET /v2/plans carries the same object. PLAN_LIMIT on a request that lies wholly before the hot window of everything it asks for (POST /v2/snapshots/history in every mode, POST /v2/trades, POST /v2/candles) names the layer, the window in days and the earliest hot instant (earliest_from) and says one of three things: the exact GET /v2/archive/{listing}/{layer}/{day} when the day has a verified file; no data before <date> when the venue has none that early; or that the archive has no file for the day. Liquidations and funding settlements are not swept and have no window: they are refused only with no data before <date> when the whole request is before the venue’s first data. Before this such a request answered not_retained row by row; a request that reaches into the window of any listing and layer still does, with the archive pointer where a file exists. GET /v2/venues items carry hot_days (days each layer is held for that venue) beside since. plan.history_days and history_from are unchanged.
2026-10-30 (values). Three response strings change, a client that compares them exactly must follow. plan.history of GET /v2/account is the database window where it was all history (Pro, Pro Plus) or one year of history (Standard); other plans keep N days of history. The PLAN_LIMIT message and its next name the plan as reading the database window instead of all history or 365 days. Studio’s history label changes the same way. No limit changed: history_days is still null for Pro and Pro Plus and 365 for Standard.
2026-10-30. Plan wording only; no limit, price or answer changed. “All history” and “365 days” are no longer how the plans are described. The database window, by layer: trades 7 days; 1 s ticker and mark 7 days; snapshots, funding, open interest, stats and depth 30 days; candles 1m 30 days, 5m and above 365 days. Older than the window is not_retained with the archive pointer, and the data is served as archive files, not as Parquet through the other endpoints: Standard 100 a month, Pro 1,000, Pro Plus 10,000, per file with x402 without a key. funding_settlement and liquidation are not swept.
2026-10-27. POST /v2/snapshots/history takes summary: true with interval (and fields, to name the fields): a layer of each listing answers with the number of readings, n, and for each numeric field its min, max, first and last as [value, time], over the window, instead of the rows. mode is summary. Funding is the same funding_8h basis the rows use. The extremes are the highest and lowest value observed at the interval’s samples (the last reading of each period), not a settled amount; a layer with no reading says why with its state and n: 0. Plan depth, limits and x402 pricing are those of the same request without summary. The MCP history tool takes both. Additions only; nothing is removed or renamed.
2026-10-27. Free, Mini and Standard read every candle timeframe, limited by depth and not by resolution. Free: 1m 48 hours, 5m and 15m 7 days, 1h 30 days, 4h and 1d 90 days. Mini: 1m 7 days, 5m and 15m 30 days, 1h 90 days, 4h and 1d 180 days. Standard: one year for every timeframe. Pro and Pro Plus read all history. A timeframe between two takes the one below it. Everything read without a timeframe (snapshots, history, trades, liquidations, funding settlements, screen) reaches 7 days on Free (was 48 hours), 30 on Mini, 365 on Standard. A PLAN_LIMIT on candles names the depth the plan has for that timeframe and the cheapest plan that reads further. GET /v2/plans adds candle_history_days, the depth for each timeframe, to every plan; history_days is unchanged. Nothing is removed or renamed.
2026-10-26. The plan’s history edge is read loosely by one bar and five minutes: a request may start at the plan’s floor rounded down to the bar of the request (the candle timeframe, the history interval) less 5 minutes, so “exactly the last 48 hours” asked a few minutes late no longer gets PLAN_LIMIT. earliest_from, retry_with and next name that edge (the first grid instant at or after it for candles and history intervals). Nothing is removed or renamed.
2026-10-26. Common venue names resolve to the registry code (an explicit table: BINANCE-PERP, BINANCE-FUTURES for BINANCE-USDM, OKX-SWAP for OKX-PERP, HYPERLIQUID-PERP for HYPERLIQUID, KRAKEN-PERP for KRAKEN-FUTURES and the like; no fuzzy matching), and every /v2 answer to a request that used one carries a top-level resolved_from object, the name written and the code it was answered as. Errors say what was probably meant: an unknown venue names the nearest code, symbol = BTC-PERP says symbol is the venue’s own symbol and suggests instrument = BTC-PERP, and instrument = PERP or perpetual points at the kind option. Additions only; a request that was an error before (DQL_UNKNOWN_VENUE for such a name) now succeeds.
2026-10-26. Documentation only: Kraken Futures’ live funding is the venue’s predicted rate for the hour in progress; the rate that settled is the venue’s own figure, which GET /v2/funding/settlements uses (rate_source: venue_history). Live samples taken before the hour differed from it on 95 of 98 prints. See Funding normalisation and Funding settlements. No field changes.
2026-10-23 (issue 619). GET /v2/funding/settlements: funding as events, one row per settlement print of a listing with the rate as the venue published it, the interval in force and its cross-check against the venue’s published one, origin (live or backfill), and the cap and floor at the time. Prints are recorded live from the venues’ funding data from now on and backfilled for 90 days from Binance USD-M, Bybit, OKX, Kraken Futures and Hyperliquid; they are in no anchored day. Plan depth is the plan’s history, with PLAN_LIMIT, earliest_from and retry_with. The MCP server has a funding_settlements tool. Additions only; nothing is removed or renamed.
2026-10-22 (issue 576). agent_rule_get and agent_rule_create carry history: how many times the rule would have fired in the last 30 days of recorded history (the Agent’s own replay), when it last would have, and what the history lacked. A fact of the past, not a forecast. POST /v2/candles marks a 1h, 4h or 1d bar fetched from the venue’s own history after the fact with "origin": "backfill". Additions only; nothing is removed or renamed.
2026-10-13 (issue 335). Agent endpoint response contracts are now documented in schemas.json for MCP clients. API answers are unchanged.
Historical route note (2026-10-12, change set 37 / issue 307). References to the public market-data /v1
routes below describe behavior at the time of each dated change; those routes and the /lab demo are retired
by change set 37. This does not apply to /runner/v1/* or third-party venue API paths.
Announced breaking changes
Section titled “Announced breaking changes”Funding caps: funding_cap, funding_floor, funding_at_cap on the funding layer and on /v2/instruments — additive, takes effect when issue 365 is deployed (2026-10-13)
Section titled “Funding caps: funding_cap, funding_floor, funding_at_cap on the funding layer and on /v2/instruments — additive, takes effect when issue 365 is deployed (2026-10-13)”POST /v2/snapshots, POST /v2/snapshots/history, GET /v2/stream, GET /v2/instruments, and DQL on POST /v2/screen. The funding layer’s values gain funding_cap, funding_floor, funding_cap_8h, funding_floor_8h and funding_at_cap (a boolean or null); a listing in /v2/instruments gains funding_cap, funding_floor, funding_cap_observed_at and funding_cap_reason. Nothing that was there changes.
What you will see. funding_at_cap is true when the funding value sits on the cap or floor the venue’s API publishes (the one in force when the value was received, never a later one), false between two known bounds, and null with a reason when no bound had been observed (not_retained), the venue’s API publishes none (venue_capability) or the platform does not collect it yet (no_value). The five names are also fields of DQL; funding_at_cap is its first boolean field.
What to change on your side. Nothing. A client that validates responses against a closed list of values keys should add these five.
x402: you pay for what we recorded, not for what we missed — additive, takes effect when task 321 is deployed (2026-10-13)
Section titled “x402: you pay for what we recorded, not for what we missed — additive, takes effect when task 321 is deployed (2026-10-13)”POST /v2/snapshots, POST /v2/snapshots/history, POST /v2/candles, POST /v2/screen, when paid with x402. The answers do not change by a byte; only the settlement does. This replaces the 2026-10-12 wording (“an answer with no present value is not charged”): a stale row is a row we recorded.
You pay for what we recorded, not for what we missed.
What changes. A 200 paid with x402 is settled when its answer holds at least one recorded row: a value or row with status: present or stale (a stale row of any reason, halted included, is one we wrote), a bar, or a raw observation. It is not settled, and the same signed payment may be presented again, exactly as for a 400 or a 5xx, when the answer holds no recorded row: the period is before retention_from, or inside a collector_gap, or the answer holds only missing, not_published or off (an at instant inside a collector gap has no recorded row of its own: it shows the last value before the hole, stale, beside the hole’s gap_cause). No ledger row is written for an unsettled call. The archive file is unchanged (paid when the file exists, 302), and so is GET /v2/agent/state.
What to change on your side. Nothing. A client that read a settled payment from every 200 should not expect one when the answer holds no recorded row.
gaps on a raw history layer — additive, takes effect when task 321 is deployed (2026-10-13)
Section titled “gaps on a raw history layer — additive, takes effect when task 321 is deployed (2026-10-13)”POST /v2/snapshots/history in raw mode. Nothing is removed or changes meaning. Every layer of a raw answer carries a gaps array: the stretches of (from, to] the platform recorded as a hole (reason: collector_gap, gap_cause says what, from/to clipped to the range, to null when the end was never recorded) and the stretch older than the layer’s retention_from (reason: not_retained), in the shape gaps has on /v2/trades and /v2/liquidations. It is empty when nothing was missed or with include_gaps: false. interval mode already lists the same holes as gap rows (collector_gap, gap_cause), and at mode beside a stale reading.
What to change on your side. Nothing. A client that rejects unknown properties must accept gaps on a raw layer.
A quantifier or aggregate with no venue in scope is unknown, no_venues — takes effect when task 313 is deployed (2026-10-09)
Section titled “A quantifier or aggregate with no venue in scope is unknown, no_venues — takes effect when task 313 is deployed (2026-10-09)”DQL wherever it runs (/v2/screen, history, MCP, Agent). One value is added to the null reasons: no_venues. No field is added or removed.
What changes. When every listing of an instrument is left out of a quantifier’s or aggregate’s scope (halted, or off or
not_published for a layer it reads; change sets 30–32), the scope is empty: scopes shows venues_in_scope: 0, “0 of N venues”. Until now
any(…) over it answered false, all(…) true and count(…) 0. All three, and max, min and sum, are now unknown (a count or an
aggregate is null) with the reason no_venues (change set 35), and the unknown propagates by the ordinary rules: count(venues where …) >= 0 is unknown, not
true. An aggregate whose venues were in scope but all dropped out by its where or a null stays null with no_value. Agent does not fire on
such an instrument and, new, records a not-fired line for it: “not fired: any(venues where …) was unknown (no_venues — no venue left in
scope) · computed over 0 of N venues; excluded: …”. Found on AERO-PERP, where all 11 listings are excluded.
What to change on your side. Nothing, unless you relied on any being false, all true or count being 0 over an instrument with no
venue in scope; such conditions now leave the instrument out of a screen (unknown) instead of answering. Accept no_venues among the null
reasons if your client checks them against a fixed list.
ref(INSTRUMENT, expr [, VENUE]) and instrument on a value — additive, takes effect when task 317 is deployed (2026-10-09)
Section titled “ref(INSTRUMENT, expr [, VENUE]) and instrument on a value — additive, takes effect when task 317 is deployed (2026-10-09)”/v2/screen and DQL wherever it runs (history, MCP, Agent), POST /v2/dql/parse, GET /v2/dql/catalogue. Nothing is removed or changes meaning.
What is added. A function, ref, that reads an expression for another instrument on the venue being evaluated (or on the venue named), at
the same instant: ref(BTC-PERP, mark_close) > ref(BTC-PERP, ema(mark_close, 50, 15m)) (change set 34, spec §7.4c). A query that did not
contain ref means what it meant. A value read through ref carries instrument beside venue — a property no other value has; the null
reason not_in_scope (the target is not listed on that venue, is halted, or a layer is off or not published for it); the machine form has a
ref node; GET /v2/dql/catalogue lists ref among the functions and reference_targets (4) among the limits.
What to change on your side. Nothing. A client that rejects unknown properties must accept instrument on a value, not_in_scope as
a reason and reference_targets among the limits; a client that walks the machine form of a query must accept the ref node.
time_to_funding and funding_interval are not_published on a continuous-funding venue — takes effect when task 310 is deployed (2026-10-09)
Section titled “time_to_funding and funding_interval are not_published on a continuous-funding venue — takes effect when task 310 is deployed (2026-10-09)”DQL wherever it runs (/v2/screen, history, MCP, Agent) and GET /v2/dql/catalogue. No field is added or removed.
What changes. On AVANTIS-PERP and GMX-PERP, which accrue funding continuously and never settle, time_to_funding and funding_interval
were present with a null value and the reason funding_continuous. They are now not_published, with the same reason: the field has no
value by construction. So status(time_to_funding) = present is false there and = not_published true, and a quantifier that reads either
field leaves the listing out (change set 31): any(venues where time_to_funding < 1h) no longer stays unknown because of them, and scopes
names them (not_published, layer funding). The catalogue lists both venues under not_published_on of the two fields. /v2/snapshots is
unchanged (its status is the layer’s; the fields stay null with funding_continuous).
What to change on your side. Nothing, unless you tested status(time_to_funding) = present or status(funding_interval) = present to
find listings, or relied on such an any being unknown.
off and not_published listings are not in the scope of any, all, count, max, min, sum; scopes added — takes effect when task 306 is deployed (2026-10-09)
Section titled “off and not_published listings are not in the scope of any, all, count, max, min, sum; scopes added — takes effect when task 306 is deployed (2026-10-09)”/v2/screen and DQL wherever it runs (history, MCP, Agent). One field is added: scopes on each instrument of a /v2/screen answer.
What changes. A listing on which a layer a quantifier’s own expression reads is off (DEBYKO does not collect it for the listing) or
not_published (the venue never publishes it) is left out of that quantifier, as a halted listing is since change set 30. Until now it
made P(v) unknown for good, so any(venues where mark > K) over all venues could never be false and all(…) never true. Those answers
are now false and true. The test is per quantifier: a venue without a published mark is left out of any(venues where mark > K) and stays in
any(venues where funding > 0). stale and missing listings stay in and still give unknown. One layer is enough: an or in the where
does not keep the listing in. Every listing keeps its row in venues and counts in venues_listed. Each instrument now carries scopes:
per quantifier and aggregate, venues_listed, venues_in_scope and excluded (venue, symbol, reason halted | off |
not_published, layer). An answer as of a past instant judges off and not_published by today’s policy and venue facts, as the status itself already did.
What to change on your side. Nothing, unless you relied on such an any being unknown or on count(venues where P) = venues_listed
being reachable while a venue does not publish the field. A client that rejects unknown fields must accept scopes.
A halted listing is not in the scope of any, all, count, max, min, sum — takes effect when task 305 is deployed (2026-10-09)
Section titled “A halted listing is not in the scope of any, all, count, max, min, sum — takes effect when task 305 is deployed (2026-10-09)”/v2/screen and DQL wherever it runs (history, MCP, Agent). No field is added or removed.
What changes. A listing the venue has halted at the instant evaluated (stale, reason halted, since change set 26) is left out of
every quantifier and aggregate over venues. Until now it made P(v) unknown, so any(venues where …) was unknown, not false, whenever
every live venue said false, and all(venues where …) was unknown, not true, whenever every live venue said true. Today that is every
BTC-PERP, ETH-PERP, … condition that does not name its venues, because COINBASE-INTX has halted all its perpetuals since 2026-10-01. Those
answers are now false and true; count(venues where status(F) = stale) no longer counts the halted listing. Its row in an instrument’s
venues stays, stale halted, and venues_listed still counts it. A query as of an instant before the halt began answers as before.
What to change on your side. Nothing, unless you relied on an any being unknown while a venue is halted; status(F) = stale on that
venue’s row still says it is.
Funding intervals and units from the registry: funding_8h on DYDX-PERP and KRAKEN-FUTURES, a recorded unit on AVANTIS-PERP and GMX-PERP — takes effect when task 264 is deployed (2026-10-09)
Section titled “Funding intervals and units from the registry: funding_8h on DYDX-PERP and KRAKEN-FUTURES, a recorded unit on AVANTIS-PERP and GMX-PERP — takes effect when task 264 is deployed (2026-10-09)”/v2 and DQL (/v2/screen, /v2/snapshots, /v2/instruments, history, MCP). Two fields are added to GET /v2/instruments
(funding_rate_unit, funding_rate_unit_source); none is removed.
What changes.
- The rule.
funding_8handfunding_short_8hare scaled only by what the registry holds for the listing: the settlement interval on a venue that settles, the period the published rate is per on one that accrues continuously. No registry figure, no eight-hour value (null) and no part in a comparison (max,min,sum,count(venues where funding_8h …)). After this change every perpetual collected has one. - DYDX-PERP and KRAKEN-FUTURES now carry their documented hour in the registry (
funding_interval: "1h",funding_interval_source: "venue_documentation"— as NADO-PERP’s now says too, where it saidvenue), so theirfunding_8his filled; it was null,venue_capability, and the catalogue no longer lists Kraken undernot_published_onforfunding_intervalandfunding_8h. The server settingMarketData__KrakenFundingIntervalFromDocumentationis gone. - AVANTIS-PERP and GMX-PERP (continuous accrual) now have their rate’s unit in the registry:
funding_rate_unit: "per_hour", with its source.funding_intervalstays null (funding_continuous). Theirfunding_8his the rate × 8, the same figure as before: what the platform stores for both is already a rate per hour (GMX’sfundingRateLongis the contracts’ per-second factor × 3600 on every one of 125 pools; Avantis’fundingRateis percent per hour). Until now the × 8 was an assumption made for every continuous venue, whatever its unit; applied to a rate per second it would have been wrong by a factor of 3 600 — GMX BTC-USDC’s per-second factor 5.576e-9 × 8 is 4.46e-8 (0.0000045 %), where the eight-hour rate is 5.576e-9 × 28 800 = 1.606e-4 (0.016 %). A continuous listing without a unit in the registry would now answer null (funding_continuous) instead. - A settled funding rate’s horizon reads the registry’s interval only (dYdX’s pair-level hour is no longer used): without one it would be
horizon_unknown.
What to change on your side. Nothing, if you compare funding_8h: two more venues take part. Read funding_rate_unit before scaling a
continuous venue’s funding yourself.
/v1: a layer the policy does not collect is off, with no age — takes effect when task 239 is deployed (2026-10-08)
Section titled “/v1: a layer the policy does not collect is off, with no age — takes effect when task 239 is deployed (2026-10-08)”/v1/snapshot and /v1/book. No field is removed; ageSeconds of a layer may now be absent.
What changes. A layer that a listing’s group does not collect — on production the book’s bands of every listing outside the core
and the top 100 — read status: "stale" (state_lag, connection_lost) with an ageSeconds of about seven days: Redis still held the
value from before the policy switched the layer off, and the snapshot aged it. It now reads status: "off", reason: "policy_off", as
DQL and /v2 say it, with no ageSeconds, receivedAgeSeconds or staleAfterSeconds; nothing of the row is read from the leftover
value. /v1/book of such a listing answers 404, as for a listing that is not collected.
What to change on your side. Read status before ageSeconds; treat off as “not collected for this listing”, not as a fault.
A settled funding rate is current for its interval and five minutes; ask status(funding) = present — takes effect when task 238 is deployed (2026-10-08)
Section titled “A settled funding rate is current for its interval and five minutes; ask status(funding) = present — takes effect when task 238 is deployed (2026-10-08)”/v1 and /v2. No field moves.
What changes. A funding rate the venue settles (dYdX, Coinbase INTX: venue_ts is the settlement it belongs to, anchor: "venue")
was current until venue_ts + the interval and stale for the minute or two every period that the next settlement took to reach us. It is
now current until venue_ts + the listing’s own funding interval (the registry’s, per listing — 1 h, 4 h, 8 h; the pair’s fact where the
registry holds none) + 5 minutes, and its valid_until and /v1/snapshot’s staleAfterSeconds say so. Its age is unchanged: time since
the venue dated it, that is since the settlement — so age(funding) < 2m is false on such a venue for most of every period, and a query
that meant “a current funding rate” by it leaves those venues out.
What to change on your side. Ask status(funding) = present for “current”, for example
count(venues where funding_8h > 1bps and status(funding) = present) >= 3. Keep age(F) for “how long since the venue dated it”.
An eighth stale reason, no_update — takes effect when task 237 is deployed (2026-10-08)
Section titled “An eighth stale reason, no_update — takes effect when task 237 is deployed (2026-10-08)”/v1 and /v2. A reason is added to the closed set a client may read in reason (NullReason in the schemas); no field moves.
What changes. A value the venue pushes only when it changes (horizon_source: "connection") used to stay present for as long as
its connection lived, however long its timestamp stood still. Each such pair now has a no-update threshold, measured on production and
listed on the Freshness page: past it, with the connection alive, the value is stale with reason no_update, and its valid_until —
null until now while the connection lived — is the instant the threshold runs out. A socket on which nothing at all arrives for longer
than its venue guarantees it speaks is now treated as a lost connection: the values on it are connection_lost, as after any
disconnection. /v1/snapshot writes the reason in each layer’s reason; /v1/status counts it under the cause us.
What to change on your side. Treat an unknown reason as “not current, see status” — which a stale value already is. Do not read
a non-null valid_until as “this value is not on its connection”: horizon_source says that.
A seventh stale reason, halted — takes effect when task 233 is deployed (2026-10-08)
Section titled “A seventh stale reason, halted — takes effect when task 233 is deployed (2026-10-08)”/v1 and /v2. A reason is added to the closed set a client may read in reason (NullReason in the schemas); no field moves.
What changes. Every layer with a value of a listing the venue said it does not trade, at the instant asked about, is stale with
reason halted (it was present, or stale for another reason): COINBASE-INTX’s listings since 2026-10-01 09:00:29 UTC, Nado’s ADA-PERP and
ARB-PERP. Their ticker and book stay not_published. A value read at an instant before the halt began is judged as it was. /v1/status
gives it the cause venue; /v1/snapshot writes it in each layer’s reason.
What to change on your side. Treat an unknown reason as “not current, see status” — which a stale value already is.
One definition of age: age_ms, age(F), freshness and /v1’s ageSeconds count from the venue’s timestamp — takes effect when task 232 is deployed (2026-10-08)
Section titled “One definition of age: age_ms, age(F), freshness and /v1’s ageSeconds count from the venue’s timestamp — takes effect when task 232 is deployed (2026-10-08)”/v1 and /v2. No field is added to /v2 or removed anywhere; numbers change where the venue dates its values.
What changes. A value’s age is T − its anchor, the clock its horizon already counted from (ADR-025): the venue’s own
timestamp (venue_ts) where the pair’s cadence fact says it dates the publication — or, for a settled funding rate, the
settlement — and our receipt (received_at) only where the venue sends no time for the layer or its time dates something
else (anchor: "receipt"; the Freshness section lists those pairs). Until now age_ms, age(F) and the freshness
bound were always T − received_at, so a quote whose venue timestamp had stopped read as young as our last poll:
COINBASE-INTX paused every perpetual on 2026-10-01 09:00:29 UTC, we polled it every two seconds, and its quotes read 0.7 s
old for a week while their status said stale.
age_msof a snapshot value, a book, a stream update, anathistory row and a screen’s value object is that age. Whereanchorisvenueit now equalsvenue_age_ms(tens to hundreds of milliseconds more than before on a healthy feed; the whole standstill on a frozen one); a settled funding rate is as old as its settlement. Whereanchorisreceiptnothing changes.venue_age_msis unchanged; how long ago we received a value isevaluated_at − received_at.age(F)in DQL is the same number, soage(mark) < 10sis false for a mark the venue dated ten minutes ago, however recently we read it.ageof a source (age(close, 1h)) still counts fromreceived_at.freshness <duration>compares the same age: a value isstalewithfreshness_boundwhen its age exceeds the bound./v1/snapshot:ageSecondsof a row and of each of itslayersis that age; each layer addsreceivedAgeSeconds(whatageSecondssaid before),anchor,status(presentorstale),reasonandstaleAfterSeconds— the verdict/v2gives the same value, and how old the age may grow before it is stale./v1/book’sbookAgeSecondsthe same./v1/statuslistsreceivedAgeMsbesideageMs.
What to change on your side. Nothing, if you read age_ms as “how old the value is” and status as “may I use it”. If
you used age_ms to measure our delivery latency, use evaluated_at − received_at.
One vocabulary of layer names, and a plan’s refusal of a venue — takes effect when change set 20 is deployed (the date is written here then)
Section titled “One vocabulary of layer names, and a plan’s refusal of a venue — takes effect when change set 20 is deployed (the date is written here then)”/v2 only. Four things a client may read differently; nothing is removed that a request needs.
GET /v2/instruments: the keys oflayersaretradeandliquidation(they weretradesandliquidations), as everywhere else a layer is named.ticker,mark,funding,oi,book,stats,candlesandbook_bandsare as they were.GET /v2/anchors/proofandGET /v2/anchors/{day}print the layers asoi,bookandcandles(they printedopen-interest,depth-derivedandcandle) inlayerand inpartitions[].layer. A request may still write either spelling in?layer=.tableand the canonical text of the row, which are hashed, are unchanged: a proof made before still verifies.- A venue that exists and is outside the key’s plan is
PLAN_VENUE_NOT_INCLUDED, 403 (it wasDQL_UNKNOWN_VENUE, 400, listing only the venues of the plan as “known”).DQL_UNKNOWN_VENUEis for a code no venue has. POST /v2/snapshots:missing[].reasoncan benot_in_plan(it saidnot_listedfor a pair that exists and that the plan does not include).
What to change on your side. Read trade and liquidation in instruments; read the anchor’s layers as oi, book, candles (or keep sending the old spellings); branch on PLAN_VENUE_NOT_INCLUDED as a plan’s limit, not as a typo; treat an unknown missing[].reason as “nothing there”.
A thousand-fold listing moves to the bare market: 1000PEPE-PERP and kPEPE-PERP become PEPE-PERP — takes effect when the Writer applies migration 0067 on 2026-10-01 at 20:55:57 UTC (sunset of the old names 2026-12-30 20:55:57 UTC)
Section titled “A thousand-fold listing moves to the bare market: 1000PEPE-PERP and kPEPE-PERP become PEPE-PERP — takes effect when the Writer applies migration 0067 on 2026-10-01 at 20:55:57 UTC (sunset of the old names 2026-12-30 20:55:57 UTC)”/v1 and /v2. Eight listings; two market names; one asset code each.
What changes. A venue that quotes a token in thousands names it with a prefix (1000PEPEUSDT, kPEPE-PERP_USDT0).
The platform used to treat that as another token and keep a market of its own for it. A prefix is a property of the
listing, not of the token, so the listing now stands on the bare market, with the factor it stands for as its
multiplier:
| Market until now | Market from now | Listings that move (venue, symbol) |
|---|---|---|
1000PEPE-PERP (asset 1000PEPE) |
PEPE-PERP (asset PEPE) |
Aster 1000PEPEUSDT; Binance 1000PEPEUSDC, 1000PEPEUSDT; Bybit 1000PEPEPERP, 1000PEPEUSDT; Synthetix 1000PEPE-USDT; Weex cmt_1000pepeusdt |
kPEPE-PERP (asset kPEPE) |
PEPE-PERP (asset PEPE) |
Nado kPEPE-PERP_USDT0 |
This is the list as /v1/markets held it on 2026-10-01; the resolver recomputes it at rollout and moves a listing only
where the bare market has no other listing on the same venue (a venue that has both is left as it is). No other market
changes its name or loses a listing; PEPE-PERP gains these, and every other listing is byte-identical in /v1 and /v2.
instrument(/v2rows and/v1/markets/{code}) andbasechange for these listings:PEPE-PERP,PEPE.multiplierof these listings was what the venue publishes,1, next to a base that already says “a thousand”. It is now the factor the listing stands for: the venue’s multiplier times the prefix,1000. A value read withunits: basewas a thousand times too large; from now on it is right. A client that corrected for it by hand now corrects twice.base_venue_aliasinGET /v2/instrumentskeeps the base as the venue publishes it (1000PEPE,kPEPE).
How the old name is answered, for 90 days from the rename.
GET /v1/markets/1000PEPE-PERPanswers308 Permanent Redirectto/v1/markets/PEPE-PERP(the query string is kept) withDeprecation: @<unix seconds>(RFC 9745, the instant of the rename),Sunset: <HTTP-date>(RFC 8594, the rename plus 90 days) andLink: </v1/markets/PEPE-PERP>; rel="successor-version". After the Sunset date the old name is404, like any name the platform does not know./v1/markets/kPEPE-PERPthe same.GET /v2/instruments?renamed_from=1000PEPE-PERPreturns the listings that carried that name, and every listing ofGET /v2/instrumentsinside its 90 days carriesrenamed_from:{"instrument": "1000PEPE-PERP", "at": "<the instant of the rename>"}. The field is absent on every other listing and after the 90 days.- DQL has no alias. A code starts with a letter, so
1000PEPE-PERPwas never writable inwhere; nobody wrote it there, and the other/v2endpoints answer to the new name only. A bot that holds the old name holds what it read out of an answer.
What to change on your side.
- Read
PEPE-PERPwhere you read1000PEPE-PERPorkPEPE-PERP, andbasePEPEwhere you read1000PEPEorkPEPE. - Read
multiplierand stop applying the thousand yourself. - Follow the
308once and store the new name; logDeprecationandSunsetwhen you see them. To find a listing you only know by the old name, askGET /v2/instruments?renamed_from=<old name>within the 90 days.
A value past its validity horizon leaves the value field — takes effect 2026-09-29 (deployed at 11:50 UTC)
Section titled “A value past its validity horizon leaves the value field — takes effect 2026-09-29 (deployed at 11:50 UTC)”/v2 only. /v1 does not change.
What changes. Today a stale value is returned where a present one would be, with a larger age_ms: a client
that forgets to look at status reads a number that was no longer current. From the date above such a value is
not in the value field at all.
-
Every value of a snapshot layer states how long it is good for.
valid_until(RFC 3339, UTC) is the instant after which the platform no longer treats it as current. It is null while a push feed’s connection is alive (see below), and null when no horizon is known.horizon_sourcesays where the horizon comes from:venue_declared— the venue said when the next value comes;measured— a cadence the platform measured;documented— the venue’s documentation;assumed— a cadence the platform assumes;connection— the value holds while the connection that delivered it is alive.
anchorisvenuewhen the horizon counts from the venue’s own timestamp, orreceiptwhen it counts fromreceived_at. There are two ages:age_msby our clock, fromreceived_at, andvenue_age_msby the venue’s clock, fromvenue_ts.venue_age_msis null when the venue sends no time. -
Past
valid_untila layer isstaleand carries a reason. Its values,rawand band figures are absent. They are returned under a separate key,withheld, only when the request setsallow_stale: true; the default isfalse. The reasons:horizon_passed— no new value within the horizon;connection_lost— the connection that delivered the value was lost, or its collector restarted;state_lag— our own live projection fell behind the bus; the venue and the connection were fine;horizon_unknown— no cadence fact covers the value, so nothing proves it was current;freshness_bound— the request’s ownfreshnessbound is tighter than the horizon.
-
The five states stay distinct everywhere:
present,stale,missing,not_published,off. None of them becomes another, a zero, or a carried-forward number.
Where. POST /v2/snapshots (layer values, reasons, raw, liquidity bands); POST /v2/snapshots/history
in at mode; GET /v2/stream (snapshot and update events; allow_stale=true on the subscription);
POST /v2/screen (a venue value that is stale has value: null, and withheld holds it only with allow_stale);
GET /v2/books (bids, asks, mid, spread of a stale book; ?allow_stale=true).
The bound is per venue and dataset, not per layer. It used to be one bound per layer for the whole platform: 30 s for ticker, mark, oi and book, 120 s for stats, 300 s for funding. Now each (venue, dataset) pair has its own horizon, derived from a published fact about its cadence. A polled value is good for twice the longer of its polling interval and the history window it is kept at. Consequences, measured on production over 24 hours:
- Stale less often. Open interest on Binance, Aster and Weex (polled every 300 s) and books on Bitget and Gate (300 s) were stale about 90 % of the time; MEXC books (600 s) 95 %. They become present. dYdX open interest went from 89 % stale to about 6 %.
- Stale more often, briefly. Kraken ticker, mark, open interest and stats are stale about 0.07 % of the time, in intervals of at most about two seconds after a reconnect (Kraken funding, sent in the same frame, likewise in the live answer). Hyperliquid mark, open interest and stats, and Gate’s ticker, about 0.02 %. Fast feeds kept at one-second history (Kraken’s book, Hyperliquid’s ticker and book) are stale up to 0.3 % of the time, where the old 30 s bound hid it.
Push feeds that send only on change. Kraken ticker, mark, open interest, stats and funding; Hyperliquid mark, open interest and stats; the Binance, Aster and Gate tickers; dYdX open interest. A quiet socket there means “nothing changed”, not “no data”. Their horizon is the life of the connection: the last value holds while the collector’s connection is up, and becomes stale from the moment a disconnection, a collector restart, a bus outage or our own projection lag is recorded (and only while that lag lasts: from 2026-10-01 a lag that has closed ends nothing, State having caught up, unless the connection broke inside its window — that break ends the value). The platform never fakes a heartbeat to keep such a value fresh.
Series over those feeds carry the value forward, and say so. A sampled series (for example last at 1m)
over a push feed used to show a gap for every period in which the value did not change. Now such a period takes
the last value, if the connection was alive through the whole period, and marks it as carried:
carried: trueon the history row (/v2/snapshots/history,intervalmode);carried: trueon a screenSeriesValue;- in DQL,
carried(S, n): the number of thenperiods ending atS[0]whose value was carried rather than observed.carried(last, 1) = 0is true only whenS[0]was observed in its own period.
Candles are never carried.
DQL.
freshness <d>can only tighten. It used to replace every layer’s bound, sofreshness 10mloosened them. Now each pair’s bound ismin(horizon, d).- A stale operand still compares as unknown, and
status(F) = staleis still never unknown. countstill does not count stale venues.
Response fields that change meaning. freshness in the snapshots, snapshots/history and screen
responses reports the request’s own bound per layer, or null where none was given (each pair’s horizon applies).
freshness_defaults in GET /v2/dql/catalogue is kept for compatibility and is no longer applied.
What is added and does not break. GET /v2/instruments states, per layer, the cadence fact behind the horizon:
mode, interval, label (documented, measured, assumed) and what the venue’s timestamp dates
(publication, settlement, none). The Freshness section of the API reference lists which pairs rest on an
assumed cadence; it is a published fact, not a hidden setting.
What to change on your side.
- If you read a value without checking
status, you getnullor no key instead of a stale number. That is the point of the change. - If you log what your strategy read, log
status,reasonandvalid_untilbeside the value. - For post-mortems, ask with
allow_stale: trueand readwithheld. Never feedwithheldinto a live decision. - If you used
freshnessto loosen a bound, remove it: the per-pair horizon already covers slow polls.
DQL v1 — Change Set 01
Section titled “DQL v1 — Change Set 01”-
- Verdict on the review
-
- C1-01 — Sampled snapshot series
- Motivation
- Amendments
-
- C1-02 — Functions:
abs,rolling_sum,percentile,percentile_rank
- C1-02 — Functions:
-
- C1-03 — Fields:
bid_size,ask_size,tick_size,lot_size,min_qty
- C1-03 — Fields:
-
- C1-04 — Book depth within a price band (replaces the top-25 sums)
-
- C1-05 —
selectclause (projection)
- C1-05 —
-
- C1-06 — Multi-key
order by
- C1-06 — Multi-key
-
- C1-07 — Numeric precision in responses
-
- C1-08 — Small corrections to draft 1
-
- Conformance additions
- 9.1 Fixture additions (§16.1)
- 9.2 Queries and expected results (§16.2, continued)
-
- Guide additions (new cookbook entries)
- 4.16 Mark price up more than half a percent in five minutes, on three venues or more
- 4.17 Spread below its own 24-hour 90th percentile
- 4.18 At least 50 000 quote units resting within 10 bps of mid on the bid side, on any venue
- 4.19 Ask for values you are not filtering on, and order by two keys
- Guide §6 (missing data) — add one line to the table
- Guide §7 (Pine) — add
- Guide §8 (SQL) — add
-
- Rejected and deferred, with reasons
-
- Notes for the data endpoints (outside DQL)
-
- Open questions added to §17
DQL and Market-Data API — Change Set 02 (bot migration readiness)
Section titled “DQL and Market-Data API — Change Set 02 (bot migration readiness)”-
- Verdict on the second review
-
- C2-01 — Timeframes
2m,3m,12h
- C2-01 — Timeframes
-
- C2-02 — Mark-price and index-price candle sources
-
- C2-03 —
percentilemethod argument
- C2-03 —
-
- C2-04 — Instruments contract:
GET /v2/instruments
- C2-04 — Instruments contract:
-
- C2-05 — Latest snapshots contract:
POST /v2/snapshots
- C2-05 — Latest snapshots contract:
-
- C2-06 — Snapshot history contract:
POST /v2/snapshots/history
- C2-06 — Snapshot history contract:
-
- C2-07 — Candles contract:
POST /v2/candles
- C2-07 — Candles contract:
-
- C2-08 — Order books contract:
POST /v2/books
- C2-08 — Order books contract:
-
- C2-09 — Batch limits, numbers, consistency
-
- Migration protocol: source change ≠ indicator change
-
- Rejected and still deferred
-
- Open questions added
-
- Conformance additions (DQL only; endpoint contracts get contract tests, not corpus entries)
DQL and Market-Data API — Change Set 03
Section titled “DQL and Market-Data API — Change Set 03”-
- Verdict on the third review
-
- C3-01 — Liquidity band metrics (supersedes C1-04 and change-set-02 §8 for liquidity)
- 1.1 Definition (ingestion time)
- 1.2 Exposure in the data endpoints
- 1.3 DQL (replaces the C1-04 rules)
- 1.4 Cadence disclosure
-
- C3-02 — Candle volume “as published”;
mark_volume/index_volumesources
- C3-02 — Candle volume “as published”;
-
- C3-03 — Corrections to change-set-02 text
- 3.1 Hydration (replaces the last paragraph of change set 02 §6)
- 3.2 Cycle alignment (replaces the
2mrow of change set 02 §0 and the sentence in §1) - 3.3 Settled replay (replaces the
settledrule of change set 02 §6) -
- C3-04 —
include_rawonGET /v2/instruments; rules normalisation
- C3-04 —
-
- C3-05 — Migration protocol additions (change set 02 §10)
-
- Open questions added
-
- Conformance updates
DQL and Market-Data API — Change Set 04 (platform decisions)
Section titled “DQL and Market-Data API — Change Set 04 (platform decisions)”-
- C4-01 — An instrument is a market;
instrumentandbasefields
- C4-01 — An instrument is a market;
-
- C4-02 —
where: selection is part of the language
- C4-02 —
- 2.1 Grammar
- 2.2 Semantics
- 2.3 Every endpoint selects with
where -
- C4-03 — Venue codes come from the registry:
GET /v2/venues
- C4-03 — Venue codes come from the registry:
-
- C4-04 — Units are requested, and every value states its own
-
- C4-05 — Alerts leave the API
-
- C4-06 — The platform as it is (facts replacing assumptions)
-
- C4-07 — Endpoint set of v2
- 7a. C4-08 —
GET /v2/stream(decision 2026-09-22) -
- Open questions — status
-
- Conformance additions
The full text of each set is in the repository under docs/engineering/api/history/. The specification
has absorbed all of them: where the two disagree, the specification is right and the change set is
history.