Skip to content

Releases

v1.17.0

Choose a tag to compare

@asokore asokore released this 23 Oct 15:32

stale_primary and stale_latest no longer repeat each other. Until now,
whenever stale_primary fired, stale_latest fired beside it with the same
gap. Syria's GDP growth came back with two codes that each said the 2022 value
was 4 years old. Each code now says one thing. stale_latest carries the age
of the value, as before. stale_primary names the other registry sources
StatCite did not query for the request, such as the IMF DataMapper API and IMF
WEO via DBnomics, and says that whether they publish a more recent period is
not known. It no longer states the gap.

Both codes still fire together. stale_latest marks every old latest-only
value whatever the source, as 1.16.0 promised, so a caller who filters on it
misses nothing. The names come from a new structured field,
stale_primary_unqueried, which lists the source labels in precedence order
and is set only with stale_primary. Prose in notes plays no part. The
field is in the MCP output schema. openapi.json now documents
stale_primary, stale_primary_years and stale_primary_unqueried, which it
had left out.

The IMF's own SDMX 3.0 WEO flow was measured as a replacement for the
DataMapper API, which rounds WEO values to one decimal place. It stays off the
live chain, because its payload carries no edition label and a single-economy
call cannot anchor the projection boundary. The measurements and the decision
are in docs/IMF-SDMX-2026-10.md.

A site design test now fails if the phone hero loses its overflow clip, which
keeps the orbit from widening the page into a sideways scroll.

v1.16.0

Choose a tag to compare

@asokore asokore released this 14 Oct 23:15

Two codes join the limitations list:

Code Fires when
stale_latest A latest-only value is more than 3 years older than the year it was retrieved
outturn_unconfirmed IMF values fall before the projection boundary StatCite infers

stale_latest fires whatever the source. Until now a latest-only value carried
a staleness code only when its indicator had a second registry source, so the
Barbados Gini index for 2016 came back with an empty list. It now carries
stale_latest with a gap of 10 years. stale_primary keeps its meaning and
still marks the case where other registry sources were left unqueried. The
threshold is the one stale_primary already used. Re-measured on the World
Bank API on 14 October 2026 for the series that have no other registry source,
it is passed by no economy's latest population figure, by 10 of 214 GDP per
capita figures and by 106 of 171 Gini figures. A gap of 1 or 2 years is an
ordinary publication lag and never fires. A full series and a request with a
pinned end year are left alone, because the caller can see the last period.

outturn_unconfirmed covers what the IMF data StatCite reads does not say. The DataMapper
API, DBnomics and the IMF's own SDMX 3.0 WEO flow carry no per-country year at
which estimates start. Each was checked on 14 October 2026. So StatCite cannot
tell an outturn from a staff estimate before the boundary it infers, and the
code states only that. It never calls a value an estimate. It fires on
DataMapper WEO and Fiscal Monitor codes and on dated DBnomics IMF vintages,
including the single value a latest-only request returns, which projection
never covers.

Both codes are read from structured fields: the citation's retrieved_at, the
latest-only request, the series id and the projection marker StatCite sets.
Each new branch has a test that fails when the branch is broken.

1.15.0

Choose a tag to compare

@asokore asokore released this 14 Oct 02:49

Every get_indicator and get_series result now carries a limitations
list, on MCP and REST alike. Each entry is a code and a one-line detail
saying what the figure is not:

Code Fires when
projection A returned value falls in the source's estimate/projection period
fallback_source A fallback source served the figure, with the reason
stale_primary The primary's latest value is years behind the clock
modeled_estimate The source publishes the series as a model output
rounded_source The IMF DataMapper served WEO values rounded to one decimal
computed_values StatCite computed the values with a transform

Each code is read from a structured field of the result or from the indicator
registry. The prose in notes plays no part, so rewording a note cannot add or
remove one. An empty list means none of these conditions was detected. It is
not a warranty. The code list is declared once in core/types.ts, the type is
derived from it, and a test asserts the MCP output schema and openapi.json
carry the same list. Each rule has a test that fails when the rule is broken.

The README gains a side-effect table: what each kind of call reaches, what it
writes and where, and what StatCite records. The stdio install line on the
README, llms-install.md, the site and the docs page now pins
mcp-remote@0.14.3 in place of @latest, so a client restart cannot pull a
new release. The pinned bridge was run against the live server before the
change.

1.14.2

Choose a tag to compare

@asokore asokore released this 30 Sep 14:03

The Eastern Caribbean Central Bank stopped printing its "Data as at" stamp on
its statistics tables in late September 2026. The collection of 2026-09-20 read
it on every table. By 2026-09-30 it was gone from the page, the rendered view
and both exports, and the collector's sentinel refused all 153 ECCB series,
although every table still parsed.

The collector now records an absent stamp as absent. It never borrows the last
stamp it saw, and it never puts its own retrieval time in its place. A stamp
printed in any shape it cannot read still fails loudly, so a restored stamp
cannot pass as missing. With no stamp to compare, a quiet-day skip rests on the
bank's live page agreeing cell for cell with the stored data. A full re-read is
forced whenever the last one is more than 7.5 days old.

Citations for ECCB figures collected since the withdrawal say the bank gave no
currency stamp, and give the date StatCite collected the copy being served,
labelled as StatCite's own. Stamped ECCB citations and Central Bank of Barbados
citations are unchanged byte for byte. The sources page, llms-full.txt and the
guide no longer promise that every ECCB value carries the bank's stamp, and a
test fails if that wording returns.

v1.14.1 - one World Bank retry ladder per snapshot

Choose a tag to compare

@asokore asokore released this 24 Sep 12:58

A World Bank outage now costs a country snapshot one retry ladder instead of two.

A snapshot calls the World Bank twice in sequence: once for the headline indicators, then again at the end of the debt indicator's source chain. The per-request host breaker exists so that a host which has already failed a full series of attempts is asked once more, not three more times, but the first call never reported to it. Measured with every upstream down: six World Bank fetches per snapshot before, four after, which is about 1.2 seconds less waiting. A single blip on the first call is still retried and recovered, and the other sources still get their own full series.

The test suite also stopped sleeping through retry backoff. Every attempt still happens in tests, only the wait is removed, and one test keeps the real schedule to prove production still pauses. Wall time fell from 24.8 seconds to 7.4 seconds on CI. No behaviour change from that part.

v1.14.0 - honest failures, honest registry, honest release

Choose a tag to compare

@asokore asokore released this 19 Sep 15:08

A second improvement pass, on the same terms as the first: every change was
proposed by a review, then attacked by a second reviewer paid to knock it down,
and shipped with a test that fails without it. This one is mostly about the
service telling the truth when something has gone wrong, or when nobody has
checked.

An outage is an outage. A dead World Bank answered 422 on the four registry
routes, which says the request was wrong, while the adapter routes answered 502
for the identical failure and the docs published 502 all along. A client
branching on status retried one and permanently gave up on the other. Snapshots
were worse: a total outage came back either as "this economy publishes none of
these", cached for an hour, or as an error blaming the caller. A source that
could not be reached is now distinguished from a source that answered and holds
nothing, on the two legs that can tell the difference, and a shortened snapshot
is served no-store. A malformed upstream document, which had matched no branch
anywhere, is no longer reported as a StatCite crash.

The registry says what it serves. The World Bank rebased its international
poverty line to $3.00 a day in 2021 PPP and every discovery surface still
advertised $2.15 a day, 2017 PPP, while the served number was correct. Tourism
receipts are sold as a headline exposure measure for small island states and the
series has published nothing for any economy since 2020, with Barbados last
reporting 2016 and Jamaica 2011. Both notes now say so. A "latest" value more
than three years behind the clock discloses the gap and names the sources
StatCite did not consult, and says only that: for several economies no other
source is fresher.

Prompts take arguments. All three declared none, so a host had nothing to
collect and prompts/get discarded what a client sent. They are optional, an
unknown key is refused by name, and a draft over the cap is refused rather than
silently shortened. server/discover now carries identity at the top level as
well as in _meta, which hosts and proxies are free to strip. Tool results are no
longer pretty-printed on the wire, which was about 30% of each response.

The site works on a phone and without JavaScript. The mobile table rule had
never fired, so the indicator registry rendered 10,584px tall at 375px; it is
now 4,257px and scrolls. Seven of the eight Connect panels were invisible with
scripting off, and the live verifier form discarded what the visitor typed.

CaribStat stops losing data quietly. A period label the parser could not
read deleted its observations and published clean, which had left three live
holes. Four of 23 catalogue sample rows did not exist, so search handed agents
selectors that answered 422. Every CBB row error read "undefined/BRB".

Release engineering. 1.13.0 was published from a tree whose equivalence job
was red, 21 seconds after it failed. The registry publish now runs behind a gate
that checks the tagged commit itself. The sitemap guard had skipped on every CI
run since it was written, because it refuses a shallow clone, and the twice-daily
live smoke never sent a request to statcite.com at all.

Eighteen findings, eighteen shipped, thirty-nine mutation tests. The full record
is in docs/IMPROVEMENTS-2026-09-19b.md.

v1.13.0 - verdicts, agent surface and cost

Choose a tag to compare

@asokore asokore released this 19 Sep 13:28

An improvement pass over the answers, the agent-facing surface and the cost of a
bad day upstream. Every change here was proposed by a review, checked by a second
pass that tried to refute it, and shipped with a test that fails without it.

Verdicts say what they mean. Near zero the percentage-point band straddled
zero, so a claimed 0.04% inflation was graded "match" against an official
-0.013%, in a payload that also reported a relative difference of 403%. Opposite
signs are now "close" at most, with the direction disagreement named. The
adjacent-year diagnostic accepted any near miss and never compared the
neighbour's fit against the matched year's, so it told exactly correct claims
that their year might be wrong: on three real World Bank series it fired on 36%
to 43% of correct claims. It now requires a real match and a strictly better fit.

Anguilla and Montserrat get a verdict. Neither is a World Bank or IMF
reporting economy, so verify_stat failed and verify_claims counted the claim as
an error, beside genuine outages. Such a claim now returns cannot_verify with
the ECCB's own related figures shown for orientation, the definition difference
stated, and official_value left null: the bank's measure is never served under
the registry key's label.

Windowed growth rates keep their first year. A year window was applied before
the transform, so a 2020 to 2024 yoy request returned 2021 to 2024, and a
single-year window errored out asking the caller to widen it. Growth transforms
now compute on the full series and re-window afterwards.

A cross rate is dated to its stalest leg. fx_convert stamped a bridged rate
with whichever leg resolved first, so a pair whose World Bank annual averages end
in different years looked current. rate_date is now the older leg, a note names
both periods, and the response carries a legs array. No converted amount changed.

The agent surface keeps its promises. search no longer emits ids fetch is
guaranteed to refuse: a euro-area-only series is searched under the euro area
rather than under whatever country the query named. Each failed claim in
verify_claims carries the same machine-readable code and details that verify_stat
returns, so a coverage gap can be told from an outage. Every /v1 body now names
an HTTP call rather than an MCP tool, including the usage field on search hits.
GET /v1/series accepts row= for Caribbean tables, because a '#' row selector
never survives a URL.

Errors that cannot succeed no longer say "retry". A euro-area aggregate asked
for with a national country is coded invalid_request rather than
upstream_unavailable.

Cost on a bad day. Identical in-flight upstream requests are shared, so five
concurrent callers cost one subrequest instead of five and five callers of a dead
URL cost one retry series instead of fifteen. Within one request, a host that has
spent a full retry series gets one attempt thereafter, so a single dead upstream
no longer starves the claims whose own source is healthy. country_snapshot
fetches the five ECCU tables together instead of one at a time.

The privacy page describes the site that is served. Cloudflare injects its
Web Analytics beacon into every page, including the page carrying the words "no analytics
scripts". The page now says what runs and names both beacon hosts, and a test
refuses any CSP host the page does not disclose.

Guards. /v1/status probes the two upstreams it had been silently omitting,
fx and the CaribStat origin, and its note names what it measures. The Apify
bundle guard rebuilds the bundle and compares bytes instead of grepping for two
sentinel strings. New tests cover the site-wide CSP, frame-ancestors, HSTS and
nosniff, and harvested bank data being force-added past .gitignore, which the
pre-push hook now refuses too.

v1.12.3 - security hardening

Choose a tag to compare

@asokore asokore released this 15 Sep 14:51

A security pass over the Worker, the site and the repository. No verdict or
figure changes for any well-formed request.

Transport. The API, the MCP endpoint and /health answered plain HTTP with
data. They now answer it with a 308 redirect to HTTPS, which keeps the method
and body. Local development is not redirected. Every Worker response, errors
included, carries HSTS and X-Content-Type-Options: nosniff.

Request limits. Request bodies over 262,144 bytes are refused with a 413
before they are read in full, on /mcp and on /v1/verify_claims. REST query
values and path parameters are capped at 200 characters, as MCP arguments
already were. A JSON-RPC batch may ask for at most the upstream work of one
15-claim verify_claims call. Messages past that budget get an error telling the
caller to send them on their own. Caller text quoted back in an error is
shortened and escaped, and a query with thousands of parameter names is
refused in linear time.

Upstream reads. An upstream response over 5 MB is refused. The fetch
timeout now covers reading the body, not only the headers. The in-memory cache
is bounded by size as well as entry count. Upstream error bodies are reduced to
a short plain-text excerpt. A World Bank row missing its date or indicator no
longer causes a 500.

Identifiers. A caribstat series id can no longer use .. or other path
tricks to reach other files on the mirror. Each part must be a plain slug. The
id served back in series_id and the citation is built from the parsed id, so
two spellings of one row get the same id and the same verdict. worldbank/..,
dbnomics/ ids with dot segments, and a malformed percent escape in
/v1/snapshot now return advice instead of a 500.

Citations. Control characters and line breaks are stripped from every
citation's text, whichever source supplied it. CaribStat labels, date stamps and
links are cleaned before use, and a source link must be https. Runs of spaces in
Central Bank of Barbados row labels are kept, so those labels still work as row
selectors. The BibTeX export can no longer be broken by a brace or a newline.

Site and repository. The homepage verifier escapes quotes and links only
https sources. security.txt is served as UTF-8 and points to the security
policy. The registry publish workflow runs a pinned, checksum-verified
mcp-publisher, and every workflow token is read-only unless a job needs more.
wrangler runs at a pinned version. .gitignore covers more secret-bearing file
names. Two unused archive bundles are removed, and a test keeps archives out of
the tree.

v1.12.2 - argument contract, Caribbean honesty, truthful error codes

Choose a tag to compare

@asokore asokore released this 12 Sep 20:44

One verdict-changing fix on the main transport, several Caribbean answers that
were quietly incomplete, and a contract that now says what the service does.

MCP tool arguments are held to the schema each tool publishes. Every tool
has always declared that extra arguments are invalid, but nothing enforced it.
Verified live before the fix: verify_stat for US 2023 inflation, claimed 4.3,
returned "mismatch" with tolerance_abs 0.01 and "close" with the key spelled
tolerence_abs, because the misspelling was dropped and the lenient default
applied. strict_source sent as the string "true" was read as false. Unknown
argument names are now refused with the name you probably meant, and boolean
arguments must be JSON booleans. REST gained the same stance for repeated
query parameters, unknown verify_claims body keys, strict_source as a string,
and value sent together with claimed_value.

Caribbean series answers say what they are.

  • A caribstat id with no row selected served the table's first row under the
    table's name. For the Barbados retail price table that is the Food index, not
    inflation. The response now names the row it served, lists the others, and
    carries the row in series_id and the citation.
  • A row label containing a percent sign, such as "Inflation Rate %", crashed
    with an internal error. It now selects the row.
  • get_indicator for Anguilla or Montserrat, which the World Bank and IMF do not
    report, answered with glued upstream errors including raw JSON. It now points
    at the Eastern Caribbean Central Bank series and states how its definition
    differs. Nothing is substituted under the registry key.
  • compare_sources for ECCU members adds the ECCB's own rows, labelled with
    their definition and kept out of the comparison spread.

Honest errors. Every REST error and MCP tool error carries a code from one
closed list, documented in openapi.json. A year window inside a published gap
is named as a gap rather than told to adjust a range that already contains it.
A mistyped worldbank/ code is reported as an unknown code, not as a coverage
fact. An unrecognised series id suggests the worldbank/ form or the nearest
registry keys. /v1/indicator accepts keys as people type them, so
/v1/indicator/GDP-growth no longer returns "Unknown endpoint". /v1/fx refuses
impossible calendar dates instead of returning a 502. compare_sources no longer
labels an old outturn as a projection and says why it compared an early year.
The percent-versus-decimal diagnostic now catches a claim rounded to its own
precision.

Protocol accuracy. serverInfo carries a PNG icon, websiteUrl and a
description. An unsupported-version error echoes the request id. A JSON-RPC
batch that declares 2026-07-28 is refused. resources/templates/list answers. The
GET descriptor lists every accepted protocol version, newest first.
get_indicator and get_series declare an outputSchema, and a test validates the
real output of every tool that declares one.

Contract and discovery. openapi.json now matches the routes, checked by a
test that reads both: latest_only on /v1/series, the claimed_value alias on
/v1/verify, the verify_claims request schema, the compare response schema, and
the error envelope. list_sources and search_indicators name every served
source. /v1 JSON responses send x-robots-tag noindex. Caribbean, IMF, BIS and
ECB series calls are no longer logged as "other" in usage metrics.

An absence is claimed only when every source says so. "None of the sources
publishes it" now requires each source in the chain to report that itself, or
to return a 404 for the exact series. A declined request, a configuration error
or another source's data outside the window no longer produces that sentence,
and outages on registry indicators are coded upstream_unavailable so a client
knows to retry. These were found by an adversarial review of this release
before it shipped, along with a one-sided year window misread as a gap, codes
that differed between routes for the same unknown indicator, and a series_id
that could not be replayed after a trailing "#".

Privacy and the website. Cloudflare invocation logs, which recorded each
request URL and so the claimed values in it, are switched off. The privacy page
now describes exactly what the traffic analytics read, and those analytics
separate genuine crawling from vulnerability probes that borrow crawler names.
Every page shares one navigation with a Connect link, short paths such as
/benchmark and /changelog redirect, the Claude instructions lead with the
connector directory, broken sentences left by an earlier punctuation pass are
rewritten, the benchmark tables carry their run date, and the sitemap has a
check that fails when a lastmod date goes stale.

Source integrity runs weekly. A GitHub workflow re-derives served values
from the World Bank and IMF with a second implementation that shares no code
with the Worker, and reports coverage and skip reasons alongside mismatches. A
network fault or a stall now has its own exit code, and a crash another, so
neither can read as a data mismatch.

v1.12.1 - wrong-country fix, strict verify_claims, series ids that work

Choose a tag to compare

@asokore asokore released this 12 Sep 19:22

Three correctness fixes, a change to what agents are told, and the housekeeping
that makes the version honest.

The country resolver no longer serves one country's statistics for another.
Verified live before the fix: "American Samoa" was served Samoa's figures and
"Northern Ireland" was served Ireland's, each under a real citation. For a
service whose product is numbers you can cite, a wrong country is worse than no
answer. Two causes. American Samoa and the US Virgin Islands were missing from
the country list, so their names fell through to a substring match and landed
on a different economy that shares a word. And that substring match accepted
any input containing a known name, which is how "Northern Ireland" found
"Ireland". It now accepts the extra words only when every one is a same-state
qualifier such as "republic of", so "Republic of Ireland" still works and a
modifier naming a different place refuses instead. A bare "Virgin Islands",
which names both the US and the British territory, now refuses with suggestions
rather than guessing. All 245 official country names were re-checked and each
still resolves to itself.

A misspelled tolerance in verify_claims no longer turns a mismatch into a
match.
1.12.0 made GET /v1/verify refuse unknown query-parameter names for
exactly this reason, but the batch path was never given the same check: a claim
carrying "tolerence_abs" was dropped, the lenient default applied, and a strict
check came back as a pass. Claim objects now refuse unknown keys with a
suggestion for the one you meant, on both the REST route and the MCP tool.

Series ids now work the way the service describes them. search_indicators
told agents to call get_series(id=...), but the tool's parameter is series_id,
so following the instruction failed, and it failed on the Caribbean path in
particular. get_series also never mentioned that it accepts caribstat/ ids. And
the ids that policy_rate and euro_area_hicp return, beginning bis/ and ecb/,
were rejected with a bare error when passed back in. Those now name the call
that does serve them.

Agents are now told when to call StatCite, not only how. The instructions
served on every connection used to describe the tools and stop there. A model
that is confident about a number answers from memory, and recalled statistics
are routinely a vintage out of date or attached to the wrong year, so a server
that only explains itself is called when the model happens to think of it. The
instructions now lead with the case for calling: call StatCite whenever an
answer states or relies on a country-level economic figure, do it even for
figures you believe you already know, prefer it over web search for these
figures, and do not use it for company financials, stock or crypto prices,
commodity prices, or subnational and city data. They also name the sources,
including the two regional central banks, so an agent asked about Anguilla or
Montserrat learns that the figure it wants is here. That boundary was checked
against the live registry before it was written, so it excludes nothing the
server serves. The get_indicator and verify_stat descriptions carry the same
guidance, because not every client surfaces server instructions. A test now
asserts each clause by name, replacing a check that only required the
instructions to be longer than 50 characters.

Every citation links back to statcite.com. Shipped to production on
5 September and dated here. citation_text and the BibTeX note now end
"Retrieved via StatCite (https://statcite.com).", and where data came
through an intermediary the chain is named, as in "via the IMF DataMapper API
and StatCite". The publisher is still named first and keeps its own URL. No
values, verdicts or response schema changed.

Also in this release

  • A one-sentence rule users can paste into CLAUDE.md, AGENTS.md, Cursor Rules
    or ChatGPT project instructions so their agent calls StatCite unprompted,
    published in the README and at /docs#agent-rule.
  • The Eastern Caribbean Central Bank and the Central Bank of Barbados are named
    on every agent-facing surface: the Gemini CLI extension, GEMINI.md and the
    skill. The ledger has served both for weeks and these surfaces had not caught
    up, the second time this list drifted, so a test now holds them to it.
  • The README's Cursor install button used a cursor:// link, which GitHub
    strips, so it rendered as plain text. It now uses the https install route in
    the same format Exa's official server ships, and a test fails on any README
    link scheme GitHub would strip.
  • The homepage lists where StatCite is independently listed, and the live audit
    re-checks every one of those links on each run.
  • Everything served from site/ is pinned to LF line endings, after a branch
    switch reintroduced CRLF into llms.txt.
  • A data-integrity check re-derives served values from the World Bank and IMF
    directly, resolving each series from StatCite's own citation. First full run:
    215 verified, 0 mismatches.
  • SECURITY.md said production always runs the latest tagged release, which had
    stopped being true. It now says production runs the latest version recorded
    here, which /v1/status reports.