Policy Config

Each API key can be attached to a policy_config profile that further tightens the engine's caps. The profile encodes a customer's mandate (max gross exposure, per-asset caps, leverage ceiling, blocked protocols, desk identifier) and is applied to every /v1/risk-state response from that key.

Tighten only — never loosens. The engine output is the upper bound. Config can lower max_size_fraction or max_leverage, but never raise them. Asking for a higher cap than the engine permits is silently ignored (no error, no upgrade).

Status: v1, shipped 2026-04-28. Owner-only writes — pilot customers do not self-serve writes in v1; RiskState walks customers through their initial config to avoid accidental self-loosening errors. v2 self-service is post-pilot.

Enforcement scope today. Cap tightening (max_gross, max_per_asset, leverage_cap) and the pre-flight blocked_protocols 403 are applied on /v1/risk-state only. The v2 alpha endpoints (/v2/portfolio-risk-state, /v2/stress-scenario) record the active profile in their audit snapshot but do not enforce any of its fields today — they apply the engine's built-in thresholds instead. See Enforcement scope by endpoint for the precise table. Customer-tightened enforcement on the v2 endpoints lands jointly with v2 stabilisation at the freeze lift (2026-10-22).

Contents

Schema

{
  "desk_id": "fund-alpha-eq",
  "max_gross": 0.40,
  "max_per_asset": { "BTC": 0.30, "ETH": 0.25 },
  "leverage_cap": 1,
  "blocked_protocols": ["aave"],
  "concentration_limit": 0.50,
  "notes": "Q3 2026 mandate — defensive posture",
  "updated_at": "2026-04-28T14:21:08.117Z",
  "updated_by": "owner",
  "version": "1"
}
FieldTypeRangeEffect
desk_idstring1–64 chars (alphanum + dot/dash/underscore/space)Free-text label, surfaced in audit + binding_constraint reason
max_grossnumber0 < x ≤ 1Caps exposure_policy.max_size_fraction
max_per_assetobject{ BTC?: 0..1, ETH?: 0..1 }Asset-specific caps applied on top of max_gross for that asset
leverage_capnumber1 ≤ x ≤ 10Caps exposure_policy.max_leverage. If < 1.01, leverage actions move from allowed to blocked.
blocked_protocolsstring[]subset of ["spark", "aave"]If the request protocol matches, returns 403 before any compute
concentration_limitnumber0 < x ≤ 1Stored + audited, NOT enforced. Persisted in the profile and surfaced in the audit policy_config_snapshot of every /v2/portfolio-risk-state call. The v2 portfolio endpoint applies the engine's built-in thresholds instead (75% WARNING / 90% DANGER for crypto, 50/80% for stablecoins). Customer-tightened enforcement is on the post-freeze track.
notesstringup to 1024 charsFree-text, surfaced in audit
updated_at, updated_by, versionServer-managed (do not send)

Endpoints

MethodPathAuthBehaviour
GET/v2/keys/policyany authenticatedReturns the caller's own profile
GET/v2/keys/policy?key_prefix=...owner onlyReturns any customer's profile
PUT/v2/keys/policy?key_prefix=...owner only (v1)Creates / updates the customer's profile
DELETE/v2/keys/policy?key_prefix=...owner onlyClears the customer's profile

All endpoints use the same Bearer token as /v1/risk-state:

Authorization: Bearer <api_key>

Set a profile (owner)

curl -X PUT "https://api.riskstate.ai/v2/keys/policy?key_prefix=rs_live_a3f2..." \
  -H "Authorization: Bearer $RISKSTATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "desk_id": "fund-alpha-eq",
    "max_gross": 0.40,
    "leverage_cap": 1,
    "blocked_protocols": ["aave"],
    "notes": "Q3 2026 mandate"
  }'

Successful response (200):

{
  "prefix": "rs_live_a3f2...",
  "policy_config": {
    "desk_id": "fund-alpha-eq",
    "max_gross": 0.4,
    "leverage_cap": 1,
    "blocked_protocols": ["aave"],
    "notes": "Q3 2026 mandate",
    "version": "1",
    "updated_at": "2026-04-28T14:21:08.117Z",
    "updated_by": "owner"
  },
  "config_version": "1"
}

Read your own profile (external key)

curl -sL "https://api.riskstate.ai/v2/keys/policy" \
  -H "Authorization: Bearer $YOUR_API_KEY"

External keys can read their profile for transparency. They cannot modify it in v1 — PUT returns 403.

Enforcement scope by endpoint

What each audited endpoint actually does with the active policy_config:

Endpointblocked_protocols (pre-flight 403)max_gross / max_per_asset capleverage_capconcentration_limitAudit policy_config_snapshotpolicy_config_applied in response
/v1/risk-state✅ Enforced✅ Enforced✅ Enforced (incl. moves leverage actions to blocked_actions when ≤ 1.01)🟡 Stored + audited (not directly applicable to single-asset)✅ Yes✅ Yes
/v2/portfolio-risk-state❌ Not enforced today❌ Not enforced today (engine's per-asset caps apply)❌ Not enforced today❌ Not enforced today (engine thresholds 75/90% crypto, 50/80% stables apply)✅ Yes ({ keys: [...] })❌ No
/v2/stress-scenario❌ Not loaded❌ Not loaded❌ Not loaded❌ Not loaded❌ No❌ No

What this means in practice for a pilot:

  • A customer profile with blocked_protocols: ["aave"] will reject the v1 request with 403 but will not reject a /v2/portfolio-risk-state call. If the v2 portfolio endpoint matters to your mandate, exclude Aave at the integration layer until v2 enforcement ships.
  • A customer profile with max_gross: 0.40 tightens the v1 response's max_size_fraction. The v2 portfolio endpoint's per-position assessments still respect the engine's per-asset max_size_fraction (often 0.42–0.48), which may be looser than your mandate.
  • The audit trail correctly records the active profile on /v1/risk-state and /v2/portfolio-risk-state (so attribution and procurement reviews can prove the config was active), but only /v1/risk-state decisions are config-enforced today.

Customer-tightened v2 enforcement (port the v1 applyConfigToResponse and checkBlockedProtocol paths into the portfolio endpoint, plus a per-position concentration check) is in scope alongside v2 stabilisation. Score_v3 freeze does not block this; it's straight enforcement plumbing.

Enforcement (on /v1/risk-state)

When /v1/risk-state is called with a key that has a policy_config, two enforcement steps run:

1. Pre-flight protocol block

If the request includes a protocol that appears in blocked_protocols, the API returns 403 before any compute or cache lookup:

{
  "error": "Protocol \"aave\" blocked by policy_config (desk_id=fund-alpha-eq)",
  "reason_codes": ["CONFIG_PROTOCOL_BLOCKED"]
}

2. Cap tightening

After the engine produces its policy, config tightening runs against the response:

Engine outputConfig appliedResult
max_size_fraction = 0.65max_gross = 0.400.40 (config bound)
max_size_fraction = 0.30max_gross = 0.400.30 (engine still tighter)
max_leverage = "2x"leverage_cap = 1"1x", leverage_allowed = false, leverage actions moved to blocked_actions
max_size_fraction = 0.50, asset=BTCmax_per_asset = { BTC: 0.20 }0.20 (asset cap binds)

When config bound the result, the response carries:

{
  "exposure_policy": {
    "max_size_fraction": 0.40,
    "max_leverage": "1x",
    "leverage_allowed": false,
    "...": "..."
  },
  "policy_level": 3,
  "binding_constraint": {
    "source": "CONFIG",
    "reason": "policy_config (desk_id=fund-alpha-eq)",
    "reason_codes": ["CONFIG_MAX_GROSS", "CONFIG_LEVERAGE_CAP"],
    "cap_value": 0.40
  },
  "policy_config_applied": {
    "desk_id": "fund-alpha-eq",
    "changes": {
      "max_gross_engine": 0.65,
      "max_gross_config": 0.40,
      "leverage_engine": 2,
      "leverage_config": 1
    },
    "binding": { "source": "CONFIG", "...": "..." },
    "profile_snapshot": {
      "desk_id": "fund-alpha-eq",
      "max_gross": 0.40,
      "leverage_cap": 1,
      "_at": "2026-04-28T14:21:08.117Z",
      "_by": "owner",
      "_v": "1"
    }
  }
}

policy_level (1–5) is re-derived from the final max_size_fraction so the integer tier reflects the post-config view, not the pre-config one.

binding_constraint.source = "CONFIG" makes it explicit that the customer's mandate is what bound this decision, not the engine's caps.

Audit trail

Audit field shape varies by endpoint.

/v1/risk-state (config enforced) records both:

Audit fieldSource
policy_config_appliedThe full { changes, binding, profile_snapshot } block as it appeared in the response
policy_config_snapshotA separate top-level canonical snapshot of the active config at decision time

The double-storage is intentional: policy_config_applied shows what the API surfaced to the caller; policy_config_snapshot is the canonical record for replay. If the customer's profile is later updated, the audit record still carries the version that produced this specific decision.

/v2/portfolio-risk-state (config loaded but not enforced today) records only:

Audit fieldSource
policy_config_snapshot{ keys: [<active field names>] } — confirms a profile was active at decision time and lists which fields were configured (full values redacted from the snapshot pending v2 enforcement). Useful for "was the right desk profile active when this portfolio ran?" attribution even before v2 enforces the cap fields themselves.

No policy_config_applied is emitted (nothing was applied).

/v2/stress-scenario does not load the profile and does not record either field.

See audit log for the full record shape per endpoint.

Validation rules

FieldConstraintError code
desk_idstring, 1–64 chars, alphanumeric + _, -, ., spacedesk_id length must be 1..64 / desk_id allows alphanumerics, dot, dash, underscore, space
max_grossnumber, (0, 1]max_gross must be a number in (0, 1]
max_per_asset[*]number per known asset, (0, 1]max_per_asset.<ASSET> must be in (0, 1]
leverage_capnumber, [1, 10]leverage_cap must be a number in [1, 10]
blocked_protocolsarray of known protocol namesblocked_protocols: unknown protocol "..."
concentration_limitnumber, (0, 1]concentration_limit must be a number in (0, 1]
notesstring, ≤ 1024 charsnotes max length is 1024 chars

PUT with no recognised fields returns 400 Empty profile — provide at least one field. Validation aggregates all errors per request — you don't need to play whack-a-mole.

Examples

Conservative single-asset BTC long-only desk

{
  "desk_id": "btc-only-conservative",
  "max_gross": 0.30,
  "max_per_asset": { "ETH": 0 },
  "leverage_cap": 1,
  "blocked_protocols": ["aave", "spark"],
  "notes": "BTC-only, no leverage, no DeFi borrowing"
}

Note: "ETH": 0 would fail validation (must be > 0). Use blocked_protocols and asset filtering at the integration layer for hard exclusions in v1; per-asset block tokens are on the v2 list.

Sized-down clone of the engine

{
  "desk_id": "small-mandate",
  "max_gross": 0.20,
  "leverage_cap": 1
}

Every decision is the engine's result, capped at 20% gross with no leverage. Useful for funds whose internal risk committee mandates a hard cap regardless of market state.

Spark-only DeFi-aware desk

{
  "desk_id": "defi-spark",
  "max_gross": 0.60,
  "leverage_cap": 1.5,
  "blocked_protocols": ["aave"],
  "notes": "Spark only — Aave V3 exposure not approved by treasury"
}

Requests with ?protocol=aave are rejected with 403 Protocol "aave" blocked by policy_config.

Limitations

  • v2 endpoints do not enforce policy_config today. /v2/portfolio-risk-state and /v2/stress-scenario shipped in May 2026 alpha. The portfolio endpoint records a snapshot of the active profile in its audit record (so attribution still works) but does not currently enforce blocked_protocols, max_gross, max_per_asset, leverage_cap or concentration_limit. Stress-scenario doesn't load the profile at all. Customers whose mandate depends on these caps must exclude protocols / cap exposure at their integration layer until v2 enforcement plumbing ships. See Enforcement scope by endpoint for the precise table.
  • No customer self-service writes in v1. Owner sets the profile during pilot setup; customer can read it for transparency.
  • No separation-of-duties workflow in v1. updated_by and updated_at are recorded, but there is no dual-approval, change-diff, version history or rollback. A risk committee that requires "who proposed, who approved, what was the diff, who can revert" should treat this as a v2 deliverable. For pilot, RiskState manages config changes manually with email confirmation.
  • concentration_limit is stored + audited but not enforced today. /v1/risk-state is single-asset and has no portfolio context. /v2/portfolio-risk-state ships in alpha and has the right context, but its v2 alpha line does not yet apply customer-tightened concentration — it uses the engine's built-in 75/90% (crypto) and 50/80% (stables) thresholds. Customer-tightened enforcement on the v2 portfolio endpoint is on the post-freeze plumbing list (no scoring change required, freeze-allowed).
  • No per-asset full-block in v1 (e.g. "this desk doesn't trade ETH"). Use the integration layer to filter; per-asset hard-block tokens are post-pilot.
  • Per-customer cache. The /v1/risk-state cache is keyed by (asset, protocol) only — config is layered per-call. Multiple customers share the cache; only the per-call delta is computed.