API v2 (alpha)

Pre-stable. /v2/portfolio-risk-state is at 2.0.0-alpha.5 and /v2/stress-scenario is at 2.0.0-alpha.2. Breaking changes may occur with 2-week notice until 2.0.0. Use v1 for production-stable workflows that don't need portfolio-level aggregation or stress.

The v2 surface adds portfolio-aware endpoints on top of the v1 per-asset engine. v1 answers "what's the policy for BTC right now?" — v2 answers "given my actual book of N positions, am I within risk and what would happen under a flash crash?"

Endpoints

EndpointUse case
POST /v2/portfolio-risk-stateAggregate up to 50 positions across BTC + ETH, evaluate per-position vs per-asset caps, compute correlation matrix and BTC-equivalent net, surface concentration warnings + stablecoin issuer/peg health.
POST /v2/stress-scenarioApply one of 6 deterministic tail templates (or a custom shock) to the same book and return baseline-vs-stressed side-by-side, position pnl, and a baseline-aware diff of triggered flags.

Both endpoints share the same positions[] request schema, the same Bearer auth, and write to the same audit-decisions blob store readable via GET /v2/audit/decisions.

Score freeze. Both endpoints inherit scoring_version: "score_v3" from v1. The score_v3 freeze is in effect until 2026-10-22 — no weight, normalisation, threshold, cap, or aggregation logic will change before then. v2 alpha changes are additive (new fields, clarified contracts) and respect the freeze.

Authentication

Identical to v1.

Authorization: Bearer <your_api_key>
TypeFormatRate limit
OwnerRISKSTATE_API_KEY env varUnlimited
Externalrs_live_ + 64 hex60 req/min sliding window

Request a key from the home page — only an email is required. Free during beta. Both v1 and v2 endpoints accept the same key.


POST /v2/portfolio-risk-state

Multi-position aggregation. Up to 50 positions, BTC + ETH only, with optional per-issuer stablecoin holdings.

POST https://riskstate.netlify.app/v2/portfolio-risk-state

Request

{
  "positions": [
    { "asset": "BTC", "size_usd": 250000, "side": "long" },
    { "asset": "ETH", "size_usd": 80000,  "side": "long" }
  ],
  "stablecoin_holdings_usd": { "USDT": 50000, "USDC": 50000 },
  "include_details": false
}
FieldTypeDefaultDescription
positions[]arrayrequired1–50 entries. Each { asset, size_usd, side, venue_type? }. assetETH. sidespot. size_usd > 0 and ≤ 1e12.
stablecoin_holdings_usdobject | number0Either a number (legacy total, breakdown unknown) or an object keyed by issuer (USDT, USDC, DAI, FDUSD, TUSD, FRAX). Unknown issuers are accepted but flagged.
include_detailsboolfalseInclude _per_asset_state (full v1 risk-state per asset that fed the aggregation).

Response — what's in it

The response groups into 5 blocks. The full schema is documented in specs/api/portfolio-risk-state-examples.md (12 worked scenarios + smoke script). The high-impact fields:

BlockFieldsWhy it matters
portfoliogross_exposure_usd, net_exposure_usd, long_exposure_usd, short_exposure_usd, total_notional_usd, per_asset_concentration, weighted_composite, weighted_risk_permission_score, portfolio_allowed, btc_equivalent_net_usd, btc_equivalent_gross_usdAggregate book metrics. btc_equivalent_* uses 90d beta against BTC for cross-asset comparability.
correlationsmatrix (BTC×BTC, BTC×ETH, ETH×ETH), beta, source: "live" | "cache"90d Pearson correlation + beta. 1h-cached. Falls back to { unavailable: true, reason } when CoinGecko is down — the rest of the response continues.
positions[]per-position { index, asset, size_usd, side, policy_level, direction_bias, max_size_fraction, max_size_usd, actual_pct, oversize_factor, direction_conflict, allowed, reason_codes, advisories }The per-position assessment against per-asset caps. reason_codes are blocking only; advisories are informational. See reason_codes vs advisories below.
portfolio_risk_flagsstructural_blockers[], context_risks[], concentration_warnings[], stablecoin_warnings[]Hierarchical flag families. Blockers and DANGER concentration / stablecoin flags drive portfolio.portfolio_allowed: false.
stablecoin_breakdownper-issuer { issuer, name, amount_usd, pct_of_stables, pct_of_total } + peg-deviation statusNull when no stablecoin holdings declared. Issuer concentration thresholds: 50% WARNING, 80% DANGER. Peg deviation: 0.5% WARNING, 2% DANGER.
auditabilitycomputed_at, latency_ms, key_type, audit_id, cache_statusaudit_id is the key in the audit-decisions blob store. cache_status[asset] = { hit, age_ms } per asset for diagnostics.

Example response (alpha.5)

A 75/25 BTC/ETH long-only book with $100k stables, BTC oversized vs the per-asset cap:

{
  "api_version": "2.0.0-alpha.5",
  "scoring_version": "score_v3",
  "ttl_seconds": 60,
  "portfolio": {
    "gross_exposure_usd": 330000,
    "long_exposure_usd": 330000,
    "short_exposure_usd": 0,
    "net_exposure_usd": 330000,
    "total_notional_usd": 430000,
    "stablecoin_holdings_usd": 100000,
    "stablecoin_concentration_pct": 23.26,
    "weighted_composite": 54,
    "weighted_risk_permission_score": 48,
    "portfolio_allowed": true,
    "btc_equivalent_net_usd": 351840,
    "btc_equivalent_gross_usd": 351840
  },
  "correlations": {
    "source": "live",
    "matrix": { "BTC": { "BTC": 1.0, "ETH": 0.81 }, "ETH": { "BTC": 0.81, "ETH": 1.0 } },
    "beta": { "BTC": 1.0, "ETH": 1.273 }
  },
  "positions": [
    {
      "index": 0, "asset": "BTC", "size_usd": 250000, "side": "long",
      "policy_level": 4, "direction_bias": "LONG_PREFERRED",
      "max_size_fraction": 0.42, "max_size_usd": 180600,
      "actual_pct": 58.14, "oversize_factor": 1.384,
      "direction_conflict": false,
      "allowed": false,
      "reason_codes": ["POSITION_OVER_MAX_SIZE"],
      "advisories": []
    },
    {
      "index": 1, "asset": "ETH", "size_usd": 80000, "side": "long",
      "policy_level": 4, "direction_bias": "LONG_PREFERRED",
      "max_size_fraction": 0.48, "max_size_usd": 206400,
      "actual_pct": 18.60, "oversize_factor": 0.388,
      "direction_conflict": false,
      "allowed": true,
      "reason_codes": [],
      "advisories": []
    }
  ],
  "portfolio_risk_flags": {
    "structural_blockers": [],
    "context_risks": ["BTC:HIGH_COUPLING"],
    "concentration_warnings": [],
    "stablecoin_warnings": []
  },
  "stablecoin_breakdown": {
    "issuers": [
      { "issuer": "USDT", "name": "Tether", "amount_usd": 50000, "pct_of_stables": 50.0, "pct_of_total": 11.63, "peg_deviation_pct": 0.02, "peg_status": "OK" },
      { "issuer": "USDC", "name": "USD Coin", "amount_usd": 50000, "pct_of_stables": 50.0, "pct_of_total": 11.63, "peg_deviation_pct": -0.01, "peg_status": "OK" }
    ]
  },
  "auditability": {
    "computed_at": "2026-05-05T11:32:14.123Z",
    "latency_ms": 184,
    "key_type": "external",
    "audit_id": "1714905134123-a3kf7p",
    "cache_status": {
      "BTC": { "hit": true, "age_ms": 14000 },
      "ETH": { "hit": true, "age_ms": 14000 }
    }
  }
}

How to read this:

  • portfolio.portfolio_allowed: true — concentration is a non-issue here (BTC 75.76% gross of crypto-only is the older view; gross of TOTAL notional including stables is 58.14%).
  • positions[0].allowed: false because BTC's actual_pct (58.14) exceeds the per-asset cap max_size_fraction (42) × 100. The fix is either reduce BTC or grow the book.
  • correlations.source: "live" means the matrix was fetched fresh; "cache" means it was served from the 1h-cached snapshot.

reason_codes vs advisories

Each per-position assessment reports two arrays. They are deliberately separated.

FieldContentsEffect on allowed
reason_codesONLY codes that drove allowed=false. Empty when allowed=true.Yes — these are the binding reasons.
advisoriesInformational codes the engine wants to surface but that don't themselves block this position.No — allowed can be true AND advisories can be non-empty.

reason_codes values:

  • STRUCTURAL_BLOCK_<TAG> — propagated from per-asset risk flags. Hard structural blocker on the underlying asset.
  • POSITION_OVER_MAX_SIZEactual_pct > max_size_fraction.

advisories values:

  • DIRECTION_BIAS_OPPOSITE — position side fights the engine's direction_bias. Mirrored by the boolean direction_conflict.
  • NEW_ENTRY_BLOCKED_BY_ENGINE — the per-asset blocked_actions includes NEW_ENTRY. Existing positions within cap aren't blocked by this endpoint, but customer's own opening workflow should respect this.
  • ALL_IN_BLOCKED_BY_ENGINE — same pattern for ALL_IN.

Pre-2026-05-04 these codes lived in reason_codes even when allowed=true, which caused integration confusion (e.g. BLOCKED_ACTION_PRESENT alongside allowed=true). The split was introduced in alpha.5. Old clients reading reason_codes for blocking checks continue to work — they just see fewer codes.

Cache layers

CacheTTLWhy
risk-state-cache (per-asset, inherited from v1)60sFast-moving market data.
portfolio-correlations1hCorrelation matrix moves slowly.
stablecoin-pegs5minPeg events develop over hours, not seconds.

When per-asset cache misses, the endpoint warms in parallel (Promise.allSettled over the missing assets, 12s timeout each). If both miss simultaneously and warming fails, the response is 503 with Retry-After: 30 and a cache_status block per asset for diagnosis.

Error responses

HTTPMeaning
200OK. Read portfolio.portfolio_allowed and portfolio_risk_flags.
400Invalid JSON / unsupported asset / invalid side / size out of range / malformed stablecoin holdings. Specific message in error.
401Missing or invalid Bearer token.
429External key over 60 req/min. Retry-After: 60.
503Per-asset cache cold AND warming failed. Retry-After: 30. Body includes cache_status.

Full reference

The complete spec — 12 worked scenarios, full per-position field reference, error code reference, smoke-test bash script, and operational notes — is in specs/api/portfolio-risk-state-examples.md. That document is the source-of-truth for v2 alpha; this page summarises the surface for landing-page readers.


POST /v2/stress-scenario

Apply a deterministic shock template to the same book and return baseline vs stressed side-by-side. Companion to /v2/portfolio-risk-state.

POST https://riskstate.netlify.app/v2/stress-scenario

Request

Same positions[] and stablecoin_holdings_usd as the portfolio endpoint, plus a scenario field:

{
  "positions": [
    { "asset": "BTC", "size_usd": 750000, "side": "long" },
    { "asset": "ETH", "size_usd": 250000, "side": "long" }
  ],
  "scenario": "flash_crash"
}
FieldTypeDescription
scenariostringOne of flash_crash, funding_blowout, stablecoin_depeg, liquidation_cascade, btc_eth_decoupling, dxy_spike. Required.
scenario_paramsobjectOptional. Deep-merges into the template's shocks. Use to model variant tails (e.g. USDT-only depeg of −5%).

Built-in templates

KeyHistorical anchorHeadline shock
flash_crashLUNA May 2022, FTX Nov 2022, COVID Mar 2020BTC −30%, ETH −35%, vol×1.5, funding −0.05
funding_blowoutApr/Oct 2021, Mar 2024Prices flat; funding +0.3% / 8h (95th pct)
stablecoin_depegUSDC Mar 2023, UST May 2022USDT −3%, USDC −1.5%, others mild
liquidation_cascadeAug 17 2024 yen carry, May 19 2021BTC −15%, ETH −20%, OI −30%, +$200M liq
btc_eth_decouplingAug 2021 (ETH 2.0), Mar 2024 ETF mismatchBTC −5%, ETH +10%
dxy_spikeSep–Oct 2022, Feb 2025 tariffsBTC −8%, ETH −10%, DXY +3%, 10Y +30bps

Response shape

BlockNotes
scenarioEchoes the template name + description + historical anchor + shocks_applied (final shocks after merging scenario_params).
baselinePre-shock book: gross / net / long / short, per-asset concentration, portfolio_allowed, flags[].
stressedPost-shock book, same shape. stressed_size_usd on shorts is post-pnl (see semantics note below).
positions[]Per-position pnl: { baseline_size_usd, shock_pct_applied, pnl_pct, pnl_usd, stressed_size_usd }.
stablecoin_breakdownNull when no stables. Otherwise per-issuer { baseline_usd, stressed_usd, pnl_pct, pnl_usd }.
portfolio_pnl_usd / portfolio_pnl_pctTotal book pnl + breakdown into positions vs stables.
triggered_flags[]NEW flags fired by the scenario but not at baseline. Diff by code across BOTH concentration AND stablecoin flag families.

Three contract details that have surprised first-time integrators

1. stressed_size_usd on hedged shorts is post-pnl, not underlying notional

A $500k short under a −30% flash-crash reports stressed_size_usd ≈ $650k (gained $150k on the short), not $500k (the unchanged underlying notional). This is so that aggregating across an asymmetric long/short book yields a clean post-shock book value.

If you need the underlying instead (e.g. for margin-requirement math):

const stressedUnderlying = Math.abs(p.stressed_size_usd - p.pnl_usd);

This is a modeling choice, not a bug. A stressed_underlying_notional_usd field is a candidate for a future minor.

2. baseline.portfolio_allowed covers BOTH crypto concentration AND stablecoin DANGER

Pre-alpha.2 it only covered concentration, so a portfolio with a healthy crypto book + 100% USDT could pass baseline despite a structural stable issue. Alpha.2 onward, baseline checks both flag families. A book with 100% USDT can return baseline.portfolio_allowed: false before any scenario shock — that's the correct read.

3. triggered_flags is a baseline-aware diff, applied to both flag families

A flag that fires under stress AND would have fired at baseline is NOT considered "triggered by the scenario." Pre-alpha.2 stablecoin flags were always reported as triggered; alpha.2 onward both concentration and stablecoin flags use the same diff logic. So a USDC-heavy book under stablecoin_depeg reports only STABLE_DEPEG_USDC as triggered (the concentration flag was already true at baseline).

Custom override (scenario_params)

{
  "positions": [{"asset":"BTC","size_usd":50000,"side":"long"}],
  "stablecoin_holdings_usd": { "USDT": 800000 },
  "scenario": "stablecoin_depeg",
  "scenario_params": {
    "stablecoin_peg_shift_pct": { "USDT": -5 },
    "vol_multiplier": 1.0
  }
}

scenario_params deep-merges into the base template. Only the keys you supply override; everything else stays as the template default. response.scenario.custom_override_applied: true confirms the merge.

Determinism

The shocks are pre-specified constants in the function source. There are no upstream API calls during scenario execution — the only inputs are the request body. Two identical requests always produce identical responses (modulo auditability.computed_at).

Full reference

The complete spec — 5 worked scenarios including the hedged-short post-pnl example, full field reference, custom-override deep-merge semantics, and smoke-test script — is in specs/api/stress-scenario-examples.md.


Forward-compatibility commitments

Will NOT break existing integrations:

  • Adding new optional fields to request body (ignore if unknown)
  • Adding new fields to response body (ignore if not consumed)
  • Adding new stablecoin issuers / scenario templates
  • Bumping api_version minor / patch

Will be announced as breaking with 2-week notice:

  • Renaming or removing any documented response field
  • Changing the type of any documented field
  • Tightening validation that currently accepts certain inputs
  • Changing the semantics of portfolio_allowed (e.g. adding a new mandatory blocker class)
  • Renaming any of the 6 built-in scenario keys
  • Changing the deep-merge semantics of scenario_params

Internal, not part of the contract:

  • Cache TTL adjustments
  • Concentration / depeg threshold tuning (will be parametric per-key in future via policy_config)
  • Adding cache layers
  • Changing internal cache key shapes

Versioning

EndpointVersionDateNotes
/v2/portfolio-risk-state2.0.0-alpha.52026-05-04reason_codes split into blocking + new advisories field; cache-warming parallelised + 12s timeout.
/v2/portfolio-risk-state2.0.0-alpha.42026-05-05Phase 4: stablecoin per-issuer breakdown + peg health.
/v2/portfolio-risk-state2.0.0-alpha.32026-05-05Phase 3: cache warming + audit trail integration.
/v2/portfolio-risk-state2.0.0-alpha.22026-05-05Phase 2: correlation matrix + BTC-beta + BTC-equivalent exposure.
/v2/portfolio-risk-state2.0.0-alpha.12026-05-05Phase 1: aggregation + per-position assessment + concentration warnings.
/v2/stress-scenario2.0.0-alpha.22026-05-04Smoke-test polish: baseline now also checks stablecoin DANGER; triggered_flags is a baseline-aware diff for both flag families.
/v2/stress-scenario2.0.0-alpha.12026-05-05Phase 1: 6 scenario templates, baseline vs stressed, position pnl, custom override.

Pre-stable. Stabilises to 2.0.0 jointly when:

  1. At least one institutional pilot has consumed both endpoints for ≥30 days
  2. Forward-compatibility commitments are explicitly published
  3. Pricing tier integration ships (free vs paid access policy)

Until 2.0.0, breaking changes may occur with 2-week notice published in the Changelog.


Looking for v1?

POST /v1/risk-state is the production-stable per-asset endpoint. Use it when you only care about a single asset and want the lowest-latency, most-tested surface. See API v1 Reference →.