{"openapi":"3.1.0","info":{"title":"opreturn.art","version":"0.1.0","description":"A public, read-only archive of Counterparty (XCP) chain events on Bitcoin L1.\n\n**What makes it worth reading is not that it has the data** — a public Counterparty node has the data too — **but that it states what it does NOT have**, per event type, in a machine-readable form you can check without trusting it. `[]` means we looked and there were none; `null` means we could not ask. They are never interchangeable.\n\n### Prices\n`GET /api/pricing` is the table of contents: every route, what question it answers, what it costs, what is free, what is not for sale and why. Each operation below also carries an `x-price` extension with the same facts, generated from the same single price catalog — the two cannot disagree.\n\n**Nothing is being charged today.** No payment is required, no 402 is emitted and no account exists. The prices are a published list, not a bill. `/api/pricing` reports the live enforcement state per request; this document deliberately does not, because it is cached and would go stale.\n\n### What this map does not yet tell you\n4 of 44 operations publish a 200 response with an EMPTY schema — i.e. \"some JSON\", with no declared shape. Treat those response bodies as undocumented and read the route description. This is a known gap, counted here rather than glossed over.","x-pricing":{"menu":"/api/pricing","source_of_truth":"the price catalog /api/pricing is generated from","live_enforcement_state":"/api/pricing — not this document, which is cached per process"}},"paths":{"/api/health":{"get":{"summary":"Health","description":"Liveness of the ARCHIVE, not of this process. A 200 from a web server that\nis serving data 400 blocks stale would be a lie by omission.","operationId":"health_api_health_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HealthEnvelope"}}}},"503":{"description":"⛔ THREE DIFFERENT BODIES SHARE THIS CODE ON THIS ROUTE. `ok: false` with the FULL health envelope means the ARCHIVE is unhealthy and this service is fine — read `ok_reasons` for which of the conditions fired. `{\"detail\": …}` means the DATABASE could not be read at all, so no envelope exists; `health()` calls the query helper before it builds the body. The `{\"error\", \"detail\", \"source\"}` branch never reached this handler at all: it is the PAYMENT MIDDLEWARE refusing before it could identify the resource, which is why a liveness probe can get one from an otherwise healthy service. ⚠️ Branch on the presence of `ok`, never on the status code. ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/HealthEnvelope"},{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/stats":{"get":{"summary":"Stats","description":"Archive size and range. The event count is EXACT.\n\nHistory of this endpoint, because the reasoning matters:\n  * `count(*)` over the events hypertable measured **1,552 ms** — far too\n    slow for a public endpoint, and a rate limiter cannot save you from a\n    query that is simply expensive.\n  * `approximate_row_count` was 40× faster, so it was used and labelled\n    `events_approx`. Then compression invalidated the planner statistics it\n    reads and it began reporting **2.09M against a true ~3.0M — 30% wrong**.\n    An honestly-labelled number that is 30% wrong is still a bad number.\n  * The backfill is now COMPLETE, so the count only changes when a block is\n    parsed. Tip-keyed caching therefore makes the EXACT count cheap: one\n    scan per block, not one per request.\n    ⛔ THIS DOCSTRING IS PUBLISHED, WHICH IS WHY A PARENTHETICAL WAS REMOVED\n    FROM THE LINE ABOVE ON 2026-09-09. FastAPI copies it verbatim into\n    /api/openapi.json at `.paths./api/stats.get.description`, and the site\n    links that document as \"check us, don't trust us\" — so the duration\n    gloss that used to sit beside \"per block\" was a false claim on a PUBLIC\n    surface, not a private note. goat's rule is absolute and it is not a\n    style preference: no arithmetic converts a block count into a duration.\n    A route docstring is a public string; write it like one.\n\nExactness beat speed here because it could be had for free. If the remaining\n43 event types are ever backfilled, this count will lag by up to one block\nwhile that runs — the cache key is the chain tip, not the row count.","operationId":"stats_api_stats_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/StatsEnvelope"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/events/counts":{"get":{"summary":"Event Counts","description":"What the archive actually holds, per type. This is our coverage, and it is\ndeliberately NOT the chain's counts — the difference is the backfill gap.\n\n⚠️ NOT tip-cached: unlike launches, these counts move while the BACKFILL runs,\nwhich is independent of the chain tip. Cached briefly on wall-clock instead —\nmeasured at 1.58s, and it is a full GROUP BY over every row.","operationId":"event_counts_api_events_counts_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max event types in one page","default":1000,"title":"Limit"},"description":"max event types in one page"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"event types to skip — the page cursor","default":0,"title":"Offset"},"description":"event types to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventCountsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/events":{"get":{"summary":"Events","description":"Raw archived events, newest first, exactly as the node emitted them.\n\n⛔⛔ CREDIT + DEBIT ARE NOT A PAIRED LEDGER, AND A BUYER WHO ASSUMES THEY ARE\nWILL COMPUTE WRONG BALANCES. Counterparty is an ESCROW model, not double-entry:\nthe DEBIT fires when funds are LOCKED and the CREDIT when they are RELEASED,\nand the two legs are routinely in different blocks, in different quantities, or\none of them is simply not there. Measured over the whole in-window population\non 2026-08-22, not sampled and not theorised:\n  · `dispense` CREDITs have NO matching DEBIT at all — the asset left the\n    dispenser's balance back at `open dispenser`, which for most dispensers is\n    a block far below this archive's floor;\n  · `pool match` CREDITs have NO matching DEBIT — the asset was debited INTO\n    the pool when the pool was opened;\n  · `escrowed fairmint` and `mpma send` carry MORE credits than debits by\n    construction — one MPMA transaction debits the sender once per asset and\n    credits every recipient separately.\n⇒ `Σ CREDIT − Σ DEBIT` IS NOT A BALANCE. It is not off by a rounding error; it\nis a different quantity that happens to have the same units. If you need\nbalances, ask a Counterparty node for balances — do not derive them from here.\n\n⛔ AND THERE IS NO OPENING BALANCE, which is the second half of the same trap.\nThis archive's contiguous floor is block 952,800, so every position an address\nalready held when that block opened is INVISIBLE here — the credits that\ncreated it are below the floor. Measured 2026-08-22 over the AMM-participant\npopulation: 148 of 1,030 (address, asset) positions computed from in-window\nrows alone go NEGATIVE, which is impossible on chain and is therefore proof of\nthe missing opening balance rather than an error in the data. Treat that\nproportion as a dated observation that moves with the population, not a\nconstant to hard-code.\n⇒ THE DISTINCTION THAT MATTERS, stated plainly: for an address that touched\nAMM we can reconstruct every MOVEMENT since block 952,800 completely, and we\nCANNOT reconstruct its POSITION. Movements yes, balances no.\n\n★ NONE OF THAT IS A HOLE IN THE EVENT RECORD, and the two claims must not be\nconfused. For blocks at and above 952,800 this archive holds the node's global\n`event_index` with zero gaps and both edges anchored against the node — a total\nenumeration, so there is no event of any type, known or unknown, that the node\nemitted in that window and we do not hold. What is missing is not rows; it is\nthe pre-floor history those rows would need in order to be read as a ledger.\n`/api/coverage` publishes the held-versus-on-chain figure for every type.","operationId":"events_api_events_get","parameters":[{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"event type, e.g. NEW_FAIRMINT","title":"Event"},"description":"event type, e.g. NEW_FAIRMINT"},{"name":"block","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":2147483647,"minimum":0},{"type":"null"}],"description":"exact block height","title":"Block"},"description":"exact block height"},{"name":"tx_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":100,"title":"Limit"}},{"name":"before_index","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","minimum":0},{"type":"null"}],"description":"cursor: event_index to page back from","title":"Before Index"},"description":"cursor: event_index to page back from"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/EventsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}},"402":{"description":"⭐ PAYMENT REQUIRED, AND THE OFFER IS THE DISCOVERY. This is not an error: the body and the `PAYMENT-REQUIRED` header both carry a complete, signable x402 v2 offer, so a machine can pay and retry without asking a human anything. ⚠️ IT IS A PROPERTY OF THE REQUEST, NOT OF THE ROUTE. Current state at display size is FREE on every route here — that is what the website itself asks for — and only a HISTORY request is charged. The same URL answers 200 and 402 depending on what you asked it for. ⚠️ AND IT IS REACHABLE ONLY WHILE A RAIL IS USABLE. With none configured a priced request is REFUSED 503 rather than offered — see this operation's 503, `reason: no_rail_configured`. A consumer that has never seen a 402 has not proved it cannot arrive.","headers":{"PAYMENT-REQUIRED":{"description":"The same offer, base64-encoded. ⛔ THIS IS THE NORMATIVE COPY — x402 v2 puts the offer in the header and calls the body a server implementation concern. Both are rendered from one method so they cannot disagree.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentOffer"}}}}},"x-price":{"access":"priced","sellable":true,"currency":"USD","unit":"request","metered":false,"price":{"micro_usd":10000,"display":"$0.01","per":"1 request","means":"the total charge for one call to this route"},"free_rows":200,"ceiling":"bounded: whatever the parameters, one response is at most 8,000,000 bytes.","question":"The raw event tape: give me archived chain events, filtered and paged."}}},"/api/launches":{"get":{"summary":"Launches","description":"XCP-69 launch state, derived entirely from events WE archived.\n\nValidated 2026-08-15 against the live API: identical totals (359\nfairminters, 43 conformant, 24 open / 19 pending, LAUNCHCOIN nonconformant).\n\nPercentages and elapsed are computed against the LIVE tip, never the block\nthis row was written at — a countdown frozen at build time was one of this\nproject's recurring bugs.","operationId":"launches_api_launches_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max launches per bucket (open/pending/closed/nonconformant/unknown_conformance), not per response","default":1000,"title":"Limit"},"description":"max launches per bucket (open/pending/closed/nonconformant/unknown_conformance), not per response"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"launches to skip WITHIN EACH bucket — the page cursor","default":0,"title":"Offset"},"description":"launches to skip WITHIN EACH bucket — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaunchesEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/mints":{"get":{"summary":"Mints","description":"EVERY VALID COUNTERPARTY FAIRMINT, newest first, straight from our own\narchive. ⛔ This route is NOT restricted to the XCP-69 launch set.\n\n⛔⛔ THE SENTENCE THAT USED TO BE HERE, AND WHY IT WAS THE DEFECT AND NOT THE\n  QUERY. This docstring named the XCP-69 launch set as the route's population,\n  and FastAPI copies this text into `/api/openapi.json` as the path's\n  `description` — the machine-readable wire an agent acts on without ever\n  reading a page.\n  ⚠️ THE OLD CLAIM IS DESCRIBED HERE AND NEVER QUOTED, DELIBERATELY. A false\n  population claim reproduced verbatim inside its own correction is still that\n  claim sitting on the wire, and the reader that needed correcting is a\n  MACHINE — it does not weigh the negation around a string. MEASURED live\n  2026-08-25:\n\n      /api/mints?limit=1  ->  total 191,244   minters 3,614\n      the real XCP-69 mint count, same archive, same minute:\n          page (baked, build-time)    totals.mints = 919\n          /api/launches, sum over 109 launches  = 945\n\n  ⇒ the sentence over-claimed the POPULATION by 202x, on a site whose entire\n    subject is XCP-69. `derive.sql`'s `v_fairmint` is `NEW_FAIRMINT` where\n    `status='valid'` with NO join to the launch set, and it never claimed one.\n\n⇒ ★ THE QUERY IS RIGHT AND THE SENTENCE WAS WRONG, and that is a decision, not\n  a preference. Narrowing the query to XCP-69 would (a) delete ~190,000 rows of\n  count-verified archive (2026-08-25) from a machine-readable surface, (b) break every\n  existing consumer of this route silently — the rows would simply stop\n  arriving — and (c) destroy the ONE claim this route rests on, because\n  `total` is count-verified against the chain for ALL of `NEW_FAIRMINT` and is\n  verifiable against nothing once filtered. And the XCP-69 population is NOT\n  lost by leaving this route broad: it is already published, per launch and in\n  total, by `/api/launches`, whose subject IS the launch set.\n  ⇒ SO THE OTHER HALF IS MADE UNREADABLE-WRONG INSTEAD: this envelope now\n    publishes `population`, `xcp69_only: false` and `population_note` beside\n    the counts, so a consumer that never reads this paragraph still cannot\n    mistake what it is holding. A sentence can rot; a field is data.\n\n`NEW_FAIRMINT` is 100% backfilled and count-verified against the chain\n(our row count equals the chain's, with no shortfall), so this is complete\nhistory, not a window.\n\n⚠️ Only `status='valid'` mints are counted — `v_fairmint` filters them.\nThe `invalid:` attempts on chain moved no XCP; counting them would overstate\nboth the funded totals and the distinct-minter count.\n\n⛔ `minters` HERE IS NOT `minters` ON `/api/launches`, AND NOT THE PAGE'S\n  EITHER. One word, three populations, MEASURED on the same minute:\n      this route      3,614  DISTINCT minters over ALL fairmints\n      /api/launches     819  the SUM of per-launch counts (an address that\n                             minted two launches is counted twice), XCP-69\n      the page          228  DISTINCT addresses, XCP-69\n  None of the three is wrong; the NAME is. `minters_basis` on this envelope\n  and the field descriptions on `LaunchTotals.minters` now say which is which.\n\nTip-keyed cached: mints only arrive in blocks, so between blocks the answer\ncannot change.","operationId":"mints_api_mints_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/resolutions":{"get":{"summary":"Resolutions","description":"Launches that have RESOLVED — graduated or refunded.\n\n⚠️ `closed` is the only resolved state. Fairminter statuses are\nopen / pending / closed / `invalid: <reason>` — four buckets, not two — so\nthis counts positively rather than treating \"not open and not pending\" as\nresolved, which would promote an invalid record to a resolution.\n\n⚠️ THE SQL BELOW USED TO CONTRADICT THIS DOCSTRING. It filtered\n`status NOT IN ('open','pending')` — the exact negative form the paragraph\nabove disclaims — so an `invalid:` fairminter that happened to be conformant\nwould have been published as a RESOLUTION. That is the LAUNCHCOIN error in\nreverse: not suppressing a real event, but INVENTING one that never happened,\nwhich §5B names as the same failure as every fail-open guard this project\nhunts. Found 2026-08-16 by the weirdness-register compilation, which read the\ncomment and the code together.\n\nMEASURED at the time of the fix: impact today is ZERO — both forms return 0,\nbecause none of the archive's 17 `invalid:` fairminters is conformant. It was\nlatent, not live. But 153 launches are `closed` and 22 are `pending`, so the\nday a conformant XCP-69 launch goes invalid is the day the old form starts\npublishing a false resolution, silently.","operationId":"resolutions_api_resolutions_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max resolutions in one page","default":1000,"title":"Limit"},"description":"max resolutions in one page"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"resolutions to skip — the page cursor","default":0,"title":"Offset"},"description":"resolutions to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ResolutionsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/blocks/{height}/events":{"get":{"summary":"Block Events","description":"Every archived event for one block, plus WHAT THIS ARCHIVE CLAIMS THERE.\n\n`complete` is the honesty flag: false means the backfill supplied a subset of\nevent types and the poller has not archived the whole block, so an absent\nevent type here does NOT mean it is absent from the chain.\n\n⛔ THE DEFECT THIS ROUTE SHIPPED WITH, MEASURED 2026-08-21 (audit F-3): three\ndifferent answers, one indistinguishable response:\n\n    /api/blocks/280000/events   200 {\"block\":null,\"complete\":false,\"result\":[]}\n    /api/blocks/9999999/events  200 {\"block\":null,\"complete\":false,\"result\":[]}\n\n280,000 is a real block BELOW this archive's reach; 9,999,999 does not exist.\nBoth read as \"there were no events here\" — a POSITIVE CLAIM ABOUT THE CHAIN\nthat nothing measured. `92-HANDOFF.md` §6.11 and `36-WEIRDNESS-REGISTER.md`\n§3C forbid exactly that, and every sibling already obeys: /api/verify returns\na `note`, /api/markets/volume an `absence_means`, /api/coverage/block/{h} a\nthree-state `coverage` + `reason` that answers BOTH heights correctly. ★ THE\nMACHINERY WAS ONE ROUTE OVER AND THIS ROUTE DID NOT CALL IT. It does now —\n`block_coverage()`, off the SAME cache slot, so the honesty is not a second\n82-108 ms query per request.\n\n⚠️ `block: null` CAN LEGITIMATELY SIT BESIDE A NON-EMPTY `result`, and that is\nthe second half of the finding. Measured 2026-08-21: /api/blocks/280312/events\nserves a real archived OPEN_ORDER while `block` is null, because\n`ingest/backfill.py:10-22` makes it an INVARIANT that the backfill \"writes\nEVENTS ONLY … must never insert a `blocks` row\". A consumer reading\n`j.block.block_index` gets a null dereference on a block we demonstrably\nhold data for. Nothing is invented to\npaper over it — `block_absent_reason` states why the row is missing instead.\n\n⚠️ THIS ROUTE ANSWERS \"events IN block N\", NOT \"state AS OF block N\". B4.13's\ntime-travel surface does not exist; do not read this as it.","operationId":"block_events_api_blocks__height__events_get","parameters":[{"name":"height","in":"path","required":true,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"block height; any height, including ones above this archive's reach","title":"Height"},"description":"block height; any height, including ones above this archive's reach"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max events in one page; the response also stops at the byte budget, whichever binds first","default":1000,"title":"Limit"},"description":"max events in one page; the response also stops at the byte budget, whichever binds first"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"events to skip, in event_index order — the page cursor. Named `offset` because server/pay/catalog.py's HISTORY_PARAMS is a closed set and a differently-named cursor tests FREE","default":0,"title":"Offset"},"description":"events to skip, in event_index order — the page cursor. Named `offset` because server/pay/catalog.py's HISTORY_PARAMS is a closed set and a differently-named cursor tests FREE"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockEventsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}},"402":{"description":"⭐ PAYMENT REQUIRED, AND THE OFFER IS THE DISCOVERY. This is not an error: the body and the `PAYMENT-REQUIRED` header both carry a complete, signable x402 v2 offer, so a machine can pay and retry without asking a human anything. ⚠️ IT IS A PROPERTY OF THE REQUEST, NOT OF THE ROUTE. Current state at display size is FREE on every route here — that is what the website itself asks for — and only a HISTORY request is charged. The same URL answers 200 and 402 depending on what you asked it for. ⚠️ AND IT IS REACHABLE ONLY WHILE A RAIL IS USABLE. With none configured a priced request is REFUSED 503 rather than offered — see this operation's 503, `reason: no_rail_configured`. A consumer that has never seen a 402 has not proved it cannot arrive.","headers":{"PAYMENT-REQUIRED":{"description":"The same offer, base64-encoded. ⛔ THIS IS THE NORMATIVE COPY — x402 v2 puts the offer in the header and calls the body a server implementation concern. Both are rendered from one method so they cannot disagree.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentOffer"}}}}},"x-price":{"access":"priced","sellable":true,"currency":"USD","unit":"request","metered":false,"price":{"micro_usd":10000,"display":"$0.01","per":"1 request","means":"the total charge for one call to this route"},"free_rows":200,"ceiling":"bounded: whatever the parameters, one response is at most 8,000,000 bytes.","question":"Everything archived at one block height."}}},"/api/pools":{"get":{"summary":"Pools","description":"Every AMM pool, with its latest reserves — from our own archive.\n\nThe headline feature: nobody else publishes this. We hold every\npool-lifecycle event from the AMM activation block 952,800 up to the chain\ntip, so this series is COMPLETE RATHER THAN SAMPLED — not a recent window,\nnot a sample, the whole tape from the venue's first block.\n\n⚠️ \"AMM HISTORY\" HERE MEANS THE POOL LIFECYCLE AND ONLY THAT: OPEN_POOL (a\npool is created, carrying its reserves at creation) UNION POOL_UPDATE (every\nlater reserve change). That union is `v_pool_reserve` in ingest/derive.sql,\nwhich is what this route reads, and it is the definition the completeness\nsentence above is scoped to. The AMM's other tapes are separate populations\nwith separate answers and separate routes: POOL_MATCH is /api/pool/swaps,\nNEW_POOL_DEPOSIT is /api/pool/deposits, NEW_POOL_WITHDRAWAL is\n/api/pool/withdrawals. ⛔ THE LAST OF THOSE IS NOT IN backfill.PRIORITY, so\nno historical backfill has ever swept it, and the sentence above must not be\nread as covering it. /api/coverage is the per-type answer and is the only\nplace that states completeness type by type.\n\n⛔ NO EVENT COUNT IS PUBLISHED HERE, AND ITS ABSENCE IS THE FIX — DO NOT\nHELPFULLY RE-ADD ONE. This docstring read \"the ENTIRE AMM history on\nCounterparty is 24 events\": a live, growing count, undated, frozen into the\nshipped OpenAPI on the very sentence the code calls the headline feature. It\nwas wrong under every reading of \"AMM history\". Re-measured against\napi.counterparty.io/v2 on 2026-08-22: OPEN_POOL 11, POOL_UPDATE 206,\nPOOL_MATCH 192, NEW_POOL_DEPOSIT 24, NEW_POOL_WITHDRAWAL 2 — so the pool\nlifecycle this route publishes was 217 and all five tapes together were 435,\nagainst a published 24. ⚠️ THE DECISIVE ARGUMENT IS NOT THAT IT WAS WRONG,\nIT IS HOW FAST IT ROTS. POOL_UPDATE was read three times on 2026-08-22 while\nthis was being fixed, hours apart, and returned 200, then 203, then 206. A\nnumber that moves three times in one day cannot be maintained by editing a\ndocstring; the activation block can, because 952,800 never moves. Keep the\ncheckable half, drop the rotting half.\n\n⚠️ AND §12 OF ops/test/t_metrics_routes.py COULD NEVER HAVE CAUGHT THIS ONE.\nIts NUM_RE matches only comma-grouped numbers or runs of four-plus digits, so\na two-digit count is invisible to the gate while `952,800` in the same\nsentence is checked and exempted by name. Re-adding a small count here would\nbe re-adding precisely the kind of number this project's own staleness oracle\nis structurally blind to, which is why the fix is deletion and not a date\nstamp. If you need the live figure, measure it against the node.\n\n⚠️ THE COMPLETENESS CLAIM IS A CURSOR CLAIM, not a live re-measurement. It\nrests on our own backfill cursor reporting done for the pool types, checked\nagainst what the node reported when that backfill ran — the same provenance\n/api/coverage now spells out. Nothing in this route re-compares it to the\nchain as it stands today; ops/verify-archive.sh is what does that.\n\n⚠️ `changed_at` is NULL until the block→time map is populated. POOL_UPDATE\ncarries only asset_a, asset_b and the reserves — no block_time of its own.\nNULL is the honest answer; `ingested_at` would be when WE fetched it, not\nwhen it happened on chain.","operationId":"pools_api_pools_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max pools in one page","default":1000,"title":"Limit"},"description":"max pools in one page"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"pools to skip — the page cursor","default":0,"title":"Offset"},"description":"pools to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/markets/volume":{"get":{"summary":"Markets Volume","description":"DEX volume per pair over a trailing window — from our own archive.\n\n⚠️ WHAT MAKES THIS DIFFERENT FROM THE BAKED MARKET LIST: the page's ranking is\na BUILD-TIME scan over a fixed ~89-day window. This is a live trailing\nwindow over `v_trade_priced`, which we hold COMPLETE for all history (every\n`ORDER_MATCH` there has ever been). B4.6 / docs/59 §8.8 is the wider job of\nre-sourcing that whole row; this is its volume field.\n\n★ ABSENCE IN THIS RESPONSE MEANS A MEASURED ZERO, NOT AN UNKNOWN — and that\nclaim is only safe because the response is COMPLETE. We hold every\nORDER_MATCH there has ever been, so \"this pair did not trade in the window\"\nis something we measured. `truncated` exists so the claim can be withdrawn if\nthe result ever outgrows the cap; a consumer MUST check it before treating\nabsence as zero. MEASURED 2026-08-19: the whole DEX traded **4 pairs, 5\nfills** in 24h, so the cap is nowhere near binding today.\n\n⚠️ `v_trade_priced` ALREADY EXCLUDES EXPIRED MATCHES and that is load-bearing:\n9.76% of all matches end `expired`, and on BTC/XCP it is 90.3%. Pricing them\npublishes trades that never happened.\n\n⚠️ `vol_xcp` IS NULL FOR A NON-XCP QUOTE, exactly as on the AMM side — only\nXCP and BTC are used as rates, because pricing through a thin market invents\nvolume. A null here means \"cannot be expressed in XCP\", never zero, and the\nsort must keep the two apart.","operationId":"markets_volume_api_markets_volume_get","parameters":[{"name":"hours","in":"query","required":false,"schema":{"type":"integer","maximum":168,"minimum":1,"default":24,"title":"Hours"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"description":"max pairs in one page","default":5000,"title":"Limit"},"description":"max pairs in one page"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"pairs to skip — the page cursor","default":0,"title":"Offset"},"description":"pairs to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketVolumeEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/markets/stats":{"get":{"summary":"Markets Stats","description":"Per-pair last fill, previous fill and lifetime fill count — from OUR archive.\n\n★ THIS IS B4.6 / docs/59 §8.8. The market row's `last`, `change` and `fills`\nhave been the BUILD-TIME ranking scan, which is why `CAPTAINDAN/XCP` could\nsay \"not in the ranking scan — nothing counted for this pair\" while the chart\nbeside it plotted that pair's fills. Two surfaces, one page, both sourced\ndifferently. goat screenshotted it twice.\n\n⚠️ DELIBERATELY BOUNDED, and the bound is the design. Every pair that has\never traded, with every completed fill behind it, is ~850 KB on a 45-second\npoll. But only **82 pairs traded in 30 days and 282 in 90**, so `days` +\nevery pooled pair covers exactly the rows whose popups are currently wrong\n— 285 today — and leaves the genuinely dormant ones to the\nhonest build-time wording, which is CORRECT for them.\n⇒ A pair absent from this response is NOT a claim that it never traded. The\nfront end must keep saying \"no fill inside the scanned window\", never \"never\ntraded\" — `BITCORN/CORNTUKTUK` has 7 completed matches on chain and that\npopup called it never traded once already.\n\n⚠️ POOL PAIRS ARE MATCHED ON THE UNORDERED PAIR, not positionally: v_pool's\nassets are counterparty-core's ALPHABETICAL order while v_trade_priced's\nbase/quote come from quote_rank, and those disagree for a ZEBRAPEPE/XCP shape.\n\n⚠️ `v_trade_priced` EXCLUDES EXPIRED MATCHES, which is what makes `last` a\nprice somebody actually paid: 9.76% of all matches expire, 90.3% on BTC/XCP.","operationId":"markets_stats_api_markets_stats_get","parameters":[{"name":"days","in":"query","required":false,"schema":{"type":"integer","maximum":3650,"minimum":1,"default":90,"title":"Days"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max pairs in one page; the byte budget may stop it sooner","default":1000,"title":"Limit"},"description":"max pairs in one page; the byte budget may stop it sooner"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"pairs to skip — the page cursor","default":0,"title":"Offset"},"description":"pairs to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketStatsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/markets/ranking":{"get":{"summary":"Markets Ranking","description":"The terminal's whole market ranking — every pair, ranked, from OUR archive.\n\n★ THE SAME ROWS `site/build_terminal.py` BAKES INTO THE PAGE, in the same\norder and with the same fifteen keys, so the page can read them live instead\nof from a snapshot that only a redeploy can move. Decision 141 R3.\n\n⚠️ `vol_quote`/`vol_xcp`/`trades` cover a TRAILING BLOCK WINDOW, not all history, and its\nwidth is published on the wire as `vol_window_blocks` rather than written here. The\nliteral used to sit in this sentence and t_metrics_routes correctly called it an\nundated chain-state number in the shipped OpenAPI; the register for those is a PAID\ndebt (2026-08-22) and re-opening it to hold a number the response already carries\nwould be buying a green gate back. Read the field.\nand `vol_window_floor_block` publish it, in blocks, and this route does not\nconvert that into a duration for you: block intervals are exponential and the\nonly honest basis is the measured mean, which `/api/stats` publishes. `trades_all`\nis every completed fill we hold for the pair, with no floor. Two different\nquestions under one row, which is why both are named on the wire.\n\n⚠️ `vol_xcp` IS NULL FOR A NON-XCP, NON-BTC QUOTE — \"cannot be expressed in\nXCP\", never zero. `chg` is measured against the PREVIOUS FILL, at whatever age\n(`prev_t` says how old), never over 24 h: most pairs have one fill.\n\n⚠️ MEMBERSHIP IS A RULE, NOT A LIMIT, and `membership_since_block` publishes\nit: a pair is in this list if it has a resting order, an AMM pool, or a fill\nat or after that block. Pairs whose last fill is older are held in the archive\nand are NOT in this response — absence here is not a claim about the chain.","operationId":"markets_ranking_api_markets_ranking_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MarketRankingEnvelope"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/rates":{"get":{"summary":"Rates","description":"BTC per XCP, from the last N XCP DISPENSES. goat's call, 2026-08-19.\n\n★ WHY DISPENSES AND NOT THE BTC/XCP ORDER BOOK — this is the whole point.\nA DISPENSE IS ATOMIC: the BTC payment IS the trigger, so the event is proof\nthat BTC actually moved. The DEX's BTC pairs are the opposite — the BTC leg\nsettles off-protocol within a deadline, and MEASURED on BTC/XCP 2026-08-19:\n18,739 order matches, only 1,815 completed. ~90% of those \"prices\" are\ntrades that never happened. Dispenses are the one BTC source on Counterparty\nthat cannot lie about settlement.\n\n⚠️ A DISPENSER PRICE IS SET BY ITS OPERATOR, not discovered by two sides. It\nis a real transacted price, not a mid. Competition is what keeps it honest,\nso `spread_ratio` is published beside the rate and is not decoration: goat\nchose a PLAIN VWAP with no clamp precisely so that a rate nobody agrees on\nannounces itself instead of being quietly absorbed. MEASURED 2026-08-19:\nspread 1.215x over 50 prints — tight, and an observation about today only.\n\n⚠️ WE HOLD `DISPENSE` ONLY SINCE THE POLLER STARTED — a poller-era slice of\nthe chain's total, historical coverage explicitly not claimed. That is ample\nfor a trailing window and useless for a long history.\n`from_block` says exactly what the window covers; do not read it as more.\n\n⚠️ NULL IS A REAL ANSWER HERE. Too few prints returns `rate: null` WITH the\nreason, never a number off three trades.","operationId":"rates_api_rates_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/RatesEnvelope"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/pool/swaps":{"get":{"summary":"Pool Swaps","description":"The AMM swap tape — every POOL_MATCH, newest first, with full txids.\n\n⚠️ UNFILTERED BY DEFAULT — with no `asset_a`/`asset_b`/`status` this is the\nwhole swap tape and `matching` is every POOL_MATCH we hold. See `_matching` above for why\n`count` alone was a truncation the reader could not see, and `_pair_where` for\nthe pair filter's convention, its per-column trap and why the applied filter is\npublished rather than assumed.\n⭐ WHY THE PAIR FILTER EXISTS AT ALL — AN OUTAGE PREVENTED, NOT ONE REPAIRED.\n⚠️ READ THAT DISTINCTION FIRST. LIVE opreturn.art has no `archiveAmm`, no\n`AMM_LIMIT` and no `ammAll`; the whole-tape fetch this replaces exists only in\nthe working tree's un-deployed migration. Nothing on production is broken by\nthis today, and any note here claiming otherwise is wrong.\nThe un-deployed design fetched this tape WHOLE at `limit=1000` and picked one\npair out of it in the browser [read from site/template.html as of 2026-08-26],\nwhich is honest only while the whole population arrives. It does not arrive\nforever:\n  · MEASURED 2026-08-26 (four reads, ~2 h apart): the tape held 389 -> 392 rows.\n  · ⛔ THE CAP TRIPS AT 1,001 ROWS AND NOT 1,000 [derived 2026-08-26 from this\n    route's own code, not from the chain]. `has_more` is `len(rows) > limit`\n    over a `LIMIT limit + 1` fetch, so `limit=1000` flips it only once the tape\n    holds 1,001 [same derivation, 2026-08-26] — from 392 rows that is 609 more,\n    not 608 and not 611.\n  · ⛔ AND THERE IS NO SCALAR GROWTH RATE TO MULTIPLY. Re-derived over the full\n    admissible window set: 4.88 to 91.00 rows/day, a 19x spread; 66 of 80\n    calendar days are ZERO; the MEDIAN day has no swaps at all; 96.9% of rows\n    landed on or after 2026-08-19. A single \"~50/day\" is a point estimate from\n    one window and reads as a fact. The honest answer is an INTERVAL: the cap is\n    reached somewhere between 2026-09-02 and 2026-09-10.\nOn that day `has_more` flips true, the page's completeness witness refuses the\nwindow (correctly), and the AMM Trades / Chart / Liquidity panels read \"could\nnot read\" against a perfectly healthy archive. Raising the client's constant\nbuys nothing: the ceiling is `le=1000` HERE, in this route's own signature as of\n2026-08-26. Asking for the pair removes the\ncliff instead of moving it — MEASURED headroom 2026-08-26: 389 rows spread over\n13 pairs, the busiest holding 113, so the busiest pair alone would have to grow\n~8.9x before it tripped the same cap.\n⚠️ /api/pool/deposits AND /api/pool/withdrawals CARRY THE SAME `le=1000` AND\nNOT THE SAME PROBLEM — their earliest cap dates are 2027-06-17 and 2028-01-06.\nThey take the filter because ONE convention across three sibling tapes is\ncheaper to read than two, NOT because they are urgent. Do not cite urgency for\nthem.\n⚠️ AFFORDABLE, MEASURED, NOT ASSUMED. Production 2026-08-20, warm, one psql\nsession: the page query costs 14.3 ms and `count(*) FROM v_pool_swap` 7.0 ms —\nthe whole tape is 63 rows. `count_when_truncated` therefore stays on.","operationId":"pool_swaps_api_pool_swaps_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"asset_a","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset A"}},{"name":"asset_b","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset B"}},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Status"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolSwapsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/pool/deposits":{"get":{"summary":"Pool Deposits","description":"Liquidity ADDED to an AMM pool — every NEW_POOL_DEPOSIT, newest first.\n\n⚠️ `status = 'valid'` IS APPLIED HERE AND DISCLOSED IN THE RESPONSE. An\ninvalid deposit is not a deposit; presenting one would be the fail-open class\nthis service exists to oppose. It is filtered from the PRESENTATION, never\nfrom the ARCHIVE — `v_pool_deposit` keeps every row the chain produced.\n⛔⛔ EVERY KEY `_lp_row` READS MUST BE NAMED IN THIS EXPLICIT COLUMN LIST.\nIt is not `SELECT *`, so a field added to the response and not here raises\nKeyError at request time — the 500 that shipped on /api/book and /api/pools.\n`ops/test/t_pool_liquidity.py` asserts the two agree.\n⚠️ `matching` COUNTS THE SAME `status = 'valid'` SET THE PAGE DOES, and it must:\na total over the unfiltered view would be LARGER than the tape it labels, which\nis a wrong number, not a generous one.\n⚠️ AFFORDABLE, MEASURED: production 2026-08-20, warm, one psql session — 14.4 ms\npage, 6.8 ms count, over 13 valid deposits.\n⭐ `asset_a`/`asset_b` FILTER THIS TAPE SERVER-SIDE — same names, same\nvalidation and the same per-column meaning as /api/pool/reserves. See\n`_pair_where` for the ordering trap and for why the applied filter is\nPUBLISHED in `filter` rather than left for the caller to assume.","operationId":"pool_deposits_api_pool_deposits_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"asset_a","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset A"}},{"name":"asset_b","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset B"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolDepositsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/pool/withdrawals":{"get":{"summary":"Pool Withdrawals","description":"Liquidity REMOVED from an AMM pool — every NEW_POOL_WITHDRAWAL, newest first.\n\n⚠️ WE HOLD EXACTLY ONE. The first withdrawal on any pool landed 2026-08-19\n(CAPTAINDAN/XCP, block 963,196). For a year the LP-burn field name could not\nbe tested against anything, and it was guessed wrong twice — so\n`v_pool_withdrawal` reads `quantity_destroyed` and falls back to\n`quantity_burned`, and to NULL rather than 0 when neither is present.\n`lp_quantity_field` publishes which one was read.\n⚠️ Same `status = 'valid'` rule and the same explicit-column-list constraint\nas /api/pool/deposits.\n⚠️ Same `matching` rule too — counted over `status = 'valid'`, the set the page\nitself shows. With exactly ONE valid withdrawal on chain the default page can\nnever be truncated, so `_matching`'s exhaustion test answers it with NO second\nquery at all; the count SQL here is exercised by `ops/test/t_route_matching.py`,\nnot by the production default. Measured 2026-08-20: 6.2 ms if it ever runs.\n⭐ `asset_a`/`asset_b` FILTER THIS TAPE SERVER-SIDE — same names, same\nvalidation and the same per-column meaning as /api/pool/reserves. See\n`_pair_where` for the ordering trap and for why the applied filter is\nPUBLISHED in `filter` rather than left for the caller to assume.","operationId":"pool_withdrawals_api_pool_withdrawals_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"asset_a","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset A"}},{"name":"asset_b","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset B"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolWithdrawalsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/book":{"get":{"summary":"Book","description":"Resting DEX orders — reconstructed from our archive.\n\n⚠️⚠️ THE FILTER IS THE WHOLE JOB, and it lives in v_order_open (derive.sql).\nSelecting on a raw status inflates the book with phantoms, every one looking\nperfectly valid: an order can be consumed down to nothing and still carry\nstatus='open' on its last ORDER_UPDATE.\n\n⛔⛔ CORRECTED 2026-08-23 — THE 774-ORDER GAP THIS DOCSTRING PUBLISHED IS GONE,\nAND IT WENT AWAY BECAUSE THE ARCHIVE WAS FIXED, NOT BECAUSE THE CHAIN MOVED.\nUntil 2026-08-23 this paragraph said \"status='open' = 2,578, v_order_open =\n1,804, 774 phantoms\" as a live description of the archive. `v_order_state.\nstatus` now applies the nothing-remaining rule ITSELF — an order with nothing\nleft is recorded there as `filled` — so the two counts agree. MEASURED\nread-only 2026-08-22 23:55Z, tip 963,646, node and archive inside one minute:\n    2026-08-22 BEFORE  status='open' 2,600, v_order_open 1,826, node 1,826\n    2026-08-22 AFTER   status='open' 1,826, v_order_open 1,826, node 1,826\n(⚠️ EVERY ROW ABOVE CARRIES ITS OWN DATE, AND THE COLUMNS ARE SEPARATED BY\nCOMMAS RATHER THAN BY A SPACED MIDDOT. This docstring is PUBLISHED through\n/api/docs, and t_metrics_routes.py §12 treats a spaced middot as a sentence\nbreak — a table split on one strands true figures in dateless fragments, and\nan undated number in the published contract is the exact defect that section\nexists to catch. Do not \"tidy\" these rows back into a middot list.)\n⭐ THE PHANTOM MECHANISM DID NOT DISAPPEAR — IT MOVED UP ONE LEVEL. The\n730 + 44 decomposition is still exactly how those orders are identified; it is\nsimply applied in `v_order_state` now instead of only in `v_order_open`.\n⛔ So do NOT read \"the gap is 0\" as \"the guards are redundant\" and delete\nv_order_open's two quantity guards — derive.sql says the same thing directly\nabove that view, and deleting them re-opens the defect with nothing to notice.\n\n⚠️⚠️ THESE ARE CHAIN-STATE COUNTS AND THEY MOVE EVERY BLOCK. Until 2026-08-21\nthis docstring published 2,550 / 1,776 / 43.7% / 259,214 / 55.3% as bare\npresent-tense facts and all five had drifted; the 2,578 / 1,804 / 774 that\nreplaced them then went stale a different way — not by drifting, but by being\nFIXED underneath. Both failures are the same failure: a measurement published\nas a standing description of the system.\n★ THIS PARAGRAPH IS A DUPLICATE OF THE ONE IN derive.sql ABOVE v_order_open.\nIF YOU CHANGE ONE, CHANGE BOTH — the copy served to the public through\n/api/docs is the one that rots unnoticed. (This edit is that rule being\nhonoured: derive.sql was corrected first, and this is the other half.)\nWhat is stable and what is not:\n  · STABLE — the mechanism, and the 730 + 44 decomposition that identifies\n    the consumed-but-open orders. Unchanged since 08-16.\n  · MOVING — every total the ratios are computed against.\n  · AND NOW ALSO MOVING — WHERE the rule is applied. It is in v_order_state\n    as of 2026-08-23; it was in v_order_open alone before.\n⇒ Do not quote a total from this docstring. Re-run:\n      SELECT (SELECT count(*) FROM v_order_state WHERE status='open'),\n             (SELECT count(*) FROM v_order_open);\n  ⚠️ and note that this query no longer measures what it was written to\n  measure: it was a PHANTOM DETECTOR, and the two counts are now expected to\n  be EQUAL. A difference today means the two definitions have drifted apart,\n  which is a finding in the opposite direction from the one it used to report.\n\n⚠️ CORRECTED 2026-08-16: this docstring previously stated that Counterparty\n\"NEVER emits an ORDER_UPDATE with status='filled'\". That is FALSE — 259,370\nof 771,241 ORDER_UPDATE rows carry it (2026-08-21; the figure published here\nread 259,214 from 08-16), and the chain also has a dedicated ORDER_FILLED\nevent type. The conclusion was right and the reason was wrong, and it was\npublished here, on a site whose pitch is \"check us instead of trusting us\".\nWhat is measurable: a consumed order's LAST ORDER_UPDATE may still say\nstatus='open' with zero remaining — at the 2026-08-21 measurement, 730 of the\nthen-2,578 had both remainings <= 0, plus 44 more with get_remaining <= 0,\nwhich is exactly the 774 phantoms of that date. ⚠️ THAT DECOMPOSITION IS THE\nMECHANISM AND IT STILL HOLDS; what changed on 2026-08-23 is only WHERE it is\napplied — `v_order_state` now applies it, so those orders no longer reach a\n`status='open'` count at all. See the correction block at the top.\n\n⚠️ CORRECTED 2026-08-21: we hold 4 ORDER_FILLED rows, not none. The node\nserves 846; ORDER_FILLED is absent from the ingest's PRIORITY backfill list,\nso the only ones we ever collected arrived inside whole-block fills at blocks\n955,834 and 955,838, as recorded 2026-08-21. FOUR ROWS IS NOT A FILL TAPE\n— this endpoint does not read them and neither should anything else.\n\n⚠️ Do NOT add an expire_index guard: re-measured 2026-08-21, it removes ZERO\norders, and 981 of the then-1,804 open orders (54.4%) have expire_index NULL\nand never expire. The 981 has not moved since 08-16; the denominator has, so\nthe percentage published here (55.3%) was wrong while the underlying count was\nright — which is how a ratio rots without either half looking suspicious.\n⚠️ THE DENOMINATOR HAS MOVED AGAIN — v_order_open read 1,826 on 2026-08-22, so\n54.4% is a figure of 08-21 and not of today. The finding that survives is\n\"removes ZERO orders\"; the ratio is not re-derived here because re-deriving it\nwould only start the same rot over. Re-run it if you need it.\n\n⚠️ PAGINATION IS A KEYSET CURSOR, NOT AN OFFSET, AND THE REASON IS THE ONE\nTHING THIS ENDPOINT EXISTS FOR. An independent reviewer could verify only\n1,000 of the then-1,776 open orders — 56% — as measured 2026-08-16, because\nthere was no way to ask for the rest. (Both numbers are kept as the historical\nrecord of that incident, not as a live description of the book.)\n`docs/TASKS.md` records the fix as \"a limit/offset pair\"; an OFFSET is\nthe version that breaks silently. This list is rebuilt whenever a block lands\n(see the cache note below), and a single order filling between page 1 and\npage 2 shifts every later row up by one — so an OFFSET reader SKIPS a row and\nreceives no signal that it happened. Gaps and duplicates that a reader cannot\ndetect are worse here than a missing feature, because the whole point of the\nendpoint is a reader checking our numbers instead of trusting them.\n\nSo the cursor is the SORT KEY itself: `<opened_block>:<event_index>`, and the\nnext page is everything strictly ordered after it. Rows already read cannot\nmove relative to that key. `(opened_block, event_index)` is a TOTAL order —\n`event_index` is globally unique in `events` — which is what makes it stable;\n`opened_block` alone is not unique and would duplicate or drop rows at every\npage boundary that falls inside a block. The same shape as /api/events'\n`before_index`/`next_cursor`, one column wider because this list is not sorted\nby a unique column on its own.\n\n⚠️ `next_cursor` is null on the last page, never the cursor that produced it.\nA repeated cursor is a pagination loop — `ingest/api.py` raises on one — so\nexhaustion must be expressible, and `has_more` says the same thing as a bool.\n\n⚠️ WHAT PAGING CANNOT PROMISE, stated rather than implied: pages are exact\nwithin one `as_of_block`. If a block lands mid-walk the book itself changed,\nand an order that filled in between is simply gone — no cursor scheme can\nshow a reader a row that no longer exists. `as_of_block` is on every page so\nthe reader can SEE that it moved and re-page for an exact snapshot.\n\n⚠️ `total_open` is the WHOLE book and ignores the filters — that is the figure\nthe 2026-08-16 set-difference verification compared, so it does not change\nmeaning here. It is computed per request; it is NOT the 1,776 that the\n2026-08-16 check recorded, and that number is not restated here as if it\nstill were. `matching` is the size of the filtered set. Both are\npage-invariant, on purpose: a count that shrinks as you page is how a\nreader concludes rows vanished.","operationId":"book_api_book_get","parameters":[{"name":"give_asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"asset the maker is selling","title":"Give Asset"},"description":"asset the maker is selling"},{"name":"get_asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"asset the maker wants","title":"Get Asset"},"description":"asset the maker wants"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"before_cursor","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"cursor: page on from the previous response's next_cursor","title":"Before Cursor"},"description":"cursor: page on from the previous response's next_cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BookEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/trades":{"get":{"summary":"Trades","description":"The DEX fill tape — every ORDER_MATCH, newest first.\n\n⚠️ TAKER-SIDE CONVENTION, verified on the FULL population (every held fill,\nzero violations): forward_asset is what the TAKER RECEIVED, i.e. what the\nMAKER GAVE. The field names here say so explicitly rather than relying on the\nreader knowing the convention.\n\n⚠️ A fill is TWO transactions. Both hashes are returned, always — a popup\ndescribing an on-chain event without a txid is not verifiable.\n\n⚠️ THE PARAMETER IS `asset`. NOT `base_asset`/`quote_asset` — FastAPI accepts\nunknown query parameters, ignores them, and answers HTTP 200 with the UNFILTERED\ntape, which looks exactly like a working filter. `/api/candles` spells the same\nidea `base`/`quote`. Anyone measuring this route must prove the parameter binds\n(compare against `?limit=1`) before believing the numbers.\n\n⛔⛔ THIS ROUTE CANNOT AFFORD A TRUE TOTAL BY DEFAULT, AND THAT IS MEASURED.\nMeasured on production 2026-08-20: 216,561 fills are held, and `?asset=XCP`\nalone matches 128,775.\n\n  ⛔ THE FOUR NUMBERS THAT USED TO SIT HERE WERE WRONG, AND AN INDEPENDENT\n    MEASURER OVERTURNED THEM ON THE SAME BOX. They claimed page ~288 ms, count\n    ~321 ms, \"MORE THAN DOUBLES … ~+111%\", and \"unfiltered it is cheaper\n    (~201 ms page)\". RE-MEASURED 2026-08-20, warm, loopback + server-side\n    EXPLAIN, 3 runs each:\n      · page ?asset=XCP  = 416 ms client / 409-422 ms server   (not 288)\n      · count ?asset=XCP = 166 ms client / 139-172 ms server   (not 321 — HALF)\n      · the count adds +33.9% server-side / +39.8% client-wall (NOT +111%;\n        overstated by ~2.7-3x)\n      · UNFILTERED IS THE SLOWER PAGE, NOT THE CHEAPER ONE — 517 ms client /\n        497-625 ms server, +8.3-9.0% for its count. The old line was INVERTED.\n      · the unfiltered count's \"~39 ms\" is the one figure that reproduced\n        (39.5-41.2 ms server-side).\n    ⇒ THE DECISION STANDS AND THE REASON IS UNCHANGED — a +34% tail on an\n    uncached route with a six-slot pool is still not something to spend on\n    every request — but it rests on +34%, not +111%. The direction was right\n    and the magnitude was not; do not re-quote the old figures.\n  · THE ROUTE IS NOT CACHED — unlike /api/book there is no `cached_by_tip` to\n    absorb a repeat, so EVERY request pays in full.\n  · IT ALREADY DEGRADES UNDER CONCURRENCY, on its own, today:\n        concurrency 1 → 0.618 s\n        concurrency 4 → 1.34-1.73 s   (wall 1.776 s)\n        concurrency 8 → 1.86-4.08 s   (wall 4.184 s)\n    against a `max_size=6` pool shared with the poller and a 10 s\n    `statement_timeout`. ⚠️ The \"eight parallel sessions … 5.439 s vs 3.992 s,\n    +36%\" figure was NOT reproduced: the independent re-measure ran c1 and c4\n    only (deliberately, on a 2 GB box serving real people) and found\n    +18-21% at c4 (2.49-2.63 s vs 1.95-2.25 s). c8 is UNMEASURED by them, so\n    the +36% is neither confirmed nor refuted — treat it as unverified rather\n    than as a number.\n  ⇒ Always counting would not be a latency regression, it would be an\n    AVAILABILITY one: it pushes the concurrent tail toward the statement\n    timeout on a route that has no cache to hide behind.\n\nSo the default is `has_more` — exact, and free from the `limit + 1` fetch — with\n`matching` null whenever the answer was truncated. A caller who genuinely needs\nthe total passes `count_total=true` and pays for it knowingly. Null is published\nrather than `limit`, because `limit` is not an estimate of the held total and\nthis service does not invent numbers. ⚠️ The honest fix is an index on the two\nasset columns, which would make the filtered count cheap; adding one is a MIGRATION\nand is not this change's to make.\n\n⛔ `params` NO LONGER CARRIES `limit`. It used to be mutated with\n`params.append(limit)` before the one query ran — fine while there was one\nquery, a landmine now: the count SQL has NO `%s` for a limit, so reusing the\nmutated list raises on every filtered request. `limit + 1` is appended at the\ncall site instead and `params` stays exactly the WHERE's parameters.","operationId":"trades_api_trades_get","parameters":[{"name":"asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"either side of the pair","title":"Asset"},"description":"either side of the pair"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"count_total","in":"query","required":false,"schema":{"type":"boolean","description":"also count the whole matching set into `matching`; OFF by default because it adds ~34% to this route's database work — see the route docstring","default":false,"title":"Count Total"},"description":"also count the whole matching set into `matching`; OFF by default because it adds ~34% to this route's database work — see the route docstring"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/TradesEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/art/{kind}/{asset}":{"get":{"summary":"Art Object","description":"The mirrored artwork for one asset — OUR bytes, not xcp.fun's.\n\n⛔ SWITCHED OFF UNTIL goat SAYS OTHERWISE (`ART_MIRROR_PUBLIC`). See the\nbanner above this function, which quotes goat's mirror authorisation row in\ndocs/TASKS.md (2026-08-16) verbatim.\n⚠️ THE LINE NUMBER IS IN THE BANNER COMMENT, NOT HERE, AND THAT IS\nDELIBERATE: `ops/test/t_metrics_routes.py` §12 scans every published route\nDOCSTRING for bare numbers that could be undated chain state, and a citation\nline number reads to it exactly like a stale mint count. Comments are not\npart of the published contract, so the precise citation lives there.\n\nAnswers four DISTINGUISHABLE states, in the `X-Art-State` header and in the\nbody: `art` (200, the bytes) · `known_absent` · `asked_no_bytes` ·\n`never_asked` (404 each). Our own breakage is 503 `archive_error` and is\nnever reported as absence.","operationId":"art_object_art__kind___asset__get","parameters":[{"name":"kind","in":"path","required":true,"schema":{"type":"string","description":"icon | full","title":"Kind"},"description":"icon | full"},{"name":"asset","in":"path","required":true,"schema":{"type":"string","description":"the Counterparty asset name","title":"Asset"},"description":"the Counterparty asset name"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"not_priced","sellable":false,"price":null,"note":"this route has NO row in the price catalog. Not free — unpriced. See /api/pricing."}}},"/api/coverage":{"get":{"summary":"Coverage","description":"What we actually hold, per event type — and what we do NOT claim.\n\nB4.11, and it is the honest machinery behind the coverage contract. The site\nsays \"derived from our archive\"; this is what lets a reader check that claim\ninstead of trusting it, which is the difference between a data source and a\nchart site.\n\n⚠️ DELIBERATELY MAKES NO OUTBOUND CALL. The on-chain figure comes from the\nbackfill cursor's own record of what the node reported WHEN IT RAN, and is\nreturned with that provenance attached. Two reasons: a request path that\ndepends on a third-party node inherits that node's downtime and rate limits,\nand this project has been 429'd before. The LIVE comparison is the weekly\nops/verify-archive.sh, which is the right place for it.\n\n⚠️ \"We hold rows of type X\" is NOT \"we claim type X is complete.\" The poller\ntakes all 61 types on every block it completes, so every type has poller-era\nrows — and `poller_runs` below is the MEASURED extent of that era, one row\nper contiguous run, never a first..last range assumed to have no hole in it.\nHistorical completeness is claimed ONLY for types with a finished\nbackfill — and this endpoint reports the difference rather than blurring it.\n\n⚠️ IT ALSO COVERS THE TWO NEW COLLECTORS, and they are NOT event types — they\nare a different KIND of record and are reported in their own top-level keys\nrather than folded into `result`. `mempool_this_node_partial` is one node's\nown view of a transient state; `art_mirror_xcp_fun` is a copy of a third\nparty's mutable web server. Both are observations WE made at a time WE record,\nnever chain facts, and both come with the qualification attached to the key\nitself so no consumer can quote the number without it. The no-outbound-call\nrule above applies to them unchanged: every figure is read from our own\ntables.\n\n⛔ AND `mempool_this_node_partial` IS BOUNDED IN TIME AS WELL AS IN SCOPE, which\nis the half that key name does not carry. Capture began at block 962,672 on\n2026-08-16; the AMM era began at block 952,800, so the 9,872 blocks in between\nhave no pre-confirmation record and never will. ⛔ It is not a gap awaiting a\nbackfill: a mempool is per-node transient state that is never written to the\nchain, so no upstream retains it and there is nothing anywhere to fetch. Unlike\nevery event-type row in `result` — where a shortfall against the node is a\nto-do with a cost in requests — this shortfall has no such cost, because it has\nno remedy. ★ Equally, do not shrink it to nothing: from 2026-08-16 forward the\ncapture is continuous, and it is the one record here that cannot be\nreconstructed later by anyone, which is exactly why it is published.\n\n⛔⛔ `last_reverify: null` DOES NOT MEAN \"NEVER ATTEMPTED\", AND THE ENVELOPE KEY\nTHAT INHERITED THAT ERROR IN ITS OWN NAME HAS BEEN RENAMED FOR IT: it is\n`types_claiming_completeness_with_no_recorded_reverify`, and it was\n`types_claiming_completeness_never_reverified` until 2026-09-10. Read this\nbefore quoting either of them.\n  · `verified` / `short` / `not_comparable` are the three verdicts a re-check\n    RECORDS. Recording them is NEW — it shipped 2026-09-08 — and the re-check\n    runs on a weekly timer, so every row that predates the first recorded run\n    reads null whether or not an attempt was made.\n  · [OBSERVED on this route, read-only, 2026-09-09] all 24 types claiming\n    historical completeness read null, and FIVE of them WERE attempted on\n    2026-09-06: the attempt found a shortfall and correctly refused to write a\n    witness. ⇒ `null` means WE HOLD NO RECORDED VERDICT FOR THIS TYPE, and\n    that covers BOTH \"never attempted\" AND \"attempted and could not be\n    certified\". The two are not the same thing and this field does not\n    currently separate them.\n  · ★ THE SEPARATION IS ALREADY IN THIS PAYLOAD AND NOTHING COUNTS IT. Among\n    the types claiming completeness, compare each row's\n    `completeness_witness_at` with the FRESHEST such value on the route. A row\n    that is behind is a row whose last re-check did NOT refresh its witness.\n    Measured here 2026-09-09: 19 types at 2026-09-06T05:40:22Z and 5 at\n    2026-08-30T05:40:05Z — and those five are exactly ASSET_ISSUANCE,\n    CANCEL_ORDER, NEW_FAIRMINT, OPEN_ORDER and ORDER_UPDATE, which are the\n    five whose re-verification failed. A monitor can read that today.\n  · ⚠️ AND NO VERDICT IS INVENTED FOR THEM HERE, deliberately. A witness that\n    is stale is an observation about a TIMESTAMP; it is not a measured `short`,\n    and writing one in would be the retro-fill this archive's witness rules\n    forbid. These rows keep publishing null until a RECORDED run produces a\n    verdict.\n\n⚠️ CACHED ON WALL CLOCK, NOT ON THE ARCHIVE HEAD — see cached_by_clock. It is\nthe one endpoint the BACKFILL moves without moving the poller's high-water\nmark, so `cached_by_tip` would pin it to a block number while the numbers it\nreports change underneath.\n\nMEASURED on production 2026-08-16, on the box to keep the network out of it:\ncold build **125 ms** (the full-archive GROUP BY is 131 ms of it), warm\n**2.4–4.2 ms**, and the cold cost returns exactly once the 30 s TTL lapses —\nso the cache is proven by a case it should MISS as well as one it should hit.\n⚠️ Do NOT quote /api/events/counts' \"1.58 s\" for this aggregate; measured\ntoday it is 131 ms. That figure is left in place on its own endpoint because it\nis that endpoint's own dated measurement, not a live claim about this one.\n⚠️ And it is 0.31 s over HTTPS from a laptop either way — the 30× difference is\nINVISIBLE from outside. Timing this endpoint through the internet measures the\ninternet.","operationId":"coverage_api_coverage_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/coverage/block/{height}":{"get":{"summary":"Coverage Block","description":"Was block N covered by this archive — and if not, what is missing?\n\nThe question /api/verify/{txid} already sends readers to /api/coverage to ask,\nwhich /api/coverage cannot answer: it reports per-TYPE totals for the whole\narchive, and \"is this particular block covered\" is a different question. This\nis `coverage_of(h, h)`, the same contract the drills and fixtures exercise.\n\n⚠️ EVERY HEIGHT GETS AN ANSWER, including heights this archive has never\nfetched and heights above the chain tip. There is deliberately no 404: an\nabsent row is exactly the ambiguity this contract exists to remove, because\n\"no row\" reads identically as \"not covered\", \"unknown\" and \"does not exist\".\n\nThe five states:\n  * `full`    — the poller fetched this block whole and committed every event\n                with the complete flag in one transaction.\n  * `partial` — covered for SOME event types only. `missing_types` names the\n                rest. This is the normal state for pre-poller history.\n  * `none`    — nothing here is covered; the absence of an event at this height\n                means NOTHING about the chain.\n  * `suspect` — a `blocks` row exists but its bookkeeping contradicts the rows\n                present. Never reported as covered. See v_coverage_suspect_block.\n  * `unknown` — ⛔ COULD NOT TELL, and it is decision log 154's honesty\n                requirement rather than an error. The per-type answer comes from\n                the maintained rollup (`event_rollup`, migration 015), and this\n                state means that rollup is out of step with the archive, so\n                WHICH TYPES are covered here is not answered. The block-level\n                fields on the same response — `poller_row`, `poller_complete`,\n                `event_count_claimed`, `events_held` — are read from `blocks`\n                and `events` directly and are still exact, and `full`,\n                `suspect` and above-the-reach `none` are still reported, because\n                none of them consults the rollup. `reason` names both heights.\n\n⚠️ `missing_types: null` is NOT an empty list, and it now has TWO meanings that\n`coverage` distinguishes: with `coverage` anything but `unknown` it means the\ntype universe is unknown because no census has been recorded, so we cannot name\nwhat is missing; with `coverage: \"unknown\"` it means we could not determine\ncoverage at all. `types_covered_basis` says which on every row. `[]` always\nmeans we can name it and nothing is.\n\n⚠️ THIS IS AN EXPENSIVE QUERY, AND THE MEASUREMENT IS KEPT HERE BECAUSE IT IS\nTHE REASON FOR EVERYTHING BELOW. An earlier comment called it \"a single-height,\nchunk-pruned lookup\". It is not: `coverage_of` reads `v_coverage_type` twice,\nand that view's `held` CTE is the FULL-ARCHIVE `GROUP BY` — the same aggregate\n/api/coverage caches, and the view's own comment justifies its cost with\n\"/api/coverage already pays for this exact GROUP BY\", which stopped being true\nthe moment that endpoint started caching. Measured on the box: 82–108 ms per\nrequest, consistently SLOWER than the cached 20 KB /api/coverage.\n\n⚠️ CACHED, AND BOUNDED BY CONSTRUCTION — see cached_bounded(). The previous\nversion of this docstring was right that the key space here is the CALLER's,\nnot ours, and that a plain `_CACHE` entry per height lets any anonymous caller\ngrow the process without bound by walking `seq`. But refusing to cache does not\nmake that go away: it trades our memory for our CPU, and NEITHER is bounded by\nthe caller. There is no rate limit in front of this route\n(ops/server-config/etc__caddy__Caddyfile has no such directive), so on a 2 GB\nbox also running the poller and a multi-hour backfill, a trivial height walk is\n~100 ms of database CPU per request for as long as the caller cares to keep\ngoing. A cap of BLOCK_COVERAGE_CACHE_MAX entries bounds the memory ARITHMETICALLY\nwhile making repeated and scripted access cheap; the CPU was never bounded at\nall. That is the docstring's own answer (\"if this ever needs caching it needs a\nBOUNDED cache first\"), implemented rather than deferred.\n\n⚠️ A CACHE IS NOT A RATE LIMIT. It removes the cheap way to burn CPU (the same\nheights over and over); a caller walking a million DISTINCT heights still pays\nus ~100 ms each, evicting as it goes. The remaining defence belongs in Caddy and\nis not in this file's control — reported, not implemented here.","operationId":"coverage_block_api_coverage_block__height__get","parameters":[{"name":"height","in":"path","required":true,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"block height; any height, including ones above this archive's reach","title":"Height"},"description":"block height; any height, including ones above this archive's reach"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CoverageBlockEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/verify/{tx_hash}":{"get":{"summary":"Verify","description":"Return the RAW archived events for a transaction hash.\n\nB4.12. The whole site shows a txid on every on-chain claim, and until now a\nreader had to take that hash somewhere else to check it. This lets them check\nit against OUR record — which is the thing we are asking them to trust.\n\n⚠️ Returns raw params, unmodified. That is the point: a derived, prettified\nanswer would be exactly what a sceptical reader cannot verify.\n\n⛔⛔ THE DEFECT THIS ROUTE SHIPPED WITH, MEASURED LIVE ON PRODUCTION 2026-08-21.\nThe guard accepted `16 <= len <= 64` while the query is EXACT EQUALITY against a\n64-character column, so EVERY accepted input of length 16-63 was GUARANTEED to\nmiss — and the miss was then reported as a fact about our archive:\n\n    /api/verify/df5cbb98…b733      -> found:true, 7 events\n    /api/verify/df5cbb980545b5fd   -> found:false, HTTP 200, note: \"no event in\n      (a correct 16-char prefix        this archive carries that hash. That is a\n       of the SAME transaction)        statement about our coverage…\"\n\n⇒ IT ACCEPTED AN INPUT IT COULD NOT ANSWER AND THEN BLAMED OUR COVERAGE FOR THE\nMISS. That is this project's named #1 defect class — a check that answers\npositively when it has nothing to check — sitting inside the route the whole\nproduct is sold on, and a sceptic pasting a truncated hash out of a block\nexplorer was told the archive does not hold a transaction it demonstrably holds.\n\n⚠️ THE FIX IS TO REFUSE THE INPUT, NOT TO MATCH A PREFIX, and the alternative was\nmeasured rather than dismissed: `tx_hash LIKE %s || '%%'` is a left-anchored LIKE\nover 3.2M rows — the same shape the comment below already refuses by name — AND a\n16-hex prefix is not unique, so the honest response would stop being `found:\nbool` and become a candidate list. A 400 that names the problem costs nobody\nanything: no caller sends a prefix (the page fetches this route not at all), and\nthe deploy smoke test sends 64 zeros, which is still accepted.\n\n⚠️ RESIDUAL, NAMED RATHER THAN PAPERED OVER: on a well-formed 64-hex hash we\ngenuinely lack, `found:false` STILL cannot separate \"the block was covered and\nthis is not in it\" from \"we never covered that block\". The note punts to\n/api/coverage/block/{height} and the reader does not know the height. That is a\nreal circularity in the pitch and this change does not touch it.","operationId":"verify_api_verify__tx_hash__get","parameters":[{"name":"tx_hash","in":"path","required":true,"schema":{"type":"string","title":"Tx Hash"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max events in one page for this transaction","default":1000,"title":"Limit"},"description":"max events in one page for this transaction"},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":2147483647,"minimum":0,"description":"events to skip — the page cursor","default":0,"title":"Offset"},"description":"events to skip — the page cursor"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VerifyEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/gaps":{"get":{"summary":"Gaps","description":"Heights with no `blocks` row inside a stated window. B5.7.\n\n⚠️ THE RANGE IS PART OF THE ANSWER, NOT METADATA. A bare `[]` cannot be\ndistinguished from a scan that silently narrowed to three blocks — that exact\nambiguity is why B5.6's gap scan had to be changed to report its population.\nEvery field needed to reproduce the claim is in the response.\n\n⚠️ `truncated` exists because `limit` caps the result: `gaps` being full means\nthere may be MORE, and a caller that reads the list as complete would conclude\nthe archive is healthier than it is.\n\n⚠️ This is NOT /api/coverage. A `blocks` row means the block was ingested; it\nsays nothing about which event types are covered. Coverage is a separate\ncontract under separate review and the two must not be conflated.\n\n⚠️ NOR IS IT `v_coverage_poller_run`, and the difference is load-bearing rather\nthan academic. That view is gaps-and-islands over blocks the POLLER wrote and\ndeliberately EXCLUDES ingest.blockfill's rows — without the writer term, a\nnormal archive reads as \"a poller era with a hole in it\". This endpoint asks\nthe opposite question: which heights have NO `blocks` row from ANY writer.\nA height blockfill wrote is a hole to that view and is NOT a hole here.\n⛔ Do not \"reconcile\" the two into one number; they answer different questions\nand the reconciliation would silently pick one and lose the other.","operationId":"gaps_api_gaps_get","parameters":[{"name":"from","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":2147483647,"minimum":0},{"type":"null"}],"description":"lowest height to scan","title":"From"},"description":"lowest height to scan"},{"name":"to","in":"query","required":false,"schema":{"anyOf":[{"type":"integer","maximum":2147483647,"minimum":0},{"type":"null"}],"description":"highest height to scan","title":"To"},"description":"highest height to scan"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"description":"max gaps returned","default":50,"title":"Limit"},"description":"max gaps returned"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/GapsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/mempool":{"get":{"summary":"Mempool","description":"This node's UNCONFIRMED sightings — mints first. B1.1's data, finally served.\n\n⛔⛔ THIS TABLE BEGINS AT BLOCK 962,672 — FIRST SIGHTING 2026-08-16 — AND\nNOTHING WILL EVER FILL IN THE TIME BEFORE IT. The AMM era opened at block\n952,800, so the 9,872 blocks between that opening and the 2026-08-16 start of\ncapture have NO pre-confirmation record of any kind: no transactions that were\nbroadcast and never confirmed, no RBF replacements, no fee conditions at\nbroadcast time, no pending durations. ⛔ THAT IS NOT A BACKLOG ITEM — IT IS\nUNRECOVERABLE, AND NOT ONLY BY US. A mempool is never written to the chain; it\nis each node's transient private state, discarded as it drains, and no node —\nours, yours, or anyone's — can serve its own past. There is no source to\nbackfill FROM, so this gap can never be closed by any amount of work.\n⇒ An empty answer here about anything below block 962,672 (2026-08-16) is a\nstatement about OUR COVERAGE and never a statement about what was pending\nthen. Do not read absence as evidence of an empty mempool.\n\n★ AND DO NOT OVERCORRECT INTO THE OPPOSITE ERROR, which is just as wrong: from\nblock 962,672 (2026-08-16) FORWARD this is continuous and it keeps growing. It\nrecords a state that ceases to exist the moment it is not written down —\nincluding the transactions that NEVER confirmed, which by construction appear\nin no block, in no node's history, and in no other archive of this chain. The\nspan actually covered is published on every response in `observed`, so the\nboundary is a value you can read rather than a caveat you have to remember.\n\n⛔ THIS IS A PARTIAL VIEW OF ONE MEMPOOL AND THE PAYLOAD SAYS SO IN EVERY\nRESPONSE. A mempool is per-node transient state, so another node legitimately\nholds a different set — and NOTHING HERE HAS COMPARED THIS SET WITH ANOTHER\nNODE'S, so that difference is unmeasured, not zero. Never render this as \"the\nmempool\".\n\n⚠️ `outcome IS NULL` means \"still in the mempool as of last_seen_at\" — it does\nNOT mean \"unknown\". A row that leaves without confirming is the entire point of\nthe table and must never be conflated with one we have not resolved yet.\n\n⚠️ A pending mint is an ATTEMPT, not a fact. `valid` is derived from the node's\nown status string and is `null` when the node did not say — never assumed true.","operationId":"mempool_api_mempool_get","parameters":[{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"type":"string"},{"type":"null"}],"description":"filter live rows to one event type","title":"Event"},"description":"filter live rows to one event type"},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"max live rows returned","default":50,"title":"Limit"},"description":"max live rows returned"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MempoolEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/metrics":{"get":{"summary":"Metrics","description":"Prometheus-style text exposition. B5.10.\n\nMakes \"is the archive healthy\" answerable by a MACHINE, which is what the\nmonitoring item actually needs — the watchdog currently re-derives all of\nthis itself over ssh.\n\n⚠️ text/plain, and it deliberately reuses the same underlying figures as\n/api/health rather than computing its own. Two monitoring surfaces that\ndisagree are worse than one.\n\n⛔ NO ENVELOPE AS-OF KEY, AND IT IS NOT AN OVERSIGHT (decision log 52). This\nroute returns Prometheus text exposition, not a JSON object, so there is no\nenvelope in which to put one — a `\"as_of_block\":` line in this body would not\nbe a dateline, it would be a parse error for every scraper.\n★ AND THE DATELINE IS ALREADY HERE, in this format's own idiom: the\n`opreturn_last_block` gauge below IS the archive head, and\n`opreturn_poll_age_seconds` and the lag gauge date it further. This route was\nnever undated; it is unDATABLE ONLY IN JSON, which is a different statement.\nops/test/t_envelope_as_of.py classifies it NOT_JSON and requires that\nclassification to be declared with a reason, so the exclusion cannot go quiet.","operationId":"metrics_api_metrics_get","responses":{"200":{"description":"Successful Response","content":{"text/plain":{"schema":{"type":"string"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/candles":{"get":{"summary":"Candles","description":"OHLCV from our own archive. B4.3.\n\n⚠️ This is what kills the chart's `showing 500 of N matches` truncation.\nThat limit exists only because a BROWSER has a fetch budget; our store does\nnot, so the full history is available here.\n\n⚠️ VENUE IS A CHOICE, NOT A FILTER TO FORGET. DEX and AMM are separate venues\nwith separate prices. This endpoint serves exactly one of them per call and\nnever unions them — the page compares them side by side, each labelled.\n\n⚠️ THESE ARE EXECUTION PRICES, fee-inclusive, which is not the same number as\nan AMM pool's marginal spot price. VERIFIED on real data, recorded 2026-08-15:\nthe PEPECASH/XCP swap in block 962,571 prices here at 0.00397681, exactly\nmatching the chart's tag, while the pool's own marginal price at the time was\n0.00399121. Both are correct and they answer different questions — finding\n#12 is the record of someone reading one as the other, so the response\nlabels which it is.\n\n⚠️ Pair orientation follows `orient()` in ecosystem/markets.py — the lower\nQUOTE_RANK asset becomes the quote (XCP 0, BTC 1, everything else 9). If a\ncaller asks for the pair the other way round, it will legitimately be empty;\nthat is the orientation rule, not a gap.\n\n★ AND AS OF 2026-08-21 THE RESPONSE SAYS SO, because the paragraph above was\nthe ONLY place it was said. MEASURED (audit F-4): three very different causes\nproduced one identical `200 {\"count\":0,\"truncated\":false}` — an asset that\ndoes not exist, a pair asked BACKWARDS, and a real pair with no priced fills.\nThe orientation case is the dangerous one: `orient()` makes XCP the QUOTE, so\n`base=XCP` is ALWAYS empty against a non-BTC asset, and a chart wired to it\nrenders \"this market has never traded\" over the 53,112 archived fills counted\nfor audit F-4 on 2026-08-21. An empty series is a POSITIVE CLAIM, and this\nroute was making it without evidence. `absence_means` is the same field\n`/api/markets/volume` already publishes.\n\n⚠️ THE ORIENTATION ANSWER IS MEASURED, NOT DERIVED, AND THAT IS A REFUSAL.\nRe-implementing quote_rank() here would be a FOURTH copy of the orientation\nrule — two other call sites in this file both refuse to make one, and\ndocs/36 §2B records the drift hazard. So on an empty answer this route ASKS\nTHE ARCHIVE whether the REVERSED pair has priced fills. That is a fact about our data\nrather than an inference from a naming rule; it stays true if the rule ever\nchanges; and it is the SAME statement as the page query with two parameters\nswapped, so there is nothing new to keep in sync.\n\n⚠️ WHAT `absence_means` DELIBERATELY DOES NOT CLAIM: \"this asset does not\nexist\". There is no asset registry in this schema — `/api/markets/stats`\nderives its asset names by scanning — so separating \"unknown asset\" from \"real\npair, never traded\" costs a full-population scan, and asserting either without\nit would be exactly the confident guess this route just stopped making. The\n`no-priced-fills` string names its own limit and points at /api/coverage.","operationId":"candles_api_candles_get","parameters":[{"name":"base","in":"query","required":true,"schema":{"type":"string","description":"base asset, e.g. PEPECASH","title":"Base"},"description":"base asset, e.g. PEPECASH"},{"name":"quote","in":"query","required":false,"schema":{"type":"string","description":"quote asset, e.g. XCP","default":"XCP","title":"Quote"},"description":"quote asset, e.g. XCP"},{"name":"venue","in":"query","required":false,"schema":{"type":"string","description":"DEX or AMM — never combined","default":"DEX","title":"Venue"},"description":"DEX or AMM — never combined"},{"name":"tf","in":"query","required":false,"schema":{"type":"string","description":"TIME buckets 5m, 15m, 1h, 4h, 1d, 1w — or TRADING-BLOCK buckets 1b, 6b, 12b, 36b, 144b, 576b, 1008b. ⚠️ CHANGED 2026-09-06, IN PLACE: `Nb` groups N blocks THAT CARRIED A PRICED FILL for this pair and venue — it is NOT N consecutive chain blocks, which is what it meant before that date. `12b` therefore always holds twelve blocks that traded, however far apart on chain they sit; the bar reaches from `first_block` to `last_block` and that distance is normally much larger than N (7.5% of chain blocks carried a fill on the pair this was measured against). `bucket_basis` reads `trading_block` on this basis, and the newest bar may be PARTIAL — `trading_blocks` on each row says how many it actually holds. The two bases are different QUESTIONS, not two spellings of one scale: a duration holds a variable number of blocks and a block count holds a variable duration, so no block key is an alias for a time key and none may be converted to one.","default":"1d","title":"Tf"},"description":"TIME buckets 5m, 15m, 1h, 4h, 1d, 1w — or TRADING-BLOCK buckets 1b, 6b, 12b, 36b, 144b, 576b, 1008b. ⚠️ CHANGED 2026-09-06, IN PLACE: `Nb` groups N blocks THAT CARRIED A PRICED FILL for this pair and venue — it is NOT N consecutive chain blocks, which is what it meant before that date. `12b` therefore always holds twelve blocks that traded, however far apart on chain they sit; the bar reaches from `first_block` to `last_block` and that distance is normally much larger than N (7.5% of chain blocks carried a fill on the pair this was measured against). `bucket_basis` reads `trading_block` on this basis, and the newest bar may be PARTIAL — `trading_blocks` on each row says how many it actually holds. The two bases are different QUESTIONS, not two spellings of one scale: a duration holds a variable number of blocks and a block count holds a variable duration, so no block key is an alias for a time key and none may be converted to one."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":5000,"minimum":1,"default":500,"title":"Limit"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CandlesEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/reorgs":{"get":{"summary":"Reorgs","description":"Forks this archive OBSERVED the chain take back — B4.15, the reorg log.\n\nA public Counterparty node keeps a current-state view: once a block is\nreorged out, it is simply forgotten. This archive copies the losing chain's\nevents and block envelopes out before the DELETE (migration 003), and this\nroute is the only thing that reads them back.\n\n⛔ THE THREE ANSWERS THIS ROUTE MUST KEEP APART, because two of them used to\nbe the same byte string everywhere else in this repo:\n\n  1. `forks: []`   — WE LOOKED. The log exists and is empty: no distinct fork\n                     has been observed since `preservation.since`.\n  2. `forks: null` — WE COULD NOT LOOK, and `preservation.reason` says why.\n                     The commonest cause is a database without migration 003,\n                     where there is no log to be empty. Still HTTP 200,\n                     because that is a stable fact and not a retryable one.\n  3. HTTP 503      — WE COULD NOT ASK AT ALL (q() failed). Try again.\n\n⚠️ AND EVEN `forks: []` IS NOT \"THERE HAVE BEEN NO REORGS\". It is a statement\nabout what THIS node saw WHILE RUNNING and WHILE MIGRATED. A reorg before\n`preservation.since`, or one that happened while the poller was stopped, left\nnothing behind here — the rows were deleted with no copy. `absence_means`\ncarries that sentence into the response so it cannot be dropped in transit.","operationId":"reorgs_api_reorgs_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"max forks returned","default":50,"title":"Limit"},"description":"max forks returned"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ReorgsEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/launches/deadline-rewrites":{"get":{"summary":"Deadline Rewrites","description":"Launches whose soft-cap deadline the node rewrote — and what it was before.\n\n⛔ THE THREE ANSWERS THIS ROUTE MUST KEEP APART, the same three the reorg log\nkeeps apart, for the same reason:\n\n  1. `rewrites: []`   — WE LOOKED. The view is here and no launch in this\n                        archive has a deadline-rewrite event.\n  2. `rewrites: null` — WE COULD NOT LOOK, and `availability.reason` says\n                        why. Still HTTP 200: a missing derived view is a\n                        stable fact about this database, not a retryable one.\n  3. HTTP 503         — WE COULD NOT ASK AT ALL (q() failed). Try again.\n\n⚠️ AND EVEN `rewrites: []` IS SCOPED. It is a statement about the update\nevents THIS archive captured. A rewrite that happened before our coverage, or\nwhile the poller was stopped, left nothing behind here. `absence_means`\ncarries that into the response.","operationId":"deadline_rewrites_api_launches_deadline_rewrites_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"description":"max rewrites returned","default":50,"title":"Limit"},"description":"max rewrites returned"}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DeadlineRewritesEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/venue-gap":{"get":{"tags":["metrics"],"summary":"AMM vs DEX price, sampled ~every 10 minutes, side by side","description":"The venue-gap series. `price_sample_dex` + `price_sample_amm` are collected\nevery ~10 minutes and, until this route, were served by **nothing**.\n\nMEASURED on production through the read-only guard, 2026-08-21:\n500 rows unfiltered — **75 ms cold, 17-28 ms warm**. Pair-filtered 200 — 49 ms.","operationId":"venue_gap_api_venue_gap_get","parameters":[{"name":"base","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Base asset, exact.","title":"Base"},"description":"Base asset, exact."},{"name":"quote","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Quote asset, exact.","title":"Quote"},"description":"Quote asset, exact."},{"name":"since","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only samples at or after this instant.","title":"Since"},"description":"Only samples at or after this instant."},{"name":"until","in":"query","required":false,"schema":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"description":"Only samples at or before this instant.","title":"Until"},"description":"Only samples at or before this instant."},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/VenueGapEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/block-times":{"get":{"tags":["metrics"],"summary":"block height -> Bitcoin timestamp, the only such map below block 952,800","description":"★ WITHOUT THIS ROUTE NO ROUTE COULD PUT DEEP HISTORY ON A TIME AXIS AT ALL.\n\nAS OF BLOCK 963,510 (2026-08-22) `block_times` held 311,763 rows spanning\n280,312-963,510 — **45.6%** of that span. The dense era above block 952,800 is\nfully mapped; the sparse era below it is not. ⚠️ Both figures are chain state\nand rot; `range_coverage` on every response re-derives them, dated.\n\n⚠️ THE HAZARD THIS ROUTE EXISTS TO MAKE VISIBLE: **953 blocks that CONTAIN\nEVENTS have no timestamp** (measured whole-archive 2026-08-21). A time-axis\nchart over deep history silently drops or mis-places them; a BLOCK-axis chart\nis unaffected. `range_coverage` publishes the arithmetic per request.\n\nMEASURED on production 2026-08-21 with this module's own SQL: 5,000 rows over\nthe widest range — **16-21 ms**. The in-range resolved COUNT adds **33-70 ms**.\n⇒ the cheapest route in this module, and the one every deep chart needs.","operationId":"block_times_api_block_times_get","parameters":[{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":280312,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"asc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"default":5000,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000000,"minimum":0,"default":0,"title":"Offset"}},{"name":"include_event_gap","in":"query","required":false,"schema":{"type":"boolean","description":"Also count event-bearing blocks in range with NO timestamp. Refused above 50000 blocks; see method.","default":false,"title":"Include Event Gap"},"description":"Also count event-bearing blocks in range with NO timestamp. Refused above 50000 blocks; see method."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/BlockTimeEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/launch/{tx_hash}/mint-curve":{"get":{"tags":["metrics"],"summary":"mints per block for one fair-mint launch","description":"The shape of a mint: how fast, in which blocks, by how many addresses.\n\n⚠️⚠️ THE FOOTGUN, AND IT IS THE REASON THIS ROUTE PUBLISHES IT RATHER THAN\nLETTING A BUYER FIND IT: `v_fairmint` — the source of this curve, of\n`/api/mints`, and of every mint number this site publishes — filters to\n`status='valid'` and therefore DROPS roughly a tenth of all `NEW_FAIRMINT`\nrows. AS OF BLOCK 963,510 (2026-08-22) that was 22,799 dropped of 213,728 held\n(190,929 valid), or 10.667%. Anyone comparing this curve to a node's raw\n`NEW_FAIRMINT` count is low by that share **and will not know**. The dropped\nrows are served by `/api/mints/failed`, whose `census` re-derives the share on\nevery response — read that, not this sentence.\n⛔ AND THEY CANNOT BE ADDED TO THIS CURVE: **not one of them carries an `asset`\nor a `fairminter_tx_hash`** (AS OF BLOCK 963,510 that is 0 of 22,799), so no\nfailed attempt is attributable to any launch. The ZERO is the claim; the\ndenominator is chain state.\n\nMEASURED on production through the read-only guard, 2026-08-21, running THIS\nMODULE'S OWN SQL TEXT: the largest launch on the chain (MINTS, 100,000 mints\nover 243 blocks) — curve **567 ms ascending / 521 ms descending**, plus the\nlaunch header at **150-170 ms warm (444 ms cold)**, plus 10 ms of freshness.\n⇒ **roughly 700-750 ms warm for the whole route.** It is the second most\nexpensive route in this module and the cost is the size of the mint tape,\nwhich grows — so the cost is a floor, not a fixed figure.","operationId":"mint_curve_api_launch__tx_hash__mint_curve_get","parameters":[{"name":"tx_hash","in":"path","required":true,"schema":{"type":"string","pattern":"^[0-9a-fA-F]{64}$","description":"The FAIRMINTER announcement tx_hash, 64 hex.","title":"Tx Hash"},"description":"The FAIRMINTER announcement tx_hash, 64 hex."},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"asc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":20000,"minimum":1,"default":5000,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MintCurveEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/pool/reserves":{"get":{"tags":["metrics"],"summary":"per-block AMM reserve series back to each pool's opening block","description":"★ FOR THE AMM WE ARE NOT MISSING HISTORY — WE **ARE** THE HISTORY.\n\nThe venue opened at block 952,800 and this archive's dense floor is block\n952,800, so the reserve series is complete from each pool's first block. A node\nserves CURRENT reserves; reserves AT BLOCK N require having been watching.\n\n`via` distinguishes the pool's creation (`OPEN_POOL`) from a later change\n(`POOL_UPDATE`). A `POOL_UPDATE`-only view returned 2 pools when 4 existed —\nthat is why creation is in the series.\n\nMEASURED on production 2026-08-21 with this module's own SQL: 500 rows\nunfiltered **descending — 123-130 ms**; **ascending — 233 ms warm, 484 ms\ncold** (oldest-first must reach the coldest chunks). Pair-filtered — 119 ms.","operationId":"pool_reserves_api_pool_reserves_get","parameters":[{"name":"asset_a","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset A"}},{"name":"asset_b","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset B"}},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/PoolReserveEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/launches/conformance":{"get":{"tags":["metrics"],"summary":"which fair-mint launches follow the XCP-69 standard, as a filter","description":"`v_launch.params_conformant` has been computed on every launch since the\nview existed and was exposed **nowhere**. This is the filter.\n\n★★ THERE ARE THREE STATES, NOT TWO, AND THE THIRD IS THE FINDING.\nMeasured 2026-08-21 over all 403 launches: **88 conformant · 298 nonconformant\n· 17 UNKNOWN**. The 17 are launches whose ANNOUNCEMENT was itself rejected by\nthe node (`invalid: Hard cap of asset ... is already reached.`, `invalid: Fair\nminter already opened for ...`) — they carry **no asset and no parameters at\nall**, so there is nothing to judge. Reporting them as nonconformant would\nover-report by 17 (a 5.7% error on that figure). `conformance='unknown'`\nreturns exactly them.\n\nMEASURED on production 2026-08-21 with THIS MODULE'S OWN SQL TEXT: 200 rows\nwithout the rollup — **113-128 ms**. WITH the rollup — **1,013 ms**, because\nselecting those columns forces a GROUP BY over the whole mint tape that the\nplanner otherwise skips. That is **~9x**, so the rollup is OFF by default.\n⚠️ AN EARLIER NOTE IN THIS LANE SAID 52-58 ms AND 16x. That was measured on a\nREDUCED column list, not on the statement this route runs; both numbers are\ncorrected here to the verbatim-SQL figures. The direction was right, the\nmagnitude was not.","operationId":"launches_conformance_api_launches_conformance_get","parameters":[{"name":"conformance","in":"query","required":false,"schema":{"type":"string","pattern":"^(all|conformant|nonconformant|unknown)$","default":"all","title":"Conformance"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}},{"name":"include_mint_rollup","in":"query","required":false,"schema":{"type":"boolean","description":"Also return mint_count/minters/xcp_raised. MEASURED ~9x more expensive (1,013 ms vs 113 ms).","default":false,"title":"Include Mint Rollup"},"description":"Also return mint_count/minters/xcp_raised. MEASURED ~9x more expensive (1,013 ms vs 113 ms)."}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ConformanceEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/dispensers":{"get":{"tags":["metrics"],"summary":"every dispenser opened, with its BTC price and inventory at open","description":"102,273 dispenser openings AS OF BLOCK 963,697 — every dispenser ever opened.\n\n⛔ THIS IS A LOG OF OPENINGS, NOT A LIST OF WHAT IS OPEN. For that, see\n`/api/dispensers/open`. What THIS route has that the other structurally cannot:\ninventory AT OPEN, and the opening terms of dispensers that have since closed.\nIts rows are unchanged by the addition of that route.\n\n⚠️ ⛔ THIS COUNT HAS NOW BEEN WRONG THREE TIMES, AND THE THIRD IS THE INSTRUCTIVE\nONE. It read \"744\", then \"745\" in the catalog, then \"769\" — each a point-in-time\nreading of a LIVE WINDOW. Then `ops/backfill-dispensers.sh` ran on 2026-08-23\nand swept the family CHAIN-WIDE, and 769 became **133x stale in one night**\nwhile every caveat AROUND it was corrected and the number itself was not.\n⇒ It is 102,273 AS OF BLOCK 963,697, and this is no longer a live window: it is\na HISTORY, complete from the family floor.\n⛔ 76,028 of these have since CLOSED, AS OF BLOCK 963,697, and are still\nreturned by this route —\nthat is 74.3%, not the 35.4% an earlier revision published — see\nDISPENSER_CAVEATS, and `/api/dispensers/open` for what is standing now.\n\nMEASURED on production 2026-08-21 with this module's own SQL: 500 rows\nunfiltered — **151 ms**; asset-filtered — **154-231 ms warm, 2,171 ms on the\nfirst cold touch of those chunks**. ⚠️ The cold figure is real and a buyer will\noccasionally meet it. The `asset` filter is cheap warm for a reason worth stating:\n`events_event_idx` narrows to 744 rows before the unindexed jsonb test runs.\nThe same filter over ALL events measures 3.06 s and is why no per-asset route\nexists in this module.","operationId":"dispensers_api_dispensers_get","parameters":[{"name":"asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset"}},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope_DispenserOpen_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/dispenses":{"get":{"tags":["metrics"],"summary":"every dispenser purchase: who bought, what, and for how much BTC","description":"The BTC-denominated fill tape of the third venue — AS OF BLOCK 963,696 it holds\n**207,606 DISPENSE rows** spanning blocks 600,790-963,694.\n\n⛔⛔ THE COUNT IN THIS DOCSTRING WAS OFF BY 197x AND THE CORRECTION IS THE\nPOINT, NOT A FOOTNOTE. It read \"1,051 dispenses as of 2026-08-23\" — measured\nread-only on production the SAME DAY, AS OF BLOCK 963,696, the archive holds\n207,606. `ops/backfill-dispensers.sh` landed between the two reads and took\nthis family from an observation window to a chain-wide history; the docstring\ndid not move with it. `server/pay/catalog.py`'s row for this route still says\n994 and is stale by the same event. ⇒ A ROUTE'S OWN PROSE IS THE LAST THING A\nBACKFILL UPDATES, and this is what that costs.\n\n★★ WHAT THIS ROUTE COULD NOT REACH UNTIL NOW. Every row has carried\n`dispenser_tx_hash` since it shipped, and nothing could FILTER on it — so one\ndispenser's history required paging the entire tape, AS OF BLOCK 963,696 all\n207,606 rows of it. MEASURED AS OF BLOCK 963,696: the busiest dispenser\n(`ae602de651fed5693ec1b9fddb8fdc3d4c8992580f9b1324f7c84eaf02c096d6`) drained\nacross **16,993 dispenses** in blocks 801,575-819,322 — 34 pages at the 500-row\ndefault, each one a full-tape offset walk. `?dispenser_tx_hash=` is that\nquestion asked directly.\n\n══ COST, MEASURED — AND THE CHEAP PATH IS NOT THE OBVIOUS ONE ═════════════\nServer-side `EXPLAIN (ANALYZE)` on production 2026-08-23, AS OF BLOCK 963,696,\nTHIS ROUTE'S VERBATIM SQL TEXT AND FULL COLUMN LIST, two runs each, taken while\nthe box was serving real traffic (which is why these are RANGES — the second\nrun was sometimes the slower one):\n\n  | request shape                          | execution time      |\n  |----------------------------------------|---------------------|\n  | first page, no filter (limit 500)      | 13 - 417 ms         |\n  | `?from_block=&to_block=` 1,000 blocks  | 2.6 - 7.8 ms        |\n  | `?dispenser_tx_hash=` + block window   | **2.2 - 2.5 ms**    |\n  | `?dispenser_tx_hash=` alone            | 253 - 2,887 ms      |\n  | `?offset=100000`                       | 4,113 - 5,336 ms    |\n  | `?offset=207000` (the tape's far end)  | 2,579 - 6,137 ms    |\n  | `?offset=300000` (past the end)        | 1,714 - 2,117 ms    |\n  | a filter that matches NOTHING          | 1,672 - 4,719 ms    |\n\n⛔⛔ A DEEP `offset` IS THE MOST EXPENSIVE THING YOU CAN ASK THIS ROUTE FOR,\nAND IT IS NOT SAFE FROM THE STATEMENT TIMEOUT. The API's database role\n(`opreturn_ro`) carries `statement_timeout=10s` — read from `pg_db_role_setting`\non production 2026-08-23, not assumed — and `?offset=207000` measured 6,137 ms,\n**61% of that budget**, on a box with 4 MB `work_mem` and 256 MB\n`shared_buffers` that has already OOM'd once. `params->>'dispenser_tx_hash'` is\nUNINDEXED (`events` carries exactly four indexes; a GIN over `params` cost\n785 MB for zero scans and was dropped), so a bare offset walk re-projects ten\njsonb extractions per row over every row it skips.\n⇒ **REACH FOR THE BLOCK WINDOW FIRST.** `from_block`/`to_block` ride\n`events_event_idx (event, block_index DESC)`, and on 2026-08-23 adding one to\nthe `dispenser_tx_hash` filter measured **2.2 ms against 2,887 ms** — a\n~1,150x difference for one extra parameter. `coverage.block_range_returned` on any page\nhands you the window for the next one, which is the paging strategy this route\nis actually cheap under.\n\n⚠️ AND A FILTER THAT MATCHES NOTHING COSTS THE FULL SCAN, not nothing:\n1,672-4,719 ms measured 2026-08-23 for zero rows. `result_state` still distinguishes\n`empty_no_match` from `empty_source` from `empty_past_end` — the answer is\nSTATED, never substituted — but it is not a cheap way to ask.\n\n══ WHAT THIS ROUTE CANNOT SEE ════════════════════════════════════\n  * **The dispenser's own terms.** A DISPENSE row says what was bought and for\n    how much BTC; the satoshirate, escrow and inventory live on OPEN_DISPENSER\n    (/api/dispensers) and DISPENSER_UPDATE. Filtering by `dispenser_tx_hash`\n    here gives you the DRAIN, never the dispenser.\n  * **`dispenser_tx_hash` is not searchable the other way round.** There is no\n    route that takes a dispenser hash and returns its OPENING; /api/dispensers\n    has no tx filter. Reported, not fixed here.\n  * **Anything before block 600,773** — the family's floor, which is where\n    dispensers begin on chain (swept chain-wide against the node's own census\n    2026-08-23, so nothing is missing below it).\n  * **The chain.** No node is consulted at request time; `freshness.as_of_block`\n    is the whole extent of what has happened here.\n  * **A dispense that has not been ingested yet.** This is our archive's belief.\n\n⚠️ `events.tx_hash` IS AN INDEXED COLUMN AND IT IS NOT THE ONE YOU WANT —\nMEASURED, AND REJECTED FOR A REASON. On all 207,606 DISPENSE rows AS OF BLOCK\n963,696, `events.tx_hash` equals the dispense's OWN `params->>'tx_hash'` (207,606\nof 207,606) and equals `params->>'dispenser_tx_hash'` on **exactly zero**. So\n`events_tx_hash_idx` cannot serve this filter at all, and using it would have\nreturned an answer that looked right and was empty.","operationId":"dispenses_api_dispenses_get","parameters":[{"name":"asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset"}},{"name":"dispenser_tx_hash","in":"query","required":false,"schema":{"anyOf":[{"type":"string","minLength":64,"maxLength":64,"pattern":"^[0-9a-fA-F]{64}$"},{"type":"null"}],"description":"ONE dispenser's whole drain tape, by the tx_hash of the OPEN_DISPENSER that created it. Case-insensitive; matched lowercased. ★ PAIR IT WITH from_block/to_block — measured 2,887 ms cold / 253 ms warm alone, and 2.2-2.5 ms with a block window, AS OF BLOCK 963,696.","title":"Dispenser Tx Hash"},"description":"ONE dispenser's whole drain tape, by the tx_hash of the OPEN_DISPENSER that created it. Case-insensitive; matched lowercased. ★ PAIR IT WITH from_block/to_block — measured 2,887 ms cold / 253 ms warm alone, and 2.2-2.5 ms with a block window, AS OF BLOCK 963,696."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Envelope_DispenseRow_"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/dispensers/open":{"get":{"tags":["metrics"],"summary":"dispensers standing RIGHT NOW, with what is actually left in them","description":"Which dispensers are open NOW, and how much is left in each — the question\n`/api/dispensers` structurally cannot answer.\n\n⛔⛔ THE HALF OF THE ANSWER THAT IS A BELIEF, NOT A READING — STATED FIRST\nBECAUSE IT IS THE PART THAT MISLEADS. Every row carries `open_evidence`:\n  * `confirmed_open` — the chain's latest word on this dispenser says open.\n  * `presumed_open`  — we watched it open and have never observed it close.\n    **THAT IS NOT THE SAME AS KNOWING IT IS OPEN.**\nAS OF BLOCK 963,676 (2026-08-23) the split was 86 confirmed and 411 presumed of\n497. ⚠️ Those are point-in-time and WILL drift; the `census` block on every\nresponse carries the live, dated, filter-matched numbers, and it is those, not\nthis sentence, that a machine should read.\n\n⚠️ WHY THE PRESUMED CLASS IS SO LARGE, AND IT IS NOT A BUG. Counterparty emits\na DISPENSER_UPDATE when a dispenser dispenses or closes. A dispenser that has\nsimply sat there, untouched, since it opened has nothing to emit — so silence\nis the expected state for a quiet dispenser AND the expected state for one\nwhose close we missed, and this archive cannot tell those two apart. It reports\nthat instead of choosing.\n★ NODE-ADJUDICATED 2026-08-23 on a sample strided across the whole block range\n(/v2/dispensers/{hash}, 5 per class, both directions): presumed_open 5/5 the\nnode calls open, confirmed_open 5/5 open, closed 5/5 closed. Evidence the\npresumption is usually right; NOT evidence that it is right, and the route does\nnot consult a node.\n\n⛔ WHAT THIS ROUTE CANNOT SEE, said in the check rather than around it:\n  1. Dispensers opened below this family's block floor. That floor is not\n     written here as a digit that can rot — it is `DISPENSER_BLOCK_FLOOR`, and\n     every response publishes it as `coverage.block_floor`.\n  2. NOTHING — and the entry is KEPT rather than deleted, because the number\n     it carried was published. It named 619 DISPENSER_UPDATE tx_hashes whose\n     OPEN_DISPENSER sat below the floor and so could not be joined. The\n     chain-wide dispenser backfill of 2026-08-23 moved the floor down to\n     where dispensers begin, and measured after it every distinct update\n     hash joins and none is orphaned. There is no class left here to exclude.\n  3. REFILL VOLUME — though no longer the refill EVENTS. REFILL_DISPENSER is\n     HELD, 260 of a node census of 260, backfilled 2026-08-23; this entry\n     read \"zero rows held\" and that is no longer true. What survives is the\n     CONSEQUENCE, unchanged: this view does not fold a refill into\n     `give_remaining`, so `give_remaining` is still a LOWER BOUND.\n  4. Anything in a block not yet ingested. `freshness.as_of_block` names it.\n  5. Whether a `presumed_open` dispenser is still open. That is the whole point\n     of the field.\n\n⚠️ AND ONE UNFLATTERING MEASUREMENT, RECORDED SO IT IS NOT MISTAKEN FOR A\nWORKING GUARD: `v_dispenser_open`'s `give_remaining > 0` filter currently\nremoves NOTHING. AS OF BLOCK 963,676 the status filter alone selects 497 and\nthe status-plus-quantity filter also selects 497 — zero phantoms, where the\norder book's identical test finds 774. The guard is kept because a dispenser\nCAN be drained without its close being observed; it is simply unproven here.\n\nMEASURED read-only on production 2026-08-23 with this route's own SQL, tip\n963,676. ⚠️ A RANGE, NOT A BEST CASE — one warm number would have been the\nflattering half of what was actually observed:\n    page, all 497 rows       186 ms · 238 ms · 546 ms   (three runs)\n    page, asset-filtered      27 ms\n    census (second pass)     139 ms · 278 ms            (two runs)\n⇒ a full unfiltered request is roughly **330-820 ms**, being both passes plus\nthe ~10 ms freshness read. The census is a SECOND pass over the same view and\nis paid on every request, empty pages included — that is deliberate, because a\nno-data answer that also drops its census cannot say WHY it is empty.\n⚠️ The view is computed per request from `events` — no materialisation and no\nindex on `params`, see this module's header — so these are the honest costs of\na live derivation, not a cache's.","operationId":"dispensers_open_api_dispensers_open_get","parameters":[{"name":"asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"title":"Asset"}},{"name":"evidence","in":"query","required":false,"schema":{"type":"string","pattern":"^(any|confirmed|presumed)$","default":"any","title":"Evidence"}},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/DispenserOpenNowEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/mints/failed":{"get":{"tags":["metrics"],"summary":"fair-mint attempts the node REJECTED: the payer, the block and the reason","description":"★ **Every fair-mint attempt the node RECORDED AND REJECTED — with the reason,\nthe payer and the block.** AS OF BLOCK 963,510 (2026-08-22) that was 22,799\nattempts, 10.667% of every `NEW_FAIRMINT` this archive holds.\n\ndoc 78 §5C TIER 1 calls *\"what did NOT happen\"* the class of data that is\nobservable now and gone forever. We have been holding it and exposing it\nnowhere: `v_fairmint` correctly filters it out of the mint tape, and no view or\nroute touched it. This route serves it as its own thing, read from `events`.\n⛔ `v_fairmint` IS NOT CHANGED BY THIS ROUTE and must not be — the mint tape is\nright to exclude attempts that moved no XCP.\n\nMEASURED on production 2026-08-21 with this module's own SQL: 500 rows\nnewest-first over the whole span — **195-220 ms warm, 878 ms cold**. Filtered\nto one reason — **162 ms**. A bounded 4,000-block window — **76 ms**.","operationId":"failed_mints_api_mints_failed_get","parameters":[{"name":"reason","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":512},{"type":"null"}],"description":"Exact match on the node's status string.","title":"Reason"},"description":"Exact match on the node's status string."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":2000,"minimum":1,"default":500,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedMintEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/mints/failed/reasons":{"get":{"tags":["metrics"],"summary":"why fair-mints failed, grouped by the node's verbatim reason string","description":"The failure taxonomy. AS OF BLOCK 963,510 (2026-08-22) there were 77 distinct\nreason strings, led by `invalid: fairminter is not open for asset: ...; asset\nsupply quantity exceeds hard cap` — people minting into a cap that was already\nfull. ⚠️ That count is chain state and grows with the number of ASSETS people\nfail to mint, not with the number of failure MODES — see the caveats.\n\nMEASURED on production 2026-08-21 with this module's own SQL: the full\ngrouping — **149-156 ms**, returning exactly 77 rows.","operationId":"failed_mint_reasons_api_mints_failed_reasons_get","parameters":[{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FailedMintReasonEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/orders":{"get":{"tags":["lifecycle"],"summary":"every order this archive holds, in whatever state it ended up in","description":"THE FULL ORDER LIFECYCLE — every terminal state, not just the open book.\n\n`/api/book` serves the 1,824 orders resting right now, AS OF BLOCK 963,697. This serves all\n566,312 orders the archive holds AS OF BLOCK 963,675 (measured read-only\n2026-08-23; it moves every block) — filled, expired, cancelled, refused and\nopen — and until this route nothing served any of them.\n\n⛔ NO TIMING IS QUOTED HERE ANY MORE, AND THAT IS THE HONEST ANSWER.\nThis route answered **503 QueryCanceled at 10.39 s** on production on\n2026-08-24, at `?limit=1`, the day it was first deployed. The figures that\nused to stand in this paragraph (1,048 ms cold, 246 ms windowed, 5,220 ms on\na status filter) were measured read-only on 2026-08-23 against a\n`v_order_state` with ONE dedupe pass; the deployed view now has TWO, and\nnothing has been re-measured since. The query was reshaped to choose its page\nfrom the `events` index instead of sorting the whole view — see\n`_ORDERS_PAGED_SQL` — but **that fix has not been timed against a real\nPostgreSQL**, so quoting a number here would be inventing one.\n\n⛔⛔ AND THE FIX REMOVES THE SORT, NOT THE FLOOR. Every read of\n`v_order_state` makes TWO full `DISTINCT ON (params->>'tx_hash')` passes over\nthe ORDER_UPDATE table — 771,373 rows AS OF BLOCK 963,675 — and no query\nagainst the view can avoid them. What was reshaped is the ORDER BY over the\nwhole view, 566,312 rows AS OF BLOCK 963,675, on top of those passes.\nMEASURED on production 2026-08-24 ~22:2xZ, against the deployed view:\n`/api/orders/outcomes` 200 in **7.216 - 9.403 s** (it was 5,930 - 6,371 ms\nthe day before, on a one-CTE view) and `/api/orders` 503 in\n**10.375 - 10.387 s**. `outcomes` reads the same view and does NOT sort it.\n⇒ **Expect this route, fixed, to land somewhere around `outcomes` — i.e.\ninside a 10 s timeout by as little as 0.6 s, against a route whose own cost\nswings 30% between runs.** Treat \"fixed\" as UNPROVEN until it is probed on\nproduction, and see register 156 W47: the lever that removes the floor is a\nchange to the VIEW, which is goat's decision and is not taken here.\n⚠️ `status` is the exception and is still expected to be slow: it is the one\nfilter the view computes itself, so it cannot choose a page. An EMPTY status\n(`?status=`) is treated as no status filter and takes the fast path.\n\n⛔ WHAT THIS ROUTE CANNOT SEE:\n  * Anything below block 280,312 — the archive's oldest event of any type, AS OF BLOCK 963,697.\n  * The order's PATH. Only the opening event and the LATEST update are joined;\n    every intermediate remaining is dropped, and they are non-monotonic.\n  * WHY an order filled — the counterparties. Those are matches: /api/matches\n    for settlement state, /api/trades for the fill tape.\n  * ORDER_FILLED events. The archive holds them, they are NOT read here, and\n    measured 2026-08-23 the node's own /v2/orders contradicts 18 of the 810\n    order hashes they carry. The status above rests on ORDER_UPDATE.","operationId":"orders_api_orders_get","parameters":[{"name":"give_asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Exact asset the order gives.","title":"Give Asset"},"description":"Exact asset the order gives."},{"name":"get_asset","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Exact asset the order wants.","title":"Get Asset"},"description":"Exact asset the order wants."},{"name":"source","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":128},{"type":"null"}],"description":"Exact address that placed it.","title":"Source"},"description":"Exact address that placed it."},{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":256},{"type":"null"}],"description":"Exact status string. ⛔ THE SLOW PATH, AND IT IS THE ONE THAT CAN STILL TIME OUT. `status` is computed by the view's own join, so unlike every other filter here it cannot be pushed down to choose the page — this is the only parameter that still makes the database sort the whole view. It measured 5,220 ms unfiltered by block on 2026-08-23, against a view that has since gained a second dedupe pass; the same route measured 503 QueryCanceled at 10.375-10.387 s on 2026-08-24, so treat 5,220 ms as a FLOOR THAT HAS ALREADY BEEN EXCEEDED and pair this with from_block. ⚠️ AN EMPTY VALUE (`?status=`) MEANS NO STATUS FILTER, not a filter on the empty string. Filtering on '' can never match a row, and until 2026-08-24 it selected this slow path to prove it. Use /api/orders/outcomes to discover the strings.","title":"Status"},"description":"Exact status string. ⛔ THE SLOW PATH, AND IT IS THE ONE THAT CAN STILL TIME OUT. `status` is computed by the view's own join, so unlike every other filter here it cannot be pushed down to choose the page — this is the only parameter that still makes the database sort the whole view. It measured 5,220 ms unfiltered by block on 2026-08-23, against a view that has since gained a second dedupe pass; the same route measured 503 QueryCanceled at 10.375-10.387 s on 2026-08-24, so treat 5,220 ms as a FLOOR THAT HAS ALREADY BEEN EXCEEDED and pair this with from_block. ⚠️ AN EMPTY VALUE (`?status=`) MEANS NO STATUS FILTER, not a filter on the empty string. Filtering on '' can never match a row, and until 2026-08-24 it selected this slow path to prove it. Use /api/orders/outcomes to discover the strings."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"description":"A block window is index-supported and is still the cheapest thing a caller can do. ⚠️ The '~900 ms to ~250 ms' this field used to promise was measured 2026-08-23 against a view definition the database no longer runs; no re-measurement has been made, so no number is quoted here.","default":0,"title":"From Block"},"description":"A block window is index-supported and is still the cheapest thing a caller can do. ⚠️ The '~900 ms to ~250 ms' this field used to promise was measured 2026-08-23 against a view definition the database no longer runs; no re-measurement has been made, so no number is quoted here."},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":1000000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderStateEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}},"402":{"description":"⭐ PAYMENT REQUIRED, AND THE OFFER IS THE DISCOVERY. This is not an error: the body and the `PAYMENT-REQUIRED` header both carry a complete, signable x402 v2 offer, so a machine can pay and retry without asking a human anything. ⚠️ IT IS A PROPERTY OF THE REQUEST, NOT OF THE ROUTE. Current state at display size is FREE on every route here — that is what the website itself asks for — and only a HISTORY request is charged. The same URL answers 200 and 402 depending on what you asked it for. ⚠️ AND IT IS REACHABLE ONLY WHILE A RAIL IS USABLE. With none configured a priced request is REFUSED 503 rather than offered — see this operation's 503, `reason: no_rail_configured`. A consumer that has never seen a 402 has not proved it cannot arrive.","headers":{"PAYMENT-REQUIRED":{"description":"The same offer, base64-encoded. ⛔ THIS IS THE NORMATIVE COPY — x402 v2 puts the offer in the header and calls the body a server implementation concern. Both are rendered from one method so they cannot disagree.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/PaymentOffer"}}}}},"x-price":{"access":"priced","sellable":true,"currency":"USD","unit":"request","metered":false,"price":{"micro_usd":10000,"display":"$0.01","per":"1 request","means":"the total charge for one call to this route"},"free_rows":200,"ceiling":"bounded by the route's own limits on one response; the largest response has not been measured in bytes.","question":"Every order this archive has ever seen, and how it ended — filled, expired, cancelled, or still resting. NOT the live book: that is /api/book. This is the history, and nobody else publishes it."}}},"/api/orders/outcomes":{"get":{"tags":["lifecycle"],"summary":"how every order ended — and Counterparty's own words for the refusals","description":"WHY ORDERS WERE REFUSED, IN COUNTERPARTY'S OWN WORDS — and where the rest went.\n\nA refused order is not an error in this archive: the chain recorded it, with a\nreason string, and nothing has ever published those strings. MEASURED\nread-only on production 2026-08-23, AS OF BLOCK 963,675: 28 distinct status\nstrings, of which 24 are rejection reasons covering 6,907 orders, against\n566,312 orders in total. Every one of those numbers moves; the response\ncarries its own.\n\n⛔ **THIS ROUTE RUNS AT THE EDGE OF THE DATABASE'S PATIENCE, AND SINCE W410 IT\nSAYS SO ON THE WIRE INSTEAD OF FAILING.** MEASURED on production 2026-08-24\n~22:2xZ, three read-only requests: 200 in **9.403 s, 7.228 s, 7.216 s**; the\n2026-09 census then measured a **mean of 9.58 s with 2 of 8 probes answering\n503** — all against `harden()`'s 10 s per-connection `statement_timeout`. The\nday before, against a `v_order_state` with one dedupe CTE, the same request\nmeasured 5,930-6,371 ms (from_block=900,000: 2,361 ms).\n⇒ THE QUERY IS UNCHANGED. What changed first (W410) is that the whole-archive\n  census is now served through a 300 s cache that keeps the LAST GOOD totals\n  when a rebuild crosses the wall, so a cancelled statement degrades to a\n  **labelled** older answer instead of a 503. Read **`stale_fallback`** and\n  **`rows_age_s`** on every response: they are not optional decoration, they\n  are how you tell the two apart, and every row's `measured_at` — and, since\n  W421, every `census` total's `measured_at` and `as_of_block` — is derived\n  from the same age, so no stamp on this response can disagree with another.\n⛔ AND ITS COST DID MOVE, WHICH THIS PARAGRAPH USED TO DENY. MEASURED\n  read-only on production 2026-09-21: the build was not scanning too much, it\n  was SPILLING — 1.12 GB of temp, a dedupe hash at 16 batches, 58.3% of\n  11,110 ms in I/O wait — and `work_mem = 64MB` measured **10,515 / 9,809 ms\n  -> 4,977 / 3,716 ms, 2.4-2.6x**. The whole-archive build now issues that\n  raise as `SET LOCAL`, in its own transaction only. See the W421 banner\n  above `_q_work_mem` for the value's derivation and its memory cost.\n⚠️ BOTH CHANGES BUY TIME; NEITHER MOVES THE FLOOR. The cost is still the\n  view's two ORDER_UPDATE dedupe passes (see the module docstring), not this\n  query's aggregation: `work_mem` stops them SPILLING, it does not stop them\n  happening, and a cold cache still pays them — see register 156 W47 for the\n  only lever that moves the floor. `from_block` remains the fast path, and it\n  is now also the path with no cache, no fallback and no raise behind it.\n\n⛔ WHAT THIS ROUTE CANNOT SEE:\n  * WHICH orders were refused — only how many, and the earliest and latest\n    block per reason. For the rows themselves: /api/orders?status=<the string>.\n  * Transactions the chain never parsed into an OPEN_ORDER at all. A refusal\n    recorded here is one Counterparty decoded and then declined.\n  * Any order below block 280,312, AS OF BLOCK 963,697.\n  * Whether a reason is still occurring TODAY. `last_block` is the honest\n    answer to that and is returned per reason instead of being asserted.","operationId":"order_outcomes_api_orders_outcomes_get","parameters":[{"name":"rejected_only","in":"query","required":false,"schema":{"type":"boolean","description":"Return only the 'invalid:' rejection reasons. The census is computed over ALL classes either way, so the denominators do not move when you filter.","default":false,"title":"Rejected Only"},"description":"Return only the 'invalid:' rejection reasons. The census is computed over ALL classes either way, so the denominators do not move when you filter."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"description":"★ THE FAST PATH. Measured 2,361 ms with a floor of 900,000 on 2026-08-23, against 5,930-6,371 ms for the whole archive that day, 7,216-9,403 ms on 2026-08-24 and a mean of 9,580 ms with 2 of 8 probes answering 503 in the 2026-09 census — all against the 10 s per-connection statement_timeout. ⚠️ IT IS ALSO THE UNCACHED PATH, AND THAT IS A REVERSAL: the whole-archive census now goes through a cache with a last-good fallback, so it is the request MORE likely to answer instantly and the one that can answer at all while a rebuild is failing. A window is cached by nothing — its key would come from this parameter — so it pays its 2,361 ms on EVERY call and a 503 on it is not caught by anything. Pass a floor for a narrow question; omit it for the census.","default":0,"title":"From Block"},"description":"★ THE FAST PATH. Measured 2,361 ms with a floor of 900,000 on 2026-08-23, against 5,930-6,371 ms for the whole archive that day, 7,216-9,403 ms on 2026-08-24 and a mean of 9,580 ms with 2 of 8 probes answering 503 in the 2026-09 census — all against the 10 s per-connection statement_timeout. ⚠️ IT IS ALSO THE UNCACHED PATH, AND THAT IS A REVERSAL: the whole-archive census now goes through a cache with a last-good fallback, so it is the request MORE likely to answer instantly and the one that can answer at all while a rebuild is failing. A window is cached by nothing — its key would come from this parameter — so it pays its 2,361 ms on EVERY call and a 503 on it is not caught by anything. Pass a floor for a narrow question; omit it for the census."},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":10000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrderOutcomeEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/matches":{"get":{"tags":["lifecycle"],"summary":"order matches that were struck and then fell through","description":"A MATCH IS NOT A TRADE UNTIL IT SETTLES, and this is the tape of the ones\nthat never did.\n\nCounterparty emits a match as `completed` or `pending`; a pending one is later\nresolved to `completed` or **`expired`** — struck, then fallen through,\ntypically because the BTC leg was never paid. MEASURED read-only on production\n2026-08-23 AS OF BLOCK 963,675: 21,125 expired against 216,585 matches held.\n`/api/trades` publishes `final_status` per fill but cannot be FILTERED on it\nand caps at one thousand rows newest-first, so the expired set has not been\nenumerable. (That cap is a `Query(le=...)` bound in source, NOT chain state.\nAn earlier revision wrote the digit and stamped it AS OF a block \"anyway\" --\nwhich is satisfying the as-of checker instead of the property it stands for.\nThe cap is spelled in words here so no reader takes it for a measurement.) It is here.\n\n⚠️ THE EXPIRED SET IS A BTC STORY, OBSERVED NOT ASSUMED: the eight commonest\npairs among the expired — all eight involving BTC — account for 19,313 of the\n21,125, led by BTC/XCP at 16,924 -- RE-DERIVED AS OF BLOCK 963,697 by grouping\nexpired matches on the unordered pair (least/greatest of forward_asset,\nbackward_asset) joined through v_match_state.match_id. ⚠️ THE TOP-EIGHT TOTAL\nIS 20,450 AS OF BLOCK 963,697 BY THAT METHOD, where an earlier revision carried\n19,313 AS OF BLOCK 963,682: the two were produced by different joins, the older\nis not reproducible here, and the method is named rather than the figure\nrestamped onto a fresh date.\n\n⚠️ THIS FILE CARRIES TWO READ HEIGHTS ON PURPOSE. Most of it is stamped AS OF\nBLOCK 963,675 -- the lane's own single read. The numbers re-derived on\n2026-08-23 after a scout found a false stamp carry AS OF BLOCK 963,697. They\nare NOT reconciled to one height, because restamping a number nobody\nre-measured is exactly the defect being corrected: PROVENANCE IS PER\nMEASUREMENT, NOT PER FILE. These five were read at BOTH heights and are\nunchanged between them, AS OF BLOCK 963,697: 216,585 / 23,999 / 21,125 /\n237 / 280,312. That is consistent with an unpaid BTC leg\nand is an observation about pairs, not a proven cause per match.\n\nMEASURED through the read-only guard, 2026-08-23: a filtered page —\n**125 ms cold, 52 ms warm**; offset=21,100 — **89 ms**; the census — **104 ms**;\nthe whole-tape denominator — **45 ms**.\n\n⛔ WHAT THIS ROUTE CANNOT SEE — AND THIS IS THE IMPORTANT ONE:\n  * **192,586 matches, AS OF BLOCK 963,697.** `v_match_state` is built from\n    ORDER_MATCH_UPDATE, so it holds only the 23,999 matches that ever needed\n    resolving (same read: 216,585 total minus 23,999 -- a DERIVED number, and\n    it now carries the SAME height as both its inputs. An earlier revision\n    stamped it LATER than the numbers it is computed from, which cannot be true.) A match that\n    settled on the spot never produced an update and is absent. Read\n    `census.matches_total` before computing any rate: `expired /\n    matches_with_an_update` overstates the chain's failure rate by 9x.\n  * Assets, quantities, prices, addresses and blocks. This view carries an id\n    and a state and nothing else. The detail is in /api/trades, keyed by the\n    same match_id, and `maker_tx` / `taker_tx` here are the two txids to look\n    up. Joining them per row would be a full unindexed pass over the match\n    tape, which is a rollup decision with a schema cost, not a route.\n  * WHY a match expired. The BTC leg is the pattern, never a per-row fact.","operationId":"matches_api_matches_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"'expired' or 'completed'. Omit for both.","title":"Status"},"description":"'expired' or 'completed'. Omit for both."},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"asc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/MatchStateEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/launches/status":{"get":{"tags":["lifecycle"],"summary":"per-launch status AND the block that status landed in","description":"WHEN a launch reached its status, which nothing else publishes.\n\n`/api/launches` already serves a launch's status — through `v_launch`, which\nCOALESCEs this view's status with the announce-time one. Two things it does\nnot serve, and this route does:\n  1. **`status_block`** — the block the status landed in. \"When did this\n     launch close\" has been unanswerable from the API.\n  2. The **distinction** the COALESCE hides: whether a launch's status came\n     from an actual FAIRMINTER_UPDATE or is just the status it was announced\n     with. Everything in this response is the first kind.\n\nMEASURED read-only on production 2026-08-23 AS OF BLOCK 963,675: 237 launches\ncarry a status update, against 417 announced — so 180 are invisible here, and\n`census` says so on every response rather than leaving the reader to assume\n237 is the population. Status blocks span 866,297-963,690, AS OF BLOCK 963,697.\n\nMEASURED through the read-only guard, 2026-08-23: a page — **35 ms**; the\ncensus — **44 ms**; the source probe — **6 ms**.\n\n⛔ WHAT THIS ROUTE CANNOT SEE:\n  * The 180 announced launches with no status update. Their only status is the\n    announce-time one, and /api/launches serves it.\n  * Asset, creator, caps, price, mint counts, XCP raised, conformance — none\n    of it is in this view. /api/launches and /api/launches/conformance carry it.\n  * The launch's status HISTORY. This is the NEWEST update per launch only.\n  * A deadline rewrite as such: an update carrying no status key leaves\n    `status` null here, and /api/launches/deadline-rewrites is where that\n    event is published properly.","operationId":"launch_status_api_launches_status_get","parameters":[{"name":"status","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Exact status, e.g. 'closed'.","title":"Status"},"description":"Exact status, e.g. 'closed'."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":1000,"minimum":1,"default":200,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/LaunchStatusEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/reorgs/events":{"get":{"tags":["lifecycle"],"summary":"the individual events a reorg took back, with both block hashes","description":"THE PER-EVENT EVIDENCE BEHIND A REORG — both hashes, side by side.\n\nA public Counterparty node keeps a current-state view: once a block is reorged\nout it is simply forgotten. This archive copies the losing chain's rows out\nbefore the delete. `/api/reorgs` answers \"which forks did we see\";\nthis answers \"which EVENTS were taken back, and under which two hashes\" — the\nrows a third party needs to check the claim rather than trust it.\n\nMEASURED read-only on production 2026-08-23 AS OF BLOCK 963,675: 366 rows\npreserved, of which **364 are re-fetches of a block whose hash never changed**\nand **2 genuinely diverged** — one fork, at a single height, two events. The\ncensus publishes all three numbers so the 2 is never read as the whole story.\n\nMEASURED through the read-only guard, 2026-08-23: a page — **12 ms**; the\ncensus — **18 ms**; the presence probe — **1.3 ms**.\n\n⛔ WHAT THIS ROUTE CANNOT SEE:\n  * Any reorg from before the preservation migration was applied, or one that\n    happened while the poller was stopped. Those rows were deleted with no\n    copy. An empty answer here is a statement about THIS NODE'S OBSERVATION\n    WINDOW, never about the chain.\n  * The winning block's events. Only the losing chain's rows are copied out;\n    the canonical ones are simply the archive's normal contents.\n  * Why the fork happened. It carries the two hashes and the height; that is\n    what makes it checkable, and it is all it claims.","operationId":"reorg_events_api_reorgs_events_get","parameters":[{"name":"event","in":"query","required":false,"schema":{"anyOf":[{"type":"string","maxLength":64},{"type":"null"}],"description":"Exact event type, e.g. NEW_BLOCK.","title":"Event"},"description":"Exact event type, e.g. NEW_BLOCK."},{"name":"from_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":0,"title":"From Block"}},{"name":"to_block","in":"query","required":false,"schema":{"type":"integer","maximum":100000000,"minimum":0,"default":100000000,"title":"To Block"}},{"name":"include_params","in":"query","required":false,"schema":{"type":"boolean","description":"Also return each event's full payload. OFF by default because a large fork would return a large payload per row; null means NOT REQUESTED, never empty.","default":false,"title":"Include Params"},"description":"Also return each event's full payload. OFF by default because a large fork would return a large payload per row; null means NOT REQUESTED, never empty."},{"name":"order","in":"query","required":false,"schema":{"type":"string","pattern":"^(asc|desc)$","default":"desc","title":"Order"}},{"name":"limit","in":"query","required":false,"schema":{"type":"integer","maximum":500,"minimum":1,"default":100,"title":"Limit"}},{"name":"offset","in":"query","required":false,"schema":{"type":"integer","maximum":100000,"minimum":0,"default":0,"title":"Offset"}}],"responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/OrphanEventEnvelope"}}}},"422":{"description":"Validation Error","content":{"application/json":{"schema":{"$ref":"#/components/schemas/HTTPValidationError"}}}},"503":{"description":"The request could not be answered. ⚠️ `{\"detail\": …}` is the DATABASE branch — `database error: QueryCanceled` is the read-only role's `statement_timeout` firing, and is RETRYABLE, ideally with a narrower block window; `database pool not initialised` is this process not being wired to Postgres yet. The `{\"error\", \"detail\", \"source\"}` branch is the PAYMENT GATE declining to SELL (a refusal to take money, not a refusal of the caller) and can only occur while payment enforcement is on. ⚠️ BRANCH ON `reason` — BUT TEST FOR ITS ABSENCE FIRST. This sentence used to say `reason` 'names all six refusals'; it does not (W139). TWO of the six 503 branches build a body with NO `reason` key at all — `unresolved_resource` and `not_in_catalog` — so a missing `reason` means 'unclassified refusal', never a default. The values that DO reach a body are `stale_data`, `freshness_unknown`, `params_not_supplied`, `needs_decision`, and whatever rail failure the catch-all carries (`no_rail_registered` and its siblings). ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/ErrorDetail"},{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}},"/api/pricing":{"get":{"summary":"The price list: what is available, and what it costs","description":"The table of contents: every route, what it answers, what it costs.\n\nFree, and it always will be — a price list you have to buy is not a\nprice list. Generated from the price catalog on every request, so it\ncannot drift from the 402 offers built from the same rows.","operationId":"pricing_api_pricing_get","responses":{"200":{"description":"Successful Response","content":{"application/json":{"schema":{}}}},"503":{"description":"The request could not be answered. ⚠️ THIS OPERATION CANNOT FAIL THE WAY THE OTHERS DO — it is free in the catalog and its handler reaches no database call, so there is no `{\"detail\": …}` branch here. The ONE 503 it can serve is the PAYMENT GATE refusing BEFORE the resource is identified: `resolve_resource()` answers UNRESOLVED when route matching itself fails, and that branch runs ahead of — and independently of — which route was asked for. It can only occur while payment enforcement is on. ⚠️ A 503 NEVER MEANS THE ARCHIVE IS WRONG; it means this question was not answered.","content":{"application/json":{"schema":{"oneOf":[{"$ref":"#/components/schemas/PaymentRefusal"}]}}}}},"x-price":{"access":"free","sellable":false,"price":null}}}},"components":{"schemas":{"BlockEventsEnvelope":{"properties":{"block":{"anyOf":[{"$ref":"#/components/schemas/BlockRow"},{"type":"null"}]},"complete":{"type":"boolean","title":"Complete","description":"The `blocks.complete` INGEST column: true only once EVERY event of this block was committed, in the same transaction, so a crash mid-block leaves it false and the block is simply re-ingested. It is an ARCHIVE HONESTY flag — false means an absent event type here is NOT evidence the chain lacks it. ⛔ IT IS NOT THE `Envelope.complete` PAGING FLAG, which is a claim about THE PAGE and pairs with `next_offset`. Same word, different field, different contract: this one is about the block, that one is about the response. Tell them apart by `result_state`: the paging flag always sits beside one, and this flag never does. Do NOT use `next_offset` for it — GET /api/blocks/{height}/events pages this block's events, and its body carries `truncated` and `next_offset` beside this flag, so `complete: true` with `truncated: true` is a normal answer: a block archived whole whose events did not fit in one page. ⚠️ Promoted from `block.complete`, and FALSE when no `blocks` row exists at all — which is the normal shape for backfilled history, where `block` is null beside a NON-EMPTY `result`. So `false` here means 'not known to be whole', never 'known to be partial'; read `coverage`, `coverage_reason` and `missing_types` for what this archive actually claims at this height."},"block_absent_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Absent Reason"},"result":{"items":{"$ref":"#/components/schemas/BlockScopedEventRow"},"type":"array","title":"Result"},"coverage":{"type":"string","title":"Coverage"},"coverage_reason":{"type":"string","title":"Coverage Reason"},"missing_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Missing Types"},"coverage_note":{"type":"string","title":"Coverage Note"}},"type":"object","required":["block","complete","block_absent_reason","result","coverage","coverage_reason","missing_types","coverage_note"],"title":"BlockEventsEnvelope","description":"GET /api/blocks/{height}/events.\n\n`block` is null whenever no `blocks` row exists — which legitimately co-occurs\nwith a NON-EMPTY `result` for backfilled history.\n`missing_types` is tri-state: null = the type universe is unknown, [] = nothing\nmissing."},"BlockRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"block_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Hash"},"block_time":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Time"},"event_count":{"type":"integer","title":"Event Count"},"complete":{"type":"boolean","title":"Complete","description":"The `blocks.complete` INGEST column: true only once EVERY event of this block was committed, in the same transaction, so a crash mid-block leaves it false and the block is simply re-ingested. It is an ARCHIVE HONESTY flag — false means an absent event type here is NOT evidence the chain lacks it. ⛔ IT IS NOT THE `Envelope.complete` PAGING FLAG, which is a claim about THE PAGE and pairs with `next_offset`. Same word, different field, different contract: this one is about the block, that one is about the response. Tell them apart by `result_state`: the paging flag always sits beside one, and this flag never does. Do NOT use `next_offset` for it — GET /api/blocks/{height}/events pages this block's events, and its body carries `truncated` and `next_offset` beside this flag, so `complete: true` with `truncated: true` is a normal answer: a block archived whole whose events did not fit in one page."}},"type":"object","required":["block_index","block_hash","block_time","event_count","complete"],"title":"BlockRow","description":"One archived block. `block_hash` and `block_time` are nullable."},"BlockScopedEventRow":{"properties":{"event_index":{"type":"integer","title":"Event Index"},"event":{"type":"string","title":"Event"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"params":{"additionalProperties":true,"type":"object","title":"Params","description":"The node's raw JSONB payload for this event, served whole and deliberately untyped — see `EventRow.params` for why. ⛔ `BLOCK_PARSED.params.messages_hash` IS NODE-LOCAL AND TIME-VARYING — it is NOT a consensus value and it is NOT comparable across nodes. A mismatch on this one key against another node is NOT evidence that this archive is wrong; it is a property of the field. MEASURED over 22 strided heights (280,000-963,400, 364 event rows, field by field, two independent operators): the event SETS agree everywhere, and so do the two CONSENSUS hashes `ledger_hash` and `txlist_hash` — `messages_hash` is the ONLY key both nodes emit and disagree on. In a whole-population count over blocks 952,800-963,459, 7,407 of those 10,660 BLOCK_PARSED rows held a value NO rostered node served, not even the same build that produced it. ⇒ Do not diff it, do not reconcile against it, and do not derive from it: nothing in this API reads it. ⚠️ AND THIS ROW CANNOT TELL YOU WHICH BUILD PRODUCED THE VALUE. Provenance is RECORDED PER ROW IN THE ARCHIVE — `events.source_node`, `events.node_version`, `events.node_commit` name the node and the build that wrote a row, where they were recorded (rows ingested before these columns existed carry none). Which build wrote a row is recorded, but it is NOT PUBLISHED HERE: `node_version` / `node_commit` are published by no route at all, so nothing in this response attributes this value to a build. The only provenance published anywhere in this API is `MempoolEnvelope.source_node`, `OrphanEvent.source_node`. On a row of the orphaned-event log, the `source_node` beside this payload is that row's own, copied from the archive when the event was orphaned (null where none was recorded), and it names the NODE, never the build; an archived event row outside that log carries no provenance field at all, and the mempool's `source_node` describes mempool sightings, not archived events. Publishing the build per row would be a response-shape change, which this API has declined, so the honest answer is that the build attribution exists in the archive and you cannot read it from here. ⇒ That is the reason to disregard a mismatch on this key rather than to reconcile it."}},"type":"object","required":["event_index","event","tx_hash","params"],"title":"BlockScopedEventRow","description":"An event inside one block.\n\n⚠️ NOT `EventRow`: it carries no `block_index`, because the block is the\nroute's own parameter."},"BlockTimeEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/BlockTimePoint"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"range_coverage":{"$ref":"#/components/schemas/BlockTimeRangeCoverage"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","range_coverage"],"title":"BlockTimeEnvelope"},"BlockTimePoint":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"block_time":{"type":"string","format":"date-time","title":"Block Time"},"source":{"type":"string","title":"Source","description":"'params' (read off an event) or 'envelope' (the block envelope)."}},"additionalProperties":false,"type":"object","required":["block_index","block_time","source"],"title":"BlockTimePoint"},"BlockTimeRangeCoverage":{"properties":{"from_block":{"type":"integer","title":"From Block","description":"Echoes your request."},"to_block":{"type":"integer","title":"To Block","description":"Echoes your request, unclamped."},"to_block_effective":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Block Effective","description":"The upper bound the COVERAGE ARITHMETIC actually used: min(to_block, archive head). null when the head could not be read. ⚠️ The default to_block is a parameter CEILING (100,000,000), not a chain fact; computing coverage against it published 99.4 million non-existent blocks as a measured number. The clamp is echoed rather than applied silently, because an unreported bound is a new way to be undated."},"clamped_to_archive_head":{"type":"boolean","title":"Clamped To Archive Head","description":"True when to_block_effective < to_block, i.e. you asked past the head."},"blocks_in_range":{"$ref":"#/components/schemas/Measured_int_"},"blocks_resolved":{"$ref":"#/components/schemas/Measured_int_","description":"Counted over the range you ASKED for, not the clamped one — this is what the archive actually holds, and clamping it would hide rows."},"blocks_unresolved":{"$ref":"#/components/schemas/Measured_int_"},"event_bearing_blocks_unresolved":{"$ref":"#/components/schemas/Measured_int_","description":"Event-bearing blocks in range with NO timestamp — these are the ones a time-axis chart drops. value may be null when not requested; method says so."}},"additionalProperties":false,"type":"object","required":["from_block","to_block","to_block_effective","clamped_to_archive_head","blocks_in_range","blocks_resolved","blocks_unresolved","event_bearing_blocks_unresolved"],"title":"BlockTimeRangeCoverage","description":"★ THE POINT OF THIS ROUTE. AS OF BLOCK 963,510 (2026-08-22) 45.6 percent of\nthe block span carries a timestamp (311,763 rows over 280,312-963,510), and a\ntime-axis chart over deep history silently drops the rest. Re-read\n`blocks_resolved` below; do not quote this sentence."},"BookEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/BookRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"matching":{"type":"integer","title":"Matching"},"total_open":{"type":"integer","title":"Total Open"},"next_cursor":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Next Cursor"},"has_more":{"type":"boolean","title":"Has More"},"as_of_block":{"type":"integer","title":"As Of Block"},"archive_head":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Archive Head","description":"The archive head this answer was SERVED against — max(block_index) FROM blocks WHERE complete, the same expression /api/health publishes as `last_block`. It is read once, by the cache helper, in the same breath as the decision that chose these rows — not a second lookup that could have moved. NOT `ingest_health.chain_tip`: that is what the poller last saw the NODE's tip to be, and it diverges from the archive under catch-up and after a reorg. Declared optional in this schema; the handler always emits it."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when `as_of_block != archive_head`, i.e. these rows were NOT built at the current archive head: the build for it did not finish and you were served the last good book instead, or you were handed a concurrent build's rows from an earlier head. The status code cannot tell you this and neither can the latency: a stale answer can arrive quickly. ⛔ THIS IS THE CACHE-vs-ARCHIVE AXIS AND IS NOT `freshness.stale` / `stale_reason_codes`, which are stable API on the metrics surface and describe ARCHIVE-vs-CHAIN. A response can carry stale_fallback=true while the archive itself is level with the chain — that is the normal shape of this outage. ⚠️ AFTER A REORG the archive head DECREASES, so a fallback can publish `as_of_block` GREATER than `archive_head`: the flag is still true and a lag computed as archive_head - as_of_block is NEGATIVE. Declared optional in this schema; the handler always emits it."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route is REMEMBERED right now — the route is inside a labelled-fallback window and is declining to re-attempt for a cooldown. ⛔ THIS IS A DIFFERENT QUESTION FROM `stale_fallback` AND THE TWO COME APART. `stale_fallback` is a HEAD COMPARISON (are these rows at the current archive head?); this is DID-THE-BUILD-WORK. All four combinations are reachable, and the one that motivated this field is FRESH + FAILING: a request whose own build failed can publish `stale_fallback: false` with `as_of_block == archive_head`, because a concurrent build stored rows at the current head while it was failing. Without this field the response reads healthy in exactly that case. ⛔ It is NOT `freshness.stale` / `stale_reason_codes` either — those are ARCHIVE-vs-CHAIN. Three axes, three names. ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'."},"venue":{"type":"string","title":"Venue"},"units":{"type":"string","title":"Units"},"note":{"type":"string","title":"Note"},"source":{"type":"string","title":"Source"},"book_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Book Source","description":"Present only while this route is switched to read the `order_state` table. `order_state` when these rows came from that table, read in the same database snapshot as `as_of_block`; `v_order_open` when the switch is on but the table was REFUSED and the view served the answer instead, and `book_source_refusal` then says why. Absent entirely when the switch is off, which is the default."},"reconciled_head":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reconciled Head","description":"Present only while switched. The archive head at which the last reconcile run with verdict `passed` found `order_state` equal to the `v_order_state` view it replaces. This is the SECOND half of the stamp: `as_of_block` says which block the table is current as of, and this says how far that has been independently checked. Null when no run has ever passed or the table could not be read."},"reconciled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reconciled At","description":"Present only while switched. When that passed reconcile run was recorded (ISO 8601), so the age of the last agreement is readable without a second request. Null exactly when `reconciled_head` is null."},"book_source_refusal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Book Source Refusal","description":"Present only while switched. Null when `order_state` served these rows. Otherwise `CODE: detail` naming the condition that withdrew the table (READ_ROLE_NOT_GRANTED, NO_ARCHIVE_HEAD, NO_PASSED_RUN, LAST_RUN_NOT_PASSED, AGREEMENT_TOO_OLD, HEAD_BELOW_CERTIFIED, HEAD_TOO_FAR_AHEAD, STREAK_TOO_SHORT, STREAK_SPAN_TOO_SHORT or READ_FAILED), and the rows are the view's, exactly as they would be with the switch off."}},"type":"object","required":["result","count","matching","total_open","next_cursor","has_more","as_of_block","venue","units","note","source"],"title":"BookEnvelope","description":"GET /api/book.\n\n`matching` is the number of open orders that match the give/get filter ACROSS\nALL PAGES — it does not shrink as you page; `count` is the number of rows on\nTHIS page. `matching` is ALWAYS an int here — unlike /api/trades, where it can\nbe null. `as_of_block` can be -1 when nothing is archived.\n\n⛔ The four source keys at the end exist only while the route is switched to\nread the `order_state` table, so a body served with the switch off — the\ndefault — does not carry them at all."},"BookRow":{"properties":{"tx":{"type":"string","title":"Tx"},"source":{"type":"string","title":"Source"},"give_asset":{"type":"string","title":"Give Asset"},"get_asset":{"type":"string","title":"Get Asset"},"give_quantity":{"type":"number","title":"Give Quantity"},"get_quantity":{"type":"number","title":"Get Quantity"},"give_remaining":{"type":"number","title":"Give Remaining"},"get_remaining":{"type":"number","title":"Get Remaining"},"give_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Give Divisible"},"get_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Get Divisible"},"give_quantity_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Give Quantity Normalized"},"get_quantity_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Get Quantity Normalized"},"give_remaining_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Give Remaining Normalized"},"get_remaining_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Get Remaining Normalized"},"expire_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expire Index"},"opened_block":{"type":"integer","title":"Opened Block"},"opened_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Opened At"}},"type":"object","required":["tx","source","give_asset","get_asset","give_quantity","get_quantity","give_remaining","get_remaining","give_divisible","get_divisible","give_quantity_normalized","get_quantity_normalized","give_remaining_normalized","get_remaining_normalized","expire_index","opened_block","opened_at"],"title":"BookRow","description":"One open order.\n\n`*_quantity_normalized` can be NON-NULL while the matching `*_divisible` flag is\nnull — they come from different sources and are NOT coupled.\n`expire_index` null means \"never expires\", not \"unknown\"."},"CandleRow":{"properties":{"t":{"type":"string","title":"T"},"o":{"type":"number","title":"O"},"h":{"type":"number","title":"H"},"l":{"type":"number","title":"L"},"c":{"type":"number","title":"C"},"v_base":{"type":"number","title":"V Base"},"v_quote":{"type":"number","title":"V Quote"},"n":{"type":"integer","title":"N"},"first_block":{"type":"integer","title":"First Block"},"last_block":{"type":"integer","title":"Last Block"},"bucket_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bucket Block","description":"TRADING-BLOCK BASIS ONLY: the bar's IDENTITY — the first block that traded in it. ⚠️ EQUAL TO `first_block` BY CONSTRUCTION, and that is a change: this used to be the ALIGNED start `(block_index / N) * N`, which could name a block that never traded. A bucket now BEGINS at its first trading block, so there is no aligned start to name. The invariants are bucket_block == first_block <= last_block, last_block - first_block + 1 >= trading_blocks, and trading_blocks <= bucket_blocks. Null on the time basis."},"bucket_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bucket Blocks","description":"TRADING-BLOCK BASIS ONLY: how many blocks THAT TRADED are grouped into one bar — the N in `tf=Nb`, i.e. the width ASKED FOR. ⛔ This is not a chain span: the bar runs from `first_block` to `last_block` and that distance is always at least this number and usually far greater. Compare `trading_blocks` for what the bar actually HOLDS. Null on the time basis, and it must stay null there — a duration does not hold a fixed number of blocks, so filling this in from an average block time would publish an equivalence that does not hold."},"trading_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Trading Blocks","description":"TRADING-BLOCK BASIS ONLY: how many blocks with a priced fill this bar ACTUALLY holds. Equals `bucket_blocks` on every bar except possibly the NEWEST, which holds the REMAINDER while the chain fills it. ⚠️ IT IS NOT ALWAYS SHORT: when the pair's total count of trading blocks divides evenly by N the newest bar is full too, and at `tf=1b` (N=1) every bar including the newest holds exactly 1, so none is ever partial. Do not read a partial newest bar as guaranteed — compare this field against `bucket_blocks`, which is the only reliable test and the reason this field exists: without it a forming bar's volume looks like a collapse rather than a bar in progress. Null on the time basis."}},"type":"object","required":["t","o","h","l","c","v_base","v_quote","n","first_block","last_block"],"title":"CandleRow","description":"One bar from `GET /api/candles`. o/h/l/c are never null: bars are built\nonly from fills that carry a price.\n\n⭐ ONE ROW CLASS, TWO BUCKETING BASES, AND THE KEY SET IS THE SAME ON BOTH.\n`/api/candles` buckets either by TIME (`tf=5m…1w`) or by TRADING-BLOCK COUNT\n(`tf=1b…1008b`); the envelope's `bucket_basis` says which. A block bar fills\n`bucket_block`/`bucket_blocks`/`trading_blocks` and a time bar publishes all\nthree as NULL — absent BY BASIS, never missing data — so no consumer has to\ntest for a key's existence.\n\n⛔ THE BLOCK BASIS WAS REDEFINED IN PLACE. `Nb` used to group N CONSECUTIVE\nCHAIN BLOCKS; it now groups N blocks THAT CARRIED A PRICED FILL. `bucket_basis`\nmoved from `block` to `trading_block` in the same change precisely so a consumer\nholding the old meaning finds out."},"CandlesEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/CandleRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"pair":{"type":"string","title":"Pair"},"venue":{"type":"string","title":"Venue"},"tf":{"type":"string","title":"Tf"},"bucket_basis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Bucket Basis","description":"Which bucketing produced these bars: `time` (tf 5m…1w, buckets are DURATIONS) or `trading_block` (tf 1b…1008b, buckets are COUNTS OF BLOCKS THAT TRADED). ⛔ This value used to read `block`, when `Nb` meant N consecutive CHAIN blocks; the word changed with the meaning so that a consumer holding the old definition can detect it rather than silently misread every bar. ⛔ The two bases are not interchangeable on this chain and neither is derivable from the other, which is why this is published rather than inferred from `tf`. Declared optional in this schema; the handler emits it on every branch."},"bucket_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Bucket Blocks","description":"The bar width in TRADING BLOCKS on the trading-block basis; null on the time basis. Same field name and meaning as `CandleRow.bucket_blocks`, so a bar detached from this envelope still carries its own width."},"price_basis":{"type":"string","title":"Price Basis"},"truncated":{"type":"boolean","title":"Truncated"},"absence_means":{"type":"string","title":"Absence Means"},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","count","pair","venue","tf","price_basis","truncated","absence_means","source"],"title":"CandlesEnvelope","description":"GET /api/candles.\n\n`absence_means` is always present; only its text varies by branch."},"Conformance":{"type":"string","enum":["conformant","nonconformant","unknown"],"title":"Conformance","description":"★ THREE STATES, AND THE THIRD IS THE FINDING. Some launches carry a NULL\n`params_conformant` because the ANNOUNCEMENT ITSELF was invalid and carries no\nparameters to judge; folding them into `nonconformant` would over-report\nnon-conformance, and that is exactly the boolean/string disagreement this\ncontract exists to prevent.\n⚠️ THE STRUCTURE IS THE CLAIM; THE COUNT IS CHAIN STATE. AS OF BLOCK 963,510\n(2026-08-22) the split was 94 conformant / 298 nonconformant / 17 unknown of\n409. It was 88 / 298 / 17 of 403 twelve hours earlier — six launches confirmed\nin between. **Read `summary`, which is dated on every response.**"},"ConformanceEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/LaunchConformanceRow"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"summary":{"$ref":"#/components/schemas/ConformanceSummary"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","summary"],"title":"ConformanceEnvelope"},"ConformanceSummary":{"properties":{"conformant":{"$ref":"#/components/schemas/Measured_int_"},"nonconformant":{"$ref":"#/components/schemas/Measured_int_"},"unknown":{"$ref":"#/components/schemas/Measured_int_","description":"Announcement itself invalid; no parameters to judge. NOT 'no'."},"start_before_announce":{"$ref":"#/components/schemas/Measured_int_"},"total_launches":{"$ref":"#/components/schemas/Measured_int_"}},"additionalProperties":false,"type":"object","required":["conformant","nonconformant","unknown","start_before_announce","total_launches"],"title":"ConformanceSummary"},"Coverage":{"properties":{"source":{"type":"string","title":"Source","description":"The view or table actually read."},"filters_applied":{"items":{"type":"string"},"type":"array","title":"Filters Applied","description":"Every filter that shaped this answer, in words."},"block_floor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Block Floor","description":"Below this block the family does not exist in this archive."},"block_range_returned":{"anyOf":[{"items":{"type":"integer"},"type":"array"},{"type":"null"}],"title":"Block Range Returned","description":"[min, max] block of the rows returned. null means EITHER no rows were returned OR this route's rows are not block-keyed (the venue-gap series is keyed by sample time; the failure taxonomy is keyed by reason). ⚠️ Read row_count to tell those apart — SCOUT 8 found null being published beside rows, contradicting the older wording of this field."},"rows_newer_than_as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Rows Newer Than As Of Block","description":"How many returned rows carry a block ABOVE freshness.as_of_block. ⚠️ Normally 0. It can be non-zero because freshness is read before the page, on a separate connection, so blocks ingested in between arrive under an older stamp. MEASURED, not promised: an agent pinning as_of_block for reproducibility can see exactly how many rows it would not get back. null = this route's rows are not block-keyed."},"source_populated":{"type":"boolean","title":"Source Populated","description":"Whether the SOURCE holds any rows at all. MEASURED, never assumed."},"source_checked_at":{"type":"string","format":"date-time","title":"Source Checked At","description":"When source_populated was established."},"excluded":{"items":{"type":"string"},"type":"array","title":"Excluded","description":"What this answer does NOT cover. An empty list is a positive claim."}},"additionalProperties":false,"type":"object","required":["source","filters_applied","block_floor","source_populated","source_checked_at","excluded"],"title":"Coverage","description":"What this answer covers, and — as a required field — what it does not."},"CoverageBlockEnvelope":{"properties":{"height":{"type":"integer","title":"Height"},"coverage":{"type":"string","title":"Coverage"},"reason":{"type":"string","title":"Reason"},"poller_row":{"type":"boolean","title":"Poller Row"},"poller_complete":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Poller Complete"},"event_count_claimed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Event Count Claimed"},"events_held":{"type":"integer","title":"Events Held"},"types_covered":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Types Covered"},"types_covered_basis":{"type":"string","title":"Types Covered Basis"},"types_on_chain":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Types On Chain"},"missing_types":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}],"title":"Missing Types"},"source":{"type":"string","title":"Source"},"note":{"type":"string","title":"Note"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["height","coverage","reason","poller_row","poller_complete","event_count_claimed","events_held","types_covered","types_covered_basis","types_on_chain","missing_types","source","note"],"title":"CoverageBlockEnvelope","description":"GET /api/coverage/block/{height}.\n\n`poller_complete` and `event_count_claimed` are null when no `blocks` row exists;\n`types_covered`, `types_on_chain` and `missing_types` are null when no census is\nrecorded — and for `missing_types`, null is semantically distinct from []."},"DeadlineRewriteRow":{"properties":{"asset":{"type":"string","title":"Asset"},"tx_hash":{"type":"string","title":"Tx Hash"},"announce_block":{"type":"integer","title":"Announce Block"},"original_deadline_block":{"type":"integer","title":"Original Deadline Block"},"rewritten_deadline_block":{"type":"integer","title":"Rewritten Deadline Block"},"deadline_delta_blocks":{"type":"integer","title":"Deadline Delta Blocks"},"moved_earlier":{"type":"boolean","title":"Moved Earlier"},"rewrite_block":{"type":"integer","title":"Rewrite Block"},"rewrite_event_index":{"type":"integer","title":"Rewrite Event Index"},"rewritten_to_own_block":{"type":"boolean","title":"Rewritten To Own Block"}},"type":"object","required":["asset","tx_hash","announce_block","original_deadline_block","rewritten_deadline_block","deadline_delta_blocks","moved_earlier","rewrite_block","rewrite_event_index","rewritten_to_own_block"],"title":"DeadlineRewriteRow"},"DeadlineRewritesEnvelope":{"properties":{"rewrites":{"items":{"$ref":"#/components/schemas/DeadlineRewriteRow"},"type":"array","title":"Rewrites"},"rewrites_observed":{"type":"integer","title":"Rewrites Observed"},"limit":{"type":"integer","title":"Limit"},"truncated":{"type":"boolean","title":"Truncated"},"availability":{"$ref":"#/components/schemas/RewriteAvailability"},"absence_means":{"type":"string","title":"Absence Means"},"direction_note":{"type":"string","title":"Direction Note"},"population_note":{"type":"string","title":"Population Note"},"original_note":{"type":"string","title":"Original Note"},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["rewrites","rewrites_observed","limit","truncated","availability","absence_means","direction_note","population_note","original_note","source"],"title":"DeadlineRewritesEnvelope","description":"GET /api/launches/deadline-rewrites."},"DispenseRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"dispenser_tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dispenser Tx Hash"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"The dispenser address."},"destination":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Destination","description":"The buyer."},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"dispense_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Dispense Quantity Normalized"},"btc_amount_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Btc Amount Normalized"},"dispensed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Dispensed At"}},"additionalProperties":false,"type":"object","required":["block_index","event_index","tx_hash","dispenser_tx_hash","source","destination","asset","dispense_quantity_normalized","btc_amount_normalized","dispensed_at"],"title":"DispenseRow"},"DispenserOpen":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"give_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Quantity Normalized"},"give_remaining_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining Normalized","description":"⚠️ AT OPEN. Refills are NOT in this archive — see coverage.excluded."},"escrow_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Escrow Quantity Normalized"},"satoshirate":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Satoshirate","description":"Satoshis per give_quantity. The dispenser's price."},"satoshirate_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Satoshirate Normalized"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"oracle_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Oracle Address"},"opened_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opened At"}},"additionalProperties":false,"type":"object","required":["block_index","event_index","tx_hash","source","origin","asset","give_quantity_normalized","give_remaining_normalized","escrow_quantity_normalized","satoshirate","satoshirate_normalized","status","oracle_address","opened_at"],"title":"DispenserOpen"},"DispenserOpenCensus":{"properties":{"confirmed_open":{"$ref":"#/components/schemas/Measured_int_","description":"Latest DISPENSER_UPDATE says open, with inventory left."},"presumed_open":{"$ref":"#/components/schemas/Measured_int_","description":"No DISPENSER_UPDATE has EVER been observed for this dispenser. Not a claim that it is open — a statement that we never saw it close."},"total_open":{"$ref":"#/components/schemas/Measured_int_","description":"confirmed_open + presumed_open. ⛔ Do not quote this alone."}},"additionalProperties":false,"type":"object","required":["confirmed_open","presumed_open","total_open"],"title":"DispenserOpenCensus","description":"The confirmed/presumed split, computed over THIS RESPONSE'S FILTERS.\n\n⚠️ THE DENOMINATOR IS THE HALF THAT ROTS, so it is not a constant here: these\nthree numbers are re-measured per request under the same `asset` / block-range\npredicates as `rows`, never over the whole view. A census under a different\nfilter than the page it describes is a number that reads as a fact and is not\none. ⛔ `total_open` is the sum, and it is the number NOBODY should quote on\nits own — publishing it beside its two components is the whole point."},"DispenserOpenNow":{"properties":{"opened_block":{"type":"integer","title":"Opened Block"},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"The dispenser address — where the tokens are."},"origin":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Origin"},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"asset_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset Divisible","description":"READ from the opening event, never assumed. NULL means unknown, which is NOT 'divisible'."},"give_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Quantity Normalized","description":"Tokens released per payment of `satoshirate`. A dispenser term, not a balance."},"escrow_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Escrow Quantity Normalized","description":"AT OPEN. History."},"give_remaining_at_open_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining At Open Normalized","description":"⚠️ AT OPEN — the same number `/api/dispensers` serves under the bare name `give_remaining_normalized`. Kept here so a row can show both."},"satoshirate":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Satoshirate","description":"Satoshis per give_quantity. The dispenser's price."},"satoshirate_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Satoshirate Normalized"},"oracle_address":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Oracle Address"},"opened_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opened At"},"give_remaining":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining","description":"NOW, raw integer."},"give_remaining_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining Normalized","description":"⭐ NOW, in the asset's own units — the field this route exists for. Read from the node's own `give_remaining_normalized` on the latest DISPENSER_UPDATE that carried one; NEVER divided by 1e8 here. ⛔⛔ ON A `presumed_open` ROW THIS IS NOT A FRESH READING. With no update ever observed there is nothing to read, so it falls back to the opening event and equals `give_remaining_at_open_normalized` exactly. Check `open_evidence` before treating this as current. ⚠️ Does NOT account for refills: REFILL_DISPENSER is not held."},"status_code":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status Code","description":"The chain's raw code. '0' open, '10' closed, '11' closing."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"Our reading of status_code. An unrecognised code becomes 'unknown_status_<code>' and is excluded from this route — a code we cannot read is not a dispenser we may tell you to buy from."},"update_observed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Update Observed","description":"⛔ false does NOT mean 'closed'. It means this archive has never seen this dispenser say anything since it opened."},"updated_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Updated Block","description":"Block of the latest DISPENSER_UPDATE, or null if none."},"open_evidence":{"type":"string","title":"Open Evidence","description":"⛔⛔ 'confirmed_open' — the chain told us it is open and we hold the update. 'presumed_open' — we watched it open and never observed it close; NOT KNOWN TO BE OPEN. The archive is a live-observation window that began at a block, so a dispenser that opened and closed before we were watching its updates is indistinguishable from one still standing."}},"additionalProperties":false,"type":"object","required":["opened_block","event_index","tx_hash","source","origin","asset","asset_divisible","give_quantity_normalized","escrow_quantity_normalized","give_remaining_at_open_normalized","satoshirate","satoshirate_normalized","oracle_address","opened_at","give_remaining","give_remaining_normalized","status_code","status","update_observed","updated_block","open_evidence"],"title":"DispenserOpenNow","description":"One dispenser this archive believes is STANDING RIGHT NOW, with what is\nleft in it — the question `/api/dispensers` cannot answer.\n\n⛔⛔ READ `open_evidence` BEFORE ANY OTHER FIELD. Two thirds of these rows are\n`presumed_open`: we watched them open and have never observed them close. That\nis NOT the same claim as `confirmed_open`, and a consumer that treats them as\none is buying a number this archive cannot support. See the field's own\ndescription and `derive.sql`'s `v_dispenser_state` banner."},"DispenserOpenNowEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/DispenserOpenNow"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/DispenserOpenCensus"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"DispenserOpenNowEnvelope"},"Envelope_DispenseRow_":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/DispenseRow"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats"],"title":"Envelope[DispenseRow]"},"Envelope_DispenserOpen_":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/DispenserOpen"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats"],"title":"Envelope[DispenserOpen]"},"EventCountRow":{"properties":{"event":{"type":"string","title":"Event"},"count":{"type":"integer","title":"Count"}},"type":"object","required":["event","count"],"title":"EventCountRow"},"EventCountsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/EventCountRow"},"type":"array","title":"Result"},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when this answer's cached rows are OLDER THAN THE ROUTE'S CACHE TTL — the rebuild did not finish and you were served the LAST GOOD answer instead of a 503. The 200 is then a labelled fallback, not a fresh answer. ⚠️ THE PREDICATE IS THE ROWS' AGE AGAINST THE TTL, not 'the fallback path ran': a concurrent rebuild can finish while this one fails, and that answer is genuinely fresh and is reported so — read `rebuild_failing` for the other axis. ⛔ Not `freshness.stale`, which is ARCHIVE-vs-CHAIN."},"rows_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rows Age S","description":"How old the cached part of this answer is, in seconds, since the build that produced it FINISHED. A DURATION from a monotonic clock, never a wall-clock timestamp, so an ntp step cannot move it. Recovered from the cache's own stamp even when another request did the build."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route's cache is REMEMBERED right now — the route is inside a labelled-fallback window and declining to re-attempt for a cooldown. ⛔ A THIRD AXIS, NOT A SECOND NAME: the combination it exists for is FRESH + FAILING (a concurrent flight stored rows at this instant while this request's own build was cancelled). ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'."},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result"],"title":"EventCountsEnvelope","description":"GET /api/events/counts.\n\n`stale_fallback`, `rows_age_s` and `rebuild_failing` describe the cached counts.\n`as_of_block` on this route is the head the counts were BUILT at, so it needs no\n`built_at_block` twin."},"EventRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"event":{"type":"string","title":"Event"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"params":{"additionalProperties":true,"type":"object","title":"Params","description":"★ DELIBERATELY UNTYPED, and this is the honest answer rather than a gap. `params` is the node's raw JSONB payload and its keys differ per `event` type across 62 event types — a NEW_FAIRMINT and a BLOCK_PARSED share no field. Declaring any one shape here would be false for 61 of them. ⛔ `BLOCK_PARSED.params.messages_hash` IS NODE-LOCAL AND TIME-VARYING — it is NOT a consensus value and it is NOT comparable across nodes. A mismatch on this one key against another node is NOT evidence that this archive is wrong; it is a property of the field. MEASURED over 22 strided heights (280,000-963,400, 364 event rows, field by field, two independent operators): the event SETS agree everywhere, and so do the two CONSENSUS hashes `ledger_hash` and `txlist_hash` — `messages_hash` is the ONLY key both nodes emit and disagree on. In a whole-population count over blocks 952,800-963,459, 7,407 of those 10,660 BLOCK_PARSED rows held a value NO rostered node served, not even the same build that produced it. ⇒ Do not diff it, do not reconcile against it, and do not derive from it: nothing in this API reads it. ⚠️ AND THIS ROW CANNOT TELL YOU WHICH BUILD PRODUCED THE VALUE. Provenance is RECORDED PER ROW IN THE ARCHIVE — `events.source_node`, `events.node_version`, `events.node_commit` name the node and the build that wrote a row, where they were recorded (rows ingested before these columns existed carry none). Which build wrote a row is recorded, but it is NOT PUBLISHED HERE: `node_version` / `node_commit` are published by no route at all, so nothing in this response attributes this value to a build. The only provenance published anywhere in this API is `MempoolEnvelope.source_node`, `OrphanEvent.source_node`. On a row of the orphaned-event log, the `source_node` beside this payload is that row's own, copied from the archive when the event was orphaned (null where none was recorded), and it names the NODE, never the build; an archived event row outside that log carries no provenance field at all, and the mempool's `source_node` describes mempool sightings, not archived events. Publishing the build per row would be a response-shape change, which this API has declined, so the honest answer is that the build attribution exists in the archive and you cannot read it from here. ⇒ That is the reason to disregard a mismatch on this key rather than to reconcile it."}},"type":"object","required":["block_index","event_index","event","tx_hash","params"],"title":"EventRow","description":"One archived Counterparty event."},"EventsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/EventRow"},"type":"array","title":"Result"},"next_cursor":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Cursor"},"truncated":{"type":"boolean","title":"Truncated"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"tx_hash_filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash Filter"},"tx_hash_filter_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash Filter Note"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","next_cursor","truncated","note","tx_hash_filter","tx_hash_filter_note"],"title":"EventsEnvelope","description":"GET /api/events.\n\nAll six keys are always present; the tx_hash fields are published as null rather\nthan omitted, on purpose."},"FailedMintAttempt":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"The address that attempted the mint and failed."},"reason":{"type":"string","title":"Reason","description":"The node's own `status` string, verbatim."},"tx_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Tx Index"},"attempted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Attempted At"}},"additionalProperties":false,"type":"object","required":["block_index","event_index","tx_hash","source","reason","tx_index","attempted_at"],"title":"FailedMintAttempt","description":"One `NEW_FAIRMINT` the node recorded as invalid: the payer, the block, and\nthe reason. ⚠️ THESE ROWS CARRY NO ASSET AND NO PAID QUANTITY — the node emits\nthem without one — so they can NOT be attributed to a launch. That is a\nproperty of the source, not an omission here."},"FailedMintCensus":{"properties":{"new_fairmint_events_held":{"$ref":"#/components/schemas/Measured_int_"},"valid_mints":{"$ref":"#/components/schemas/Measured_int_","description":"What v_fairmint and /api/mints serve."},"failed_attempts":{"$ref":"#/components/schemas/Measured_int_","description":"What v_fairmint drops — and this route serves. AS OF BLOCK 963,510 (2026-08-22) this was 22,799 of 213,728, or 10.667 percent; both counts come back dated on every response, so use those, not this sentence."},"failed_share_pct":{"$ref":"#/components/schemas/Measured_Decimal_"},"distinct_reasons":{"$ref":"#/components/schemas/Measured_int_"}},"additionalProperties":false,"type":"object","required":["new_fairmint_events_held","valid_mints","failed_attempts","failed_share_pct","distinct_reasons"],"title":"FailedMintCensus","description":"★★ THE FOOTGUN, PUBLISHED. Anyone computing a mint rate from `v_fairmint`\n(or from `/api/mints`) while comparing to a node's raw `NEW_FAIRMINT` count is\nlow by `failed_share_pct` and will not know."},"FailedMintEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/FailedMintAttempt"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/FailedMintCensus"},"built_at_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Built At Block","description":"The archive head the rows and the census were COUNTED at. Equals `freshness.as_of_block` on a fresh answer; on a `stale_fallback` answer it is the older head the last good pass ran against, and every census number's own `as_of_block` says the same."},"stale_fallback":{"type":"boolean","title":"Stale Fallback","description":"True when this answer was NOT counted at the current archive head: the per-head rebuild failed and you were served the LAST GOOD pass (`built_at_block`). The 200 is then a labelled fallback. ⚠️ A HEAD COMPARISON, like `/api/book`'s — never 'the fallback path ran'.","default":false},"rebuild_failing":{"type":"boolean","title":"Rebuild Failing","description":"True when a failed rebuild of this route's cached pass is REMEMBERED right now (a cooldown is in force). A third axis: it can be true beside `stale_fallback: false` when a concurrent rebuild succeeded while this request's failed. A process restart clears it with no transition.","default":false}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"FailedMintEnvelope","description":"⭐ LANE 845 · the DEFAULT request (every parameter at its default) is served\nfrom a per-archive-head cache, and these three fields say how it was served —\nthe same axes and names `/api/markets/ranking` and `/api/book` publish for their\nper-head caches. Always present; on every non-default request (which is never\ncached) they read `built_at_block == freshness.as_of_block`, `false`, `false`.\n⛔ `stale_fallback` is CACHE-vs-ARCHIVE and is NOT `freshness.stale`\n(ARCHIVE-vs-CHAIN): both appear on this response and can disagree without either\nbeing wrong."},"FailedMintReason":{"properties":{"reason":{"type":"string","title":"Reason"},"attempts":{"type":"integer","title":"Attempts"},"first_block":{"type":"integer","title":"First Block"},"last_block":{"type":"integer","title":"Last Block"},"distinct_payers":{"type":"integer","title":"Distinct Payers"},"measured_at":{"type":"string","format":"date-time","title":"Measured At","description":"These are aggregates. This is when they were computed."}},"additionalProperties":false,"type":"object","required":["reason","attempts","first_block","last_block","distinct_payers","measured_at"],"title":"FailedMintReason"},"FailedMintReasonEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/FailedMintReason"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/FailedMintCensus"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"FailedMintReasonEnvelope"},"ForkRow":{"properties":{"first_orphaned_at":{"type":"string","title":"First Orphaned At"},"from_block":{"type":"integer","title":"From Block"},"to_block":{"type":"integer","title":"To Block"},"blocks_removed":{"type":"integer","title":"Blocks Removed"},"blocks_replaced":{"type":"integer","title":"Blocks Replaced"},"blocks_no_canonical":{"type":"integer","title":"Blocks No Canonical"},"blocks_returned_identical":{"type":"integer","title":"Blocks Returned Identical"},"distinct_block_hashes":{"type":"integer","title":"Distinct Block Hashes"},"events_removed":{"type":"integer","title":"Events Removed"},"events_truly_orphaned":{"type":"integer","title":"Events Truly Orphaned"},"first_block_time":{"type":"string","title":"First Block Time"},"last_block_time":{"type":"string","title":"Last Block Time"}},"type":"object","required":["first_orphaned_at","from_block","to_block","blocks_removed","blocks_replaced","blocks_no_canonical","blocks_returned_identical","distinct_block_hashes","events_removed","events_truly_orphaned","first_block_time","last_block_time"],"title":"ForkRow"},"Freshness":{"properties":{"answered_at":{"type":"string","format":"date-time","title":"Answered At","description":"When this response was built."},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"Highest COMPLETE block in the archive. null = could not read it."},"as_of_block_time":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"As Of Block Time","description":"Bitcoin timestamp of as_of_block, or null if unmapped."},"chain_tip":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chain Tip","description":"The tip the poller last saw upstream."},"lag_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lag Blocks","description":"chain_tip - as_of_block. null when either is unknown."},"poll_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Poll Age S","description":"Seconds since the poller last checked in."},"poller_alive":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Poller Alive","description":"poll_age_s <= max_poll_age_s. null = could not determine."},"ingest_error_present":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Ingest Error Present","description":"Whether ingest_health carries an error. Text NOT republished."},"head_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Head Age S","description":"answered_at - as_of_block_time, in seconds. ★ THE ONE STALENESS SIGNAL THE POLLER DOES NOT WRITE: lag is chain_tip minus as_of_block and BOTH are written by the poller, so they freeze together when it dies. Negative means the head block is timestamped in the future."},"max_poll_age_s":{"type":"number","title":"Max Poll Age S","description":"The threshold poller_alive was judged against."},"max_lag_blocks":{"type":"integer","title":"Max Lag Blocks","description":"The threshold lag was judged against."},"max_head_age_s":{"type":"number","title":"Max Head Age S","description":"The threshold head_age_s was judged against. Derived as max_lag_blocks x 600s, so there is ONE operator knob, not three."},"stale":{"type":"boolean","title":"Stale","description":"TRUE if this answer must not be trusted as current."},"stale_reasons":{"items":{"type":"string"},"type":"array","title":"Stale Reasons","description":"Why, in prose. Empty iff stale is false."},"stale_reason_codes":{"items":{"type":"string"},"type":"array","title":"Stale Reason Codes","description":"The same reasons as STABLE MACHINE CODES — branch on these, not on the prose. Four of them (poller_stalled, lag_unknown, lag_exceeded, ingest_error_recorded) are spelled exactly as /api/health spells them, so the free and paid surfaces can be diffed by a machine."}},"additionalProperties":false,"type":"object","required":["answered_at","as_of_block","as_of_block_time","chain_tip","lag_blocks","poll_age_s","poller_alive","ingest_error_present","head_age_s","max_poll_age_s","max_lag_blocks","max_head_age_s","stale","stale_reasons","stale_reason_codes"],"title":"Freshness","description":"The archive's own state at the moment this response was built.\n\n⚠️ THE ONE FIELD A MACHINE SHOULD BRANCH ON IS `stale`. Everything else is the\nevidence for it. `stale_reasons` is empty if and only if `stale` is false."},"GapsEnvelope":{"properties":{"from":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"From"},"to":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To"},"scanned_blocks":{"type":"integer","title":"Scanned Blocks"},"limit":{"type":"integer","title":"Limit"},"gaps":{"items":{"type":"integer"},"type":"array","title":"Gaps"},"truncated":{"type":"boolean","title":"Truncated"},"note":{"type":"string","title":"Note"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["from","to","scanned_blocks","limit","gaps","truncated","note"],"title":"GapsEnvelope","description":"GET /api/gaps. Every branch returns the same key set.\n\n`from` and `to` are null only when the archive holds no COMPLETE block — an\narchive holding only incomplete blocks answers the same way — and `note` then\nsays so. `gaps` is a FLAT LIST OF HEIGHTS, not row objects."},"HTTPValidationError":{"properties":{"detail":{"items":{"$ref":"#/components/schemas/ValidationError"},"type":"array","title":"Detail"}},"type":"object","title":"HTTPValidationError"},"HealthEnvelope":{"properties":{"ok":{"type":"boolean","title":"Ok"},"ok_reasons":{"items":{"type":"string"},"type":"array","title":"Ok Reasons"},"disk_free_gb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Disk Free Gb"},"disk_total_gb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Disk Total Gb"},"disk_ok":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Disk Ok"},"disk_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Disk Error"},"min_free_gb":{"type":"number","title":"Min Free Gb"},"mem_total_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mem Total Mb","description":"MemTotal, MB. NULL when memory could not be measured at all — see `mem_error`."},"mem_available_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Mem Available Mb","description":"MemAvailable, MB — the number `mem_ok` is judged on. NULL when unmeasured. ⚠️ NOT MemFree, which exists on kernels before 3.14 and is a different, much smaller number; a partial read is reported as an error rather than substituted for."},"swap_total_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Swap Total Mb","description":"SwapTotal, MB. NULL when unmeasured."},"swap_used_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Swap Used Mb","description":"SwapTotal - SwapFree, MB. NULL unless BOTH were read."},"mem_ok":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Mem Ok","description":"⛔ TRI-STATE, exactly like `disk_ok`, and the third state is the point: NULL means memory could not be measured on this host (`/proc/meminfo` does not exist off Linux) and that is NOT A PASS — a check that passes when it cannot verify is worse than no check, because it is believed. false = measured and below `min_avail_mb`. true = measured and at or above it."},"mem_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Mem Error","description":"Names WHY the probe could not produce a number — the exception for an unreadable /proc/meminfo, or the specific lines that were missing from it. NULL when the probe succeeded."},"min_avail_mb":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Min Avail Mb","description":"The threshold `mem_ok` was judged against, published so the judgement is auditable from the payload instead of from the source."},"mem_gates_ok":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Mem Gates Ok","description":"⛔ THE DECISION, ON THE WIRE. false — memory contributes to NEITHER `ok` NOR `ok_reasons`, deliberately. A consumer seeing `mem_ok: false` beside `ok: true` is NOT looking at a contradiction and must not have to read the source to learn that: memory is reported here, and this route's status code does not answer for it. If memory is ever made to gate, this field flips with it and says so."},"last_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Block"},"chain_tip":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Chain Tip"},"lag_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Lag Blocks"},"poller_alive":{"type":"boolean","title":"Poller Alive"},"poll_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Poll Age S"},"last_poll_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Poll At"},"last_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error"},"last_error_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Error At"},"last_error_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Last Error Age S"},"build_sha":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Sha","description":"Git sha of the tree the server deploy shipped, written to the build-identity file at deploy time and read ONCE at process start. ⛔ null means the identity could NOT be read and `build_id_error` says why — it NEVER means 'unchanged' and is never guessed at. There is no git checkout on the server, so this file is the only thing on the machine that knows."},"build_committed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Committed At","description":"COMMITTER date of `build_sha` (git %cI), ISO-8601. Distinguishes 'deployed an hour ago' from 'deployed an hour ago from a three-week-old commit'. Committer date is the deliberate choice — author date survives rebases and cherry-picks unchanged, so it CAN in principle predate the deployed content — but that is a property of git, not a measurement of any particular commit."},"build_branch":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Branch","description":"Branch the deploy ran from, normally `main`. ⛔ null is AMBIGUOUS: a detached HEAD publishes null, and the identity reader also returns null for the FILE-DERIVED keys when the build-identity file is absent, malformed or unreadable — the state a server is in before its first identity-carrying deploy. ⚠️ NOT every key: `build_id_read_at` and `build_id_error` are still populated, which is what makes the discriminator below work at all. ⇒ READ `build_id_error` TO TELL THEM APART — null with a null error is a detached HEAD; null with an error is an identity that could not be read. Publishing the literal \"HEAD\" (what `rev-parse --abbrev-ref` returns when detached) would be worse than either."},"build_dirty":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Build Dirty","description":"⚠️ true = the deploying tree had UNCOMMITTED changes, so `build_sha` does not fully describe what shipped. The deploy is allowed to be dirty; the reader is not allowed to be misled about it."},"build_deployed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Deployed At","description":"When the deploy wrote the build-identity file, ISO-8601 UTC. ⛔ THIS IS THE DEPLOYING MACHINE'S CLOCK, and `build_id_read_at` is the BOX's — they cross two machines and are NOT comparable to the second. For 'did the running process actually re-read this?' use `build_restart_pending`, which is a same-machine mtime comparison and answers that question directly."},"build_commits_server":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Build Commits Server","description":"★ THE API's VERSION NUMBER, and the one the terminal page renders as `api`. Commits reachable from `build_sha` that touched the server's code (`git rev-list --count <sha> -- server`), computed at deploy time from THAT sha and not from HEAD. ⛔ IT DESCRIBES THE RUNNING PROCESS, NOT THE DEPLOYED PAYLOAD — this module is read once at import, so when `build_restart_pending` is true a NEWER build is on disk and this number is still the correct one for the code answering you. Read the two fields together; calling this 'the deployed version' is the exact lie the pair exists to prevent. ⛔ NOT COMPARABLE TO THE PAGE's `site` COUNTER and nothing may subtract them: they are overlapping path-filtered subsets of ONE history (some commits touch both the site and the server), not two halves of a total. Each answers 'did my deploy land' for its own surface only. ⚠️ NOT AN ORDERING AND NOT SEMVER: a reachable-commit count is monotonic along one line of deploys but two branches can produce the same number. ⛔ NO FIGURE IS QUOTED IN THIS DESCRIPTION — it moves with every server commit, so any number written here would be stale before the next deploy. null = this server's build-identity file predates the field, or carried a value that was not a non-negative int (a bool included); absence never becomes a guess."},"build_id_read_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Id Read At","description":"When THIS PROCESS read the file, by the BOX's clock. ⚠️ Comparing it to `build_deployed_at` crosses two machines' clocks and is only a coarse signal. `build_restart_pending` is the exact answer to the same question and is measured entirely on the box."},"build_restart_pending":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Build Restart Pending","description":"⛔ THE HONEST FIELD. true = the build-identity file on disk has moved past what this process read — code was deployed and the service was NOT restarted, so every other route is served by a build this identity does not describe. ★ TRI-STATE like `disk_ok`: null means undeterminable and is explicitly NOT false. Without this field a skipped restart would make /api/health announce a fresh sha for stale code."},"build_id_error":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Build Id Error","description":"null when the identity was read cleanly; otherwise the reason `build_sha` is null (file absent, unparseable, or missing a required key). Absence always carries a reason here."},"max_lag_blocks":{"type":"integer","title":"Max Lag Blocks"},"max_poll_age_s":{"type":"number","title":"Max Poll Age S"}},"type":"object","required":["ok","ok_reasons","disk_free_gb","disk_total_gb","disk_ok","disk_error","min_free_gb","last_block","chain_tip","lag_blocks","poller_alive","poll_age_s","last_poll_at","last_error","last_error_at","last_error_age_s","max_lag_blocks","max_poll_age_s"],"title":"HealthEnvelope","description":"GET /api/health.\n\n⚠️ Served with status 200 OR 503 and THE SAME BODY SHAPE when the archive is\nhealthy or unhealthy — only the code differs. ⛔ When the DATABASE is down the\n503 body is `{\"detail\": \"...\"}` — an `ErrorDetail`, not this envelope. So\n/api/health is the ONE operation that can serve TWO DIFFERENT 503 SHAPES, and\nits declaration says so (`oneOf`, not one model). A consumer that assumes \"same\nbody shape\" and reads `body[\"ok\"]` gets a KeyError on exactly the failure it was\nwritten to detect.\n★ `disk_ok` is TRI-STATE on purpose: null means unmeasured and is explicitly NOT\nfalse. `last_error_at` can be non-null while `last_error` is null, and\n`last_error_age_s` is then deliberately suppressed."},"LaunchConformanceRow":{"properties":{"tx_hash":{"type":"string","title":"Tx Hash"},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"creator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator"},"block_index":{"type":"integer","title":"Block Index"},"announced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Announced At"},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status"},"conformance":{"$ref":"#/components/schemas/Conformance"},"params_conformant":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Params Conformant"},"start_after_announce":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Start After Announce","description":"A mint window cannot open before its own launch tx confirmed. False is a real finding."},"mint_count":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Mint Count","description":"null unless include_mint_rollup=true — the rollup costs a measured ~9x (1,013 ms vs 113 ms wall-clock on production 2026-08-21; independently re-measured at 1,244 ms vs 106 ms)."},"minters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Minters"},"xcp_raised":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Xcp Raised"}},"additionalProperties":false,"type":"object","required":["tx_hash","asset","creator","block_index","announced_at","status","conformance","params_conformant","start_after_announce","mint_count","minters","xcp_raised"],"title":"LaunchConformanceRow"},"LaunchConstants":{"properties":{"soft_cap_xcp":{"type":"number","title":"Soft Cap Xcp"},"min_participants":{"type":"integer","title":"Min Participants"},"mint_window_blocks":{"type":"integer","title":"Mint Window Blocks"},"max_per_addr_xcp":{"type":"number","title":"Max Per Addr Xcp"}},"type":"object","required":["soft_cap_xcp","min_participants","mint_window_blocks","max_per_addr_xcp"],"title":"LaunchConstants"},"LaunchHeader":{"properties":{"tx_hash":{"type":"string","title":"Tx Hash"},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"creator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator"},"block_index":{"type":"integer","title":"Block Index"},"announced_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Announced At"},"start_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Start Block"},"deadline_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Deadline Block"},"hard_cap":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Hard Cap"},"price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Price"},"params_conformant":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Params Conformant","description":"null means UNKNOWN, never 'no'. See /api/launches/conformance."},"start_after_announce":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Start After Announce"}},"additionalProperties":false,"type":"object","required":["tx_hash","asset","creator","block_index","announced_at","start_block","deadline_block","hard_cap","price","params_conformant","start_after_announce"],"title":"LaunchHeader"},"LaunchRow":{"properties":{"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"status":{"type":"string","title":"Status"},"creator":{"type":"string","title":"Creator"},"tx":{"type":"string","title":"Tx"},"pct":{"type":"number","title":"Pct"},"xcp":{"type":"number","title":"Xcp"},"minters":{"type":"integer","title":"Minters"},"mints":{"type":"integer","title":"Mints"},"start":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Start"},"deadline":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Deadline"},"to_start":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Start"},"to_deadline":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Deadline"},"elapsed":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Elapsed"},"pace":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Pace"}},"type":"object","required":["asset","status","creator","tx","pct","xcp","minters","mints","start","deadline","to_start","to_deadline","elapsed","pace"],"title":"LaunchRow","description":"One fair-mint launch, as it appears in all five lists of /api/launches.\n\n★ `asset` IS NULLABLE and that is the finding, not an oversight: the\n`unknown_conformance` bucket holds launches whose ANNOUNCEMENT was itself\nrejected, so the archive has no asset name for them — and with it, no start,\ndeadline or pace either.\n`pace`/`elapsed` are null for every non-open launch."},"LaunchStatus":{"properties":{"tx_hash":{"type":"string","title":"Tx Hash","description":"The launch's FAIRMINTER announcement tx_hash."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"The newest status. ⚠️ null is REAL and means the newest update carried no status key at all — a deadline rewrite. That is 'un-statused', which is a different fact from 'un-updated', and it is not filtered out."},"status_block":{"type":"integer","title":"Status Block","description":"The block that newest update confirmed in."}},"additionalProperties":false,"type":"object","required":["tx_hash","status","status_block"],"title":"LaunchStatus","description":"The newest FAIRMINTER_UPDATE per launch.\n\n★ `status_block` IS THE COLUMN NOTHING ELSE PUBLISHES. `/api/launches` serves\na launch's status (via v_launch, which COALESCEs this view's status with the\nannounce-time one) but NOT the block that status landed in — so \"when did this\nlaunch close\" has been unanswerable from the API."},"LaunchStatusCensus":{"properties":{"launches_with_a_status_update":{"$ref":"#/components/schemas/Measured_int_","description":"Rows in v_fairminter_status."},"launches_announced":{"$ref":"#/components/schemas/Measured_int_","description":"NEW_FAIRMINTER events held. The denominator."},"launches_with_no_status_update":{"$ref":"#/components/schemas/Measured_int_","description":"Announced but never updated — INVISIBLE to this route. For those, the only status that exists is the announce-time one, served by /api/launches."}},"additionalProperties":false,"type":"object","required":["launches_with_a_status_update","launches_announced","launches_with_no_status_update"],"title":"LaunchStatusCensus"},"LaunchStatusEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/LaunchStatus"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/LaunchStatusCensus"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"LaunchStatusEnvelope"},"LaunchTotals":{"properties":{"launches":{"type":"integer","title":"Launches"},"open":{"type":"integer","title":"Open"},"pending":{"type":"integer","title":"Pending"},"closed":{"type":"integer","title":"Closed"},"xcp_escrowed":{"type":"number","title":"Xcp Escrowed"},"minters":{"type":"integer","title":"Minters","description":"⚠️ THE SUM OF THE PER-LAUNCH `minters` COUNTS, NOT A DISTINCT ADDRESS COUNT. An address that minted two different launches is counted TWICE, so this is an upper bound on the number of distinct people and is NOT comparable to /api/mints' `minters` (DISTINCT, and over ALL fairmints rather than XCP-69). MEASURED with the archive at block 964,058: 819 here vs 3,614 distinct all-fairmint addresses on /api/mints, while the terminal page showed 228 distinct XCP-69 addresses."},"unknown_conformance":{"type":"integer","title":"Unknown Conformance"}},"type":"object","required":["launches","open","pending","closed","xcp_escrowed","minters","unknown_conformance"],"title":"LaunchTotals"},"LaunchesEnvelope":{"properties":{"block":{"type":"integer","title":"Block"},"open":{"items":{"$ref":"#/components/schemas/LaunchRow"},"type":"array","title":"Open"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The ARCHIVE block these rows were built at — the archive head, read once per request and used as the cache key, carried inside the cached value so a request that waited on another's build publishes that build's block. ⛔ NOT `block`: `block` is `ingest_health.chain_tip`, the node tip used as the COUNTDOWN reference, and it is an UPPER bound on this — during catch-up the node tip stands still while the archive advances. Age this answer by `as_of_block`."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"Always false on this route: it has no fallback arm (a failed rebuild is a 503, never old rows). ⛔ It is NOT `/api/markets/ranking`'s head comparison (`as_of_block != archive_head`): a request that waited on another request's build can publish an `as_of_block` below the head it read and still say false here. Age this answer by `as_of_block`."},"pending":{"items":{"$ref":"#/components/schemas/LaunchRow"},"type":"array","title":"Pending"},"closed":{"items":{"$ref":"#/components/schemas/LaunchRow"},"type":"array","title":"Closed"},"nonconformant":{"items":{"$ref":"#/components/schemas/LaunchRow"},"type":"array","title":"Nonconformant"},"unknown_conformance":{"items":{"$ref":"#/components/schemas/LaunchRow"},"type":"array","title":"Unknown Conformance"},"totals":{"$ref":"#/components/schemas/LaunchTotals"},"unknown_conformance_note":{"type":"string","title":"Unknown Conformance Note"},"constants":{"$ref":"#/components/schemas/LaunchConstants"}},"type":"object","required":["block","open","pending","closed","nonconformant","unknown_conformance","totals","unknown_conformance_note","constants"],"title":"LaunchesEnvelope","description":"GET /api/launches.\n\n⚠️ Five sibling lists, ONE row shape. `unknown_conformance` is the third value of\na three-valued answer and is NOT \"nonconformant\" — see /api/launches/conformance."},"LegSupply":{"properties":{"supply":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Supply","description":"⚠️ LOSSY above 2^53 satoshis — see the class note. Kept for existing consumers; prefer `supply_exact`."},"burned":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Burned","description":"⚠️ Lossy twin of `burned_exact`."},"circulating":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Circulating","description":"⚠️ Lossy twin of `circulating_exact`. This is the field the 2^53 defect was MEASURED on."},"locked":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Locked"},"supply_exact":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Supply Exact","description":"Total supply, exact, as a JSON string — PARSE IT AS A DECIMAL, never as a float. Null when `supply` is null, and null in the one further case no string can state: a non-finite figure."},"burned_exact":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Burned Exact","description":"Burn-address holdings, exact, as a JSON string. Null whenever the burn snapshot is unusable OR its figure for this asset is not a real number — never a zero standing in for one; `burn_reason` on the row says which."},"circulating_exact":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Circulating Exact","description":"`supply_exact - burned_exact`, computed in decimal and published as a JSON string. Null whenever either input is."},"supply_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Supply Reason"},"burned_is_lower_bound":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Burned Is Lower Bound","description":"True whenever `burned` is published at all: the figure covers ONLY the curated burn-address list, so it can be too LOW and never too high. Null when `burned` is null — there is no bound on a figure that was never published, and `false` would be a claim we cannot support."},"circulating_is_upper_bound":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Circulating Is Upper Bound","description":"True whenever `circulating` is published at all. It is `supply - burned` with `burned` a lower bound, so it can only be too HIGH — and so can any market cap built on it. Null when `circulating` is null."},"burned_zero_meaning":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Burned Zero Meaning","description":"⛔ NON-NULL EXACTLY WHEN `burned == 0`, and it says that the zero is NOT a claim that none was burned. THIS IS THE FIELD TO BRANCH ON: do not render `burned == 0` as 'none burned' while this is set. Null when `burned` is positive (no ambiguity to resolve) and null when `burned` is null (`burn_reason` on the row already says why). ⚠️ This archive has NO state meaning 'we verified none was burned' — proving that needs an exhaustive holder scan it does not perform — so this field never turns off to mean the opposite."}},"type":"object","required":["supply","burned","circulating","locked","supply_reason"],"title":"LegSupply","description":"One leg's supply figures (`a` or `b` of a pool row). THE SAME ELEVEN KEYS ON\nEVERY BRANCH. `supply_reason` is null whenever a supply IS published.\n\n⛔ THE `*_exact` TWINS ARE THE ONES TO PARSE; THE FLOATS CANNOT CARRY THE NUMBER.\n  AS OF BLOCK 963,682, DANKMEMECASH's circulating figure was\n  `882537456.50777774` = 88,253,745,650,777,774 satoshis, past 2^53\n  (9,007,199,254,740,992) where a float64 mantissa runs out — as a float it\n  comes back `882537456.5077777`, one digit short. That is a property of the\n  TYPE, not of the arithmetic: computing in Decimal and narrowing once gives the\n  identical wrong answer, so there is no float that can publish this.\n  ⇒ `*_exact` ARE JSON **strings** on the wire, constrained by a decimal\n    pattern that rejects scientific notation. Parse them as decimals.\n  ⇒ The float twins are RETAINED and unchanged so no existing consumer moves.\n    They are the deprecation candidates, not the contract.\n⚠️ `supply_exact` is exact end to end (numeric off a decimal string in our own\n  archive). `burned_exact` is only as exact as the third-party burn snapshot it\n  is read from, and `circulating_exact` inherits that. \"Exact\" here means WE\n  added no error, never that the upstream figure is exact."},"MarketRankingEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/MarketRankingRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"withheld_below_floor":{"type":"integer","title":"Withheld Below Floor"},"vol_window_blocks":{"type":"integer","title":"Vol Window Blocks"},"vol_window_floor_block":{"type":"integer","title":"Vol Window Floor Block"},"membership_depth_blocks":{"type":"integer","title":"Membership Depth Blocks"},"membership_since_block":{"type":"integer","title":"Membership Since Block"},"derived_at_block":{"type":"integer","title":"Derived At Block"},"as_of_block":{"type":"integer","title":"As Of Block"},"archive_head":{"type":"integer","title":"Archive Head"},"stale_fallback":{"type":"boolean","title":"Stale Fallback"},"rebuild_failing":{"type":"boolean","title":"Rebuild Failing"},"book_as_of":{"type":"string","title":"Book As Of"},"absence_means":{"type":"string","title":"Absence Means"},"note":{"type":"string","title":"Note"},"venue":{"type":"string","title":"Venue"},"source":{"type":"string","title":"Source"},"book_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Book Source","description":"Present only while the book is switched to read the `order_state` table (the switch `/api/book` reads). Names the relation the `bids`/`asks` columns came from: `order_state` when the reconciler's trust rule held for this request and for the build that produced these rows; `v_order_open` when it did not, and `book_source_refusal` then says why. Absent entirely when the switch is off, which is the default."},"reconciled_head":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Reconciled Head","description":"Present only while switched. The archive head at which the last reconcile run with verdict `passed` found `order_state` equal to the view it replaces, as read by THIS request's trust check. Null when no run has ever passed or that check could not read the table."},"reconciled_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reconciled At","description":"Present only while switched. When that passed reconcile run was recorded (ISO 8601). Null exactly when `reconciled_head` is null."},"book_source_refusal":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Book Source Refusal","description":"Present only while switched. Null when `order_state` served the book columns. Otherwise `CODE: detail` — the same codes `/api/book` publishes — naming why the table was refused, and the book columns are the view's, exactly as they would be with the switch off."}},"type":"object","required":["result","count","withheld_below_floor","vol_window_blocks","vol_window_floor_block","membership_depth_blocks","membership_since_block","derived_at_block","as_of_block","archive_head","stale_fallback","rebuild_failing","book_as_of","absence_means","note","venue","source"],"title":"MarketRankingEnvelope","description":"GET /api/markets/ranking — the whole ranking, not paged.\n\n⛔ NO `truncated`, NO `next_offset`, NO `ceiling_note`, by design: the ranking\nships as ONE free response, cached per block and rate limited, not paged.\n`count` is the whole population the membership rule admits, so a 200 carries\nthe entire ranking."},"MarketRankingRow":{"properties":{"pair":{"type":"string","title":"Pair"},"base":{"type":"string","title":"Base"},"quote":{"type":"string","title":"Quote"},"last":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Last"},"chg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Chg"},"prev_t":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Prev T"},"vol_xcp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vol Xcp"},"vol_quote":{"type":"number","title":"Vol Quote"},"trades":{"type":"integer","title":"Trades"},"trades_all":{"type":"integer","title":"Trades All"},"bids":{"type":"integer","title":"Bids"},"asks":{"type":"integer","title":"Asks"},"has_pool":{"type":"boolean","title":"Has Pool"},"last_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Block"},"last_t":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last T"}},"type":"object","required":["pair","base","quote","last","chg","prev_t","vol_xcp","vol_quote","trades","trades_all","bids","asks","has_pool","last_block","last_t"],"title":"MarketRankingRow","description":"One row of the terminal's market ranking — /api/markets/ranking.\n\n⭐ The fifteen keys are exactly what the terminal page renders, so the page can\nswap its baked array for this response without a render change.\n\n⚠️ THREE NULLS THAT ARE NOT ZEROS. `vol_xcp` is null when the quote is\nneither XCP nor BTC — \"cannot be expressed in XCP\". `chg`/`prev_t` are null\nuntil a pair has TWO fills on record, and `chg` is measured against the\nprevious fill at whatever age, never over 24 h. `last`/`last_block`/`last_t`\nare null for a pair that has never traded and is listed on a resting order or\nan AMM pool alone."},"MarketStatsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/MarketStatsRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"window_days":{"type":"integer","title":"Window Days"},"absence_means":{"type":"string","title":"Absence Means"},"venue":{"type":"string","title":"Venue"},"as_of_block":{"type":"integer","title":"As Of Block","description":"The archive block these rows were BUILT at. After a new archive block, a background refresher rebuilds this route's default request, at most once per minimum gap (60 s by default) measured from the start of its previous rebuild; a request that finds no rows built at the current block yet (for example, for a block that landed inside that gap) builds them itself, waits for a build already running, or — within a short cooldown after a failed build — is answered from the last good rows (`rebuild_failing`). In every case, on a fresh answer this equals `archive_head`; on a `stale_fallback` answer it is the TRUE, older block the last good build ran at — never the live head."},"source":{"type":"string","title":"Source"},"archive_head":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Archive Head","description":"The archive head THIS request read. `as_of_block != archive_head` is exactly `stale_fallback`."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when these rows were NOT built at the archive head THIS request read (`archive_head`). Two causes: a rebuild failed and you were served the LAST GOOD rows instead of a 503; or a new block landed while a build was already running and this request waited for that build rather than starting its own. `rebuild_failing` tells them apart: it is true while a failed build is remembered. A HEAD comparison, the ranking's predicate — read `as_of_block` for the block they are from. ⛔ Not `freshness.stale` (ARCHIVE-vs-CHAIN)."},"rows_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rows Age S","description":"Seconds since the build that produced these rows finished — monotonic, never a timestamp. It matters here because the route's window is WALL-CLOCK (a trailing interval ending when the rows were built): between blocks the window is the build's, up to one block interval old."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route's cache is REMEMBERED right now — the route is inside a labelled-fallback window and declining to re-attempt for a cooldown. ⛔ A THIRD AXIS, NOT A SECOND NAME: the combination it exists for is FRESH + FAILING (a concurrent flight stored rows at this instant while this request's own build was cancelled). ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'."}},"type":"object","required":["result","count","window_days","absence_means","venue","as_of_block","source"],"title":"MarketStatsEnvelope","description":"GET /api/markets/stats."},"MarketStatsRow":{"properties":{"pair":{"type":"string","title":"Pair"},"base":{"type":"string","title":"Base"},"quote":{"type":"string","title":"Quote"},"last":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Last"},"last_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Block"},"last_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last At"},"prev":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Prev"},"prev_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Prev At"},"chg":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Chg"},"chg_basis":{"type":"string","title":"Chg Basis"},"fills_all":{"type":"integer","title":"Fills All"}},"type":"object","required":["pair","base","quote","last","last_block","last_at","prev","prev_at","chg","chg_basis","fills_all"],"title":"MarketStatsRow","description":"One pair's price stats. `chg_basis` names WHY `chg` is null rather than\nleaving the reader to guess."},"MarketVolumeEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/MarketVolumeRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"window_hours":{"type":"integer","title":"Window Hours"},"truncated":{"type":"boolean","title":"Truncated"},"absence_means":{"type":"string","title":"Absence Means"},"venue":{"type":"string","title":"Venue"},"as_of_block":{"type":"integer","title":"As Of Block"},"source":{"type":"string","title":"Source"},"built_at_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Built At Block","description":"The archive head the cached rows were BUILT under — the head the request that ran the build had read. `as_of_block` beside it keeps its old meaning (the head THIS request read), so on a fallback the two differ and the pair says by how much: the rule is that a fallback carries its TRUE age, never the live head. ⚠️ This route's window is WALL-CLOCK (a trailing interval ending when the rows were built), so `rows_age_s` is the age that bounds it; this field says which archive it was computed against."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when this answer's cached rows are OLDER THAN THE ROUTE'S CACHE TTL — the rebuild did not finish and you were served the LAST GOOD answer instead of a 503. The 200 is then a labelled fallback, not a fresh answer. ⚠️ THE PREDICATE IS THE ROWS' AGE AGAINST THE TTL, not 'the fallback path ran': a concurrent rebuild can finish while this one fails, and that answer is genuinely fresh and is reported so — read `rebuild_failing` for the other axis. ⛔ Not `freshness.stale`, which is ARCHIVE-vs-CHAIN."},"rows_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rows Age S","description":"How old the cached part of this answer is, in seconds, since the build that produced it FINISHED. A DURATION from a monotonic clock, never a wall-clock timestamp, so an ntp step cannot move it. Recovered from the cache's own stamp even when another request did the build."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route's cache is REMEMBERED right now — the route is inside a labelled-fallback window and declining to re-attempt for a cooldown. ⛔ A THIRD AXIS, NOT A SECOND NAME: the combination it exists for is FRESH + FAILING (a concurrent flight stored rows at this instant while this request's own build was cancelled). ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'."}},"type":"object","required":["result","count","window_hours","truncated","absence_means","venue","as_of_block","source"],"title":"MarketVolumeEnvelope","description":"GET /api/markets/volume."},"MarketVolumeRow":{"properties":{"pair":{"type":"string","title":"Pair"},"base":{"type":"string","title":"Base"},"quote":{"type":"string","title":"Quote"},"fills":{"type":"integer","title":"Fills"},"vol_quote":{"type":"number","title":"Vol Quote"},"vol_xcp":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vol Xcp"}},"type":"object","required":["pair","base","quote","fills","vol_quote","vol_xcp"],"title":"MarketVolumeRow","description":"One pair's volume. `vol_xcp` is null when the quote is not XCP — \"cannot be\nexpressed in XCP\", never zero."},"MatchSettlementCensus":{"properties":{"matches_with_an_update":{"$ref":"#/components/schemas/Measured_int_","description":"Rows in v_match_state: every match that received an ORDER_MATCH_UPDATE."},"expired":{"$ref":"#/components/schemas/Measured_int_","description":"Struck, then fell through. Never settled."},"completed":{"$ref":"#/components/schemas/Measured_int_"},"matches_total":{"$ref":"#/components/schemas/Measured_int_","description":"EVERY ORDER_MATCH ever held, the honest denominator. Only populated when include_match_total=true was asked for; otherwise value is null and method says so, because it costs a full pass over the match tape."}},"additionalProperties":false,"type":"object","required":["matches_with_an_update","expired","completed","matches_total"],"title":"MatchSettlementCensus","description":"⛔ THE DENOMINATOR IS THE POINT. `v_match_state` is built from\nORDER_MATCH_UPDATE and therefore sees ONLY matches that needed resolving. A\nmatch that settled on the spot never produced an update and is not in it —\nreading `expired / matches_with_an_update` as the chain's failure rate\noverstates it by more than 9x."},"MatchState":{"properties":{"match_id":{"type":"string","title":"Match Id","description":"Counterparty's match id, verbatim: '<tx0_hash>_<tx1_hash>'."},"status":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Status","description":"'expired' — struck and never settled — or 'completed'. null if an update arrived carrying no status at all, which is a different fact from 'no update'."},"maker_tx":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Maker Tx","description":"tx0_hash, split from match_id. null if the id did not parse."},"taker_tx":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Taker Tx","description":"tx1_hash, split from match_id. null if the id did not parse."}},"additionalProperties":false,"type":"object","required":["match_id","status","maker_tx","taker_tx"],"title":"MatchState","description":"One order match's FINAL settlement state.\n\n★ `maker_tx` / `taker_tx` ARE SPLIT OUT OF `match_id`, AND THE SPLIT IS\nGUARDED. The id is `<tx0_hash>_<tx1_hash>` — MEASURED 2026-08-23 AS OF BLOCK\n963,675: 23,999 of 23,999 ids match `^[0-9a-f]{64}_[0-9a-f]{64}$`. A row that\ndoes not match returns BOTH halves as null rather than a mis-split hash,\nbecause half a txid that looks like a txid is worse than an admitted absence."},"MatchStateEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/MatchState"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/MatchSettlementCensus"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"MatchStateEnvelope"},"Measured_Decimal_":{"properties":{"value":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Value","description":"The measured number, or null if it was not measured."},"measured_at":{"type":"string","format":"date-time","title":"Measured At","description":"When this number was computed. Never the time of the underlying event."},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this number was computed against."},"method":{"type":"string","title":"Method","description":"How it was derived, in words. Never blank."},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit","description":"Unit, when it is not a plain count."}},"additionalProperties":false,"type":"object","required":["value","measured_at","method"],"title":"Measured[Decimal]"},"Measured_int_":{"properties":{"value":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Value","description":"The measured number, or null if it was not measured."},"measured_at":{"type":"string","format":"date-time","title":"Measured At","description":"When this number was computed. Never the time of the underlying event."},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this number was computed against."},"method":{"type":"string","title":"Method","description":"How it was derived, in words. Never blank."},"unit":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Unit","description":"Unit, when it is not a plain count."}},"additionalProperties":false,"type":"object","required":["value","measured_at","method"],"title":"Measured[int]"},"MempoolEnvelope":{"properties":{"scope":{"type":"string","title":"Scope"},"source_node":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Node","description":"Which node told us — NULL when we do not know. A failover partway through a collection cycle makes that cycle a union of two nodes' pending views, and the collector drops the label instead of guessing one. Never read NULL here as 'no node'; read it as 'not attributable to a single node'. ⚠️ AND A NON-NULL VALUE IS NOT A CLAIM THAT ONE NODE WROTE THIS ARCHIVE. It is the MODAL label — the node that recorded the most rows in `mempool_events`, counting live and resolved rows together. A row's label is written once at first sighting and NEVER corrected (later sightings update the row's last-seen time, poll count and outcome, never its label), and nothing purges the table, so after a PERMANENT failover the old node keeps its rows, the new node accumulates its own, and this field names whichever has more — which is history, not necessarily the node answering now. The counts beside it (`observed`, `live`, `resolved`) are aggregated over ALL rows regardless of label."},"observed":{"anyOf":[{"$ref":"#/components/schemas/MempoolObserved"},{"type":"null"}],"description":"The span of this node's sightings. NULL means this node has recorded no mempool sighting at all — there is no observation to describe. It is NOT a claim that the mempool is empty."},"live":{"$ref":"#/components/schemas/MempoolLive"},"resolved":{"$ref":"#/components/schemas/MempoolResolved"},"event":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Event","description":"The event-type filter this answer was computed with (default NEW_FAIRMINT; a comma-separated list echoes back joined by commas). ABSENT — not null — when this node has recorded no sighting yet, because no filter was ever applied."},"mints":{"items":{"additionalProperties":true,"type":"object"},"type":"array","title":"Mints"},"note":{"type":"string","title":"Note"},"as_of":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of","description":"max(last_seen_at) — this node's most recent mempool sighting, and the dateline for this LIVE answer. A stale value means the collector has stopped, NOT that the mempool is empty; null means this node has never recorded a sighting. Not a block height, deliberately: see as_of_basis. Declared optional in this schema."},"as_of_basis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"As Of Basis","description":"States in the payload itself that as_of is a wall clock and not a block height, so a consumer does not have to infer it from the type. Constant text."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when this answer's cached rows are OLDER THAN THE ROUTE'S CACHE TTL — the rebuild did not finish and you were served the LAST GOOD answer instead of a 503. The 200 is then a labelled fallback, not a fresh answer. ⚠️ THE PREDICATE IS THE ROWS' AGE AGAINST THE TTL, not 'the fallback path ran': a concurrent rebuild can finish while this one fails, and that answer is genuinely fresh and is reported so — read `rebuild_failing` for the other axis. ⛔ Not `freshness.stale`, which is ARCHIVE-vs-CHAIN."},"rows_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rows Age S","description":"How old the cached part of this answer is, in seconds, since the build that produced it FINISHED. A DURATION from a monotonic clock, never a wall-clock timestamp, so an ntp step cannot move it. Recovered from the cache's own stamp even when another request did the build."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route's cache is REMEMBERED right now — the route is inside a labelled-fallback window and declining to re-attempt for a cooldown. ⛔ A THIRD AXIS, NOT A SECOND NAME: the combination it exists for is FRESH + FAILING (a concurrent flight stored rows at this instant while this request's own build was cancelled). ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'."}},"type":"object","required":["scope","source_node","observed","live","resolved","mints","note"],"title":"MempoolEnvelope","description":"GET /api/mempool.\n\n⚠️ `mints` is typed as a list of objects and is NOT given a row model: it was\nempty in every sample this schema was derived from, and this schema does not\nguess a row shape.\n⛔ THIS ROUTE HAS THREE 200 SHAPE BRANCHES:\n  1 · healthy      — populated archive, a single known upstream.\n  2 · unattributed — populated archive, `source_node` NULL. ONE upstream\n                     failover is enough to produce it, not a fresh install.\n  3 · empty        — no sighting recorded: `observed` NULL and `event` ABSENT\n                     ENTIRELY.\nA fourth case leaves every key and type untouched: after a PERMANENT failover\n`source_node` names one node over an archive two nodes wrote — see its\ndescription."},"MempoolLive":{"properties":{"distinct_tx":{"type":"integer","title":"Distinct Tx"},"events":{"type":"integer","title":"Events"},"by_event":{"additionalProperties":{"type":"integer"},"type":"object","title":"By Event","description":"Event name -> count. A free-form MAP, not a fixed record: the keys are whatever event types this node currently holds in its own mempool, so naming them would be false the next minute."}},"type":"object","required":["distinct_tx","events","by_event"],"title":"MempoolLive"},"MempoolObserved":{"properties":{"first_seen_earliest":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Seen Earliest"},"last_seen_latest":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Seen Latest"},"rows_total":{"type":"integer","title":"Rows Total"},"tx_total":{"type":"integer","title":"Tx Total"}},"type":"object","required":["first_seen_earliest","last_seen_latest","rows_total","tx_total"],"title":"MempoolObserved"},"MempoolResolved":{"properties":{"confirmed":{"type":"integer","title":"Confirmed"},"vanished":{"type":"integer","title":"Vanished"}},"type":"object","required":["confirmed","vanished"],"title":"MempoolResolved"},"MintCurveEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/MintCurvePoint"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"launch":{"$ref":"#/components/schemas/LaunchHeader"},"totals":{"$ref":"#/components/schemas/MintCurveTotals"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","launch","totals"],"title":"MintCurveEnvelope"},"MintCurvePoint":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"mints":{"type":"integer","title":"Mints"},"minters_in_block":{"type":"integer","title":"Minters In Block","description":"⚠️ DISTINCT WITHIN THIS BLOCK. Summing this column over blocks does NOT give the launch's distinct minter count."},"paid_quantity_raw":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Paid Quantity Raw","description":"Satoshi-XCP. Divide by 1e8 for XCP."},"paid_xcp":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Paid Xcp","description":"paid_quantity_raw / 1e8. XCP is divisible, so this is correct."},"earned_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Earned Normalized","description":"The node's own normalized figure — correct for INDIVISIBLE assets too."},"first_minted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"First Minted At"},"last_minted_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Minted At"}},"additionalProperties":false,"type":"object","required":["block_index","mints","minters_in_block","paid_quantity_raw","paid_xcp","earned_normalized","first_minted_at","last_minted_at"],"title":"MintCurvePoint"},"MintCurveTotals":{"properties":{"blocks_with_mints":{"$ref":"#/components/schemas/Measured_int_"},"total_mints":{"$ref":"#/components/schemas/Measured_int_"},"total_paid_raw":{"$ref":"#/components/schemas/Measured_Decimal_"},"returned_blocks":{"$ref":"#/components/schemas/Measured_int_","description":"Blocks actually in THIS response, after limit/offset."}},"additionalProperties":false,"type":"object","required":["blocks_with_mints","total_mints","total_paid_raw","returned_blocks"],"title":"MintCurveTotals"},"MintRow":{"properties":{"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Asset"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"tx":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx"},"block":{"type":"integer","title":"Block"},"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"t":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"T"},"xcp":{"type":"number","title":"Xcp"},"tok":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Tok"},"asset_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset Divisible"}},"type":"object","required":["asset","source","tx","block","block_index","event_index","t","xcp","tok","asset_divisible"],"title":"MintRow","description":"One valid fairmint. `xcp` is never null (a missing value reads 0); `tok`\nstays null when it could not be read — \"0 minted\" and \"we could not read it\" are\ndifferent claims. `asset_divisible` may be null."},"MintsEnvelope":{"properties":{"block":{"type":"integer","title":"Block"},"result":{"items":{"$ref":"#/components/schemas/MintRow"},"type":"array","title":"Result"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The ARCHIVE block these rows were built at — the archive head, read once per request and used as the cache key, carried inside the cached value so a request that waited on another's build publishes that build's block. ⛔ NOT `block`: `block` is `ingest_health.chain_tip`, the node tip used as the COUNTDOWN reference, and it is an UPPER bound on this — during catch-up the node tip stands still while the archive advances. Age this answer by `as_of_block`."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"Always false on this route: it has no fallback arm (a failed rebuild is a 503, never old rows). ⛔ It is NOT `/api/markets/ranking`'s head comparison (`as_of_block != archive_head`): a request that waited on another request's build can publish an `as_of_block` below the head it read and still say false here. Age this answer by `as_of_block`."},"total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Total","description":"Count of EVERY valid fairmint in the archive — all assets, not only XCP-69 launches. Roughly 200x the XCP-69 mint count at block 964,058 (191,244 against 945). Count-verified against the chain for the whole NEW_FAIRMINT population; that verification is what would be destroyed by filtering this route."},"minters":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Minters","description":"⚠️ ONE WORD, THREE POPULATIONS ACROSS THIS SITE. Here it is count(DISTINCT minter) over ALL valid fairmints (3,614 at block 964,058). /api/launches' `totals.minters` is the SUM of per-launch counts over XCP-69 only (819 at the same block, and it double-counts an address that minted two launches). The terminal page shows DISTINCT addresses over XCP-69 only (228 when those two were read). None is wrong; the NAME is. See `minters_basis`."},"showing":{"type":"integer","title":"Showing"},"population":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Population","description":"What the rows and counts on this envelope ARE, in one sentence, as DATA rather than as prose in a description."},"xcp69_only":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Xcp69 Only","description":"⛔ Always false on this route, and it is a statement about what `v_fairmint` IS — not a filter that might flip. If you need XCP-69 only, use /api/launches; do not filter these rows by hand, because the launch-set join lives in v_launch."},"population_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Population Note","description":"Where to go for the XCP-69 population instead, and why not to reconstruct it from these rows."},"minters_basis":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Minters Basis","description":"Which of the three `minters` populations this one is."}},"type":"object","required":["block","result","showing"],"title":"MintsEnvelope","description":"GET /api/mints — EVERY VALID COUNTERPARTY FAIRMINT, not only XCP-69.\n\n`v_fairmint` is `NEW_FAIRMINT WHERE status='valid'` with NO join to the XCP-69\nlaunch set, so `total` is far larger than the XCP-69 mint count. The population\nis stated as DATA — `population`, `xcp69_only`, `population_note` — because a\nsentence can rot and a field is data. For XCP-69 only, use /api/launches."},"OrderOutcome":{"properties":{"status":{"type":"string","title":"Status","description":"Counterparty's own status string, verbatim and unedited."},"outcome_class":{"$ref":"#/components/schemas/OutcomeClass"},"orders":{"type":"integer","title":"Orders","description":"Orders whose latest state is this status."},"first_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Block","description":"Earliest opened_block among them."},"last_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Block","description":"Latest opened_block among them."},"measured_at":{"type":"string","format":"date-time","title":"Measured At","description":"These are aggregates. This is when they were computed."}},"additionalProperties":false,"type":"object","required":["status","outcome_class","orders","first_block","last_block","measured_at"],"title":"OrderOutcome","description":"One status string, with how many orders ended there and when it was counted.\n\n⚠️ THE REJECTION REASONS USE U+2010 (HYPHEN), NOT AN ASCII HYPHEN-MINUS.\n`status LIKE '%non-positive%'` typed normally matches ZERO rows while thousands\nexist. Match on the string this route returns, byte for byte."},"OrderOutcomeCensus":{"properties":{"orders_total":{"$ref":"#/components/schemas/Measured_int_","description":"Every order in scope, all classes summed."},"rejected_total":{"$ref":"#/components/schemas/Measured_int_"},"distinct_statuses":{"$ref":"#/components/schemas/Measured_int_"},"distinct_rejection_reasons":{"$ref":"#/components/schemas/Measured_int_"}},"additionalProperties":false,"type":"object","required":["orders_total","rejected_total","distinct_statuses","distinct_rejection_reasons"],"title":"OrderOutcomeCensus","description":"The denominators. ⛔ Computed from the SAME single pass that produced the\nrows, so they cannot disagree with them the way two queries can."},"OrderOutcomeEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/OrderOutcome"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/OrderOutcomeCensus"},"status_semantics":{"$ref":"#/components/schemas/OrderStatusSemantics"},"stale_fallback":{"type":"boolean","title":"Stale Fallback","description":"True when the census rebuild did not finish and you were served the LAST GOOD totals instead. The 200 is then a labelled fallback, not a fresh answer. ⚠️ THE PREDICATE IS THE ROWS' AGE AGAINST THE CACHE TTL, not 'the fallback code path ran': a concurrent rebuild can finish while this one is failing, and that answer is genuinely fresh and is reported so. ⚠️ ALWAYS false on a request that passed `from_block`/`to_block`, because a windowed census is NOT cached — see the route's own note on why the key may not come from the URL.","default":false},"rows_age_s":{"type":"number","title":"Rows Age S","description":"How old this census is, in seconds: the elapsed time since the single GROUP BY pass that produced it finished. A DURATION from a monotonic clock, never a wall-clock timestamp, so an ntp step cannot move it. 0.0 means this request's own pass had just completed — which is always the case for a windowed (uncached) request. Every row's `measured_at` is derived from this, so the stamps beside the numbers cannot disagree with it.","default":0.0},"rebuild_failing":{"type":"boolean","title":"Rebuild Failing","description":"True when a build failure for this route's cached census is REMEMBERED right now — the route is inside a labelled-fallback window and is declining to re-attempt for a cooldown. ⛔ A THIRD AXIS, NOT A SECOND NAME. `stale_fallback` is the census's AGE against the cache TTL; this is DID-THE-BUILD-WORK, and the combination that motivated the field is FRESH + FAILING: the fallback arm re-reads the cache slot, so a concurrent flight can have stored a census AT this instant — `rows_age_s` 0.0, `stale_fallback` false — while this request's own pass was being cancelled and its log line said THE ROUTE IS NOT HEALTHY. Before this field the log and the wire contradicted each other in the same request and the wire was the one that read healthy. ⚠️ ALWAYS false on a request that passed `from_block`/`to_block`, for the same reason `stale_fallback` is: a windowed census is built by that request and reads no cache, so there is no fallback for it to be inside. ⛔ It is NOT `freshness.stale` / `stale_reason_codes`, which are ARCHIVE-vs-CHAIN. Three axes, three names. ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'.","default":false}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census","status_semantics"],"title":"OrderOutcomeEnvelope","description":"⛔ THE TWO STALENESS FIELDS SHIP IN THE SAME COMMIT AS THE FALLBACK THAT\nMAKES THEM NECESSARY, because `/api/book` shipped its fallback WITHOUT them on\n2026-08-27 and doc 304/340 then measured the cost: 654 of 1,417 organic 200s\n(46.2%) were stale, and 259 of those answered in under 5 s, so neither the\nstatus code nor the latency carried the signal. `cached_by_clock_or_last`'s\ndocstring states the rule as an obligation on the CALLER — every caller MUST\npublish the second element — and this is that publication.\n⚠️ SAME NAMES AS `/api/pools` AND `/api/book`, deliberately: it is the same\nCACHE-vs-ARCHIVE axis, and it is NOT `freshness.stale` / `stale_reason_codes`,\nwhich are stable API on the metrics surface and mean ARCHIVE-vs-CHAIN. Both\nappear on this response and they can disagree without either being wrong."},"OrderState":{"properties":{"opened_block":{"type":"integer","title":"Opened Block","description":"The block the OPEN_ORDER confirmed in."},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"type":"string","title":"Tx Hash","description":"The order's own transaction hash."},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source","description":"The address that placed the order."},"give_asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Give Asset"},"get_asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Get Asset"},"give_quantity":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Quantity","description":"RAW opening size. See the class note."},"get_quantity":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Get Quantity"},"give_remaining":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining","description":"RAW, from the LAST ORDER_UPDATE."},"get_remaining":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Get Remaining","description":"RAW, from the LAST ORDER_UPDATE that carried remainings. ⛔ THIS FIELD CAN BE NEGATIVE, AND A NEGATIVE IS NOT AN ERROR. [OBSERVED on production 2026-08-24] 73 of 200 rows on one page of /api/orders carried a negative value, every one of them status='filled'. The sign is CHAIN TRUTH: Counterparty's own ORDER_UPDATE params carry it and this archive publishes what the chain said. Read `-265193370` as 'this order was consumed past the quantity it asked for'. ⚠️ IF YOU DO ARITHMETIC ON THIS FIELD, CLAMP OR BRANCH: `filled = get_quantity - get_remaining` OVERSHOOTS `get_quantity` on these rows, and any percentage built on it exceeds 100%. See this response's `caveats` for the mechanism."},"status":{"type":"string","title":"Status","description":"The order's latest state: filled / expired / cancelled / open, or an 'invalid: ...' rejection reason straight from Counterparty. ⚠️ Read status_semantics on the envelope before treating 'open' as resting."},"expire_index":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Expire Index","description":"null means the order never expires. Measured: that is the majority case for the resting book, not an edge case."},"opened_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Opened At","description":"From the OPEN_ORDER's own block_time."},"give_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Give Divisible"},"get_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Get Divisible"},"give_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Quantity Normalized","description":"The NODE's own normalized opening size — read, not derived."},"get_quantity_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Get Quantity Normalized"},"give_remaining_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Give Remaining Normalized","description":"DERIVED, because ORDER_UPDATE carries no normalized remaining. null when divisibility is unknown: a quantity we cannot scale is one we must not publish as if we could."},"get_remaining_normalized":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Get Remaining Normalized","description":"DERIVED the same way as its give twin, and null under the same condition. ⛔ THE SIGN PROPAGATES AND THAT IS CORRECT, NOT A SECOND DEFECT: normalizing is a divide by 1e8 (divisible) or by nothing (indivisible), and division by a positive constant preserves sign. A negative `get_remaining` therefore appears here as a negative too WHENEVER THIS FIELD HAS A VALUE AT ALL. ⚠️ CORRECTED 2026-08-25 (W139): this sentence used to say ALWAYS, which contradicts the null rule stated one line above it — when divisibility is unknown this field is NULL, not negative, so a reader branching on 'always negative' would never test for null on the very rows where it is the answer. Everything the raw field's description says about arithmetic applies here, and this is the field you should be doing that arithmetic on."}},"additionalProperties":false,"type":"object","required":["opened_block","event_index","tx_hash","source","give_asset","get_asset","give_quantity","get_quantity","give_remaining","get_remaining","status","expire_index","opened_at","give_divisible","get_divisible","give_quantity_normalized","get_quantity_normalized","give_remaining_normalized","get_remaining_normalized"],"title":"OrderState","description":"One order, opened once, in whatever state it ended up in.\n\n⚠️ `*_remaining` ARE RAW CHAIN INTEGERS; `*_remaining_normalized` are the\ndivisibility-adjusted twins and are the ONLY ones safe to do arithmetic on.\nServing raw where normalized was meant is a 1e8 error and this project has\nshipped \"1,163.4353 BITCORN\" once already.\n⛔⛔ A REMAINDER CAN BE NEGATIVE. Not a null, not a zero — a NEGATIVE, on a row\nthat says `status: filled`. It is what the chain said and we publish what the\nchain said; the defect W108 filed was that we published it SILENTLY.\n⚠️ CORRECTED 2026-08-25 (W139). THIS PARAGRAPH USED TO SAY the two remaining\nfields were \"the only ones of their twinned pairs carrying no description at\nall\". THAT WAS FALSE WHEN IT WAS WRITTEN, and it is checkable in one pass over\n`model_fields`: `get_quantity` and `get_quantity_normalized` carry no\ndescription either, and still do. What was true — and is the only claim W108\nneeded — is that these two were the undescribed pair whose values can be\nNEGATIVE. Both descriptions and the route's `caveats` now say so. ⇒ If you\nsubtract this from an opening quantity, CLAMP.\n⚠️ `give_divisible` / `get_divisible` are READ from the chain, never assumed.\nnull means the flag was absent — it does NOT mean divisible."},"OrderStateEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/OrderState"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"status_semantics":{"$ref":"#/components/schemas/OrderStatusSemantics"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","status_semantics"],"title":"OrderStateEnvelope"},"OrderStatusSemantics":{"properties":{"nothing_remaining_rule_applied":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Nothing Remaining Rule Applied","description":"True when the deployed v_order_state reclassifies a status='open' order with a zero remaining as 'filled'. False when it does not, in which case status='open' OVER-COUNTS the resting book — use /api/book, which applies the rule itself, for the book. null when the probe could not run."},"probe":{"type":"string","title":"Probe","description":"The exact check that produced the field above, in words. Never blank."},"measured_at":{"type":"string","format":"date-time","title":"Measured At","description":"When the probe ran. Not the time of any event."}},"additionalProperties":false,"type":"object","required":["nothing_remaining_rule_applied","probe","measured_at"],"title":"OrderStatusSemantics","description":"WHICH DEFINITION OF `status` THE DATABASE ANSWERING THIS REQUEST IS RUNNING.\n\n⛔ THIS IS A MEASUREMENT, NOT A CONFIGURATION FLAG. Nothing sets it; the route\nasks the database and reports what it said. See this module's docstring §2 for\nwhy it exists."},"OrphanEvent":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"event":{"type":"string","title":"Event","description":"The event type, e.g. NEW_BLOCK."},"tx_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Tx Hash"},"orphaned_block_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Orphaned Block Hash","description":"The hash this event was recorded under."},"canonical_block_hash":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Canonical Block Hash","description":"The hash at that height now. null means this archive holds no canonical block there at all, which is itself a finding."},"orphaned_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Orphaned At","description":"When WE copied the row out. Not chain time."},"fetched_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Fetched At","description":"When WE originally fetched it."},"source_node":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source Node"},"params":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"title":"Params","description":"The event's full payload. Only present when include_params=true; null otherwise, which means 'not requested', never 'empty'. ⛔ `BLOCK_PARSED.params.messages_hash` IS NODE-LOCAL AND TIME-VARYING — it is NOT a consensus value and it is NOT comparable across nodes. A mismatch on this one key against another node is NOT evidence that this archive is wrong; it is a property of the field. MEASURED over 22 strided heights (280,000-963,400, 364 event rows, field by field, two independent operators): the event SETS agree everywhere, and so do the two CONSENSUS hashes `ledger_hash` and `txlist_hash` — `messages_hash` is the ONLY key both nodes emit and disagree on. In a whole-population count over blocks 952,800-963,459, 7,407 of those 10,660 BLOCK_PARSED rows held a value NO rostered node served, not even the same build that produced it. ⇒ Do not diff it, do not reconcile against it, and do not derive from it: nothing in this API reads it. ⚠️ AND THIS ROW CANNOT TELL YOU WHICH BUILD PRODUCED THE VALUE. Provenance is RECORDED PER ROW IN THE ARCHIVE — `events.source_node`, `events.node_version`, `events.node_commit` name the node and the build that wrote a row, where they were recorded (rows ingested before these columns existed carry none). Which build wrote a row is recorded, but it is NOT PUBLISHED HERE: `node_version` / `node_commit` are published by no route at all, so nothing in this response attributes this value to a build. The only provenance published anywhere in this API is `MempoolEnvelope.source_node`, `OrphanEvent.source_node`. On a row of the orphaned-event log, the `source_node` beside this payload is that row's own, copied from the archive when the event was orphaned (null where none was recorded), and it names the NODE, never the build; an archived event row outside that log carries no provenance field at all, and the mempool's `source_node` describes mempool sightings, not archived events. Publishing the build per row would be a response-shape change, which this API has declined, so the honest answer is that the build attribution exists in the archive and you cannot read it from here. ⇒ That is the reason to disregard a mismatch on this key rather than to reconcile it."}},"additionalProperties":false,"type":"object","required":["block_index","event_index","event","tx_hash","orphaned_block_hash","canonical_block_hash","orphaned_at","fetched_at","source_node"],"title":"OrphanEvent","description":"One event this archive copied out of a block the chain later took back,\nbeside the hash of the block that replaced it.\n\n⛔ BOTH HASHES, ALWAYS. An orphan record with only one hash cannot be checked\nby anybody: the whole claim is that these two differ at the same height."},"OrphanEventCensus":{"properties":{"preserved_rows":{"$ref":"#/components/schemas/Measured_int_","description":"Every row in events_orphaned, INCLUDING re-fetches of a block whose hash never changed. The superset this route filters."},"diverged_rows":{"$ref":"#/components/schemas/Measured_int_","description":"Rows where the height's hash actually differs — what this route returns."},"distinct_forks":{"$ref":"#/components/schemas/Measured_int_","description":"From v_orphaned_fork. null if that view is not present."}},"additionalProperties":false,"type":"object","required":["preserved_rows","diverged_rows","distinct_forks"],"title":"OrphanEventCensus"},"OrphanEventEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/OrphanEvent"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"census":{"$ref":"#/components/schemas/OrphanEventCensus"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","census"],"title":"OrphanEventEnvelope"},"OutcomeClass":{"type":"string","enum":["rejected","filled","expired","cancelled","open","other"],"title":"OutcomeClass","description":"⛔ ASSIGNED POSITIVELY, NEVER AS \"not one of the others\". `rejected` is the\nclass for every status Counterparty prefixes with `invalid:`; the four named\nlifecycle states get their own class; anything else this archive has never seen\nlands in `other` and is VISIBLE rather than silently folded into a neighbour."},"PoolDepositRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx":{"type":"string","title":"Tx"},"tx_index":{"type":"integer","title":"Tx Index"},"provider":{"type":"string","title":"Provider"},"asset_a":{"type":"string","title":"Asset A"},"asset_b":{"type":"string","title":"Asset B"},"quantity_a":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity A"},"quantity_a_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity A Normalized"},"asset_a_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset A Divisible"},"quantity_b":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity B"},"quantity_b_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity B Normalized"},"asset_b_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset B Divisible"},"lp_quantity":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lp Quantity"},"lp_quantity_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lp Quantity Normalized"},"lp_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Lp Divisible"},"lp_quantity_field":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lp Quantity Field"},"status":{"type":"string","title":"Status"},"deposited_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Deposited At"}},"type":"object","required":["block_index","event_index","tx","tx_index","provider","asset_a","asset_b","quantity_a","quantity_a_normalized","asset_a_divisible","quantity_b","quantity_b_normalized","asset_b_divisible","lp_quantity","lp_quantity_normalized","lp_divisible","lp_quantity_field","status","deposited_at"],"title":"PoolDepositRow","description":"One LP deposit. The timestamp key is `deposited_at` here and `withdrawn_at`\non withdrawals, which is why the two are separate row schemas."},"PoolDepositsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/PoolDepositRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"matching":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Matching"},"has_more":{"type":"boolean","title":"Has More"},"venue":{"type":"string","title":"Venue"},"filter":{"type":"string","title":"Filter"},"units":{"type":"string","title":"Units"},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","count","matching","has_more","venue","filter","units","source"],"title":"PoolDepositsEnvelope","description":"GET /api/pool/deposits."},"PoolReserveEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/PoolReservePoint"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"points_without_time":{"$ref":"#/components/schemas/Measured_int_","description":"Rows in THIS response whose changed_at is null — a time-axis chart drops them."}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","points_without_time"],"title":"PoolReserveEnvelope"},"PoolReservePoint":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"asset_a":{"type":"string","title":"Asset A"},"asset_b":{"type":"string","title":"Asset B"},"reserve_a":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Reserve A","description":"NORMALIZED. The node did the divisibility work."},"reserve_b":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Reserve B"},"reserve_a_raw":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Reserve A Raw","description":"The raw on-chain integer, kept beside the normalized twin."},"reserve_b_raw":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Reserve B Raw"},"via":{"type":"string","title":"Via","description":"'OPEN_POOL' (pool creation) or 'POOL_UPDATE' (a reserve change)."},"changed_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Changed At","description":"From block_times. Null where the block has no timestamp."}},"additionalProperties":false,"type":"object","required":["block_index","event_index","asset_a","asset_b","reserve_a","reserve_b","reserve_a_raw","reserve_b_raw","via","changed_at"],"title":"PoolReservePoint"},"PoolRow":{"properties":{"asset_a":{"type":"string","title":"Asset A"},"asset_b":{"type":"string","title":"Asset B"},"reserve_a":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reserve A"},"reserve_b":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Reserve B"},"reserves_known":{"type":"boolean","title":"Reserves Known"},"reserve_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reserve Source"},"tx":{"type":"string","title":"Tx"},"opened_block":{"type":"integer","title":"Opened Block"},"lp_asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lp Asset","description":"The pool's LP token, from its OPEN_POOL birth event — the same row `tx` and `opened_block` come from, so it is a fact about the pool's CREATION, never its current state. Null only if that event carries no `lp_asset`. Declared optional in this schema; the handler always emits it."},"creator":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Creator","description":"The address that opened the pool, from OPEN_POOL's `source`. ⛔ NOT to be confused with this row's `supply_source`, `burn_source` and `reserve_source`, which are PROVENANCE strings and not addresses. Declared optional in this schema; the handler always emits it."},"last_change_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Change Block"},"changed_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Changed At"},"quote_asset":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Quote Asset"},"swaps_all":{"type":"integer","title":"Swaps All"},"swaps_24h":{"type":"integer","title":"Swaps 24H"},"vol_quote_all":{"type":"number","title":"Vol Quote All"},"vol_quote_24h":{"type":"number","title":"Vol Quote 24H"},"vol_xcp_all":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vol Xcp All"},"vol_xcp_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Vol Xcp 24H"},"last_swap_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Swap Block"},"last_swap_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Swap At"},"last_swap_price":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Last Swap Price"},"chg_24h":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Chg 24H"},"chg_24h_basis":{"type":"string","title":"Chg 24H Basis"},"fee_bps_min":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Fee Bps Min"},"fee_bps_max":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Fee Bps Max"},"a":{"$ref":"#/components/schemas/LegSupply"},"b":{"$ref":"#/components/schemas/LegSupply"},"divisible_a":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Divisible A"},"divisible_b":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Divisible B"},"supply_source":{"type":"string","title":"Supply Source"},"burn_ok":{"type":"boolean","title":"Burn Ok"},"burn_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Burn Reason"},"burn_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Burn Source"},"burn_fetched_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Burn Fetched At"},"burn_addresses":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Burn Addresses"},"burn_addresses_counted":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Burn Addresses Counted","description":"How many addresses in this snapshot COUNT TOWARD BURN TOTALS AT ALL — the subset of `burn_addresses` the snapshot marks `counts_as_burn`. ⛔ SNAPSHOT-WIDE, NOT PER-ASSET, AND NOT PER-ROW. This row has TWO LEGS with TWO DIFFERENT ASSETS, and each leg's `burned` is summed over only the counting addresses that actually HOLD THAT ASSET — which is nearly always far fewer. ⚠️ SO DO NOT READ THIS AS THE COVERAGE OF EITHER LEG'S `burned`. It is a property of the SNAPSHOT: how wide the curated list was when it was taken. Use it against `burn_addresses` (the watched total) and nothing else. Null on the burn-failure path, exactly like `burn_source` and `burn_fetched_at`. Declared optional in this schema; the handler always emits it."},"burn_addresses_are_a_curated_list":{"type":"boolean","title":"Burn Addresses Are A Curated List"},"burn_list_caveat":{"type":"string","title":"Burn List Caveat"}},"type":"object","required":["asset_a","asset_b","reserve_a","reserve_b","reserves_known","reserve_source","tx","opened_block","last_change_block","changed_at","quote_asset","swaps_all","swaps_24h","vol_quote_all","vol_quote_24h","vol_xcp_all","vol_xcp_24h","last_swap_block","last_swap_at","last_swap_price","chg_24h","chg_24h_basis","fee_bps_min","fee_bps_max","a","b","divisible_a","divisible_b","supply_source","burn_ok","burn_reason","burn_source","burn_fetched_at","burn_addresses","burn_addresses_are_a_curated_list","burn_list_caveat"],"title":"PoolRow","description":"One AMM pool.\n\n`vol_xcp_all` / `vol_xcp_24h` are this pool's volume in XCP, all-time and over\nthe trailing 24 hours, and each has three cases. 0.0: the pool had no swap in\nthat window — a MEASURED zero, whatever its quote asset. The quote volume\nitself: the quote asset is XCP. null: the pool swapped in a quote asset that\ncannot be expressed in XCP; or the zero is NOT confirmed — the swap figures\nin this response hold no row for the pool while its reserves last moved at a\nblock those figures did not reach, and `chg_24h_basis` then reads\n`swap-row-not-in-this-window-build`. So a non-XCP pool with no swaps reads\n0.0, not null.\n`changed_at` is the block time of `last_change_block`, the pool's last reserve\nchange. POOL_UPDATE carries no timestamp of its own, so the time is looked up\nby block, and it is null when that block's time is not known here or when no\nreserve change is recorded.\n⚠️ `burn_source`/`burn_fetched_at`/`burn_addresses` are always present, and null\non the burn-failure path — never absent."},"PoolSwapRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx_hash":{"type":"string","title":"Tx Hash"},"swapper":{"type":"string","title":"Swapper"},"asset_a":{"type":"string","title":"Asset A"},"asset_b":{"type":"string","title":"Asset B"},"received_asset":{"type":"string","title":"Received Asset"},"received_quantity":{"type":"number","title":"Received Quantity"},"paid_asset":{"type":"string","title":"Paid Asset"},"paid_quantity":{"type":"number","title":"Paid Quantity"},"received_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Received Divisible"},"paid_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Paid Divisible"},"fee_quantity":{"type":"number","title":"Fee Quantity"},"fee_bps":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Fee Bps"},"status":{"type":"string","title":"Status"},"swapped_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Swapped At"}},"type":"object","required":["block_index","event_index","tx_hash","swapper","asset_a","asset_b","received_asset","received_quantity","paid_asset","paid_quantity","received_divisible","paid_divisible","fee_quantity","fee_bps","status","swapped_at"],"title":"PoolSwapRow","description":"One AMM swap. ⚠️ `tx_hash` here, NOT `tx` as on the book and LP tapes."},"PoolSwapsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/PoolSwapRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"matching":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Matching"},"has_more":{"type":"boolean","title":"Has More"},"venue":{"type":"string","title":"Venue"},"filter":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Filter","description":"WHICH FILTER ACTUALLY RAN, including 'none — the whole tape, unfiltered'. The LP tapes have published this since they shipped; the swap tape gained it with the asset_a/asset_b pair filter, because before those params existed an unknown query param was ACCEPTED AND SILENTLY IGNORED — /api/pool/swaps?asset_a=XCP answered HTTP 200 with every row, unfiltered. A 200 is not evidence a filter ran. Declared optional in this schema; the handler always emits it."},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","count","matching","has_more","venue","source"],"title":"PoolSwapsEnvelope","description":"GET /api/pool/swaps. No `units` key, unlike the LP tapes."},"PoolWithdrawalRow":{"properties":{"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"tx":{"type":"string","title":"Tx"},"tx_index":{"type":"integer","title":"Tx Index"},"provider":{"type":"string","title":"Provider"},"asset_a":{"type":"string","title":"Asset A"},"asset_b":{"type":"string","title":"Asset B"},"quantity_a":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity A"},"quantity_a_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity A Normalized"},"asset_a_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset A Divisible"},"quantity_b":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity B"},"quantity_b_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Quantity B Normalized"},"asset_b_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Asset B Divisible"},"lp_quantity":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lp Quantity"},"lp_quantity_normalized":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Lp Quantity Normalized"},"lp_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Lp Divisible"},"lp_quantity_field":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Lp Quantity Field"},"status":{"type":"string","title":"Status"},"withdrawn_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Withdrawn At"}},"type":"object","required":["block_index","event_index","tx","tx_index","provider","asset_a","asset_b","quantity_a","quantity_a_normalized","asset_a_divisible","quantity_b","quantity_b_normalized","asset_b_divisible","lp_quantity","lp_quantity_normalized","lp_divisible","lp_quantity_field","status","withdrawn_at"],"title":"PoolWithdrawalRow"},"PoolWithdrawalsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/PoolWithdrawalRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"matching":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Matching"},"has_more":{"type":"boolean","title":"Has More"},"venue":{"type":"string","title":"Venue"},"filter":{"type":"string","title":"Filter"},"units":{"type":"string","title":"Units"},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","count","matching","has_more","venue","filter","units","source"],"title":"PoolWithdrawalsEnvelope","description":"GET /api/pool/withdrawals."},"PoolsArchiveFreshness":{"properties":{"age_s":{"type":"number","title":"Age S"},"stale_fallback":{"type":"boolean","title":"Stale Fallback"},"built_at_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Built At Block","description":"The head half of the stamp these rows were BUILT at — not the envelope's `as_of_block`, which dates the REQUEST. ⛔ NOT THE PREDICATE: `stale_fallback` can be true while this equals `as_of_block`, and that combination means THE BACKFILL MOVED. Never infer staleness from the head alone. Can be BEHIND a reorg'd chain or AHEAD of the archive after one."},"keyed_on":{"type":"string","title":"Keyed On"},"rebuild_failing":{"type":"boolean","title":"Rebuild Failing"}},"type":"object","required":["age_s","stale_fallback","built_at_block","keyed_on","rebuild_failing"],"title":"PoolsArchiveFreshness","description":"`freshness.archive` — the half carrying pools, supply and reserves.\n\n⚠️ ITS PREDICATE IS A STAMP, NOT AN AGE. `stale_fallback` here is\n`built stamp != current stamp`, where the stamp is\n(archive head, backfill fingerprint) — an EXACT statement about which\narchive state these rows describe. So a body can carry `age_s: 600` with\n`stale_fallback: false` and be perfectly correct: nothing has changed."},"PoolsEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/PoolRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"as_of_block":{"type":"integer","title":"As Of Block"},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"True when the pool rows in this response finished building at least 60 s ago — this route's cache TTL. ⛔ IT IS AN AGE COMPARISON AND NOT A REPORT ON THE REBUILD: a rebuild that SUCCEEDED but took longer than the TTL reads true, and a fallback whose re-read found rows a concurrent rebuild had just stored reads false. The 200 is then a LABELLED fallback and this field is the label — it states the rows' age, never the outcome of a build. Read `rows_age_s` for that age and `rebuild_failing` for whether the rebuild is broken. ⛔ THIS IS THE CACHE-vs-ARCHIVE AXIS and it is NOT `freshness.stale` / `stale_reason_codes`, which are stable API on the metrics surface and describe ARCHIVE-vs-CHAIN: a response can carry stale_fallback=true while the archive itself is level with the chain, which is the normal shape of this outage. It is the same field name /api/book publishes, deliberately, because it is the same axis. ⚠️ THE PREDICATE IS THE ROWS' AGE AGAINST THE CACHE TTL, not 'the fallback code path ran': a concurrent rebuild can finish while this one is failing, and that fallback answer is genuinely fresh and is reported so. ⚠️ IT IS NOT DERIVABLE FROM `as_of_block`, unlike /api/book's: this route caches on the wall clock, so its rows carry no block height of their own — read `rows_age_s`. Declared optional in this schema; the handler always emits it."},"rows_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rows Age S","description":"How old the pool rows are, in seconds: the elapsed time since the build that produced THESE rows finished, measured from the stamp the cache itself wrote — including where a concurrent request was handed somebody else's completed build. A DURATION from a monotonic clock, never a wall-clock timestamp, so it cannot be moved by an ntp step. 0.0 means this request's own build had just completed. Compare it to the 60 s cache TTL: `stale_fallback` is exactly `rows_age_s >= 60`. Declared optional in this schema; the handler always emits it."},"rebuild_failing":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Rebuild Failing","description":"True when a build failure for this route is REMEMBERED right now — the route is inside a labelled-fallback window and is declining to re-attempt for a cooldown. ⛔ THIS IS A DIFFERENT QUESTION FROM `stale_fallback` AND THE TWO COME APART. `stale_fallback` is the ROWS' AGE against the cache TTL; this is DID-THE-BUILD-WORK. The combination that motivated the field is FRESH + FAILING, and on this route it is MORE reachable than on /api/book: the fallback arm re-reads the cache slot, so a concurrent flight can have stored rows at this instant — `rows_age_s` 0.0, `stale_fallback` false — while THIS request's own build was being cancelled and its log line said THE ROUTE IS NOT HEALTHY. ⛔ It is NOT `freshness.stale` / `stale_reason_codes` either — those are ARCHIVE-vs-CHAIN. Three axes, three names. ⚠️ A process RESTART clears the memory with no transition, so `false` means 'nothing remembered in THIS process'. ⛔ IT IS AN `or` ACROSS BOTH CACHE KEYS and never under-states; read `freshness` to learn WHICH half is broken."},"freshness":{"anyOf":[{"$ref":"#/components/schemas/PoolsFreshness"},{"type":"null"}],"description":"Per-half staleness. The three summary fields above are the WORST of the two halves and keep their meaning; this is the decomposition, never a replacement. Declared optional in this schema; the handler always emits it."},"rebuild_health":{"anyOf":[{"$ref":"#/components/schemas/PoolsRebuildHealthPair"},{"type":"null"}],"description":"Per-half rebuild health over a WINDOW. `rebuild_failing` is an instant and cannot be trended; this is what an operator alerts on — gated on `observed_s`. Declared optional in this schema; the handler always emits it."},"halves":{"anyOf":[{"$ref":"#/components/schemas/PoolsHalves"},{"type":"null"}],"description":"What the merge of the two caches did. `window_pairs_unmatched` is normally 0; non-zero means the pool list is one build behind its own population. Declared optional in this schema; the handler always emits it."},"pools_total":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pools Total","description":"⚠️ THE POPULATION, where `count` is THIS PAGE. Their equality today is a measurement of the data, not a property of the code, which is exactly why both are published. Declared optional in this schema; the handler always emits it."},"truncated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Truncated","description":"True when the row cap OR the byte budget cut this page short. Declared optional in this schema; the handler always emits it."},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Where to resume, or null when nothing was cut. ⚠️ `offset + rows RETURNED`, never `offset + limit`: when the BYTE budget cut the page short those differ and the latter would silently skip the rows the budget refused. Declared optional in this schema; the handler always emits it."},"ceiling_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Ceiling Note","description":"Why this page was cut, or null when it was not — a constant string stops meaning anything. ⛔ A statement about THIS RESPONSE, never about the archive. Declared optional in this schema; the handler always emits it."},"source":{"type":"string","title":"Source"},"note":{"type":"string","title":"Note"}},"type":"object","required":["result","count","as_of_block","source","note"],"title":"PoolsEnvelope","description":"GET /api/pools. `as_of_block` can be -1."},"PoolsFreshness":{"properties":{"archive":{"$ref":"#/components/schemas/PoolsArchiveFreshness"},"window":{"$ref":"#/components/schemas/PoolsWindowFreshness"},"note":{"type":"string","title":"Note"}},"type":"object","required":["archive","window","note"],"title":"PoolsFreshness","description":"`freshness` — /api/pools is assembled from TWO caches, so it has TWO ages.\n\n⛔ THE TOP-LEVEL `stale_fallback` / `rows_age_s` ARE THE WORST OF THE TWO,\nNEVER AN AVERAGE, and they keep their old meaning. This block is the\nDECOMPOSITION: without it a consumer cannot tell a stale 24-hour window\n(harmless, 60 s deep) from a stale supply table."},"PoolsHalves":{"properties":{"archive_pools":{"type":"integer","title":"Archive Pools"},"window_pairs":{"type":"integer","title":"Window Pairs"},"pools_with_swap_row":{"type":"integer","title":"Pools With Swap Row"},"pools_without_swap_row":{"type":"integer","title":"Pools Without Swap Row"},"pools_without_swap_row_unconfirmed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Pools Without Swap Row Unconfirmed","description":"How many of `pools_without_swap_row` are pools whose reserves moved ABOVE `window_tape_head` — so the window build could not have seen them swap and their \"never swapped\" is NOT a measurement. Those rows publish `chg_24h_basis: \"swap-row-not-in-this-window-build\"` and a NULL `vol_xcp_all` rather than a measured 0.0. Normally 0. ⛔ It does NOT catch an arbitrary single window row going missing — only the lag produced by the two caches being invalidated differently."},"window_tape_head":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Window Tape Head","description":"The highest POOL_MATCH block the WINDOW build observed — the height the two halves are compared at. Null when that build saw no swaps at all. Pair it with `freshness.archive.built_at_block`: the two halves are routinely NOT level, and that is the design rather than a fault."},"window_pairs_unmatched":{"type":"integer","title":"Window Pairs Unmatched"}},"type":"object","required":["archive_pools","window_pairs","pools_with_swap_row","pools_without_swap_row","window_pairs_unmatched"],"title":"PoolsHalves","description":"`halves` — what the merge of the two caches actually did, published.\n\n⛔ `window_pairs_unmatched` IS THE ONE THAT MATTERS AND IT IS NORMALLY 0.\nNon-zero means the swap tape knows a pair the pool list does not — a pool\nopened between the two cache builds — so the list in hand is one build behind\non its OWN population. Bounded (one block) and DISCLOSED, because the\nalternative is a reader concluding \"no such pool\" from a body that has simply\nnot caught up."},"PoolsRebuildHealth":{"properties":{"failing_now":{"type":"boolean","title":"Failing Now"},"outages":{"type":"integer","title":"Outages"},"armed_s":{"type":"number","title":"Armed S"},"armed_exceeds_observed":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Armed Exceeds Observed","description":"⚠️ Normally false. True means a ledger entry claims to predate the process, so `armed_fraction` has been CLAMPED to 1.0 and is not a health reading — the unclamped `armed_s` and `observed_s` beside it are. It is published rather than clamped silently."},"armed_fraction":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Armed Fraction","description":"`armed_s / observed_s`, clamped to [0, 1] — see `armed_exceeds_observed`. ⚠️ NULL, never 0.0, when nothing has been observed yet — 'what fraction of nothing' has no answer and a zero there reads as a healthy hour."},"longest_armed_s":{"type":"number","title":"Longest Armed S"},"window_s":{"type":"number","title":"Window S"},"observed_s":{"type":"number","title":"Observed S"}},"type":"object","required":["failing_now","outages","armed_s","armed_fraction","longest_armed_s","window_s","observed_s"],"title":"PoolsRebuildHealth","description":"One key's rebuild health over a window.\n\n⛔ READ `observed_s` BEFORE `armed_fraction`. The ledger is per-process and a\nrestart empties it, so a young process honestly reports 0.0 for the seconds it\nsaw. An alert that does not gate on `observed_s` is alerting on an empty\nsample. `rebuild_failing` alone is an instant: it can read `false` on poll\nafter poll on a day with many failed rebuilds."},"PoolsRebuildHealthPair":{"properties":{"archive":{"$ref":"#/components/schemas/PoolsRebuildHealth"},"window":{"$ref":"#/components/schemas/PoolsRebuildHealth"}},"type":"object","required":["archive","window"],"title":"PoolsRebuildHealthPair","description":"`rebuild_health` — the same measurement for each of the two cache keys."},"PoolsWindowFreshness":{"properties":{"age_s":{"type":"number","title":"Age S"},"stale_fallback":{"type":"boolean","title":"Stale Fallback"},"ttl_s":{"type":"number","title":"Ttl S"},"keyed_on":{"type":"string","title":"Keyed On"},"rebuild_failing":{"type":"boolean","title":"Rebuild Failing"}},"type":"object","required":["age_s","stale_fallback","ttl_s","keyed_on","rebuild_failing"],"title":"PoolsWindowFreshness","description":"`freshness.window` — the half carrying the swap tape (volume, prices, fees).\n\n⚠️ ITS PREDICATE IS AN AGE, because a wall-clock answer has no stamp to\ncompare. A trailing 24-hour window CAN change with no block landing, which is\nwhy this half is not keyed on the archive."},"RateResult":{"properties":{"pair":{"type":"string","title":"Pair"},"unit":{"type":"string","title":"Unit"},"method":{"type":"string","title":"Method"},"why_dispenses":{"type":"string","title":"Why Dispenses"},"samples":{"type":"integer","title":"Samples"},"samples_target":{"type":"integer","title":"Samples Target"},"samples_floor":{"type":"integer","title":"Samples Floor"},"rate":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Rate"},"spread_low":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Spread Low"},"spread_high":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Spread High"},"spread_ratio":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Spread Ratio"},"from_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"From Block"},"to_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"To Block"},"newest_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Newest At"},"blocks_since_last":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Blocks Since Last"},"below_target":{"type":"boolean","title":"Below Target"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["pair","unit","method","why_dispenses","samples","samples_target","samples_floor","rate","spread_low","spread_high","spread_ratio","from_block","to_block","newest_at","blocks_since_last","below_target"],"title":"RateResult","description":"The rate `/api/rates` publishes.\n\n⚠️ `reason` IS ABSENT ENTIRELY on the publishable branch — it is present only\nwhen `samples` is below `samples_floor`. `spread_ratio` and `newest_at` can\nalso be null on the SUCCESS branch, not only on the refusal branch."},"RatesEnvelope":{"properties":{"result":{"$ref":"#/components/schemas/RateResult"},"as_of_block":{"type":"integer","title":"As Of Block"},"source":{"type":"string","title":"Source"}},"type":"object","required":["result","as_of_block","source"],"title":"RatesEnvelope","description":"GET /api/rates. `result` is an OBJECT, not a list."},"ReorgPreservation":{"properties":{"available":{"type":"boolean","title":"Available"},"since":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since"},"since_source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Since Source"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["available","since","since_source","reason"],"title":"ReorgPreservation"},"ReorgTotals":{"properties":{"forks":{"type":"integer","title":"Forks"},"blocks_removed":{"type":"integer","title":"Blocks Removed"},"blocks_replaced":{"type":"integer","title":"Blocks Replaced"},"blocks_no_canonical":{"type":"integer","title":"Blocks No Canonical"},"blocks_returned_identical":{"type":"integer","title":"Blocks Returned Identical"},"events_removed":{"type":"integer","title":"Events Removed"},"events_truly_orphaned":{"type":"integer","title":"Events Truly Orphaned"},"first_seen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"First Seen"},"last_seen":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Last Seen"}},"type":"object","required":["forks","blocks_removed","blocks_replaced","blocks_no_canonical","blocks_returned_identical","events_removed","events_truly_orphaned","first_seen","last_seen"],"title":"ReorgTotals"},"ReorgsEnvelope":{"properties":{"forks":{"anyOf":[{"items":{"$ref":"#/components/schemas/ForkRow"},"type":"array"},{"type":"null"}],"title":"Forks","description":"NULL means WE COULD NOT LOOK — orphan preservation is not present on this database, so there is no log to read. It is NOT an empty log: `[]` would be a positive claim that this archive has observed no fork. Read `preservation.reason` and `absence_means` before inferring anything about the chain."},"forks_observed":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Forks Observed","description":"Distinct forks observed — a LOWER BOUND, see `counting_note`. NULL, not 0, when `forks` is null: zero would be a count of a log that does not exist."},"limit":{"type":"integer","title":"Limit"},"truncated":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Truncated","description":"Whether `forks` was cut at `limit`. NULL when `forks` is null — there was no list to cut."},"totals":{"anyOf":[{"$ref":"#/components/schemas/ReorgTotals"},{"type":"null"}],"description":"Whole-log totals, independent of `limit`. NULL when the log cannot be read; every member would otherwise be a fabricated zero."},"preservation":{"$ref":"#/components/schemas/ReorgPreservation"},"absence_means":{"type":"string","title":"Absence Means"},"counting_note":{"type":"string","title":"Counting Note"},"over_capture_note":{"type":"string","title":"Over Capture Note"},"block_hash_note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Block Hash Note","description":"How to read the block hashes in `forks`. ABSENT — not null — on the not-preserved branch, where there are no forks to describe."},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["forks","forks_observed","limit","truncated","totals","preservation","absence_means","counting_note","over_capture_note","source"],"title":"ReorgsEnvelope","description":"GET /api/reorgs.\n\n⛔ THIS ROUTE HAS TWO 200 BRANCHES:\n  1 · preserved     — orphan preservation is present on this database; every\n                      field below carries a value.\n  2 · NOT preserved — there is no fork log to read. `forks`, `forks_observed`,\n                      `truncated` and `totals` are NULL and `block_hash_note` is\n                      ABSENT ENTIRELY. Still HTTP 200: that is a stable fact,\n                      not a retryable one.\n`[]` and `0` would be POSITIVE CLAIMS ABOUT THE CHAIN over a database on which a\nfork could not have been recorded at all — `null` is the honest answer."},"ResolutionRow":{"properties":{"asset":{"type":"string","title":"Asset"},"tx":{"type":"string","title":"Tx"},"outcome":{"type":"string","title":"Outcome"},"outcome_basis":{"type":"string","title":"Outcome Basis"},"soft_cap_reading_agrees":{"type":"boolean","title":"Soft Cap Reading Agrees"},"xcp":{"type":"number","title":"Xcp"},"minters":{"type":"integer","title":"Minters"},"deadline":{"type":"integer","title":"Deadline"},"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"closed_at":{"type":"string","title":"Closed At"}},"type":"object","required":["asset","tx","outcome","outcome_basis","soft_cap_reading_agrees","xcp","minters","deadline","block_index","event_index","closed_at"],"title":"ResolutionRow"},"ResolutionsEnvelope":{"properties":{"block":{"type":"integer","title":"Block"},"result":{"items":{"$ref":"#/components/schemas/ResolutionRow"},"type":"array","title":"Result"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The ARCHIVE block these rows were built at — the archive head, read once per request and used as the cache key, carried inside the cached value so a request that waited on another's build publishes that build's block. ⛔ NOT `block`: `block` is `ingest_health.chain_tip`, the node tip used as the COUNTDOWN reference, and it is an UPPER bound on this — during catch-up the node tip stands still while the archive advances. Age this answer by `as_of_block`."},"stale_fallback":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Stale Fallback","description":"Always false on this route: it has no fallback arm (a failed rebuild is a 503, never old rows). ⛔ It is NOT `/api/markets/ranking`'s head comparison (`as_of_block != archive_head`): a request that waited on another request's build can publish an `as_of_block` below the head it read and still say false here. Age this answer by `as_of_block`."},"count":{"type":"integer","title":"Count"},"outcome_not_established":{"type":"integer","title":"Outcome Not Established"},"soft_cap_reading_disagreements":{"type":"integer","title":"Soft Cap Reading Disagreements"},"outcome_note":{"type":"string","title":"Outcome Note"}},"type":"object","required":["block","result","count","outcome_not_established","soft_cap_reading_disagreements","outcome_note"],"title":"ResolutionsEnvelope","description":"GET /api/resolutions."},"ResultState":{"type":"string","enum":["complete","truncated","empty_past_end","empty_no_match","empty_source"],"title":"ResultState","description":"⛔ `complete` and `truncated` are the ONLY states that may carry rows; the\nthree `empty_*` states are the ONLY ones that may not. Enforced on `Envelope`,\nboth directions.\n\n⛔⛔ EVERY MEMBER HERE DESCRIBES THE PAGE, NOT THE ANSWER. Ruled 2026-08-24, and\nit is written at the top because the enum gloss and the route disagreed for as\nlong as both existed: this enum said \"every matching row is in this response\"\nwhile `lifecycle_routes.py` had always used it as the paging signal — a response\ncan answer `complete` for its page while a census beside it is a lower bound\n(`_VOCABULARY_CAP`). ⇒ `result_state` answers \"is there another page\"; whether an\nAGGREGATE was computed over a complete population is answered by that number's\nown `Measured.method`, which is the one place a truncation can be said per number\ninstead of once for the whole envelope. No member was added for it: see the\n\"free exactly once\" note below — the one free change was spent on\n`EMPTY_PAST_END`.\n\n★ `EMPTY_PAST_END` EXISTS BECAUSE THE PARTITION WAS NOT TOTAL. SCOUT 8:\n*\"Any request whose `offset` exceeds the matching row count falls through to\n`EMPTY_NO_MATCH`, defined as 'we looked; the source holds rows; none matched'.\nRows did match. They are simply before the offset.\"* Observed on the largest\nlaunch on the chain: 243 blocks matched and the response said none did.\n⇒ The scout preferred a non-breaking boolean over a fifth enum member, on this\nmodule's own \"a contract an agent embeds must not move under it\" rule. **I\ntook the enum instead, and the reason is that rule's own logic:**\n`result_state` is the field a machine branches on, and leaving it saying\n`none matched` while a boolean beside it whispers otherwise reproduces the\n`/api/coverage` shape this module exists to make impossible. `CONTRACT_VERSION`\nhas not been embedded by any agent yet, so the enum is free exactly once —\ntoday. It will not be free again.\n⚠️ AND IT IS MEASURED, NOT INFERRED: a route only emits this after re-running\nits own filtered query at `offset=0` and SEEING a row. If it cannot prove rows\nmatched, it says `empty_no_match` instead."},"RewriteAvailability":{"properties":{"available":{"type":"boolean","title":"Available"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Reason"}},"type":"object","required":["available","reason"],"title":"RewriteAvailability"},"SamplerHealth":{"properties":{"last_run_at":{"anyOf":[{"type":"string","format":"date-time"},{"type":"null"}],"title":"Last Run At"},"last_run_as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Run As Of Block"},"last_run_pairs_sampled":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Run Pairs Sampled"},"last_run_dex_rows":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Run Dex Rows"},"last_run_amm_rows":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Run Amm Rows"},"last_run_errors":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Run Errors"},"last_run_had_error":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Last Run Had Error","description":"Whether the run recorded an error. Text NOT republished."},"run_age_s":{"anyOf":[{"type":"number"},{"type":"null"}],"title":"Run Age S"},"sampler_alive":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Sampler Alive","description":"run_age_s within max_run_age_s. null = could not determine."},"max_run_age_s":{"type":"number","title":"Max Run Age S"}},"additionalProperties":false,"type":"object","required":["last_run_at","last_run_as_of_block","last_run_pairs_sampled","last_run_dex_rows","last_run_amm_rows","last_run_errors","last_run_had_error","run_age_s","sampler_alive","max_run_age_s"],"title":"SamplerHealth","description":"The price sampler's OWN liveness. Distinct from archive freshness: the\npoller can be healthy while the sampler is dead, and then this series is stale\nwhile `freshness.stale` is false."},"StatsEnvelope":{"properties":{"events":{"type":"integer","title":"Events"},"first_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"First Block"},"last_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Last Block"},"complete_blocks":{"type":"integer","title":"Complete Blocks"},"complete_contiguous_from":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Complete Contiguous From"},"complete_contiguous_to":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Complete Contiguous To"},"complete_contiguous":{"type":"integer","title":"Complete Contiguous"},"complete_outside_contiguous":{"type":"integer","title":"Complete Outside Contiguous"},"complete_below_floor_top":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Complete Below Floor Top"},"complete_blocks_note":{"type":"string","title":"Complete Blocks Note"},"exact":{"type":"boolean","title":"Exact"}},"type":"object","required":["events","first_block","last_block","complete_blocks","complete_contiguous_from","complete_contiguous_to","complete_contiguous","complete_outside_contiguous","complete_below_floor_top","complete_blocks_note","exact"],"title":"StatsEnvelope","description":"GET /api/stats.\n\n`complete_below_floor_top` is null in the NORMAL case."},"TradeRow":{"properties":{"block":{"type":"integer","title":"Block"},"block_index":{"type":"integer","title":"Block Index"},"event_index":{"type":"integer","title":"Event Index"},"match_id":{"type":"string","title":"Match Id"},"maker_tx":{"type":"string","title":"Maker Tx"},"taker_tx":{"type":"string","title":"Taker Tx"},"maker_address":{"type":"string","title":"Maker Address"},"taker_address":{"type":"string","title":"Taker Address"},"taker_received_asset":{"type":"string","title":"Taker Received Asset"},"taker_received_quantity":{"type":"number","title":"Taker Received Quantity"},"taker_received_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Taker Received Divisible"},"taker_paid_asset":{"type":"string","title":"Taker Paid Asset"},"taker_paid_quantity":{"type":"number","title":"Taker Paid Quantity"},"taker_paid_divisible":{"anyOf":[{"type":"boolean"},{"type":"null"}],"title":"Taker Paid Divisible"},"fee_paid":{"type":"number","title":"Fee Paid"},"final_status":{"type":"string","title":"Final Status"},"status_at_match":{"type":"string","title":"Status At Match"},"taker_received_raw":{"type":"number","title":"Taker Received Raw"},"taker_paid_raw":{"type":"number","title":"Taker Paid Raw"},"traded_at":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Traded At"}},"type":"object","required":["block","block_index","event_index","match_id","maker_tx","taker_tx","maker_address","taker_address","taker_received_asset","taker_received_quantity","taker_received_divisible","taker_paid_asset","taker_paid_quantity","taker_paid_divisible","fee_paid","final_status","status_at_match","taker_received_raw","taker_paid_raw","traded_at"],"title":"TradeRow","description":"One matched trade. Quantities are never null (a missing value reads 0); the\n`*_divisible` flags are passed through raw and CAN be null; `traded_at` is null\nwhen the block's time is not known."},"TradesEnvelope":{"properties":{"result":{"items":{"$ref":"#/components/schemas/TradeRow"},"type":"array","title":"Result"},"count":{"type":"integer","title":"Count"},"matching":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Matching"},"has_more":{"type":"boolean","title":"Has More"},"venue":{"type":"string","title":"Venue"},"note":{"type":"string","title":"Note"},"source":{"type":"string","title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["result","count","matching","has_more","venue","note","source"],"title":"TradesEnvelope","description":"GET /api/trades.\n\n★ `matching` is null BY DESIGN, and it does not mean zero: it is null exactly\nwhen the page was truncated and `count_total` was not asked for. Its type\ndiffers per route — see BookEnvelope, where it is always an int."},"ValidationError":{"properties":{"loc":{"items":{"anyOf":[{"type":"string"},{"type":"integer"}]},"type":"array","title":"Location"},"msg":{"type":"string","title":"Message"},"type":{"type":"string","title":"Error Type"}},"type":"object","required":["loc","msg","type"],"title":"ValidationError"},"VenueGapEnvelope":{"properties":{"contract":{"type":"string","title":"Contract","description":"Contract version. Pin it.","default":"1"},"route":{"type":"string","title":"Route","description":"The route that produced this. Stable identifier."},"rows":{"items":{"$ref":"#/components/schemas/VenueGapPoint"},"type":"array","title":"Rows"},"row_count":{"type":"integer","title":"Row Count","description":"len(rows). Duplicated so a truncated read is detectable."},"result_state":{"$ref":"#/components/schemas/ResultState"},"complete":{"type":"boolean","title":"Complete","description":"True if and only if result_state == 'complete'. ⛔ IT IS A CLAIM ABOUT THE PAGE, NOT THE ANSWER — ruled 2026-08-24, and the enum gloss beside it used to say the opposite. It is the PAGING signal and it pairs with `next_offset`: `complete: true` means no page follows this one (`next_offset` is null), and it is what a machine branches on to stop paging. ⚠️ IT DOES NOT CERTIFY THAT EVERY NUMBER BESIDE IT WAS COMPUTED OVER A COMPLETE POPULATION. A route may hit an internal census cap and still answer `complete: true` for the page — census truncation is carried SEPARATELY, on each affected `Measured.method` string (and in `caveats`), never by flipping this boolean. The live case is `_VOCABULARY_CAP` in lifecycle_routes.py. ⇒ To learn whether you have every ROW, read this. To learn whether an AGGREGATE is a count or a lower bound, read that number's own `method`. Refusing COMPLETE at a census cap would need a sixth ResultState member or a new boolean, i.e. a contract break; see this enum's own note."},"limit":{"type":"integer","title":"Limit"},"offset":{"type":"integer","title":"Offset"},"next_offset":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Next Offset","description":"Non-null if and only if result_state == 'truncated'."},"coverage":{"$ref":"#/components/schemas/Coverage"},"freshness":{"$ref":"#/components/schemas/Freshness"},"caveats":{"items":{"type":"string"},"type":"array","title":"Caveats","description":"Known ways this answer misleads if read naively."},"sampler":{"$ref":"#/components/schemas/SamplerHealth"}},"additionalProperties":false,"type":"object","required":["route","rows","row_count","result_state","complete","limit","offset","next_offset","coverage","freshness","caveats","sampler"],"title":"VenueGapEnvelope"},"VenueGapPoint":{"properties":{"sampled_at":{"type":"string","format":"date-time","title":"Sampled At"},"base_asset":{"type":"string","title":"Base Asset"},"quote_asset":{"type":"string","title":"Quote Asset"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this sample was answered against."},"dex_trade_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Dex Trade Price","description":"Quote per base from the LAST settled match IN THIS WINDOW."},"dex_trades_in_window":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dex Trades In Window","description":"0 is a real observation: we looked and nothing settled."},"dex_absent_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Dex Absent Reason"},"dex_last_trade_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Dex Last Trade Price","description":"⚠️ A FOSSIL. The most recent settled match at ANY age."},"dex_last_trade_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dex Last Trade Block"},"dex_last_trade_age_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Dex Last Trade Age Blocks"},"amm_spot_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Amm Spot Price","description":"Marginal price from the reserves."},"amm_reserve_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amm Reserve Block"},"amm_reserve_age_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amm Reserve Age Blocks"},"amm_spot_absent_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amm Spot Absent Reason"},"amm_exec_price":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Amm Exec Price","description":"Realised execution price IN THIS WINDOW."},"amm_swaps_in_window":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Amm Swaps In Window"},"amm_exec_absent_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Amm Exec Absent Reason"},"gap_ratio":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Gap Ratio","description":"amm_spot_price / dex_trade_price. BOTH IN-WINDOW. Null unless both exist."},"gap_ratio_absent_reason":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Gap Ratio Absent Reason","description":"Why there is no honest gap. Exactly one of this and gap_ratio is set."},"gap_ratio_vs_fossil":{"anyOf":[{"type":"string","pattern":"^(?!^[-+.]*$)[+-]?0*\\d*\\.?\\d*$"},{"type":"null"}],"title":"Gap Ratio Vs Fossil","description":"⚠️ amm_spot_price / dex_LAST_trade_price. Compares a live number to an OLD one."},"gap_ratio_vs_fossil_dex_age_blocks":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"Gap Ratio Vs Fossil Dex Age Blocks","description":"How old the DEX side of gap_ratio_vs_fossil is. Required whenever it is set."}},"additionalProperties":false,"type":"object","required":["sampled_at","base_asset","quote_asset","as_of_block","dex_trade_price","dex_trades_in_window","dex_absent_reason","dex_last_trade_price","dex_last_trade_block","dex_last_trade_age_blocks","amm_spot_price","amm_reserve_block","amm_reserve_age_blocks","amm_spot_absent_reason","amm_exec_price","amm_swaps_in_window","amm_exec_absent_reason","gap_ratio","gap_ratio_absent_reason","gap_ratio_vs_fossil","gap_ratio_vs_fossil_dex_age_blocks"],"title":"VenueGapPoint","description":"One sample, one pair, both venues side by side. ⛔ NEVER SUMMED OR AVERAGED\nACROSS VENUES — that rule is inherited from `main.py`'s header and is not\nnegotiable here either."},"VerifyEnvelope":{"properties":{"tx_hash":{"type":"string","title":"Tx Hash"},"found":{"type":"boolean","title":"Found"},"events":{"items":{"$ref":"#/components/schemas/EventRow"},"type":"array","title":"Events"},"note":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Note"},"source":{"anyOf":[{"type":"string"},{"type":"null"}],"title":"Source"},"as_of_block":{"anyOf":[{"type":"integer"},{"type":"null"}],"title":"As Of Block","description":"The archive head this answer was computed against — max(block_index) FROM blocks WHERE complete. This route publishes it at the envelope level, so 'we have never seen one' and 'our archive stopped four hours ago' are never the same bytes on the wire. Declared optional in this schema; the handler always emits it."}},"type":"object","required":["tx_hash","found","events"],"title":"VerifyEnvelope","description":"GET /api/verify/{tx_hash}.\n\n⚠️ TWO DIFFERENT KEY SETS ON THE 200 PATH:\n    not found -> {tx_hash, found:false, events: [], note}      -- no `source`\n    found     -> {tx_hash, found:true,  events: [...], source} -- no `note`\nThey are mutually exclusive and discriminated by `found`, so `note` and `source`\nare both optional: test whether the key is present, not whether it is null."},"ErrorDetail":{"additionalProperties":false,"description":"FastAPI's own `HTTPException` body — `{\"detail\": \"...\"}` and nothing else.\n\nThis is what a caller receives when the DATABASE could not answer: no database\nconnection is available, or the read-only role's `statement_timeout` fired.\n⚠️ The database error is rendered as its exception CLASS NAME, never its\nmessage — `database error: QueryCanceled` — so no query text, no table name and\nno server detail reaches the wire. `QueryCanceled` specifically means the\nrequest exceeded the server's `statement_timeout`; it is a TIMEOUT, not a\nmalformed request, and the same request can succeed on a retry. It says nothing\nabout the archive being wrong, only that this particular question took too long\nto answer.","properties":{"detail":{"description":"Why the request could not be answered, in words. For a database failure the form is `database error: <ExceptionClassName>` — `QueryCanceled` is the statement_timeout, and is retryable, ideally with a narrower block window.","title":"Detail","type":"string"}},"required":["detail"],"title":"ErrorDetail","type":"object"},"PaymentOffer":{"additionalProperties":true,"description":"HTTP 402 — the offer IS the discovery. Emitted by middleware; no handler ran.\n\n⛔ THE OFFER IS ALSO IN A HEADER, AND THE HEADER IS THE NORMATIVE ONE.\n`PAYMENT-REQUIRED` carries the same object base64-encoded, because x402 v2\nsays the offer travels in the header and calls response bodies \"a server\nimplementation concern\". This body is emitted anyway — real sellers do, and a\nv1-era client reads only the body — and both are rendered from ONE method so\nthey cannot disagree about what is being sold.\n\n⚠️ Additional keys are allowed, for the reason `X402Accepts` gives: the\nprimary rail owns this top level, and a different rail would legitimately\nproduce a different envelope.","properties":{"x402Version":{"description":"2. ⚠️ Was 1; the live ecosystem is v2 and several key names moved with it (`maxAmountRequired` → `amount`). Branch on this before reading anything else.","title":"X402Version","type":"integer"},"error":{"description":"`payment required`. Prose — branch on the status code and on `accepts`, never on this string.","title":"Error","type":"string"},"resource":{"$ref":"#/components/schemas/X402ResourceInfo"},"accepts":{"description":"Every way this server will take the money, in preference order. ⛔ An EMPTY list cannot reach you: a request with no usable rail is refused with a 503 rather than offered at a price nobody can pay.","items":{"$ref":"#/components/schemas/X402Accepts"},"title":"Accepts","type":"array"},"payment_rejected":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"⭐ PRESENT ONLY WHEN YOU ALREADY TRIED TO PAY AND IT DID NOT STAND — every candidate's reason, joined by ` · `. Its presence is the difference between 'nobody bought' and 'a buyer exists and cannot get through', and this archive keeps those apart deliberately. ⚠️ It is added AFTER the rail renders the envelope, so it survives the translation while the neutral body's other keys do not.","title":"Payment Rejected"}},"required":["x402Version","error","resource","accepts"],"title":"PaymentOffer","type":"object"},"PaymentRefusal":{"additionalProperties":false,"description":"The payment gate refusing to sell — 503, emitted by middleware, no handler run.\n\n⛔ IT IS A REFUSAL TO **SELL**, NOT A REFUSAL OF THE CALLER, and that is why the\ncode is 503 and never 402: a 402 here would invite payment for an answer this\nservice already believes it should not take money for. A settlement rail's\ncancel-on-error guard keys on HTTP STATUS, so this code is load-bearing.\n⚠️ REACHABLE ONLY WHILE PAYMENT ENFORCEMENT IS ON. With it off the gate is a\npass-through and this shape cannot occur — so a consumer that has never seen it\nhas not proved it cannot arrive.","properties":{"error":{"description":"The refusal in one line, e.g. 'payment required but this server cannot take it'. Prose, and it may be reworded — branch on `reason` instead.","title":"Error","type":"string"},"detail":{"description":"Which decision or wiring is missing, at length. Written for a human reading the body, not for a matcher.","title":"Detail","type":"string"},"source":{"description":"Constant provenance marker carried by every refusal this gate emits.","title":"Source","type":"string"},"reason":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"⭐ THE MACHINE-READABLE ONE, AND THE ONLY KEY SAFE TO BRANCH ON. MEASURED on all six branches: `no_rail_registered` · `freshness_unknown` · `stale_data` · `params_not_supplied` · `not_in_catalog` · `unresolved_resource`. ABSENT on the unresolved_resource and not_in_catalog bodies, which is why it is optional here — treat a missing `reason` as 'unclassified refusal', never as a default.","title":"Reason"},"resource":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The route template the gate was pricing, e.g. `/api/orders`. ABSENT when route resolution itself failed, which is precisely the case where it would have been most useful.","title":"Resource"},"rails_unusable":{"anyOf":[{"additionalProperties":{"type":"string"},"type":"object"},{"type":"null"}],"default":null,"description":"Rail name -> why that rail cannot take a payment right now. Present ONLY on the `no_rail_registered` branch; an empty mapping and an absent key are different claims and neither means 'a rail is usable'.","title":"Rails Unusable"}},"required":["error","detail","source"],"title":"PaymentRefusal","type":"object"},"X402Accepts":{"additionalProperties":true,"description":"One way to pay, as x402 v2 spells it.\n\n⛔ ADDITIONAL KEYS ARE ALLOWED, ON PURPOSE. This block is rendered by the payment\nRAIL, and a rail may add a field the spec later requires; declaring the key set\nclosed would promise something this design refuses to promise.","properties":{"scheme":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"`exact` on this rail.","title":"Scheme"},"network":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"⚠️ A **CAIP-2** identifier, e.g. `eip155:8453`. The v1 spellings (`base`, `base-sepolia`) are REFUSED at configuration time — a v2 offer carrying one is a mismatch the facilitator rejects only AFTER the buyer has signed.","title":"Network"},"amount":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"⛔ A **STRING**, in the asset's own smallest unit — not a number and not micro-USD. `extra.amount_micro_usd` carries the price this archive actually quoted; `extra.asset_decimals` is what relates the two.","title":"Amount"},"asset":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The token contract to pay in.","title":"Asset"},"payTo":{"anyOf":[{"type":"string"},{"type":"null"}],"default":null,"description":"The payee address. Absent means unconfigured, and an unconfigured rail emits a 503, not this.","title":"Payto"},"resource":{"anyOf":[{"$ref":"#/components/schemas/X402ResourceInfo"},{"type":"null"}],"default":null,"description":"The SAME object as the top-level `resource`, repeated per accept-block because the x402 spec puts it in both places. Optional because it is the RAIL that renders this block: a rail that omits it is legal here and the top-level copy is the one to read."},"maxTimeoutSeconds":{"anyOf":[{"type":"integer"},{"type":"null"}],"default":null,"description":"How long the facilitator may take. It is not a lifetime for the offer, and this archive publishes none: the window that binds is the signed authorization's own `validBefore`.","title":"Maxtimeoutseconds"},"extra":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"description":"The rail's own diagnostics, and the ONLY place this archive's own vocabulary survives the translation: `amount_micro_usd`, `asset_decimals`, `binding_currency`, `binding_unit`, `binding_quantity`.","title":"Extra"},"extensions":{"anyOf":[{"additionalProperties":true,"type":"object"},{"type":"null"}],"default":null,"description":"x402's own forward-compatibility slot. Empty today on every offer this archive emits; absent entirely if a future rail does not render it, which is why it is optional rather than declared as an always-present empty object.","title":"Extensions"}},"title":"X402Accepts","type":"object"},"X402ResourceInfo":{"additionalProperties":true,"description":"What is being sold, in x402's own words. Appears TWICE on purpose.\n\n⚠️ Once at the top level of the 402 and once inside each `accepts` entry — the\nx402 spec's shape in both directions. A body disagreeing with the\n`PAYMENT-REQUIRED` header about what is being sold would be its own bug, so both\nare rendered from one method.","properties":{"url":{"description":"The route template being priced, e.g. `/api/events`.","title":"Url","type":"string"},"description":{"description":"Human-readable, e.g. 'one request — $0.01'. Prose; do not parse a price out of it — read `accepts[].amount`.","title":"Description","type":"string"},"mimeType":{"description":"What you get for the money. `application/json` on every route this archive sells.","title":"Mimetype","type":"string"}},"required":["url","description","mimeType"],"title":"X402ResourceInfo","type":"object"}}},"servers":[{"url":"https://opreturn.art","description":"the public deployment. Every path below is relative to it; this block exists so the map states its own base URL instead of leaving a client to guess one."}]}