API v2 (alpha)
Pre-stable.
/v2/portfolio-risk-stateis at2.0.0-alpha.5and/v2/stress-scenariois at2.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
| Endpoint | Use case |
|---|---|
POST /v2/portfolio-risk-state | Aggregate 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-scenario | Apply 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>
| Type | Format | Rate limit |
|---|---|---|
| Owner | RISKSTATE_API_KEY env var | Unlimited |
| External | rs_live_ + 64 hex | 60 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
}
| Field | Type | Default | Description |
|---|---|---|---|
positions[] | array | required | 1–50 entries. Each { asset, size_usd, side, venue_type? }. asset ∈ ETH. side ∈ spot. size_usd > 0 and ≤ 1e12. |
stablecoin_holdings_usd | object | number | 0 | Either 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_details | bool | false | Include _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:
| Block | Fields | Why it matters |
|---|---|---|
portfolio | gross_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_usd | Aggregate book metrics. btc_equivalent_* uses 90d beta against BTC for cross-asset comparability. |
correlations | matrix (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_flags | structural_blockers[], context_risks[], concentration_warnings[], stablecoin_warnings[] | Hierarchical flag families. Blockers and DANGER concentration / stablecoin flags drive portfolio.portfolio_allowed: false. |
stablecoin_breakdown | per-issuer { issuer, name, amount_usd, pct_of_stables, pct_of_total } + peg-deviation status | Null when no stablecoin holdings declared. Issuer concentration thresholds: 50% WARNING, 80% DANGER. Peg deviation: 0.5% WARNING, 2% DANGER. |
auditability | computed_at, latency_ms, key_type, audit_id, cache_status | audit_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: falsebecause BTC'sactual_pct (58.14)exceeds the per-asset capmax_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.
| Field | Contents | Effect on allowed |
|---|---|---|
reason_codes | ONLY codes that drove allowed=false. Empty when allowed=true. | Yes — these are the binding reasons. |
advisories | Informational 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_SIZE—actual_pct > max_size_fraction.
advisories values:
DIRECTION_BIAS_OPPOSITE— position side fights the engine'sdirection_bias. Mirrored by the booleandirection_conflict.NEW_ENTRY_BLOCKED_BY_ENGINE— the per-assetblocked_actionsincludesNEW_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 forALL_IN.
Pre-2026-05-04 these codes lived in
reason_codeseven whenallowed=true, which caused integration confusion (e.g.BLOCKED_ACTION_PRESENTalongsideallowed=true). The split was introduced in alpha.5. Old clients readingreason_codesfor blocking checks continue to work — they just see fewer codes.
Cache layers
| Cache | TTL | Why |
|---|---|---|
risk-state-cache (per-asset, inherited from v1) | 60s | Fast-moving market data. |
portfolio-correlations | 1h | Correlation matrix moves slowly. |
stablecoin-pegs | 5min | Peg 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
| HTTP | Meaning |
|---|---|
| 200 | OK. Read portfolio.portfolio_allowed and portfolio_risk_flags. |
| 400 | Invalid JSON / unsupported asset / invalid side / size out of range / malformed stablecoin holdings. Specific message in error. |
| 401 | Missing or invalid Bearer token. |
| 429 | External key over 60 req/min. Retry-After: 60. |
| 503 | Per-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"
}
| Field | Type | Description |
|---|---|---|
scenario | string | One of flash_crash, funding_blowout, stablecoin_depeg, liquidation_cascade, btc_eth_decoupling, dxy_spike. Required. |
scenario_params | object | Optional. Deep-merges into the template's shocks. Use to model variant tails (e.g. USDT-only depeg of −5%). |
Built-in templates
| Key | Historical anchor | Headline shock |
|---|---|---|
flash_crash | LUNA May 2022, FTX Nov 2022, COVID Mar 2020 | BTC −30%, ETH −35%, vol×1.5, funding −0.05 |
funding_blowout | Apr/Oct 2021, Mar 2024 | Prices flat; funding +0.3% / 8h (95th pct) |
stablecoin_depeg | USDC Mar 2023, UST May 2022 | USDT −3%, USDC −1.5%, others mild |
liquidation_cascade | Aug 17 2024 yen carry, May 19 2021 | BTC −15%, ETH −20%, OI −30%, +$200M liq |
btc_eth_decoupling | Aug 2021 (ETH 2.0), Mar 2024 ETF mismatch | BTC −5%, ETH +10% |
dxy_spike | Sep–Oct 2022, Feb 2025 tariffs | BTC −8%, ETH −10%, DXY +3%, 10Y +30bps |
Response shape
| Block | Notes |
|---|---|
scenario | Echoes the template name + description + historical anchor + shocks_applied (final shocks after merging scenario_params). |
baseline | Pre-shock book: gross / net / long / short, per-asset concentration, portfolio_allowed, flags[]. |
stressed | Post-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_breakdown | Null when no stables. Otherwise per-issuer { baseline_usd, stressed_usd, pnl_pct, pnl_usd }. |
portfolio_pnl_usd / portfolio_pnl_pct | Total 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_versionminor / 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
| Endpoint | Version | Date | Notes |
|---|---|---|---|
/v2/portfolio-risk-state | 2.0.0-alpha.5 | 2026-05-04 | reason_codes split into blocking + new advisories field; cache-warming parallelised + 12s timeout. |
/v2/portfolio-risk-state | 2.0.0-alpha.4 | 2026-05-05 | Phase 4: stablecoin per-issuer breakdown + peg health. |
/v2/portfolio-risk-state | 2.0.0-alpha.3 | 2026-05-05 | Phase 3: cache warming + audit trail integration. |
/v2/portfolio-risk-state | 2.0.0-alpha.2 | 2026-05-05 | Phase 2: correlation matrix + BTC-beta + BTC-equivalent exposure. |
/v2/portfolio-risk-state | 2.0.0-alpha.1 | 2026-05-05 | Phase 1: aggregation + per-position assessment + concentration warnings. |
/v2/stress-scenario | 2.0.0-alpha.2 | 2026-05-04 | Smoke-test polish: baseline now also checks stablecoin DANGER; triggered_flags is a baseline-aware diff for both flag families. |
/v2/stress-scenario | 2.0.0-alpha.1 | 2026-05-05 | Phase 1: 6 scenario templates, baseline vs stressed, position pnl, custom override. |
Pre-stable. Stabilises to 2.0.0 jointly when:
- At least one institutional pilot has consumed both endpoints for ≥30 days
- Forward-compatibility commitments are explicitly published
- 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 →.