docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage (#3209)

* docs(mintlify): cover MCP, OAuth, non-RPC endpoints, and usage

Audit against api/ + proto/ revealed 9 OpenAPI specs missing from nav,
the scenario/v1 service undocumented, and MCP (32 tools + OAuth 2.1 flow)
with no user-facing docs. The stale Docs_To_Review/API_REFERENCE.md still
pointed at pre-migration endpoints that no longer exist.

- Wire 9 orphaned specs into docs.json: ConsumerPrices, Forecast, Health,
  Imagery, Radiation, Resilience, Sanctions, Thermal, Webcam
- Hand-write ScenarioService.openapi.yaml (3 RPCs) until it's proto-backed
  (tracked in issue #3207)
- New MCP page with tool catalog + client setup (Claude Desktop/web, Cursor)
- New MDX for OAuth, Platform, Brief, Commerce, Notifications, Shipping v2,
  Proxies
- New Usage group: quickstart, auth matrix, rate limits, errors
- Remove docs/Docs_To_Review/API_REFERENCE.md and EXTERNAL_APIS.md
  (referenced dead endpoints); add README flagging dir as archival

* docs(mintlify): move scenario docs out of generated docs/api/ tree

The pre-push hook enforces that docs/api/ is proto-generated only.
Replace the hand-written ScenarioService.openapi.yaml with a plain
MDX page (docs/api-scenarios.mdx) until the proto migration lands
(tracked in issue #3207).

* docs(mintlify): fix factual errors flagged in PR review

Reviewer caught 5 endpoints where I speculated on shape/method/limits
instead of reading the code. All fixes cross-checked against the
source:

- api-shipping-v2: route-intelligence is GET with query params
  (fromIso2, toIso2, cargoType, hs2), not POST with a JSON body.
  Response shape is {primaryRouteId, chokepointExposures[],
  bypassOptions[], warRiskTier, disruptionScore, ...}.
- api-commerce: /api/product-catalog returns {tiers, fetchedAt,
  cachedUntil, priceSource} with tier groups free|pro|api_starter|
  enterprise, not the invented {currency, plans}. Document the
  DELETE purge path too.
- api-notifications: Slack/Discord /oauth/start are POST + Clerk
  JWT + PRO (returning {oauthUrl}), not GET redirects. Callbacks
  remain GET.
- api-platform: /api/version returns the latest GitHub Release
  ({version, tag, url, prerelease}), not deployed commit/build
  metadata.
- api-oauth + mcp: /api/oauth/register limit is 5/60s/IP (match
  code), not 10/hour.

Also caught while double-checking: /api/register-interest and
/api/contact are 5/60min and 3/60min respectively (1-hour window,
not 1-minute). Both require Turnstile. Removed the fabricated
limits for share-url, notification-channels, create-checkout
(they fall back to the default per-IP limit).

* docs(mintlify): second-round fixes — verify every claim against source

Reviewer caught 7 more cases where I described API behavior I hadn't
read. Each fix below cross-checked against the handler.

- api-commerce (product-catalog): tiers are flat objects with
  monthlyPrice/annualPrice/monthlyProductId/annualProductId on paid
  tiers, price+period for free, price:null for enterprise. There is
  no nested plans[] array.
- api-commerce (referral/me): returns {code, shareUrl}, not counts.
  Code is a deterministic 8-char HMAC of the Clerk userId; binding
  into Convex is fire-and-forget via ctx.waitUntil.
- api-notifications (notification-channels): actual action set is
  create-pairing-token, set-channel, set-web-push, delete-channel,
  set-alert-rules, set-quiet-hours, set-digest-settings. Replaced
  the made-up list.
- api-shipping-v2 (webhooks): alertThreshold is numeric 0-100
  (default 50), not a severity string. Subscriber IDs are wh_+24hex;
  secret is raw 64-char hex (no whsec_ prefix). POST registration
  returns 201. Added the management routes: GET /{id},
  POST /{id}/rotate-secret, POST /{id}/reactivate.
- api-platform (cache-purge): auth is Authorization: Bearer
  RELAY_SHARED_SECRET, not an admin-key header. Body takes keys[]
  and/or patterns[] (not {key} or {tag}), with explicit per-request
  caps and prefix-blocklist behavior.
- api-platform (download): platform+variant query params, not
  file=<id>. Response is a 302 to a GitHub release asset; documented
  the full platform/variant tables.
- mcp: server also accepts direct X-WorldMonitor-Key in addition to
  OAuth bearer. Fixed the curl example which was incorrectly sending
  a wm_live_ API key as a bearer token.
- api-notifications (youtube/live): handler reads channel or videoId,
  not channelId.
- usage-auth: corrected the auth-matrix row for /api/mcp to reflect
  that OAuth is one of two accepted modes.

* docs(mintlify): fix Greptile review findings

- mcp.mdx: 'Five' slow tools → 'Six' (list contains 6 tools)
- api-scenarios.mdx: replace invalid JSON numeric separator
  (8_400_000_000) with plain integer (8400000000)

Greptile's third finding — /api/oauth/register rate-limit contradiction
across api-oauth.mdx / mcp.mdx / usage-rate-limits.mdx — was already
resolved in commit 4f2600b2a (reviewed commit was eb5654647).
This commit is contained in:
Elie Habib
2026-04-19 15:03:16 +04:00
committed by GitHub
parent 38e6892995
commit e4c95ad9be
17 changed files with 1602 additions and 3436 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,26 @@
# Docs To Review (archival)
This directory is **not published by Mintlify** (`.mintignore` excludes it).
Files here are internal / archival / pending-audit. They are **not** the source of truth.
## Canonical docs live at the root of `docs/`
| If you want... | See |
|---|---|
| Architecture overview | `/docs/architecture.mdx` |
| API reference (RPC) | `/docs/api/*.openapi.yaml` + https://docs.worldmonitor.app |
| Non-RPC API endpoints | `/docs/api-{platform,brief,commerce,notifications,shipping-v2,proxies,oauth}.mdx` |
| MCP server | `/docs/mcp.mdx` |
| External data sources | `/docs/data-sources.mdx` |
| Release / desktop packaging | `/docs/release-packaging.mdx`, `/docs/desktop-app.mdx` |
| Country instability index | `/docs/country-instability-index.mdx` |
## Removed (2026-04-19)
- `API_REFERENCE.md` — referenced legacy pre-migration endpoints (`/api/acled`, `/api/finnhub`, `/api/fred-data`, etc.) that no longer exist. Superseded by the auto-generated OpenAPI specs under `/docs/api/`.
- `EXTERNAL_APIS.md` — duplicated content now in `/docs/data-sources.mdx`.
## Remaining files
The rest of this directory needs a case-by-case audit — contents may be stale, useful, or mergeable into canonical docs. Don't cite them externally until audited.

81
docs/api-brief.mdx Normal file
View File

@@ -0,0 +1,81 @@
---
title: "AI Brief Endpoints"
description: "Read, share, and render the AI-composed daily intelligence brief."
---
WorldMonitor composes a per-user **daily intelligence brief** on Railway, stores it in Redis at `brief:{userId}:{issueDate}`, and exposes these routes for dashboard readback, public sharing, and Telegram/Slack carousel rendering.
<Info>
All read routes require a valid Clerk session and a PRO tier, except the public share route (`/api/brief/public/{hash}`).
</Info>
## Latest brief (authenticated)
### `GET /api/latest-brief`
Returns a short summary of the caller's most recent composed brief, or `{ status: "composing" }` if today's run hasn't produced a brief yet.
| Status | Response |
|--------|----------|
| 200 OK | `{ issueDate, dateLong, greeting, threadCount, magazineUrl }` |
| 200 OK | `{ status: "composing" }` — no brief for today yet |
| 401 | Missing / invalid Clerk JWT |
| 403 | `pro_required` |
| 503 | `BRIEF_URL_SIGNING_SECRET` not configured |
The `magazineUrl` is a freshly-signed URL — the HMAC binds `{userId, issueDate}` so it only works for the authenticated owner.
### `GET /api/brief/{userId}/{issueDate}`
Full brief body for `issueDate` (YYYY-MM-DD). HMAC-signed URL required. Returns the structured brief JSON used by the magazine reader view.
## Sharing
### `POST /api/brief/share-url?date=YYYY-MM-DD`
Materialises a public share pointer for the caller's brief on `date`. Idempotent — hash is a pure function of `{userId, issueDate, BRIEF_SHARE_SECRET}`.
| Status | Response |
|--------|----------|
| 200 | `{ shareUrl, hash, issueDate }` |
| 400 | `invalid_date_shape` / `invalid_payload` |
| 401 | `UNAUTHENTICATED` |
| 403 | `pro_required` |
| 404 | `brief_not_found` — reader can't share what doesn't exist |
| 503 | `service_unavailable` |
### `GET /api/brief/public/{hash}`
**Unauthenticated** public read of a previously-shared brief. The hash resolves to a `brief:public:{hash} → {userId, issueDate}` Redis pointer; if absent, the brief was never shared. Share pointers are written lazily (on share, not on compose).
## Carousel (images for social)
### `GET /api/brief/carousel/{userId}/{issueDate}/{page}.png`
Server-rendered PNG page (`page` = 1..N) of the brief, intended for Telegram `sendMediaGroup`, Slack `chat.postMessage`, LinkedIn, etc.
- Rendered via `@resvg/resvg-js` with the bundled Linux native binding.
- `Content-Type: image/png`, 1080×1350 (4:5 portrait).
- Not gated — uses the HMAC'd path as the capability.
## Ancillary
### `GET /api/story?date=YYYY-MM-DD`
Public read-only "story view" (web reader) for a shared brief. SEO-friendly HTML response.
### `GET /api/og-story?date=YYYY-MM-DD`
Open Graph preview image for `/api/story`. Returns `image/png`, cached aggressively.
### `POST /api/chat-analyst`
Streaming chat endpoint for the "Ask the analyst" in-dashboard assistant. Takes a user prompt + recent-signal context; returns SSE tokens.
- Auth: Clerk JWT + PRO
- Streams: `text/event-stream`
- Back-end: `intelligence/v1/chat-analyst-*` handlers compose context + prompt
### `POST /api/widget-agent`
Single-shot completion endpoint used by embedded widget iframes. Auth via `X-WorldMonitor-Key` (partner keys). Rate-limited per key.

117
docs/api-commerce.mdx Normal file
View File

@@ -0,0 +1,117 @@
---
title: "Commerce Endpoints"
description: "Checkout, customer portal, product catalog, and referrals. Thin edge proxies over Convex + Dodo Payments."
---
WorldMonitor uses [Dodo Payments](https://dodopayments.com) for PRO subscriptions and [Convex](https://convex.dev) as the source-of-truth for entitlements. These edge endpoints are thin auth proxies — they validate the Clerk JWT, then forward to Convex HTTP actions via `RELAY_SHARED_SECRET`.
## Checkout
### `POST /api/create-checkout`
Creates a Dodo checkout session and returns the hosted-checkout URL.
- **Auth**: Clerk bearer (required)
- **Body**:
```json
{ "productId": "pro-monthly", "returnUrl": "https://www.worldmonitor.app/pro/success" }
```
- **Response**: `{ "checkoutUrl": "https://checkout.dodopayments.com/..." }`
- **returnUrl** is validated against an allowlist on the Convex side.
### `POST /api/customer-portal`
Creates a Dodo customer-portal session for an existing subscriber (update card, cancel, view invoices).
- **Auth**: Clerk bearer + active entitlement
- **Response**: `{ "portalUrl": "..." }`
## Product catalog
### `GET /api/product-catalog`
Returns the tier view-model used by the `/pro` pricing page. Cached in Redis under `product-catalog:v2` for 1 hour; on cache miss, fetches live prices from Dodo Payments and falls back to `_product-fallback-prices.js` if Dodo is unreachable. Response carries an `X-Product-Catalog-Source` header so probes can tell cache hits from live fetches.
**Response** (tiers ordered `free`, `pro`, `api_starter`, `enterprise`):
```json
{
"tiers": [
{
"name": "Free",
"description": "Get started with the essentials",
"features": ["Core dashboard panels", "..."],
"cta": "Get Started",
"href": "https://worldmonitor.app",
"highlighted": false,
"price": 0,
"period": "forever"
},
{
"name": "Pro",
"description": "Full intelligence dashboard",
"features": ["..."],
"highlighted": true,
"monthlyPrice": 20,
"monthlyProductId": "pdt_0Nbtt71uObulf7fGXhQup",
"annualPrice": 180,
"annualProductId": "pdt_0NbttMIfjLWC10jHQWYgJ"
},
{
"name": "API",
"description": "Programmatic access to intelligence data",
"features": ["..."],
"highlighted": false,
"monthlyPrice": 99,
"monthlyProductId": "pdt_0NbttVmG1SERrxhygbbUq",
"annualPrice": 990,
"annualProductId": "pdt_0Nbu2lawHYE3dv2THgSEV"
},
{
"name": "Enterprise",
"description": "Custom solutions for organizations",
"features": ["..."],
"cta": "Contact Sales",
"href": "mailto:enterprise@worldmonitor.app",
"highlighted": false,
"price": null
}
],
"fetchedAt": "2026-04-19T12:00:00Z",
"cachedUntil": "2026-04-19T13:00:00Z",
"priceSource": "dodo"
}
```
Notes:
- Price fields are flat on the tier. Paid tiers expose `monthlyPrice` / `monthlyProductId` and `annualPrice` / `annualProductId`. Free uses `price: 0, period: "forever"`; Enterprise uses `price: null`.
- Prices are dollars (Dodo returns cents; the handler divides by 100). Currency is implicit USD for the published catalog.
### `DELETE /api/product-catalog`
Purges the cached catalog. Requires `Authorization: Bearer $RELAY_SHARED_SECRET`. Internal.
## Referrals
### `GET /api/referral/me`
Returns the caller's deterministic referral code (an 8-char HMAC of the Clerk userId, stable for the life of the account) and a pre-built share URL. Clerk bearer required. The handler also fires a best-effort `ctx.waitUntil` Convex binding so future `/pro?ref=<code>` signups can attribute — this never blocks the response.
```json
{
"code": "a1b2c3d4",
"shareUrl": "https://worldmonitor.app/pro?ref=a1b2c3d4"
}
```
Errors:
- `401 UNAUTHENTICATED` — missing or invalid Clerk JWT.
- `503 service_unavailable` — `BRIEF_URL_SIGNING_SECRET` not configured (the referral-code HMAC reuses that secret).
No `referrals` count or `rewardMonths` is returned today — Dodo's `affonso_referral` attribution doesn't yet flow into Convex, and exposing only the waitlist-side count would mislead.
## Waitlist
### `POST /api/register-interest`
Captures an email into the Convex waitlist table. Turnstile-verified, rate-limited per IP. See [Platform endpoints](/api-platform) for the request shape.

115
docs/api-notifications.mdx Normal file
View File

@@ -0,0 +1,115 @@
---
title: "Notifications & Integrations"
description: "Notification channels, webhook delivery, and Telegram/Slack/Discord/YouTube integration endpoints."
---
## Notification channels
Users can register multiple delivery channels (webhook, Telegram, Slack, Discord, email) and bind alert rules to them.
### `GET /api/notification-channels`
Lists the caller's registered channels and alert rules.
```json
{
"channels": [
{ "id": "chn_01", "type": "webhook", "url": "https://hooks.example.com/...", "active": true },
{ "id": "chn_02", "type": "telegram", "chatId": "@alerts_xyz", "active": true }
],
"alertRules": [
{ "id": "rul_01", "channelId": "chn_01", "trigger": "brief_ready", "filter": null }
]
}
```
### `POST /api/notification-channels`
Action-dispatched writer. The body's `action` field selects the mutation:
| action | Purpose |
|--------|---------|
| `create-pairing-token` | Mint a one-time pairing token (optional `variant`) for the mobile / Tauri client to bind a push channel. |
| `set-channel` | Register or update a channel. For `webhook` channels the `webhookEnvelope` URL is validated HTTPS-only, must not resolve to a private/loopback address, and is AES-256-GCM encrypted before storage. Optional `email`, `webhookLabel` (truncated to 100 chars). |
| `set-web-push` | Register a browser Web Push subscription for the signed-in user. |
| `delete-channel` | Remove a channel by type (`email`, `webhook`, `telegram`, `web-push`, etc.). |
| `set-alert-rules` | Replace the caller's alert-rules set in one shot. |
| `set-quiet-hours` | Set do-not-disturb windows. |
| `set-digest-settings` | Configure digest cadence and channel routing. |
All actions require Clerk bearer + PRO (`tier >= 1`). Invalid actions return `400 Unknown action`. Requests are forwarded to Convex via `RELAY_SHARED_SECRET`.
## Webhook delivery contract
When an alert fires, registered webhook URLs receive:
- **Method**: `POST`
- **Headers**:
- `Content-Type: application/json`
- `X-WM-Signature: sha256=<HMAC-SHA256(body, channelSecret)>`
- `X-WM-Delivery-Id: <ulid>`
- `X-WM-Event: <event-name>`
- **Body** (envelope v1):
```json
{
"envelope": 1,
"event": "brief_ready",
"deliveryId": "01HX...",
"occurredAt": "2026-04-19T06:00:00Z",
"data": { "issueDate": "2026-04-19", "magazineUrl": "..." }
}
```
Signature verification: `hmac_sha256(rawBody, channelSecret) == X-WM-Signature[7:]`.
<Warning>
The envelope version is **shared across three producers** (`notification-relay`, `proactive-intelligence`, `seed-digest-notifications`). Bumping it requires coordinated updates.
</Warning>
### `POST /api/notify`
Internal ingestion endpoint called by Railway producers to enqueue a notification. Requires `RELAY_SHARED_SECRET`. Not a public API.
## Telegram
### `GET /api/telegram-feed?userId=...`
Returns the pre-rendered brief feed for a given Telegram-linked user. Used by the Telegram mini-app.
## YouTube
### `GET /api/youtube/embed?videoId=...`
SSR'd YouTube embed iframe with CSP-compatible wrapping. Used to bypass WKWebView autoplay restrictions on the desktop app.
### `GET /api/youtube/live?channel=<handle>` or `?videoId=<11-char-id>`
Returns live-stream metadata for a YouTube channel (`channel` — handle with or without `@` prefix) or a specific video (`videoId` — 11-char YouTube id). At least one of the two params is required; returns `400 Missing channel or videoId parameter` otherwise. Response cached 10 min for channel lookups, 1 hour for videoId lookups.
Proxies to the Railway relay first (residential proxy for YouTube scraping). On relay failure, falls back to YouTube oEmbed (for `videoId`) or direct channel scraping — both are unreliable from datacenter IPs.
## Slack integration
### `POST /api/slack/oauth/start`
Authenticated (Clerk JWT + PRO). Body is empty. Server generates a one-time CSRF state token, stores the caller's userId in Upstash keyed by that state (10-min TTL), and returns the Slack authorize URL for the frontend to open in a popup.
```json
{ "oauthUrl": "https://slack.com/oauth/v2/authorize?client_id=...&scope=incoming-webhook&..." }
```
Errors: 401 (missing/invalid JWT), 403 `pro_required`, 503 (OAuth not configured or Upstash unavailable).
### `GET /api/slack/oauth/callback`
Unauthenticated — the popup lands here after Slack redirects. Validates the state token, exchanges `code` for an incoming-webhook URL, AES-256-GCM encrypts the webhook, and stores it in Convex. Returns a tiny HTML page that `postMessage`s the opener and closes.
## Discord integration
### `POST /api/discord/oauth/start`
Authenticated (Clerk JWT + PRO). Same shape as the Slack start route — returns `{ oauthUrl }` for a popup.
### `GET /api/discord/oauth/callback`
Unauthenticated. Exchanges `code`, stores the guild webhook, and `postMessage`s the opener.

118
docs/api-oauth.mdx Normal file
View File

@@ -0,0 +1,118 @@
---
title: "OAuth 2.1 Server"
description: "Dynamic client registration, authorization, and token exchange endpoints that back the WorldMonitor MCP server."
---
WorldMonitor runs a minimal OAuth 2.1 authorization server whose only client-facing purpose today is **granting access to the MCP server** at `/api/mcp`. It implements:
- [RFC 7591](https://datatracker.ietf.org/doc/html/rfc7591) — Dynamic Client Registration
- [RFC 7636](https://datatracker.ietf.org/doc/html/rfc7636) — PKCE (required, S256 only)
- [RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414) — Authorization Server Metadata
- [RFC 9728](https://datatracker.ietf.org/doc/html/rfc9728) — Protected Resource Metadata
## Discovery
| URL | Purpose |
|-----|---------|
| `/.well-known/oauth-authorization-server` | AS metadata (endpoints, supported grants, PKCE methods) |
| `/.well-known/oauth-protected-resource` | Resource metadata (authorization servers, scopes) |
## Endpoints
### `POST /api/oauth/register`
Dynamic Client Registration. Returns a `client_id` (public clients, no secret).
**Request**:
```json
{
"redirect_uris": ["https://claude.ai/api/mcp/auth_callback"],
"client_name": "Claude Desktop",
"token_endpoint_auth_method": "none"
}
```
**Response**: `{ "client_id": "c_01HX...", "client_id_issued_at": 1713456789, ... }`
**Redirect URI allowlist**: only these prefixes are accepted:
- `https://claude.ai/api/mcp/auth_callback`
- `https://claude.com/api/mcp/auth_callback`
- `http://localhost:<port>` / `http://127.0.0.1:<port>` — any port
**Rate limit**: 5 registrations / 60 s / IP.
**Client TTL**: 90 days sliding (every successful token exchange refreshes).
### `GET /api/oauth/authorize`
Starts the OAuth flow. Renders a consent page that redirects to Clerk for sign-in, then issues an authorization code bound to the caller's PRO entitlement.
**Required query params**:
- `response_type=code`
- `client_id` — from DCR
- `redirect_uri` — must match the one registered
- `code_challenge` — PKCE S256
- `code_challenge_method=S256`
- `state` — opaque
- `scope` (optional)
**Code TTL**: 10 minutes. Single-use (atomic `GETDEL` on exchange).
### `POST /api/oauth/token`
Exchanges an authorization code for an access token, or refreshes an existing token.
**Grant type: `authorization_code`**:
```
grant_type=authorization_code
code=<from /authorize>
code_verifier=<PKCE>
client_id=<from DCR>
redirect_uri=<same as /authorize>
```
**Response**:
```json
{
"access_token": "wm_oat_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "wm_ort_...",
"scope": "mcp:read"
}
```
**Grant type: `refresh_token`**:
```
grant_type=refresh_token
refresh_token=<from previous exchange>
client_id=<from DCR>
```
**Rate limit**: 10 token requests / minute / IP.
**Token TTLs**:
- Access token: 1 hour
- Refresh token: 7 days
All token-endpoint responses include `Cache-Control: no-store, Pragma: no-cache`.
## Using tokens
Pass the access token on every MCP request:
```
Authorization: Bearer wm_oat_...
```
Tokens are bound to the user's account and re-check entitlement on every call — a downgrade revokes access on the next request.
## Error responses
Per [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2):
```json
{ "error": "invalid_grant", "error_description": "..." }
```
Common errors: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.

155
docs/api-platform.mdx Normal file
View File

@@ -0,0 +1,155 @@
---
title: "Platform Endpoints"
description: "Bootstrap, health, versioning, cache-purge, and user preferences — the plumbing endpoints every WorldMonitor client talks to."
---
These endpoints are not part of a domain RPC service — they sit at the root of the API surface and handle platform concerns.
## Bootstrap
### `GET /api/bootstrap`
Single round-trip hydration for the dashboard. Returns **all bootstrap-registered Redis cache keys** unwrapped from their seed envelopes in one response.
- **Auth**: browser origin OR `X-WorldMonitor-Key`
- **Cache**: `Cache-Control: public, max-age=30, s-maxage=30`
- **Shape**: `{ earthquakes, outages, marketQuotes, commodityQuotes, imfMacro, bisPolicy, ... }` — ~40+ top-level fields, each the unwrapped payload of one seeded domain.
Use this on initial page load to avoid 40 parallel RPC calls.
## Version
### `GET /api/version`
Returns the latest **GitHub Release** of `koala73/worldmonitor`. Used by the desktop app to detect a newer published release and prompt the user to update. It is **not** the currently-deployed Vercel commit.
```json
{
"version": "2.6.7",
"tag": "v2.6.7",
"url": "https://github.com/koala73/worldmonitor/releases/tag/v2.6.7",
"prerelease": false
}
```
Cached `public, s-maxage=300, stale-while-revalidate=60, stale-if-error=3600`. Returns `502 { "error": "upstream" }` or `502 { "error": "fetch_failed" }` when the GitHub API is unreachable.
## Cache purge
### `POST /api/cache-purge`
Internal. Invalidates Redis cache keys by explicit list or glob patterns.
- **Auth**: `Authorization: Bearer $RELAY_SHARED_SECRET` (timing-safe compared). Anything else returns `401`.
- **Body** (at least one of `keys` / `patterns` required):
```json
{
"keys": ["market:stocks-bootstrap:v1", "infra:outages:v1"],
"patterns": ["market:sectors:*"],
"dryRun": false
}
```
- **Limits**: up to 20 explicit keys, up to 3 patterns (each must end in `*`, bare `*` rejected), up to 200 deletions total, up to 5 SCAN iterations per pattern.
- **Safety**: keys with prefixes `rl:` / `__` are always skipped; patterns that would match `military:bases:*`, `conflict:iran-events:*`, `conflict:ucdp-events:*` (durable seeds) are skipped.
- **Non-production**: on preview / development deploys, keys are auto-prefixed with `{env}:{git-sha}:` so purges can't affect production data.
- **Response**:
```json
{ "matched": 4, "deleted": 4, "keys": ["..."], "dryRun": false, "truncated": false }
```
## Health
### `GET /api/health`
Aggregated freshness report for **all registered seed keys**. Returns `HEALTHY`, `WARNING` (stale), or `CRIT` (missing / empty with `emptyDataIsFailure`).
Monitor via UptimeRobot / Better Stack — alert on any status other than `HEALTHY`.
```json
{
"status": "HEALTHY",
"checkedAt": "2026-04-19T12:00:00Z",
"keys": {
"market:stocks-bootstrap:v1": { "status": "HEALTHY", "fetchedAt": "...", "recordCount": 78 },
"seismology:earthquakes:v1": { "status": "WARNING", "staleMin": 12 }
}
}
```
### `GET /api/seed-health`
Parallel registry for Railway-cron-driven seeders with their own cadence thresholds. Distinct from `/api/health` — both must be updated when cadence changes. See [health endpoints](/health-endpoints).
### `POST /api/seed-contract-probe`
Internal probe that validates each seed producer's envelope shape matches its consumers. Returns violations if any consumer reads a field the producer no longer emits.
## User preferences
### `GET /api/user-prefs`
### `POST /api/user-prefs`
Per-user dashboard preferences (layout, toggles, filters). Clerk bearer required. Backed by Convex.
```json
{
"layout": "classic",
"enabledLayers": ["conflict", "aviation", "maritime"],
"defaultCountry": "US"
}
```
## API key cache invalidation
### `POST /api/invalidate-user-api-key-cache`
Invalidates a user's entitlement cache after a subscription change (Dodo webhook → Convex → this endpoint). Internal — requires `RELAY_SHARED_SECRET`.
## Geo utilities
### `GET /api/geo?iso2=US`
Returns country metadata: centroid, bbox, capital, ISO codes.
### `GET /api/reverse-geocode?lat=40.7&lon=-74.0`
Reverse geocodes a lat/lon to the nearest country + city using the bundled coordinate dataset.
### `GET /api/data/city-coords?q=Tokyo`
City name → coordinates lookup.
## Utilities
### `GET /api/download?platform=<id>&variant=<id>`
Redirects to the matching asset on the latest GitHub release of `koala73/worldmonitor`. Returns `302` to the asset URL on success, or `302` to [releases/latest](https://github.com/koala73/worldmonitor/releases/latest) on any failure (unknown platform, no match, GitHub error).
**`platform`** (required, exact string):
| value | matches |
|-------|---------|
| `windows-exe` | `*_x64-setup.exe` |
| `windows-msi` | `*_x64_en-US.msi` |
| `macos-arm64` | `*_aarch64.dmg` |
| `macos-x64` | `*_x64.dmg` (excluding `*setup*`) |
| `linux-appimage` | `*_amd64.AppImage` |
| `linux-appimage-arm64` | `*_aarch64.AppImage` |
**`variant`** (optional):
| value | filters asset name to |
|-------|-----------------------|
| `full` / `world` | `worldmonitor` |
| `tech` | `techmonitor` |
| `finance` | `financemonitor` |
Caches the 302 for 5 minutes (`s-maxage=300`, `stale-while-revalidate=60`, `stale-if-error=600`).
### `POST /api/contact`
Public contact form. Turnstile-verified, rate-limited per IP.
### `POST /api/register-interest`
Captures email for desktop-app early-access waitlist. Writes to Convex.

65
docs/api-proxies.mdx Normal file
View File

@@ -0,0 +1,65 @@
---
title: "Proxies & Raw-Data Passthroughs"
description: "Thin server-side proxies for upstream APIs — used by the dashboard to avoid CORS, hide API keys, and add rate-limiting/caching."
---
These endpoints pass caller requests through to an upstream data source. They exist to:
1. Hide upstream API keys server-side
2. Work around CORS
3. Add caching + per-IP rate limiting
4. Normalize response shapes
<Warning>
Most proxies are intended for **our own dashboard** and are lightly gated. They are not part of the public API contract and may change or be removed without notice. Prefer the domain RPC services (`/api/<domain>/v1/*`) for stable integrations.
</Warning>
## Raw-data passthroughs
| Endpoint | Upstream | Purpose |
|----------|----------|---------|
| `GET /api/eia/[...path]` | EIA v2 (energy.gov) | Catch-all proxy for US Energy Information Administration series. Path segments are appended to `https://api.eia.gov/v2/...`. |
| `GET /api/opensky` | OpenSky Network | Live ADS-B state vectors. Used by the flights layer. |
| `GET /api/polymarket` | Polymarket gamma-api | Active event contracts. |
| `GET /api/satellites` | CelesTrak | LEO/GEO satellite orbital elements. |
| `GET /api/military-flights` | ADS-B Exchange / adsb.lol | Identified military aircraft. |
| `GET /api/ais-snapshot` | Internal AIS seed | Snapshot of latest vessel positions for the currently-loaded bbox. |
| `GET /api/gpsjam` | gpsjam.org | GPS interference hotspot reports. |
| `GET /api/sanctions-entity-search?q=...` | OFAC SDN | Fuzzy-match sanctions entity search. |
| `GET /api/oref-alerts` | OREF (Israel Home Front Command) | Tzeva Adom rocket alert mirror. |
| `GET /api/supply-chain/hormuz-tracker` | Internal AIS + registry | Real-time Hormuz transit dashboard data. |
All proxies:
- Respect the origin CORS allowlist.
- Apply per-IP rate limits via `_ip-rate-limit.js` (~ 60 req/min/IP default).
- Cache aggressively (`s-maxage` varies by upstream).
## Content proxies
### `GET /api/rss-proxy?url=<allowed-feed>`
Fetches an RSS/Atom feed and returns the parsed JSON. The URL must match one of the patterns in `_rss-allowed-domains.js` — arbitrary URLs are refused to prevent SSRF.
### `GET /api/enrichment/company?domain=<host>`
Returns company metadata (name, logo, industry, HQ country) for a website domain. Composite of public sources.
### `GET /api/enrichment/signals?domain=<host>`
Returns trust and risk signals (TLS grade, DNS age, WHOIS country, threat-list membership) for a domain.
## Skills registry
### `GET /api/skills/fetch-agentskills`
Returns the catalog of "skills" (agent capabilities) the WorldMonitor chat analyst can invoke. Used internally by `chat-analyst.ts` and the widget agent.
## Legacy / internal
### `POST /api/fwdstart`
Forward-starting scenario helper used by the desktop app during first-run. Internal.
### `GET /api/mcp-proxy`
Legacy MCP shim — forwards to the current MCP route. Deprecated; use [`/api/mcp`](/mcp) directly.

130
docs/api-scenarios.mdx Normal file
View File

@@ -0,0 +1,130 @@
---
title: "Scenarios API"
description: "Run pre-defined supply-chain disruption scenarios against any country and poll for worker-computed results."
---
The **scenarios** API is a PRO-only, job-queued surface on top of the WorldMonitor chokepoint + trade dataset. Callers enqueue a named scenario template against an optional country, then poll a job-id until the worker completes.
<Info>
This service is documented inline (not yet proto-backed). Proto migration is tracked in [issue #3207](https://github.com/koala73/worldmonitor/issues/3207) and will replace this page with auto-generated reference.
</Info>
## List templates
### `GET /api/scenario/v1/templates`
Returns the catalog of pre-defined scenario templates. Cached `public, max-age=3600`.
**Response**:
```json
{
"templates": [
{
"id": "hormuz-closure-30d",
"name": "Strait of Hormuz closure (30 days)",
"affectedChokepointIds": ["hormuz"],
"disruptionPct": 100,
"durationDays": 30,
"affectedHs2": ["27"],
"costShockMultiplier": 1.8
}
]
}
```
## Run a scenario
### `POST /api/scenario/v1/run`
Enqueues a job. Returns `202 Accepted` with a `jobId` the caller must poll.
- **Auth**: `X-WorldMonitor-Key` (or trusted browser origin) + PRO
- **Rate limits**:
- 10 jobs / minute / user
- Global queue capped at 100 in-flight jobs; excess rejected with `429` + `Retry-After: 30`
**Request**:
```json
{
"scenarioId": "hormuz-closure-30d",
"iso2": "SG"
}
```
- `scenarioId` — id from `/templates`. Required.
- `iso2` — optional ISO-3166-1 alpha-2 (uppercase). Scopes the scenario to one country.
**Response (`202`)**:
```json
{
"jobId": "scenario:1713456789012:a1b2c3d4",
"status": "pending",
"statusUrl": "/api/scenario/v1/status?jobId=scenario:1713456789012:a1b2c3d4"
}
```
**Errors**:
| Status | `error` | Cause |
|--------|---------|-------|
| 400 | `Invalid JSON body` | Body is not valid JSON |
| 400 | `scenarioId is required` | Missing field |
| 400 | `Unknown scenario: ...` | `scenarioId` not in the template catalog |
| 400 | `iso2 must be a 2-letter uppercase country code` | Malformed `iso2` |
| 403 | `PRO subscription required` | Not PRO |
| 405 | — | Method other than `POST` |
| 429 | `Rate limit exceeded: 10 scenario jobs per minute` | Per-user rate limit |
| 429 | `Scenario queue is at capacity, please try again later` | Global queue > 100 |
| 502 | `Failed to enqueue scenario job` | Redis enqueue failure |
| 503 | `Service temporarily unavailable` | Missing env |
## Poll job status
### `GET /api/scenario/v1/status?jobId=<jobId>`
Returns the job result or `{ status: "pending" }` while the worker is still running.
- **Auth**: same as `/run`
- **jobId format**: `scenario:{unix-ms}:{8-char-suffix}` — strictly validated to guard against path traversal
**Pending response (`200`)**:
```json
{
"jobId": "scenario:1713456789012:a1b2c3d4",
"status": "pending"
}
```
**Completed response (`200`)** — shape is scenario-specific; always includes:
```json
{
"jobId": "scenario:1713456789012:a1b2c3d4",
"scenarioId": "hormuz-closure-30d",
"iso2": "SG",
"status": "completed",
"completedAt": 1713456890123,
"result": {
"costShockPct": 14.2,
"affectedImportValueUsd": 8400000000,
"topExposedSectors": ["refined_petroleum", "chemicals"]
}
}
```
**Errors**:
| Status | `error` | Cause |
|--------|---------|-------|
| 400 | `Invalid or missing jobId` | Missing or malformed `jobId` |
| 403 | `PRO subscription required` | Not PRO |
| 405 | — | Method other than `GET` |
| 500 | `Corrupted job result` | Worker wrote invalid JSON |
| 502 | `Failed to fetch job status` | Redis read failure |
| 503 | `Service temporarily unavailable` | Missing env |
## Polling strategy
- First poll: ~1s after enqueue.
- Subsequent polls: exponential backoff (1s → 2s → 4s, cap 10s).
- Workers typically complete in 5-30 seconds depending on scenario complexity.
- If still pending after 2 minutes, the job is probably dead — re-enqueue.

170
docs/api-shipping-v2.mdx Normal file
View File

@@ -0,0 +1,170 @@
---
title: "Shipping v2 API"
description: "Chokepoint route-intelligence queries and webhook subscriptions for supply-chain disruption alerts."
---
The v2 shipping API is a **PRO-gated** read + webhook-subscription surface on top of WorldMonitor's chokepoint registry and AIS tracking data.
<Info>
All v2 shipping endpoints require `X-WorldMonitor-Key` (server-to-server). Browser origins are **not** trusted here — `validateApiKey` runs with `forceKey: true`.
</Info>
## Route intelligence
### `GET /api/v2/shipping/route-intelligence`
Scores a country-pair trade route for chokepoint exposure and current disruption risk.
**Query parameters**:
| Param | Required | Description |
|-------|----------|-------------|
| `fromIso2` | yes | Origin country, ISO-3166-1 alpha-2 (uppercase). |
| `toIso2` | yes | Destination country, ISO-3166-1 alpha-2 (uppercase). |
| `cargoType` | no | One of `container` (default), `tanker`, `bulk`, `roro`. |
| `hs2` | no | 2-digit HS commodity code (default `27` — mineral fuels). |
**Example**:
```
GET /api/v2/shipping/route-intelligence?fromIso2=AE&toIso2=NL&cargoType=tanker&hs2=27
```
**Response (`200`)**:
```json
{
"fromIso2": "AE",
"toIso2": "NL",
"cargoType": "tanker",
"hs2": "27",
"primaryRouteId": "ae-to-eu-via-hormuz-suez",
"chokepointExposures": [
{ "chokepointId": "hormuz", "chokepointName": "Strait of Hormuz", "exposurePct": 100 },
{ "chokepointId": "suez", "chokepointName": "Suez Canal", "exposurePct": 100 }
],
"bypassOptions": [
{
"id": "cape-of-good-hope",
"name": "Cape of Good Hope",
"type": "maritime_detour",
"addedTransitDays": 12,
"addedCostMultiplier": 1.35,
"activationThreshold": "DISRUPTION_SCORE_60"
}
],
"warRiskTier": "WAR_RISK_TIER_ELEVATED",
"disruptionScore": 68,
"fetchedAt": "2026-04-19T12:00:00Z"
}
```
- `disruptionScore` is 0-100 on the **primary** chokepoint for the route (higher = more disruption).
- `warRiskTier` is one of the `WAR_RISK_TIER_*` enum values from the chokepoint status feed.
- `bypassOptions` are filtered to those whose `suitableCargoTypes` includes `cargoType` (or is unset).
**Caching**: `Cache-Control: public, max-age=60, stale-while-revalidate=120`.
**Errors**:
| Status | Cause |
|--------|-------|
| 400 | `fromIso2` or `toIso2` missing/malformed |
| 401 | API key required or invalid |
| 403 | `PRO subscription required` |
| 405 | Method other than `GET` |
## Webhook subscriptions
### `POST /api/v2/shipping/webhooks`
Registers a webhook for chokepoint disruption alerts. Returns `201 Created`.
**Request**:
```json
{
"callbackUrl": "https://hooks.example.com/shipping-alerts",
"chokepointIds": ["hormuz", "suez", "bab-el-mandeb"],
"alertThreshold": 60
}
```
- `callbackUrl` — required, HTTPS only, must not resolve to a private/loopback address (SSRF guard at registration).
- `chokepointIds` — optional. Omitting or passing an empty array subscribes to **all** registered chokepoints. Unknown IDs return `400`.
- `alertThreshold` — numeric 0-100 (default `50`). Values outside that range return `400 "alertThreshold must be a number between 0 and 100"`.
**Response (`201`)**:
```json
{
"subscriberId": "wh_a1b2c3d4e5f6a7b8c9d0e1f2",
"secret": "64-char-lowercase-hex-string"
}
```
- `subscriberId` — `wh_` prefix + 24 hex chars (12 random bytes).
- `secret` — raw 64-char lowercase hex (32 random bytes). There is no `whsec_` prefix. Persist it — the server never returns it again except on rotation.
- **TTL**: 30 days on the record and the owner-index set (re-register or rotate to extend).
- Ownership is tracked via SHA-256 of the caller's API key (never secret — stored as `ownerTag`).
Auth: `X-WorldMonitor-Key` (forceKey: true) + PRO. Returns `401` / `403` otherwise.
### `GET /api/v2/shipping/webhooks`
Lists the caller's registered webhooks (filtered by the SHA-256 owner tag of the calling API key).
```json
{
"webhooks": [
{
"subscriberId": "wh_...",
"callbackUrl": "https://hooks.example.com/...",
"chokepointIds": ["hormuz", "suez"],
"alertThreshold": 60,
"createdAt": "2026-04-19T12:00:00Z",
"active": true
}
]
}
```
The `secret` is intentionally omitted from list and status responses.
### `GET /api/v2/shipping/webhooks/{subscriberId}`
Status read for a single webhook. Returns the same record shape as in `GET /webhooks` (no `secret`). `404` if unknown, `403` if owned by a different API key.
### `POST /api/v2/shipping/webhooks/{subscriberId}/rotate-secret`
Generates and returns a **new** secret. The record's `secret` is replaced in place; the old secret stops validating immediately.
```json
{ "subscriberId": "wh_...", "secret": "new-64-char-hex", "rotatedAt": "2026-04-19T12:05:00Z" }
```
### `POST /api/v2/shipping/webhooks/{subscriberId}/reactivate`
Flips `active: true` on the record (use after investigating and fixing a delivery failure that caused deactivation).
```json
{ "subscriberId": "wh_...", "active": true }
```
### Delivery format
```
POST <callbackUrl>
Content-Type: application/json
X-WM-Signature: sha256=<HMAC-SHA256(body, secret)>
X-WM-Delivery-Id: <ulid>
X-WM-Event: chokepoint.disruption
{
"subscriberId": "wh_...",
"chokepointId": "hormuz",
"score": 74,
"alertThreshold": 60,
"triggeredAt": "2026-04-19T12:03:00Z",
"reason": "ais_congestion_spike",
"details": { ... }
}
```
The delivery worker re-resolves `callbackUrl` before each send and re-checks against `PRIVATE_HOSTNAME_PATTERNS` to mitigate DNS rebinding. Delivery is at-least-once — consumers must handle duplicates via `X-WM-Delivery-Id`.

View File

@@ -63,6 +63,15 @@
"architecture"
]
},
{
"group": "Usage",
"pages": [
"usage-quickstart",
"usage-auth",
"usage-rate-limits",
"usage-errors"
]
},
{
"group": "Platform & Features",
"pages": [
@@ -109,6 +118,25 @@
"desktop-app"
]
},
{
"group": "MCP & Integrations",
"pages": [
"mcp",
"api-oauth",
"api-notifications"
]
},
{
"group": "HTTP API",
"pages": [
"api-platform",
"api-brief",
"api-commerce",
"api-scenarios",
"api-shipping-v2",
"api-proxies"
]
},
{
"group": "Developer Guide",
"pages": [
@@ -160,6 +188,10 @@
{
"group": "Cyber",
"openapi": "api/CyberService.openapi.yaml"
},
{
"group": "Sanctions",
"openapi": "api/SanctionsService.openapi.yaml"
}
]
},
@@ -181,6 +213,14 @@
{
"group": "Wildfires",
"openapi": "api/WildfireService.openapi.yaml"
},
{
"group": "Radiation",
"openapi": "api/RadiationService.openapi.yaml"
},
{
"group": "Thermal",
"openapi": "api/ThermalService.openapi.yaml"
}
]
},
@@ -203,9 +243,17 @@
"group": "Supply Chain",
"openapi": "api/SupplyChainService.openapi.yaml"
},
{
"group": "Consumer Prices",
"openapi": "api/ConsumerPricesService.openapi.yaml"
},
{
"group": "Predictions",
"openapi": "api/PredictionService.openapi.yaml"
},
{
"group": "Forecasts",
"openapi": "api/ForecastService.openapi.yaml"
}
]
},
@@ -223,6 +271,27 @@
{
"group": "Infrastructure",
"openapi": "api/InfrastructureService.openapi.yaml"
},
{
"group": "Resilience",
"openapi": "api/ResilienceService.openapi.yaml"
}
]
},
{
"group": "Health & Environment",
"pages": [
{
"group": "Public Health",
"openapi": "api/HealthService.openapi.yaml"
},
{
"group": "Imagery",
"openapi": "api/ImageryService.openapi.yaml"
},
{
"group": "Webcams",
"openapi": "api/WebcamService.openapi.yaml"
}
]
},

244
docs/mcp.mdx Normal file
View File

@@ -0,0 +1,244 @@
---
title: "MCP Server"
description: "Connect Claude, Cursor, and other MCP-compatible clients to WorldMonitor's live global-intelligence data via the Model Context Protocol."
---
WorldMonitor exposes its intelligence stack as a [Model Context Protocol](https://modelcontextprotocol.io) server so any MCP-compatible client (Claude Desktop, Claude web, Cursor, MCP Inspector, custom agents) can pull live conflict, market, aviation, maritime, economic, and forecasting data directly into a model's context.
<Info>
The MCP server is gated behind **PRO**. Free-tier users see a 401 at the OAuth step.
</Info>
## Endpoints
| Endpoint | Purpose |
|----------|---------|
| `https://api.worldmonitor.app/api/mcp` | JSON-RPC server (Streamable HTTP transport, protocol `2025-03-26`) |
| `https://api.worldmonitor.app/api/oauth/register` | Dynamic Client Registration (RFC 7591) |
| `https://api.worldmonitor.app/api/oauth/authorize` | OAuth 2.1 authorization endpoint (PKCE required) |
| `https://api.worldmonitor.app/api/oauth/token` | Token endpoint (authorization_code + refresh_token) |
| `https://api.worldmonitor.app/.well-known/oauth-authorization-server` | AS metadata (RFC 8414) |
| `https://api.worldmonitor.app/.well-known/oauth-protected-resource` | Resource server metadata (RFC 9728) |
Protocol version: `2025-03-26`. Server identifier: `worldmonitor` v1.0.
## Authentication
The MCP handler accepts two auth modes, in priority order:
1. **OAuth 2.1 bearer** — `Authorization: Bearer <token>` where `<token>` was issued by `/api/oauth/token`. This is what Claude Desktop, claude.ai, Cursor, and MCP Inspector use automatically. Required for any client that hits MCP from a browser origin.
2. **Direct API key** — `X-WorldMonitor-Key: wm_live_...`. Intended for server-side scripts, `curl`, and custom integrations. Do **not** send a `wm_live_...` key as a `Bearer` token — it will fail OAuth resolution and return `401 invalid_token`.
Both modes check the same PRO entitlement before every tool call, so a subscription downgrade revokes access on the next request.
### Redirect URI allowlist
Dynamic Client Registration is **not** open to arbitrary HTTPS redirects. Only these prefixes are accepted:
- `https://claude.ai/api/mcp/auth_callback`
- `https://claude.com/api/mcp/auth_callback`
- `http://localhost:<port>` / `http://127.0.0.1:<port>` (any port) — for Claude Code, MCP Inspector, local development
Other clients must proxy via one of these redirects or run locally.
### Token lifetimes
| Artifact | TTL |
|----------|-----|
| Authorization code | 10 min |
| Access token | 1 hour |
| Refresh token | 7 days |
| Registered client record | 90 days (sliding) |
## Rate limits
- **MCP calls**: 60 requests / minute / API key (sliding window)
- **OAuth authorize**: 10 requests / minute / IP
- **OAuth token**: 10 requests / minute / IP
- **Dynamic registration**: 5 registrations / minute / IP
Exceeding returns `429` with a `Retry-After` header.
## Client setup
### Claude Desktop
`~/Library/Application Support/Claude/claude_desktop_config.json` (macOS) — use the remote MCP entry:
```json
{
"mcpServers": {
"worldmonitor": {
"url": "https://api.worldmonitor.app/api/mcp"
}
}
}
```
Claude Desktop handles the OAuth flow automatically on first connection.
### Claude web (claude.ai)
Add via **Settings → Connectors → Add custom connector**:
- Name: `WorldMonitor`
- URL: `https://api.worldmonitor.app/api/mcp`
### Cursor
`~/.cursor/mcp.json`:
```json
{
"mcpServers": {
"worldmonitor": {
"url": "https://api.worldmonitor.app/api/mcp"
}
}
}
```
### MCP Inspector (debugging)
```bash
npx @modelcontextprotocol/inspector https://api.worldmonitor.app/api/mcp
```
## Tool catalog
The server exposes 32 tools. Most are cache-reads over pre-seeded Redis keys (sub-second). Six (`get_world_brief`, `get_country_brief`, `analyze_situation`, `generate_forecasts`, `search_flights`, `search_flight_prices_by_date`) call live LLMs or external APIs and are slower.
### Markets & economy
| Tool | Description |
|------|-------------|
| `get_market_data` | Equity quotes, commodity prices (incl. gold GC=F), crypto, FX, sector performance, ETF flows, Gulf markets. |
| `get_economic_data` | Fed Funds, economic calendar, fuel prices, ECB FX, EU yield curve, earnings, COT, energy storage, BIS DSR + property prices. |
| `get_country_macro` | IMF WEO bundle per country: inflation, current account, GDP/capita, unemployment, savings-investment gap (~210 countries). |
| `get_eu_housing_cycle` | Eurostat annual house price index (prc_hpi_a), 10-yr sparkline per EU member + EA20/EU27. |
| `get_eu_quarterly_gov_debt` | Eurostat quarterly gross government debt (%GDP), 8-quarter sparkline. |
| `get_eu_industrial_production` | Eurostat monthly industrial production index, 12-month sparkline. |
| `get_prediction_markets` | Active Polymarket event contracts with live probabilities. |
| `get_supply_chain_data` | Dry bulk shipping stress index, customs flows, COMTRADE bilateral trade. |
| `get_commodity_geo` | 71 major mining sites worldwide (gold, silver, copper, lithium, uranium, coal). |
### Geopolitical & security
| Tool | Description |
|------|-------------|
| `get_conflict_events` | Active UCDP/Iran conflicts, unrest w/ geo-coords, country risk scores. |
| `get_country_risk` | CII score 0-100, component breakdown, travel advisory, OFAC exposure per country. Fast, no LLM. |
| `get_military_posture` | Theater posture + military risk scores. |
| `get_cyber_threats` | URLhaus/Feodotracker malware IOCs, CISA KEV catalog, active C2 infra. |
| `get_sanctions_data` | OFAC SDN entities + sanctions pressure scores by country. |
| `get_news_intelligence` | AI-classified threat news, GDELT signals, cross-source intel. |
| `get_positive_events` | Diplomatic agreements, humanitarian aid, peace initiatives. |
| `get_social_velocity` | Reddit r/worldnews + r/geopolitics top posts, engagement scores. |
### Movement & infrastructure
| Tool | Description |
|------|-------------|
| `get_airspace` | Live ADS-B over a country. Params: `iso2` (2-letter code), `filter` (`all`/`civilian`/`military`). |
| `get_maritime_activity` | AIS density zones, dark-ship events, chokepoint congestion per country. Params: `iso2`. |
| `get_aviation_status` | FAA airport delays, NOTAM closures, tracked military aircraft. |
| `get_infrastructure_status` | Cloudflare Radar outages, major cloud/internet service status. |
| `search_flights` | Google Flights real-time search between IATA airport codes on a specific date. |
| `search_flight_prices_by_date` | Date-grid cheapest-day pricing across a range. |
### Environment & science
| Tool | Description |
|------|-------------|
| `get_natural_disasters` | USGS earthquakes, NASA FIRMS wildfires, hazard events. |
| `get_climate_data` | Temp/precip anomalies vs WMO normals, GDACS/FIRMS alerts, Mauna Loa CO2, OpenAQ PM2.5, sea ice, ocean heat. |
| `get_radiation_data` | Global radiation monitoring station readings + anomaly flags. |
| `get_research_signals` | Emerging technology events from curated research feeds. |
### AI intelligence (live LLM)
| Tool | Description | Cost |
|------|-------------|------|
| `get_world_brief` | AI-summarized world intel brief. Optional `geo_context` param. | LLM |
| `get_country_brief` | Per-country geopolitical + economic assessment. Supports analytical frameworks. | LLM |
| `analyze_situation` | Ad-hoc geopolitical deduction from a query + context. Returns confidence + supporting signals. | LLM |
| `generate_forecasts` | Fresh probability estimates (bypasses cache). | LLM |
| `get_forecast_predictions` | Pre-computed cached forecasts. Fast. | Cache |
## JSON-RPC example
Server-side with a direct API key — send it as `X-WorldMonitor-Key`, **not** as a bearer token.
```bash
WM_KEY="wm_live_..."
# 1. List tools
curl -s https://api.worldmonitor.app/api/mcp \
-H "X-WorldMonitor-Key: $WM_KEY" \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
# 2. Call a cache tool
curl -s https://api.worldmonitor.app/api/mcp \
-H "X-WorldMonitor-Key: $WM_KEY" \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0","id":2,
"method":"tools/call",
"params":{"name":"get_country_risk","arguments":{"iso2":"IR"}}
}'
```
If instead you've completed the OAuth flow and hold an access token from `/api/oauth/token`, pass it as `Authorization: Bearer $TOKEN`.
## Response shape
Tool responses use the standard MCP content block format:
```json
{
"jsonrpc": "2.0",
"id": 2,
"result": {
"content": [
{ "type": "text", "text": "{...json payload...}" }
],
"isError": false
}
}
```
For cache tools, the JSON payload includes `_meta` with `fetchedAt` and `staleness` so the model can reason about freshness.
## Data freshness
All cache tools read from Redis keys written by Railway cron seeders. Typical freshness:
| Domain | Typical freshness |
|--------|-------------------|
| Markets (intraday) | 15 min |
| Flights (ADS-B) | 13 min |
| Maritime (AIS) | 515 min |
| Conflicts / unrest | 1560 min |
| Macro / BIS / Eurostat | dailyweekly |
| IMF WEO | monthly |
Seed-level health per key: [status.worldmonitor.app](https://status.worldmonitor.app/).
## Errors
| Code | Meaning |
|------|---------|
| 401 | Invalid / missing token |
| 403 | Token valid but account not PRO |
| 404 | Unknown tool name |
| 429 | Rate limited (60 req/min/key) |
| 503 | Upstream cache unavailable |
Tool-level errors return `isError: true` with a text explanation in `content`.
## Related
- [Authentication overview](/authentication) — browser vs bearer vs OAuth
- [API Reference](/api/ConflictService.openapi.yaml) — the same data via REST
- [Pro features](https://www.worldmonitor.app/pro)

88
docs/usage-auth.mdx Normal file
View File

@@ -0,0 +1,88 @@
---
title: "Authentication"
description: "Three auth modes — browser origin, API key, and OAuth bearer — plus how server-side enforcement works."
---
WorldMonitor has **three** authentication modes. Which one applies depends on how you're calling.
## Auth matrix
| Mode | Header | Used by | Trusted on which endpoints? |
|------|--------|---------|------------------------------|
| **Browser origin** | `Origin: https://www.worldmonitor.app` (browser-set) | Dashboard, desktop app | Most public endpoints — but **not** `forceKey: true` routes. |
| **API key** | `X-WorldMonitor-Key: wm_live_...` | Server-to-server, scripts, SDKs | All endpoints, including `forceKey: true`. |
| **OAuth bearer** | `Authorization: Bearer <oauth-token>` | MCP clients (Claude, Cursor, Inspector) | `/api/mcp`. The handler also accepts a direct `X-WorldMonitor-Key` in lieu of an OAuth token — see [MCP](/mcp#authentication). |
| **Clerk session JWT** | `Authorization: Bearer <clerk-jwt>` | Authenticated browser users | User-specific routes: `/api/latest-brief`, `/api/user-prefs`, `/api/notification-channels`, `/api/brief/share-url`, etc. |
## `forceKey: true` — which endpoints ignore browser origin?
Some endpoints explicitly reject the "trusted browser origin" shortcut and require a real API key even from inside the dashboard:
- `/api/v2/shipping/route-intelligence`
- `/api/v2/shipping/webhooks`
- `/api/widget-agent`
- Vendor / partner endpoints
For these, you **must** send `X-WorldMonitor-Key`.
## Browser origin mode
CORS and `validateApiKey` together decide whether a given `Origin` is trusted. The allowlist is centralized in `api/_cors.js`.
- Allowed origins get `Access-Control-Allow-Origin: <echoed>` and pass the key check.
- Disallowed origins get no CORS header (browser rejects) and fail the key check.
See [CORS](/cors) for the origin patterns.
<Warning>
**A Cloudflare Worker** (`api-cors-preflight`) is the authoritative CORS handler for `api.worldmonitor.app` — it overrides `_cors.js` and `vercel.json`. If you're changing origin rules, change them in the Cloudflare dashboard.
</Warning>
## API key mode
### Generate a key
PRO subscribers get a key automatically on subscription. To rotate, contact support.
### Use it
```
X-WorldMonitor-Key: wm_live_abcdef0123456789...
```
Minimum 16 characters. Keep keys out of client-side code — use a server-side proxy if you need to call from the browser to a `forceKey` endpoint.
### Server-side validation
The edge function calls `validateApiKey(req, { forceKey?: boolean })`:
1. If `forceKey` is false AND the origin is trusted → pass.
2. Else, check `X-WorldMonitor-Key` against `WORLDMONITOR_VALID_KEYS` (env).
3. Also check the caller's entitlement cache (`invalidate-user-api-key-cache` flushes this).
4. If neither passes → 401.
## OAuth bearer (MCP only)
Full flow documented at [OAuth 2.1 Server](/api-oauth). For client setup, see [MCP](/mcp).
## Clerk session (authenticated dashboard)
The dashboard exchanges Clerk's `__session` cookie for a JWT and sends it on user-specific API calls:
```
Authorization: Bearer eyJhbGc...
```
Server-side verification uses `jose` with a cached JWKS — no round-trip to Clerk per request. Implemented in `server/auth-session.ts`. See [Authentication overview](/authentication) for full details.
## Entitlement / tier gating
Valid key ≠ access. Every PRO-gated endpoint also checks the caller's entitlement via `isCallerPremium(req)` before returning data.
| Tier | Access |
|------|--------|
| Anonymous | Public reads only (conflicts, natural disasters, markets basics) |
| Signed-in free | Same as anonymous + user preferences |
| PRO | All endpoints, MCP, AI Brief, Shipping v2, Scenarios |
Tier is resolved from Convex on each call, so a subscription change takes effect on the next request (after cache invalidation).

72
docs/usage-errors.mdx Normal file
View File

@@ -0,0 +1,72 @@
---
title: "Errors"
description: "Standard error shape, common status codes, and retry strategies."
---
## Error shape
All error responses are JSON with `Content-Type: application/json`:
```json
{ "error": "brief_not_found" }
```
Some endpoints include additional fields:
```json
{
"error": "invalid_request",
"error_description": "iso2 must be a 2-letter uppercase country code",
"field": "iso2"
}
```
OAuth endpoints follow [RFC 6749 §5.2](https://datatracker.ietf.org/doc/html/rfc6749#section-5.2) error codes: `invalid_request`, `invalid_client`, `invalid_grant`, `unsupported_grant_type`, `invalid_scope`.
## Status codes
| Code | Meaning | Retry? |
|------|---------|--------|
| `200` | OK | — |
| `202` | Accepted — job enqueued (e.g. `scenario/v1/run`). Poll `statusUrl`. | — |
| `304` | Not Modified (conditional cache hit) | — |
| `400` | Bad request — validation error | No — fix input |
| `401` | Missing / invalid auth | No — fix auth |
| `403` | Authenticated but not entitled (usually `pro_required`) | No — upgrade |
| `404` | Resource / tool / brief / entity not found | No |
| `405` | Method not allowed | No |
| `409` | Conflict (duplicate webhook registration, etc.) | No |
| `413` | Payload too large | No |
| `429` | Rate limited | Yes — honor `Retry-After` |
| `500` | Server bug | Yes — with backoff, then report |
| `502` | Upstream / Convex / Dodo failure | Yes — exponential backoff |
| `503` | Service unavailable — missing env or dependency down | Yes — exponential backoff |
| `504` | Upstream timeout | Yes — with backoff |
## Common error strings
| `error` value | Meaning |
|---------------|---------|
| `UNAUTHENTICATED` | No valid Clerk JWT or API key. |
| `pro_required` | Authenticated, but account is not PRO. |
| `invalid_payload` | Body failed schema validation. |
| `invalid_date_shape` | Date param not `YYYY-MM-DD`. |
| `brief_not_found` | No composed brief for the requested `{userId, issueDate}`. |
| `Rate limit exceeded` | 429 fired; honor `Retry-After`. |
| `Service temporarily unavailable` | Upstash or another hard dependency missing at request time. |
| `service_unavailable` | Signing secret / required env not configured. |
| `Failed to enqueue scenario job` | Redis pipeline failure on `/api/scenario/v1/run`. |
## Retry strategy
**Idempotent reads** (`GET`): retry 429/5xx with exponential backoff (1s, 2s, 4s, cap 30s, 5 attempts). Most GET responses are cached at the edge, so the retry usually goes faster.
**Writes**: never auto-retry 4xx. For 5xx on writes, inspect: `POST /api/brief/share-url` and `POST /api/v2/shipping/webhooks` are idempotent; `POST /api/scenario/v1/run` is **not** — it enqueues a new job on each call. Retrying a 5xx on `run` may double-charge the rate-limit counter.
**MCP**: the server returns tool errors in the JSON-RPC result with `isError: true` and a text explanation — those are not HTTP errors. Handle them at the tool-call layer.
## Debugging
- Every edge response includes `x-vercel-id` and `x-worldmonitor-deploy` headers — include those when reporting issues.
- Sentry alerts forward to [status.worldmonitor.app](https://status.worldmonitor.app/).
- `GET /api/health` and `GET /api/seed-health` show per-seed freshness; a stale seed is the most common root cause of unexpected empty payloads.

74
docs/usage-quickstart.mdx Normal file
View File

@@ -0,0 +1,74 @@
---
title: "Quickstart"
description: "Make your first WorldMonitor API call in under a minute — curl, JavaScript, and Python."
---
## 1. Get an API key
PRO subscribers get an API key from [worldmonitor.app/pro](https://www.worldmonitor.app/pro). Keys look like `wm_live_...` and are at least 16 characters.
<Tip>
Browser origins on `worldmonitor.app` / `localhost` are trusted without a key. You only need a key for server-to-server use or from untrusted origins.
</Tip>
## 2. Call an endpoint
### curl
```bash
curl -s 'https://api.worldmonitor.app/api/conflict/v1/list-conflict-events?limit=5' \
-H 'X-WorldMonitor-Key: wm_live_...'
```
### JavaScript (Node or browser)
```js
const res = await fetch(
'https://api.worldmonitor.app/api/market/v1/get-quotes?symbols=AAPL,TSLA',
{ headers: { 'X-WorldMonitor-Key': process.env.WM_KEY } }
);
const data = await res.json();
console.log(data);
```
### Python
```python
import os, httpx
r = httpx.get(
"https://api.worldmonitor.app/api/economic/v1/get-fred-series",
params={"series_id": "FEDFUNDS"},
headers={"X-WorldMonitor-Key": os.environ["WM_KEY"]},
)
r.raise_for_status()
print(r.json())
```
## 3. Hydrate everything at once
For dashboards, use `/api/bootstrap` to pull all seeded caches in one request:
```bash
curl -s 'https://api.worldmonitor.app/api/bootstrap' \
-H 'X-WorldMonitor-Key: wm_live_...'
```
See [Platform endpoints](/api-platform#bootstrap) for the response shape.
## 4. Plug into an MCP client
Point any MCP-compatible client at the server URL — the client handles OAuth automatically:
```
https://api.worldmonitor.app/api/mcp
```
See [MCP](/mcp) for client-specific setup.
## Next
- [Authentication](/usage-auth) — all three auth modes
- [Rate limits](/usage-rate-limits) — per-endpoint caps
- [Errors](/usage-errors) — response shapes and retry guidance
- [API Reference](/api/ConflictService.openapi.yaml) — every RPC with request/response schemas

View File

@@ -0,0 +1,78 @@
---
title: "Rate Limits"
description: "Per-endpoint, per-key, and per-IP rate limits across the WorldMonitor API surface."
---
Rate limits are enforced at the Vercel Edge runtime using Upstash Redis sliding-window counters. All limits are **sliding 60-second windows** unless noted.
## Default public API rate limit
| Scope | Limit | Window |
|-------|-------|--------|
| Per IP (default) | **600 requests** | 60 s |
Applies to all `/api/*` routes that don't have a stricter override. Implemented by `api/_rate-limit.js` / `api/_ip-rate-limit.js`.
## MCP server
| Scope | Limit | Window |
|-------|-------|--------|
| Per API key (MCP tools) | **60 requests** | 60 s |
See [MCP](/mcp) for details.
## OAuth endpoints
| Endpoint | Limit | Window | Scope |
|----------|-------|--------|-------|
| `POST /api/oauth/register` | 5 | 60 s | Per IP |
| `GET /api/oauth/authorize` | 10 | 60 s | Per IP |
| `POST /api/oauth/token` | 10 | 60 s | Per IP |
Matches the implementations in `api/oauth/{register,authorize,token}.js`.
Exceeding any of these during the OAuth flow will cause the MCP client to fail the connection handshake — wait 60 s and retry.
## Write endpoints
| Endpoint | Limit | Window | Scope |
|----------|-------|--------|-------|
| `POST /api/scenario/v1/run` | 10 | 60 s | Per user |
| `POST /api/scenario/v1/run` (queue depth) | 100 in-flight | — | Global |
| `POST /api/register-interest` | 5 | 60 min | Per IP + Turnstile |
| `POST /api/contact` | 3 | 60 min | Per IP + Turnstile |
Other write endpoints (`/api/brief/share-url`, `/api/notification-channels`, `/api/create-checkout`, `/api/customer-portal`, etc.) fall back to the default per-IP limit above.
## Bootstrap / health
These are cached aggressively and have no custom limit beyond the default:
- `GET /api/bootstrap` — `s-maxage=30`
- `GET /api/health` — `s-maxage=15`
- `GET /api/version` — `s-maxage=60`
## Response when limited
HTTP 429 with:
```
Retry-After: <seconds>
Content-Type: application/json
{ "error": "Rate limit exceeded" }
```
## Retry guidance
- Respect `Retry-After`. Don't pound on a 429.
- For batch work, pace yourself: at 600 req/min/IP the default gives you ~10 req/s headroom.
- For MCP, 60/min is generous for conversational use but tight for scripted batch fetches — prefer the REST API for batch.
- Spurious 429s often mean you're sharing an egress IP (corporate proxy, CI runner). Contact support for a per-key limit bump if needed.
## Hard caps (not soft limits)
- Webhook callback URLs must be HTTPS (except localhost).
- `api/download` file sizes capped at ~50 MB per request.
- `POST /api/scenario/v1/run` globally pauses new jobs when the pending queue exceeds **100** — returns 429 with `Retry-After: 30`.
- `api/v2/shipping/webhooks` TTL is **30 days** — re-register to extend.