Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: RiskModels Discovery API
version: 3.0.0-agent
description: 'POST /decompose returns variance shares and ETF hedge ratios for any US equity, separating factor exposure from the residual bet so you can hedge what you don''t want. Daily history to 2000 across ~3,000 US equities.
Precision equity risk intelligence platform with AI agent integration.
**New in v3.0.0-agent:**
- OAuth2 client credentials flow for machine-to-machine auth
- Plaid Investments integration for live portfolio sync
- MCP (Model Context Protocol) server for AI agent tools
- Compliance manifests for AI marketplace integration
- Per-key rate limiting and scoped access control
**Data**: Universe `uni_mc_3000` (top ~3,000 US stocks by market cap), date range 2000 to present (deep panel; stock-specific skill series from ~2001 after warmup), updated daily end-of-day. Model outputs are computed by the ERM3 regression system (Huber/Ridge), with Zarr v2 archives on Google Cloud Storage for research pipelines. The live API reads primarily from Supabase **V3** tables: long-form `security_history` (returns, volatility, L1/L2/L3 hedge ratios and explained-risk metrics), wide latest rows in `security_history_latest`, identity in `symbols`, plus supporting surfaces such as `trading_calendar`, `erm3_landing_chart_cache`, and `macro_factors`. Cross-sectional **rankings** (`GET /rankings/*`) are served from precomputed rank metric keys in `security_history`, not a separate `erm3_rankings` table. Hedge ratios (`*_hr`) are **dollar_ratio** ETF notionals per $1 stock and are distinct from regression **betas** (`l1_mkt_beta`, `l2_sec_beta`, `l3_sub_beta`), which are dimensionless coefficients from the hierarchical L1/L2/L3 regression. Both are exposed under `MetricsV3` and on `security_history_latest`. See `SEMANTIC_ALIASES.md` for the full HR-vs-beta convention.
**Authentication Methods:**
1. Session cookies (web app)
2. API Key/Secret via Bearer token (external integrations)
3. OAuth2 client credentials (AI agents, npm package)
**Security Features:**
- GCP KMS envelope encryption for portfolio data
- Cryptographic shredding on key deletion
- HMAC-SHA256 webhook verification
- Row-level security (RLS) via Supabase
**Pricing**: Per-request billing with prepaid balance. Cached responses are free. Many metered responses include `_agent.cost_usd` in the JSON body; others bill silently — always check the `X-API-Cost-USD` response header when present. Metered operations also expose `x-pricing` (OpenAPI extension) with `capability_id`, `tier` (`baseline` | `premium`), `model`, `cost_usd`, `billing_code`, and optional `min_charge` / per-token fields — aligned with `GET /pricing` and `lib/agent/capabilities.ts`.
**Lineage**: JSON data responses include `_metadata` (model version, data_as_of, factor set) and headers `X-Risk-Model-Version`, `X-Data-As-Of`, `X-Factor-Set-Id`, `X-Universe-Size`. Parquet and CSV bodies omit `_metadata`; use the same `X-Risk-*` headers on the HTTP response.
**Tabular exports (Parquet / CSV):** For bulk historical loads, time-series endpoints accept `format=parquet` or `format=csv`, or (when `format` is omitted) `Accept: application/vnd.apache.parquet` or `Accept: text/csv`. Successful responses use `Content-Type: application/vnd.apache.parquet` or `text/csv; charset=utf-8` and `Content-Disposition: attachment` with a descriptive filename. Each export is a single flat table; column names and types match the per-operation schemas referenced in the response media type descriptions (aligned with JSON row objects or zipped parallel arrays). Parquet is produced via schema inference from the first row (UTF8 strings, DOUBLE floats, INT64 integers, BOOLEAN, TIMESTAMP_MILLIS for JavaScript Date values). CSV uses a header row, comma separators, UTF-8, and RFC 4180-style quoting when needed. Prefer Parquet for typed columnar stacks (pandas, Polars, Arrow); use CSV for spreadsheets and lightweight scripting.'
contact:
name: RiskModels Support
email: service@riskmodels.app
url: https://riskmodels.app
termsOfService: https://riskmodels.app/legal
license:
name: Commercial
url: https://riskmodels.app/legal
servers:
- url: https://riskmodels.app/api
description: Production — all metered endpoints (`/metrics`, `/snapshot`, `/decompose`, `/batch/analyze`, `/portfolio/*`, etc.).
- url: https://riskmodels.app
description: Discovery origin — `/.well-known/*` manifests (`mcp.json`, `ai-plugin.json`, `agentic-disclosure.json`) live at the site origin without the `/api` prefix. Generated clients that hard-code the first server entry must strip `/api` before requesting these paths.
security:
- BearerAuth: []
tags:
- name: Discovery
description: Service discovery and capability manifests.
paths:
/.well-known/ai-plugin.json:
get:
summary: OpenAI GPT Plugin Manifest
description: 'Plugin manifest for OpenAI GPT Store integration. Enables ChatGPT and other OpenAI models to discover and use the RiskModels API.
**Discovery URL:** Served at the **site origin** (`https://riskmodels.app/.well-known/ai-plugin.json`), not under `/api`. Generated clients that combine the first `servers[0].url` with this path must strip `/api` first.'
operationId: getAIPluginManifest
servers:
- url: https://riskmodels.app
description: Site origin (discovery)
tags:
- Discovery
security: []
responses:
'200':
description: Plugin manifest
content:
application/json:
schema:
type: object
properties:
schema_version:
type: string
example: v1
name_for_human:
type: string
example: RiskModels
name_for_model:
type: string
example: riskmodels
description_for_human:
type: string
description_for_model:
type: string
auth:
type: object
properties:
type:
type: string
enum:
- service_http
authorization_type:
type: string
enum:
- bearer
api:
type: object
properties:
type:
type: string
enum:
- openapi
url:
type: string
format: uri
logo_url:
type: string
format: uri
contact_email:
type: string
format: email
/.well-known/mcp.json:
get:
summary: MCP Server Manifest
servers:
- url: https://riskmodels.app
description: Site origin (discovery)
description: 'Model Context Protocol server connection manifest. Enables AI model clients to discover and connect to the RiskModels MCP server.
**Discovery URL:** Served at the **site origin** (`https://riskmodels.app/.well-known/mcp.json`), not under `/api`.'
operationId: getMCPManifest
tags:
- Discovery
security: []
responses:
'200':
description: MCP server manifest
content:
application/json:
schema:
type: object
properties:
mcpServers:
type: object
additionalProperties:
type: object
properties:
name:
type: string
description:
type: string
url:
type: string
format: uri
transport:
type: string
enum:
- sse
- stdio
auth:
type: object
capabilities:
type: object
tools:
type: array
items:
$ref: '#/components/schemas/MCPTool'
/artifacts/capability:
get:
summary: Artifact render capability
description: 'The verified (slug, subject_kind) pairs render-svc can serve, each slug''s applicable render params, and the pairs that were measured and refused.
Derived from the audited capability table on every request, so a pair that was never verified cannot be advertised. Answers "what renders for this subject?" before a client mounts anything — previously the only way to learn the answer was to request something that failed, because the reconciled table appeared solely inside error payloads.
**Authentication:** None. Discovery only: it names renderers and subject kinds and returns no subject content, no holdings and no numbers.
**Billing:** Free.'
operationId: getArtifactCapability
tags:
- Discovery
x-pricing:
capability_id: artifact-capability
tier: baseline
model: per_request
cost_usd: 0
currency: USD
billing_code: artifact_capability
security: []
parameters:
- name: subject_kind
in: query
required: false
schema:
type: string
enum:
- fund
- etf
- filer_13f
- cohort
- stock
- client_portfolio
description: 'Narrow `pairs` to one subject kind. `unavailable` is never narrowed: a caller needs to tell "measured and refused" from "never asked".
'
example: fund
- name: slug
in: query
required: false
schema:
type: string
description: Narrow `pairs` to one artifact slug.
example: cumulative_return_strip
responses:
'200':
description: 'Capability document. A narrowing query that matches nothing returns 200 with an empty `pairs` list, not 404.
'
content:
application/json:
schema:
$ref: '#/components/schemas/ArtifactCapabilityResponse'
'400':
description: Unknown subject_kind.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/artifacts/as-of:
get:
summary: Artifact pre-rendered vintages
description: 'The pre-rendered `as_of` dates for one (slug, subject_id), with the formats stored under each date and their object URIs. Read-only: no render is triggered by asking.
Pre-rendered subject kinds (filers, cohorts) have no live loader, so a caller must know which dates exist. For cohort subjects `as_of=latest` on the render call resolves to the newest date listed here; for filers pass one of these dates explicitly. Both spellings of a filer id are searched and merged.
**Authentication:** API key or session. Unmetered.
**Billing:** Free.'
operationId: getArtifactAsOf
tags:
- Discovery
x-pricing:
capability_id: artifact-as-of
tier: baseline
model: per_request
cost_usd: 0
currency: USD
billing_code: artifact_as_of_v1
parameters:
- name: slug
in: query
required: true
schema:
type: string
pattern: ^[a-z][a-z0-9_]*$
description: Artifact slug.
example: risk_dna_stacked
- name: subject_id
in: query
required: true
schema:
type: string
description: 'Subject id with a known prefix (BW-COHORT-, BW-FILER-, BW-FUND-, BW-STOCK-, BW-PORTFOLIO-, BW-ETF-).
'
example: BW-COHORT-RES-MAG7
- name: version
in: query
required: false
schema:
type: string
pattern: ^v\d+$
default: v1
description: Artifact version tag.
responses:
'200':
description: At least one vintage is pre-rendered.
content:
application/json:
schema:
$ref: '#/components/schemas/ArtifactAsOfResponse'
'400':
description: Missing or malformed slug, subject_id, or version.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'401':
description: Missing or invalid Bearer token.
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
'404':
description: 'Nothing pre-rendered. `slug_populated` in the body distinguishes an unbuilt slug (false) from an unknown subject on a populated slug (true).
'
content:
application/json:
schema:
$ref: '#/components/schemas/ErrorResponse'
/pricing:
get:
summary: Public capability pricing manifest
description: Machine-readable pricing for all registered capabilities (tier, model, `cost_usd`, token rates, `min_charge`, `billing_code`). Public; no authentication. Cached at the edge.
operationId: getPricingManifest
tags:
- Discovery
security: []
responses:
'200':
description: Pricing manifest JSON (`version`, `currency`, `tiers`, `endpoints`, `estimate_endpoint`, `docs`).
content:
application/json:
schema:
type: object
additionalProperties: true
components:
schemas:
ArtifactCapabilityResponse:
type: object
required:
- derived_from
- subject_kinds
- params
- pairs
- unavailable
- counts
properties:
derived_from:
type: string
description: The source constant the response is computed from.
example: lib/artifacts/render-client.ts::ARTIFACT_RENDER_CAPABILITY
discovery:
type: object
description: Routes to call before forming a render request.
properties:
capability:
type: string
example: GET /api/artifacts/capability?subject_kind=&slug=
as_of:
type: string
example: GET /api/artifacts/as-of?slug=&subject_id=&version=
subject_kinds:
type: array
items:
type: string
description: Every kind render-svc resolves from a subject_id prefix.
prerendered_subject_kinds:
type: array
items:
type: string
description: 'Kinds render-svc can only read from the store. See each pair''s `as_of_latest` for what `latest` does; `GET /api/artifacts/as-of` lists the dates that exist.
'
store_resolved_latest_kinds:
type: array
items:
type: string
description: 'Pre-rendered kinds whose `as_of=latest` resolves to the newest stored vintage (cohorts).
'
unimplemented_subject_kinds:
type: object
additionalProperties:
type: string
description: 'Kinds resolvable from an id prefix that no loader serves, keyed to the reason. Present so a zero-capability kind reads as measured rather than unaudited.
'
params:
type: object
properties:
vocabulary:
type: array
items:
type: string
description: Every key the render request schema accepts.
by_slug:
type: object
additionalProperties:
type: array
items:
type: string
description: 'Applicability per slug. A slug absent from this map accepts no params. Applicability is not the accepted value set — the artifact module narrows further and answers 422 on a value it does not take.
'
pairs:
type: array
description: Verified pairs only. Nothing else may appear here.
items:
type: object
required:
- slug
- version
- subject_kind
- params
- prerendered
properties:
slug:
type: string
version:
type: string
subject_kind:
type: string
params:
type: array
items:
type: string
prerendered:
type: boolean
as_of_latest:
type: string
enum:
- loader
- newest_prerendered
- unsupported
description: 'How `as_of=latest` resolves for this pair. `newest_prerendered` reads the newest date from `GET /api/artifacts/as-of`; `unsupported` means pass one of those dates explicitly.
'
notes:
type: string
unavailable:
type: array
description: 'Pairs measured against prod and refused, with the reason. Recorded rather than deleted so a later audit does not re-add them hopefully.
'
items:
type: object
required:
- slug
- subject_kind
- status
- reason
properties:
slug:
type: string
subject_kind:
type: string
status:
type: string
enum:
- degraded
- unavailable
reason:
type: string
counts:
type: object
properties:
verified:
type: integer
unavailable:
type: integer
slugs:
type: integer
ArtifactAsOfResponse:
type: object
required:
- slug
- version
- subject_id
- subject_kind
- as_of_latest
- latest
- as_of
- vintages
- count
properties:
slug:
type: string
example: risk_dna_stacked
version:
type: string
example: v1
subject_id:
type: string
description: The id as requested.
example: BW-COHORT-RES-MAG7
subject_kind:
type: string
example: cohort
canonical_subject_id:
type: string
description: The canonical spelling (differs from `subject_id` only for filers).
subject_id_spellings_searched:
type: array
items:
type: string
as_of_latest:
type: string
enum:
- loader
- newest_prerendered
- unsupported
description: 'What `as_of=latest` does on the render call for this subject kind: `newest_prerendered` (cohorts — the newest date below), `unsupported` (filers — pass a date from `as_of`), or `loader` (live data; this listing is informational).
'
latest:
type: string
format: date
description: Newest pre-rendered date.
example: '2026-04-21'
as_of:
type: array
description: Date-only view of `vintages`, oldest first.
items:
type: string
format: date
vintages:
type: array
items:
type: object
required:
- as_of
- formats
- params_variants
- gcs_path
properties:
as_of:
type: string
format: date
formats:
type: array
description: Formats stored for the default render (no params).
items:
type: string
example:
- json
- png
params_variants:
type: array
description: Params fragments stored alongside, e.g. `peer_n-5`.
items:
type: string
gcs_path:
type: object
description: Object URI per default-render format.
additionalProperties:
type: string
count:
type: integer
ErrorResponse:
type: object
properties:
error:
type: string
description: Machine-readable error code.
example: TICKER_NOT_FOUND
message:
type: string
description: Human-readable error description with suggested action.
example: Ticker 'XYZABC' not found in universe 'uni_mc_3000'. Check ticker spelling or try a different universe.
code:
type: integer
description: HTTP status code.
example: 404
history_starts:
type: string
format: date
description: 'Earliest non-null daily close or return date for the resolved security. Present only when a historical request ends before the security''s own history begins.
'
details:
type: object
properties:
field:
type: string
example: ticker
received:
type: string
example: XYZABC
universe:
type: string
example: uni_mc_3000
suggestion:
type: string
example: Check for typos or use ticker search endpoint
MCPTool:
type: object
description: 'MCP tool descriptor as returned by `tools/list`. Tools are discoverable via `https://riskmodels.app/.well-known/mcp.json` or by connecting to the SSE endpoint at `https://riskmodels.app/api/mcp/sse` and calling `tools/list`.
'
properties:
name:
type: string
enum:
- riskmodels_analyze_portfolio
- riskmodels_compare
- riskmodels_decompose
- riskmodels_decompose_selection_vs_drift
- riskmodels_get_benchmark_fit
- riskmodels_get_cohorts
- riskmodels_get_etf
- riskmodels_get_factor_returns
- riskmodels_get_filer
- riskmodels_get_fund
- riskmodels_get_fundamentals
- riskmodels_get_macro_correlation
- riskmodels_get_my_positions
- riskmodels_get_peers
- riskmodels_get_portfolio_risk_snapshot
- riskmodels_get_rankings
- riskmodels_get_residual_returns
- riskmodels_get_residual_signal
- riskmodels_get_return_attribution
- riskmodels_get_returns
- riskmodels_get_stock_snapshot
- riskmodels_portfolio_exposure
- riskmodels_render_artifact
- riskmodels_screen_rankings
- riskmodels_search
description: 'MCP tool names (tool set 2.0, `@riskmodels/mcp` 2.0.0). One registry (`registerRiskModelsTools`) serves all
three surfaces: the npm stdio server registers 24 tools; the hosted endpoints `/api/mcp/sse` and
`/api/mcp/openai` register the same 24 plus `riskmodels_render_artifact` (25). Every tool is read-only and
calls an existing REST route, billed at that route''s price; each description states when to use the tool,
what it returns, data freshness and price. The REST catalog, response schemas and this spec are MCP
resources (`riskmodels:///capabilities`, `riskmodels:///schemas/{path}`, `riskmodels:///openapi`).
Tool-to-route table and the 1.x migration table: mcp/README.md (Tool set 2.0).
'
description:
type: string
inputSchema:
type: object
description: JSON Schema describing the tool's input parameters.
outputSchema:
type: object
description: JSON Schema describing the tool's output structure.
securitySchemes:
BearerAuth:
type: http
scheme: bearer
bearerFormat: rm_agent_{environment}_{random}_{checksum}
description: 'API key in format `rm_agent_{live|test}_{random}_{checksum}` or `rm_user_{random}_{checksum}`. Obtain via `POST /api/auth/provision` or from riskmodels.app/settings → API Keys. Include as: `Authorization: Bearer rm_agent_live_...`
'
OAuth2AuthorizationCode:
type: oauth2
description: 'OAuth 2.0 authorization code flow with PKCE (S256), used by MCP clients (Claude Desktop, Cursor, ChatGPT Developer Mode, Grok) that self-register via `POST /api/oauth/register`. Public clients only — no `client_secret` is issued and `token_endpoint_auth_method` is `none`.
The access token this flow returns **is** an `rm_user_*` API key (1 h TTL, 30 d rotating refresh token), so it authenticates every `BearerAuth` operation below, not just `/api/mcp/sse`. Scope is currently informational: the API records the presented scope for telemetry but authorises by key validity and account balance, not by scope.
Discovery metadata: `/.well-known/oauth-authorization-server`.
'
flows:
authorizationCode:
authorizationUrl: https://riskmodels.app/oauth/authorize
tokenUrl: https://riskmodels.app/api/oauth/token
refreshUrl: https://riskmodels.app/api/oauth/token
scopes:
mcp:read: Read access to risk analytics (the only scope the authorization server advertises)