# Starwell: full developer documentation > The verified data layer for AI. This file is the complete /docs content as one markdown document for LLMs and agents. Machine endpoints: OpenAPI at https://starwell.dev/openapi.json, MCP at https://starwell.dev/api/starwell/mcp, full catalog at https://starwell.dev/api/starwell/v1/catalog. # Verified official statistics, one API. Harmonized series from official statistical agencies. Every observation carries provenance: the exact source URL it came from, when it was retrieved, and by which connector version. Every series carries a verification status backed by golden-value and freshness checks against the live source. Answers are computed by real code in a sandbox, never estimated by a model. Quickstart Try it Auth and limits Data endpoints POST /answer Monitors Verification statuses Errors MCP server Recipes Sources and attribution ## Two calls to see the whole idea ``` # 1. The latest US unemployment rate, with provenance on every value (no key needed): curl "https://starwell.dev/api/starwell/v1/sources/fred/series/UNRATE/observations?latest=3" # 2. A computed, cited answer (free key — create one at /account): curl -X POST "https://starwell.dev/api/starwell/v1/answer" \ -H "Authorization: Bearer dlk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{"question": "What was the average US unemployment rate in 2024?"}' ``` Base URL: https://starwell.dev/api/starwell/v1. All responses use { success, data, meta } envelopes. Dates are ISO YYYY-MM-DD. Machine-readable spec: OpenAPI 3.1. ## Press a button, see the live API These run against the live store from your browser — no key, no setup. The equivalent curl appears with each response. The sample analysis runs the real answers engine (allow 10–40 seconds on the day's first run; repeats are instant from the answers cache). Search the catalog Latest unemployment Series stats Sample computed analysis ## Open data reads, free keys for answers Data reads need no key: anonymous callers get 120 data requests per minute per IP across the whole read surface (sources, catalog, series, observations, stats, CSV). Computed answers and monitors require an API key — keys are free, no card: sign in at /account, mint a key (shown once, stored hashed), and watch your usage there. A keyless sample answer lives at GET /v1/answer/demo (or press the buttons in Try it). Send the key as Authorization: Bearer dlk_live_... or X-API-Key. Keys lift you onto daily tier quotas with usage metering and per-response X-RateLimit-* headers. | Tier | Data requests | Answers | | Anonymous | 120 / minute (IP) | — (key required; sample: GET /v1/answer/demo) | | Free key | 2,000 / day | 25 / day | | Pro key | 50,000 / day | 500 / day | ## Read the store ### GET/v1/sources The sources served, with counts, cadence notes, and terms links. ### GET/v1/catalog The whole catalog flat, in one call: every served series with its indicator, unit, frequency, geography, coverage, verification status, license, and dataset context. The right first call for bulk consumers and agents that want the full universe without walking sources one by one. Parameters: q (ranked search across series ids, indicators, geographies, and dataset titles, e.g. ?q=unemployment+canada), source (restrict to one slug), limit (1..1000; defaults to 50 with q, else all). A human-browsable view lives at /catalog. ### GET/v1/sources/:source/datasets Datasets (official releases and tables) in one source, with coverage and links to the official table pages. ### GET/v1/sources/:source/datasets/:datasetId One dataset plus its series, each with its verification status. The envelope carries the citation block. ### GET/v1/sources/:source/series/:seriesId Full series metadata, current verification status, and the most recent golden and freshness check records. ### GET/v1/sources/:source/series/:seriesId/observations The values. Parameters: start, end, latest (1..5000), limit, offset, format (json or csv; the CSV variant is spreadsheet-ready and carries its attribution as leading # comment lines). Series are addressed by the ids practitioners already use: fred/UNRATE, statcan/v41690973. ``` { "success": true, "data": [ { "period": "2026-06-01", "value": 4.2, "status": "normal", "revision": 1, "provenance": { "sourceUrl": "https://api.stlouisfed.org/fred/series/observations?series_id=UNRATE&...&api_key=REDACTED", "retrievedAt": "2026-07-04T13:38:34.644+00:00", "connectorVersion": "1.0.0" } } ], "meta": { "series": { "externalId": "UNRATE", "indicator": "Unemployment Rate", "unit": "Percent" }, "verification": { "status": "passing", "lastVerifiedAt": "2026-07-04T13:41:05..." }, "citation": { "source": "Federal Reserve Economic Data (FRED)", "dataset": "Employment Situation", "url": "https://fred.stlouisfed.org/release?rid=50" } } } ``` ### GET/v1/sources/:source/series/:seriesId/stats Summary statistics computed over the verified store: latest and previous values, all-time min and max, mean, and the change versus the previous period and versus a year ago. The cheap way to answer "what is it now and how has it moved" without a full analysis run. The envelope carries verification, license, and citation like every other endpoint. ### Pagination and stability Observations paginate with limit (1..5000, default 1000) and offset; meta.hasMore is computed honestly (the server fetches one extra row to know), never guessed from counts. latest=N is the shortcut for "the newest N" without offset math. Responses are never silently truncated — CSV exports even carry a # NOTE: truncated comment when a window is cut. v1 is frozen: changes are additive only (new fields, new endpoints); nothing documented here is removed or repurposed. ## POST/v1/answer Ask a question in plain language. The engine matches it to served series (or use series to pin them), materializes the verified observations, writes and executes real Python in a sandbox, and returns the computed result. Nothing in the numbers is model-estimated. Requires a free API key (401 key_required without one) — the keyless sample is GET /v1/answer/demo. ``` curl -X POST "https://starwell.dev/api/starwell/v1/answer" \ -H "Authorization: Bearer dlk_live_YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "question": "What was the average US unemployment rate in 2024?", "series": [{ "source": "fred", "seriesId": "UNRATE" }], "start": "2024-01-01", "end": "2024-12-01" }' ``` ``` { "success": true, "data": { "answer": "The average US unemployment rate in 2024 was 4.03%.", "chart": { "...": "Plotly figure JSON" }, "python": "import pandas as pd\ndf = pd.DataFrame(data)\n...", "series": [{ "source": "fred", "seriesId": "UNRATE", "verification": { "status": "passing", "lastVerifiedAt": "..." }, "citation": { "dataset": "Employment Situation", "url": "https://fred.stlouisfed.org/release?rid=50" } }] }, "meta": { "observationsUsed": 12, "cached": false, "durationMs": 12085 } } ``` Expect 10 to 40 seconds on a cache miss; repeated questions are served from the answers cache in milliseconds and invalidated the moment the underlying official data moves. When no served series can answer, the API refuses honestly with a 422 instead of inventing a number. ### Deep analysis: "mode": "deep" Add "mode": "deep" to the same request and the engine goes beyond one computation: it plans 3–4 orthogonal analyses for your question (trend, distribution, outliers and turning points, cross-series relationships), executes real Python for each in the sandbox, and synthesizes one decision-ready report. The response adds data.sections[] — each section carries its own computed output (stdout), its python, and its charts, so every number in the report is traceable to code that ran over verified observations. Works with up to 4 pinned series for comparative deep-dives ("US vs Canada inflation since 2020"). Expect 60 to 180 seconds on a cache miss; deep reports cache separately from default answers. Also exposed to agents as the deep_analysis MCP tool. ## POST/v1/monitors Watch a series; get a webhook the moment it moves. Body: {"source": "fred", "seriesId": "DGS10", "webhookUrl": "https://..."}. Requires an API key (keys are free). Detection rides the store's own refresh loop: when a refresh lands a new period or a revised value, Starwell POSTs a series.updated event with the new value, the previous period, the verification status, and the citation. Delivery is https-only, one attempt per refresh, and the monitor's snapshot only advances on successful delivery, so a missed delivery re-fires on the next refresh. Manage with GET /v1/monitors and DELETE /v1/monitors/:id (scoped to your key). Monitors per key: 5 free, 100 pro. ``` { "event": "series.updated", "source": "fred", "seriesId": "DGS10", "indicator": "Market Yield on U.S. Treasury Securities at 10-Year Constant Maturity", "period": "2026-07-10", "value": 4.21, "previousPeriod": "2026-07-09", "verification": "passing", "citation": { "dataset": "H.15 Selected Interest Rates", "url": "https://fred.stlouisfed.org/release?rid=53" } } ``` ### Verify deliveries Every delivery is signed. The create response returns a webhookSecret (shown once; re-creating the same monitor rotates it), and each POST carries X-Starwell-Signature: sha256= — the HMAC-SHA256 of the raw request body. Verify before trusting: ``` import crypto from "node:crypto"; const expected = "sha256=" + crypto.createHmac("sha256", process.env.STARWELL_WEBHOOK_SECRET) .update(rawBody) // the exact request body bytes, before any JSON parsing .digest("hex"); const authentic = crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected)); ``` ## Verification statuses | Status | Meaning | | passing | Golden-value and freshness checks pass against the live source. Golden values are hand-verified reference points confirmed through two independent delivery channels. | | stale | Values verify, but the source has published newer data than we currently hold, or (for sources that publish no release time) the source has not been re-checked within the series' own cadence window. Staleness is never silently passed over. | | failing | A golden check disagrees with the live source. The series stays visible and honestly labeled; do not rely on it until it clears. | | unverified | Ingested but not yet through the verification harness. | ## Errors Every error is the same shape — { "success": false, "error": "human-readable message", "code": "machine_code" } — where code appears when there is a branchable condition. Agents: branch on the status and code, show error to humans. | Status | Code | Meaning, and what to do | | 400 | — | Malformed parameter (dates are YYYY-MM-DD, limits are bounded). Fix the request; do not retry as-is. | | 401 | key_required | This endpoint needs an API key (answers, monitors). Keys are free at /account. | | 401 | — | Malformed, unknown, or revoked key. Check the header; mint a fresh key if revoked. | | 403 | source_not_allowed | The key is scoped to specific sources and this series is outside them. | | 404 | — | Unknown source, dataset, or series. Discover ids with GET /v1/catalog?q=.... | | 413 | — | Request body over 32KB. Answer requests are a question and a few ids. | | 422 | — | No served series can answer the question. Starwell refuses honestly rather than inventing a number. | | 429 | monitor_cap / key_cap / — | Rate or quota limit. Read X-RateLimit-Remaining and X-RateLimit-Reset (exposed to browsers) and back off; daily quotas reset at midnight UTC. | | 451 | license_restricted | The series' license does not permit redistribution; it is excluded from every surface by policy, not by accident. | | 500 / 502 | — | Internal or sandbox failure (502 carries an errorId for support). Safe to retry once after a pause. | | 503 | — | A dependency is down (key service, mailer). Retry with backoff; /status and /healthz show live health. | Self-pacing: every keyed response carries X-RateLimit-Limit, X-RateLimit-Used, X-RateLimit-Remaining, and X-RateLimit-Reset — CORS-exposed, so browser-based agents can read them and pace themselves. ## MCP server The same capabilities as native tools for any MCP client, served over Streamable HTTP with no session state: ``` https://starwell.dev/api/starwell/mcp ``` Tools: search_catalog, list_sources, list_datasets, get_series, get_series_stats, get_observations, answer, deep_analysis, and monitor management (create_monitor, list_monitors, delete_monitor). Data reads work keyless; answer, deep_analysis, and the monitor tools need a free key from /account — remote clients pass it as an Authorization: Bearer header, the stdio bridge reads STARWELL_API_KEY. ### Connect your client Claude Code ``` claude mcp add --transport http starwell https://starwell.dev/api/starwell/mcp ``` Claude Desktop / claude.ai — Settings → Connectors → Add custom connector with the URL above. On plans without remote connectors, use the stdio bridge in the desktop config: ``` { "mcpServers": { "starwell": { "command": "npx", "args": ["-y", "starwell-mcp"] } } } ``` Cursor — .cursor/mcp.json: ``` { "mcpServers": { "starwell": { "command": "npx", "args": ["-y", "starwell-mcp"] } } } ``` VS Code (Copilot agent mode) — .vscode/mcp.json: ``` { "servers": { "starwell": { "type": "http", "url": "https://starwell.dev/api/starwell/mcp" } } } ``` OpenAI Codex CLI — ~/.codex/config.toml: ``` [mcp_servers.starwell] command = "npx" args = ["-y", "starwell-mcp"] ``` Gemini CLI — ~/.gemini/settings.json: ``` { "mcpServers": { "starwell": { "httpUrl": "https://starwell.dev/api/starwell/mcp" } } } ``` ChatGPT — Settings → Apps → enable developer mode, add a connector with the remote URL. LangChain / agent frameworks — point any MCP adapter (e.g. langchain-mcp-adapters) at the same URL; the tool surface loads dynamically, so new sources and tools appear without code changes. ### Every data page has a machine twin The site itself is agent-native. Every public data page carries a rel="alternate" link to its API equivalent, and requesting it with Accept: application/json (or ?format=json) redirects straight to the data: /catalog to /api/starwell/v1/catalog, a source page to its datasets endpoint, a series page to its observations. Series pages also publish schema.org Dataset markup with CSV and JSON distributions. The full pattern is written down at /agent-native. Machine entry points: /llms.txt (site map for LLMs), /llms-full.txt (this documentation as one markdown file), /openapi.json, /changelog.xml. Agents ride the same rate limits and keys as everyone else, and only public data is ever served this way; accounts and their data stay behind authentication. ## Three ways in, thirty seconds each ### Python and pandas The CSV endpoint reads straight into a DataFrame; the # attribution lines are skipped with comment. ``` import pandas as pd df = pd.read_csv( "https://starwell.dev/api/starwell/v1/sources/fred/series/UNRATE/observations?format=csv", comment="#", parse_dates=["period"], ) print(df.tail()) ``` ### JavaScript ``` const r = await fetch( "https://starwell.dev/api/starwell/v1/sources/fred/series/UNRATE/stats" ); const { data, meta } = await r.json(); console.log(data.latest, meta.citation.text); ``` ### Google Sheets One formula, live verified data in a sheet (re-fetches on open): ``` =IMPORTDATA("https://starwell.dev/api/starwell/v1/sources/fred/series/UNRATE/observations?format=csv&latest=120") ``` ## Sources and attribution Every API response carries the series' license code, license URL, and exact attribution text; the authoritative per-source view is the live catalog. The licensing gate is enforced at the serving layer: a series whose license does not permit redistribution is excluded from the catalog and from /answer, and direct requests for it return 451 Unavailable For Legal Reasons with the license context instead of data. ### Australian Bureau of Statistics Data API Based on Australian Bureau of Statistics data, used under Creative Commons Attribution 4.0 International. Licence · Source API docs. ### European Central Bank Data Portal Source: European Central Bank Data Portal. Reproduction is permitted provided that the source is acknowledged. Licence · Source API docs. ### Federal Reserve Economic Data (FRED) Source: U.S. federal statistical agencies via FRED, Federal Reserve Bank of St. Louis. This product uses the FRED API but is not endorsed or certified by the Federal Reserve Bank of St. Louis. Licence · Source API docs. ### INSEE Banque de données macro-économiques (BDM) Source: INSEE (Institut national de la statistique et des études économiques), Banque de données macro-économiques. Licence Ouverte / Etalab 2.0. Licence · Source API docs. ### OECD Data Source: OECD. Licensed under CC BY-4.0 (OECD open-access policy). Licence · Source API docs. ### SEC EDGAR company fundamentals (XBRL) Source: U.S. Securities and Exchange Commission, EDGAR. SEC filings data are in the public domain. Licence · Terms · Source API docs. ### Statistics Canada Source: Statistics Canada. Contains information licensed under the Statistics Canada Open Licence. This product is neither produced nor endorsed by Statistics Canada. Licence · Source API docs. ### U.S. Bureau of Labor Statistics Public Data API Source: U.S. Bureau of Labor Statistics. BLS data are in the public domain. Licence · Terms · Source API docs. ### U.S. Treasury Fiscal Data Source: U.S. Department of the Treasury, Bureau of the Fiscal Service, Fiscal Data. Licence · Source API docs. ### UK Office for National Statistics (website timeseries API) Contains public sector information licensed under the Open Government Licence v3.0. Source: Office for National Statistics. Licence · Terms · Source API docs. ### World Bank Open Data (World Development Indicators) Source: The World Bank, World Development Indicators. Licensed under CC BY-4.0. Licence · Source API docs. Values are served as published by the source (current vintage). Revisions are tracked append-only; historical snapshots are on the roadmap as an institutional feature. Roadmap: /execute (sandboxed Python as a service) follows the same contract. Questions, keys, pilots: create an account or write to hello@starwell.dev. /* Scroll-spy: highlight the section the reader is currently in. Reads only (rect top), then toggles color/border classes that never reflow, so the direct scroll handler is cheap for this short list. */ (function(){ var links = Array.prototype.slice.call(document.querySelectorAll('.docs-side a')); if(!links.length) return; var sections = links.map(function(a){ return document.getElementById(a.getAttribute('href').slice(1)); }); function update(){ var idx = 0; for(var i=0;i /* Try-it panel: live fetches against this same origin's /v1 (all keyless surfaces). Chart JSON is huge and meaningless as text, so it is elided from the printed envelope; everything else is shown verbatim. */ (function(){ var out = document.getElementById('try-out'); var curlEl = document.getElementById('try-curl'); var jsonEl = document.getElementById('try-json'); var btns = Array.prototype.slice.call(document.querySelectorAll('.try-btn')); if(!out || !btns.length) return; function elideCharts(obj){ if(!obj || typeof obj !== 'object') return obj; if(obj.chart) obj.chart = '(Plotly figure JSON — elided for display)'; if(Array.isArray(obj.charts)) obj.charts = '(' + obj.charts.length + ' Plotly figures — elided for display)'; if(Array.isArray(obj.sections)) obj.sections.forEach(elideCharts); if(obj.data) elideCharts(obj.data); return obj; } btns.forEach(function(btn){ btn.addEventListener('click', function(){ var url = btn.getAttribute('data-url'); var label = btn.textContent; btns.forEach(function(b){ b.disabled = true; }); btn.textContent = btn.getAttribute('data-slow') ? 'Computing… (up to 40s)' : 'Running…'; out.hidden = false; curlEl.textContent = 'curl "https://' + location.host + url + '"'; jsonEl.textContent = '…'; fetch(url, { headers: { Accept: 'application/json' } }) .then(function(r){ return r.json(); }) .then(function(body){ var pretty = JSON.stringify(elideCharts(body), null, 2); if(pretty.length > 12000) pretty = pretty.slice(0, 12000) + '\n… (truncated for display)'; jsonEl.textContent = pretty; }) .catch(function(){ jsonEl.textContent = 'The request failed — try again in a moment.'; }) .then(function(){ btns.forEach(function(b){ b.disabled = false; }); btn.textContent = label; }); }); }); })();