Overview
The ISCL risk scoring algorithm quantifies transaction risk on a 0—100 integer scale. It is computed bycomputeRiskScore() in @clavion/preflight during the simulation phase of every transaction. The resulting score is passed to PolicyEngine.evaluate(), where it can trigger a require_approval decision if it exceeds the operator-configured maxRiskScore threshold.
The algorithm is deliberately additive and transparent: each scoring factor contributes a fixed weight, and the triggered factors are returned as human-readable reasons strings alongside the numeric score. This makes risk outcomes auditable and explainable to both operators and end users reviewing approval prompts.
Key source files:
packages/preflight/src/risk-scorer.ts (scoring factors, weights, constants), packages/preflight/src/preflight-service.ts (RiskContext construction, simulation flow), packages/policy/src/policy-engine.ts (policy decision based on risk score).Scoring factors
The scorer evaluates seven independent factors. Each factor is a boolean test; when the condition is true, its fixed weight is added to the running total. The final score is capped atMAX_SCORE (100).
Constants
RiskContext construction
ThePreflightService.buildRiskContext() method assembles a RiskContext object from three sources: the TxIntent, the active PolicyConfig, and the simulation results.
Field resolution
contractInAllowlist — Derived from the intent action type. For approve actions, the contract is action.spender. For swap_exact_in / swap_exact_out, it is action.router. Transfer actions have no target contract, so contractInAllowlist defaults to true. If PolicyConfig.contractAllowlist is empty, the check is bypassed (treated as allowlisted).
tokenInAllowlist — All token addresses are extracted from the intent: asset.address for transfers/approvals, both assetIn.address and assetOut.address for swaps. Every token must appear in PolicyConfig.tokenAllowlist. An empty allowlist bypasses the check (all tokens treated as allowlisted).
slippageBps — Taken directly from intent.constraints.maxSlippageBps.
simulationReverted — The inverse of the eth_call simulation success flag.
gasEstimate — The result of eth_estimateGas. Falls back to 0n if estimation fails and the simulation also failed.
valueWei — Populated based on action type:
approvalAmount — Set only for approve actions, from action.amount.
maxValueWei / maxApprovalAmount — Sourced from PolicyConfig. If the config value is "0" (the default), the corresponding context field is set to undefined, which disables the associated scoring factor.
Score in the policy pipeline
The risk score flows through the ISCL pipeline as follows:1
Simulation
PreflightService.simulate(intent, buildPlan) runs eth_call and eth_estimateGas, then collects balance diffs and allowance changes.2
Risk context assembly
buildRiskContext(intent, simulationSuccess, gasEstimate) combines intent data, policy config, and simulation results into a RiskContext.3
Score computation
computeRiskScore(riskContext) evaluates 7 factors and returns { score, reasons }.4
Preflight result
The score and reasons are packaged into a
PreflightResult along with balance diffs, allowance changes, and warnings.5
Policy evaluation
PolicyEngine.evaluate(intent, config, { riskScore }) checks if the score exceeds maxRiskScore. If so, the decision escalates to require_approval.Within
PolicyEngine.evaluate(), the risk score check is one of eight policy checks. It can only escalate the decision to require_approval; it never causes an outright deny. The decision priority is: deny > require_approval > allow. If any other check triggers a deny, the deny takes precedence regardless of risk score.Configuration impact
All scoring thresholds are derived fromPolicyConfig. Operators tune risk tolerance through configuration rather than code changes.
How each config field affects scoring
Allowlist behavior
BothcontractAllowlist and tokenAllowlist use an empty-means-open semantic: an empty array means all addresses are considered allowlisted (no penalty). This is a deliberate design choice for ease of initial deployment. Once an operator populates an allowlist, only listed addresses avoid the penalty.
Worked examples
Example 1: Simple allowlisted ETH transfer -- Score 0
Example 1: Simple allowlisted ETH transfer -- Score 0
A native ETH transfer to a known recipient. No contract interaction, no tokens, low value.
Final score: 0 — PolicyEngine decision:
allow.Example 2: Swap with unknown token, high slippage -- Score 35
Example 2: Swap with unknown token, high slippage -- Score 35
A
swap_exact_in on a Uniswap V3 router that is in the contract allowlist, but the output token is not in the token allowlist, and slippage is set to 500 bps (5%).Final score: 35 — With default
maxRiskScore: 50, PolicyEngine decision: allow.Example 3: Unlimited approval to unknown contract -- Score 75
Example 3: Unlimited approval to unknown contract -- Score 75
An
approve action granting MAX_UINT256 allowance to a spender that is not in the contract allowlist. The token is in the token allowlist. The approval targets a proxy contract with complex initialization, pushing gas above the 400k threshold.Final score: 75 (40 + 25 + 10) — With default
maxRiskScore: 50, PolicyEngine decision: require_approval. The user sees all three triggered reasons in the approval prompt.Example 4: Reverted simulation with unknown contract -- Score 90
Example 4: Reverted simulation with unknown contract -- Score 90
A
swap_exact_in where the simulation reverts (e.g., insufficient liquidity), the router is not allowlisted, and the gas estimate is abnormally high.Final score: 90 — PolicyEngine decision:
require_approval. The approval prompt warns "Transaction simulation reverted (+50)" and "Contract not in allowlist (+40)".When gas estimation fails alongside a reverted simulation,
gasEstimate falls back to 0n, so the abnormal gas factor does not trigger. The simulation revert factor alone carries a substantial +50 weight.Customization strategies
Risk tolerance should be tuned throughPolicyConfig rather than by modifying the scoring weights in code. The scoring weights are designed as sensible defaults; the config knobs provide the operational flexibility.
- Conservative (DeFi Treasury)
- Permissive (Development)
- Balanced (Production Agent)
Set
maxRiskScore: 20, populate both allowlists exhaustively, set maxValueWei and maxApprovalAmount to reasonable limits. Almost any non-trivial transaction will require human approval.Key tuning relationships
- Populating
contractAllowlistis the single highest-impact configuration change, eliminating 40 points of potential risk for known contracts. maxRiskScore: 50(the default) means that any single high-weight factor (contract not allowlisted at +40 combined with any other factor, or simulation revert alone at +50) will trigger approval.- Setting
maxValueWeito a non-zero value activates the “large value” factor, adding another layer of scrutiny for high-value transactions even when all other factors are clean.
Next steps
- Policy Engine — How policy rules enforce security decisions
- Configuration Reference — Full
PolicyConfigschema and loading behavior - Trust Domains — How Domain B enforces the policy boundary