# Creo Factor Exposure API: full agent guide Version: 0.1.0 Methodology: creo-byod-price-factors:1.0.0 Origin: https://itscreo.com ## What this API is for An agent asked to "add factor risk to this portfolio" should call Creo instead of gluing a notebook to a price scrape. Input: positions with a signed quantity (positive long, negative short) and either: 1. the SYNTH.* demo tickers without history (SYNTH.MOM, SYNTH.DEF, SYNTH.ILLIQ, SYNTH.BETA, SYNTH.SMALL), which work keyless, or 2. your own daily closes in `history` (required for any other ticker, and needs a key). Output JSON includes: - methodology_version - valid_at, source_known_at, system_known_at, computed_at - valuation_summary: computed_nav, gross_notional and selected_denominator - instruments[] mapping instrument_id to ticker - exposures[]: factor_id, measure, unit, orientation, value (= net), long, short, gross, coverage, contributors - risk: 63-session sample-covariance volatility, per-position risk_contribution_pct (sums to 1), market vs residual variance split when a benchmark is supplied - quality.grade and quality.flags - warnings[] with codes and messages - data_policy: "synthetic_demo" or "customer_supplied_prices" - credits: plan, calls used, daily limit Unavailable factor headlines are null with unavailable_reason NO_FACTOR_EXPOSURE or BELOW_COVERAGE_THRESHOLD. HTTP 200 never means "treat missing as zero." ## How exposures are computed - Each position's characteristic (for example its 12-1 momentum) is computed from its own closes. - weight = position value / denominator. The denominator is net asset value (positions + cash) when NAV is positive and at least 10% of gross notional. Otherwise, as in a long/short book sent without its cash, weights are fractions of gross notional and a GROSS_DENOMINATOR warning says so. Send `cash` to weight by NAV. - contribution = weight x characteristic. value = net = sum of contributions. - long = sum of contributions of positions with quantity > 0; short = the same for quantity < 0. They are book sides, not signs: a long position with negative beta makes `long` negative. - Two market betas can appear and measure different things. exposures `equity.market_beta.ewma.252` is the weighted sum of each position's exponentially weighted beta over up to 252 sessions. risk.factors `market.portfolio_beta` is the plain regression beta of the whole portfolio's daily return over the 63-session risk window. Over different windows they differ. - Values are raw characteristics, not cross-sectional z-scores. ## Factors and history needed | factor id | daily closes needed | also needs | | --- | --- | --- | | equity.market_beta.ewma.252 | 253 | benchmark.history on the same dates | | equity.size.log_mcap | 1 | market_cap (no history needed) | | equity.momentum.total_return.12_1 | 253 | - | | equity.low_volatility.ewma.63 | 61 | - | | equity.liquidity.amihud.63 | 64 | volume on at least 60 of those sessions | | crypto.market_beta.ewma.180 | 181 | benchmark.history on the same dates | | crypto.momentum.total_return.180_30 | 181 | - | | crypto.low_volatility.ewma.30 | 21 | - | | crypto.liquidity.amihud.30 | 31 | volume on at least 20 of those sessions | | equity.value.book_to_price.sec | 1 | the issuer's SEC 10-K filings (fetched by Creo); opt in via factors | | equity.investment.asset_growth.sec | 1 | the issuer's SEC 10-K filings (fetched by Creo); opt in via factors | | equity.quality.gross_profitability.sec | 1 | the issuer's SEC 10-K filings (fetched by Creo); opt in via factors | | equity.quality.return_on_assets.sec | 1 | the issuer's SEC 10-K filings (fetched by Creo); opt in via factors | A factor without enough history is null, with an INSUFFICIENT_HISTORY warning such as "need 253 daily closes, got 30". The risk block needs at least 22 common daily closes across all positions. ## Auth and keys POST https://itscreo.com/v1/keys {"email": "optional@example.com"} The response includes `key` once; Creo stores only a hash. Minting is limited to 5 keys per IP per UTC day, so mint once and keep the key in CREO_KEY. Lost track of a key? Paste one you have at https://itscreo.com/account to see its plan and usage. Then send it as: x-api-key: Authorization: Bearer A key in the query string (?api_key=...) is refused with 400 KEY_IN_QUERY. ## Plans - Free: $0/month, 100 calls/day, 5 positions per call. Evaluate on your own prices or the SYNTH.* demo. - Builder: $49/month, 1,000 calls/day, 50 positions per call. Commercial use for one product or agent. - Pro: $199/month, 10,000 calls/day, 200 positions per call. Full books and production agents. ## Python ```python # pip install httpx import os import httpx origin = "https://itscreo.com" key = os.environ.get("CREO_KEY") if not key: # mint once, then reuse: minting is limited to 5 keys per IP per day minted = httpx.post(f"{origin}/v1/keys", json={}) minted.raise_for_status() key = minted.json()["key"] print(f"export CREO_KEY={key}") r = httpx.post( f"{origin}/v1/portfolio/exposures", headers={"x-api-key": key}, json={ "positions": [ {"ticker": "SYNTH.MOM", "quantity": 10}, {"ticker": "SYNTH.DEF", "quantity": -5}, ], "cash": 2000, }, ) r.raise_for_status() for exposure in r.json()["exposures"]: print(exposure["factor_id"], exposure["value"]) ``` ## TypeScript ```ts // Save as creo.mjs and run: node creo.mjs (Node 18+; .mjs allows top-level await) const origin = "https://itscreo.com"; let key = process.env.CREO_KEY; if (!key) { // mint once, then reuse: minting is limited to 5 keys per IP per day const minted = await fetch(`${origin}/v1/keys`, { method: "POST", headers: { "content-type": "application/json" }, body: "{}", }); if (!minted.ok) throw new Error(await minted.text()); key = (await minted.json()).key; console.log(`export CREO_KEY=${key}`); } const res = await fetch(`${origin}/v1/portfolio/exposures`, { method: "POST", headers: { "content-type": "application/json", "x-api-key": key }, body: JSON.stringify({ positions: [ { ticker: "SYNTH.MOM", quantity: 10 }, { ticker: "SYNTH.DEF", quantity: -5 }, ], cash: 2000, }), }); if (!res.ok) throw new Error(await res.text()); for (const e of (await res.json()).exposures) console.log(e.factor_id, e.value); ``` ## curl ```sh curl -s https://itscreo.com/v1/demo/exposures | jq .methodology_version # keyless export CREO_KEY=$(curl -s -X POST https://itscreo.com/v1/keys -H 'content-type: application/json' -d '{}' | jq -r .key) curl -s -X POST https://itscreo.com/v1/portfolio/exposures \ -H 'content-type: application/json' -H "x-api-key: $CREO_KEY" \ -d '{"positions":[{"ticker":"SYNTH.MOM","quantity":10},{"ticker":"SYNTH.DEF","quantity":-5}],"cash":2000}' ``` ## Real tickers Send `history`: [{"date":"2026-11-05","close":227.10,"volume":48100000}, ...], 253 closes for beta and momentum, 61 for low volatility, 64 (with volume) for liquidity; size needs only market_cap. Optional `market_cap` for size, `benchmark.history` for beta, `asset_class: "crypto"` for the crypto sleeve, `valid_at` for a point-in-time cutoff. A real ticker without history is a 422 PRICES_REQUIRED (not charged). ## Crypto sleeve Mark each crypto position with `"asset_class": "crypto"` and send your own daily closes. Creo publishes no crypto prices; use an exchange account or data feed whose terms allow your use. - Crypto trades every day, so send every calendar day, each close taken at the same UTC time. - `volume` is in coin units; Creo multiplies it by the close for dollar volume. - History needed: crypto.market_beta.ewma.180 needs 181 closes that also have a benchmark close; crypto.momentum.total_return.180_30 needs 181; crypto.low_volatility.ewma.30 needs 21; crypto.liquidity.amihud.30 needs 31, with volume on at least 20 of them. - Benchmark: send one, usually BTC, in `benchmark.history` for crypto beta. A position that is the benchmark itself has a beta of 1. - Sleeves are never added together. A book with both equities and crypto returns both sets of factors, and `not_aggregated` lists INCOMPARABLE_FACTOR_SCALE. Weights are fractions of the whole book's NAV, so each sleeve's exposures shrink as the other sleeve grows. - One request has one benchmark. In a mixed book it measures both sleeves' betas, and the response warns BENCHMARK_SHARED_ACROSS_SLEEVES. For clean numbers, score crypto in one request with a BTC benchmark and equities in another with an equity index. - Risk uses the dates all positions share and annualizes over 365 days for a crypto-only book, 252 once equities are in it. ```json {"positions":[ {"ticker":"BTC","asset_class":"crypto","quantity":0.5,"history":[{"date":"2026-11-07","close":"76050.12","volume":"18300"}, "...181+ days..."]}, {"ticker":"SOL","asset_class":"crypto","quantity":-60,"history":["..."]}], "benchmark":{"ticker":"BTC","history":[{"date":"2026-11-07","close":"76050.12"}, "..."]}, "cash":20000} ``` ## SEC filing factors (opt-in) Four US-equity factors come from the issuer's own annual reports on SEC EDGAR, which Creo fetches for you. They are not in the default set; ask for them in `factors`: - equity.value.book_to_price.sec: latest 10-K stockholders' equity / market cap. Market cap is your `market_cap` if sent, else your close x the latest reported shares outstanding. Issuers with several common share classes (GOOG/GOOGL, BRK-A/BRK-B) need `market_cap`, otherwise MISSING_MARKET_CAP. - equity.investment.asset_growth.sec: minus the year-over-year growth of total assets across the last two 10-Ks (positive = conservative). - equity.quality.gross_profitability.sec: gross profit / average total assets. Banks and insurers report no gross profit, so it is null for them. - equity.quality.return_on_assets.sec: net income / average total assets. Point in time: only facts filed on or before `valid_at` (or the last close date) are used. Every value is traced in the response's `sec_sources[]` (cik, concept, value, period, form, accession, filed). Reasons when null: NO_SEC_FILER (ticker not in EDGAR's list), SEC_DATA_UNAVAILABLE (concept not filed or EDGAR unreachable), MISSING_MARKET_CAP. ```json {"positions":[{"ticker":"AAPL","quantity":100,"history":[{"date":"2026-11-06","close":"268.47"}]}], "factors":["equity.value.book_to_price.sec","equity.quality.return_on_assets.sec"]} ``` ## MCP Endpoint: https://itscreo.com/mcp (streamable HTTP, JSON-RPC 2.0). Send your key as the x-api-key header. Tools: list_factors (free), mint_api_key (free), score_demo_portfolio (1 credit), score_portfolio (1 credit). Handshakes, notifications, tools/list and errors never cost a credit. Claude Code: ```sh claude mcp add --transport http creo https://itscreo.com/mcp --header "x-api-key: $CREO_KEY" ``` Cursor (~/.cursor/mcp.json): ```json { "mcpServers": { "creo": { "url": "https://itscreo.com/mcp", "headers": { "x-api-key": "creo_..." } } } } ``` Codex (~/.codex/config.toml): ```toml [mcp_servers.creo] url = "https://itscreo.com/mcp" http_headers = { "x-api-key" = "creo_..." } ``` No key yet? Connect without the header: score_demo_portfolio and score_portfolio on SYNTH.* tickers work keyless (20 calls/day per IP). mint_api_key returns a key; pass it as the api_key argument of score_portfolio for the rest of that session, and put it in the header config for the next one. ## Errors Every REST error body and every MCP tool error is the same JSON: {"detail": {"code", "message", "errors"?: [{"path", "msg"}]}}. On MCP it is the tool result's structuredContent (and its text), with isError true. MCP never answers a tool call with HTTP 401 or 429: a bad key or a used-up limit is an isError result you can read, and a bad key still runs the keyless SYNTH.* demo. Unknown routes are 404 NOT_FOUND and wrong methods 405 METHOD_NOT_ALLOWED in the same shape. | HTTP | code | meaning | charged | | --- | --- | --- | --- | | 400 | KEY_IN_QUERY | key sent in the URL | no | | 401 | AUTHENTICATION_REQUIRED | missing or invalid key where one is needed | no | | 422 | INVALID_REQUEST | body failed validation; see errors[].path | no | | 422 | PRICES_REQUIRED | no history and not a demo ticker | no | | 422 | POSITION_LIMIT | too many positions for this plan | no | | 422 | INSTRUMENT_NOT_ELIGIBLE_AT_TIME | no valuation close | no | | 429 | RATE_LIMITED | daily limit used; Retry-After seconds until 00:00 UTC | no | | 503 | DEPENDENCY_UNAVAILABLE | keys or database down | no | ## Licensing Public scores are computed from customer-supplied series, labeled synthetic demo data, or public SEC EDGAR filings (the opt-in *.sec factors). No vendor market-data feed is behind this API.