Skip to content

Rating Engine & Underwriting

The @openinsure/rating and @openinsure/underwriting packages together implement the actuarial and operational heart of the platform. The rating engine is deterministic and auditable — given identical inputs and rate table version, it always produces the same output with a full step-by-step calculation log.

Every quote starts with a RatingInput object that captures the risk characteristics:

interface RatingInput {
orgId: string;
programId: string;
lineOfBusiness: 'GL' | 'CYBER' | 'EO' | 'WC' | 'PROPERTY' | 'UMBRELLA';
state: string; // 2-letter state code
effectiveDate: string; // ISO 8601
expirationDate: string;
// Risk characteristics
naicsCode: string;
annualRevenue?: number;
payroll?: number; // WC only
squareFootage?: number; // Property
tiv?: number; // Property total insured value
yearsInBusiness?: number;
priorCarrier?: string;
// Requested terms
occurrenceLimit: number;
aggregateLimit: number;
deductible: number;
// Loss history (from prior carrier's loss run)
lossHistory?: LossYear[]; // Past 5 years
}

The engine applies factors in a fixed order. Each step records the intermediate premium value, the factor applied, and the source table:

1. Base Rate
└── Lookup: program_id + state + naics_code → base_rate_per_$1k_revenue
└── Base Premium = (annualRevenue / 1000) × base_rate
2. Limit Factor
└── Lookup: occurrence_limit + aggregate_limit → limit_factor
└── Premium × limit_factor
3. Deductible Credit
└── Lookup: deductible_amount → deductible_credit (negative factor)
└── Premium × (1 - deductible_credit)
4. Territory/State Modifier
└── Lookup: state → state_modifier
└── Premium × state_modifier
5. Industry Class Modifier
└── Lookup: naics_code → class_modifier
└── Premium × class_modifier
6. Revenue Band Modifier
└── Lookup: revenue_band(annualRevenue) → revenue_modifier
└── Premium × revenue_modifier
7. Loss History Modifier (Experience Rating)
└── Compute: loss_ratio = total_incurred / expected_losses
└── Apply: experience_mod = credibility × (loss_ratio - 1.0) + 1.0
└── Premium × experience_mod [capped at program min/max]
8. Schedule Rating
└── Underwriter-applied credits/debits: -25% to +25%
└── Must be documented with reason code
9. Minimum Premium Check
└── max(computed_premium, program_minimum_premium)
10. Fees & Taxes
└── Policy fee (flat)
└── Inspection fee (flat, if applicable)
└── State surplus lines tax (% of premium, if non-admitted)
└── SLSF fee (if applicable)

Every rating step is stored in the rating_audit table:

{
"quoteId": "quo_01J8...",
"steps": [
{ "step": 1, "name": "base_rate", "factor": 0.0042, "input": 2500000, "output": 10500, "tableRef": "rt_gl_vt_238160_v3" },
{ "step": 2, "name": "limit_factor", "factor": 1.15, "input": 10500, "output": 12075, "tableRef": "rt_limits_1m_2m" },
{ "step": 7, "name": "experience_mod", "factor": 0.92, "input": 13124, "output": 12074, "credibility": 0.45, "lossRatio": 0.83 },
...
],
"grossPremium": 14250,
"netPremium": 13087,
"fees": { "policyFee": 150, "inspectionFee": 0, "surplusLinesTax": 1013 }
}
General Liability (GL)

Key rating factors: NAICS code, annual revenue, state, occurrence/aggregate limit, deductible, loss history.

Supported forms: CG 00 01 (ISO CGL), plus program-specific endorsements for contractors, habitational, products liability.

Experience rating: Applied when ≥ $25,000 standard premium with 3+ years of loss history available.

Minimum premium: Configurable per program, typically $500–$2,500 depending on class.

Cyber Liability

Key rating factors: Annual revenue, data records count, industry sector, security controls score (0–100 from intake questionnaire), prior cyber incidents.

Coverage components: First-party (data recovery, business interruption, ransomware) + Third-party (network security liability, privacy liability, media liability).

Security discount: Carriers can configure credits for MFA adoption, EDR deployment, SOC 2 certification, and cyber insurance training programs.

Sublimits: Ransomware, social engineering, and PCI fines are typically sublimited. Configured per program in the rate table.

Professional Liability / E&O

Key rating factors: Profession type, annual billings/fees, claims history, years in practice, state.

Supported professions: Technology companies, consultants, architects/engineers, accountants, real estate agents, insurance agents (Meta E&O).

Retroactive date: Mandatory field — policies are claims-made, so the retroactive date determines prior acts coverage.

Tail coverage: Extended reporting period (ERP) endorsements are handled as a separate one-time premium calculation in the billing module.

Workers’ Compensation (WC)

Key rating factors: Payroll by class code (NCCI or independent bureau), experience modification factor (EMF), state, industry.

EMF integration: The WC rating engine accepts the NCCI EMF directly from the producer’s submission. Verification against NCCI’s data feed is optional but recommended for large accounts.

Split payroll: Employees working across multiple class codes are pro-rated by actual payroll allocation.

Audit basis: WC policies are issued on estimated payroll and audited at expiration. The @openinsure/billing module supports mid-term and annual audit adjustments.

Rate tables are managed through the Admin UI (/admin/rate-tables) or via the API.

{
"id": "rt_gl_vt_v3",
"programId": "prog_gl_standard",
"lineOfBusiness": "GL",
"version": 3,
"effectiveDate": "2025-01-01",
"state": "VT",
"baseRates": [
{
"naicsCode": "238160",
"description": "Roofing Contractors",
"ratePerThousand": 4.2,
"minimumPremium": 1500
}
],
"limitFactors": [
{ "occurrence": 500000, "aggregate": 1000000, "factor": 0.85 },
{ "occurrence": 1000000, "aggregate": 2000000, "factor": 1.0 },
{ "occurrence": 2000000, "aggregate": 4000000, "factor": 1.22 }
],
"stateModifier": 1.05,
"minimumPremium": 750
}
Terminal window
POST /v1/rate-tables
Authorization: Bearer <admin_token>
Content-Type: application/json
{
"programId": "prog_gl_standard",
"lineOfBusiness": "GL",
"state": "VT",
"effectiveDate": "2025-01-01",
"baseRates": [ ... ],
"limitFactors": [ ... ]
}

The Underwriting Workbench is a real-time web application for underwriters to review, decision, and bind submissions that require human judgment.

A submission is automatically escalated to the Workbench when:

  • The quoted premium exceeds the program’s auto-bind threshold (configured per program).
  • The risk falls in an excluded class that requires prior approval.
  • The experience mod exceeds 1.5 (poor loss history).
  • The submission was referred by DA enforcement (exceeds per-policy or aggregate limits).
  • An underwriter manually escalates it from the producer portal.

Real-Time Queue

New submissions appear instantly via WebSocket (Durable Objects). Underwriters claim submissions to prevent double-decisioning.

Risk Scorecard

AI-generated summary of the risk with flagged items, peer comparisons, and recommended premium adjustments.

Schedule Rating

Apply credits/debits (-25% to +25%) with required reason codes. Changes are logged to the audit trail.

Referral Notes

Threaded notes between MGA underwriters and carrier technical underwriters. Full history preserved on the submission record.

The Workbench includes a full-featured Aspire form for commercial auto submissions, organized into five tabs:

  1. Insured Information — Company details, virtualized contacts grid (paste from Excel), addresses
  2. Commercial Automobile — Vehicles (VIN-decoded), drivers (MVR), coverage & limits, mileage distribution, filings, additional interests
  3. Prior Carrier & Loss History — Prior carriers, loss records with summary panel, work experience for new ventures
  4. Notes & Attachments — Categorized notes, file uploads with optimistic UI updates
  5. Other Coverages — Cargo, physical damage, trailer interchange, officers, knockout underwriting questions with risk indicators

All editable tables (vehicles, drivers, contacts, prior carriers, loss records, additional interests) use TanStack Table + React Virtual for performance with large datasets, and useTableSpreadsheet for keyboard navigation and clipboard paste.

The QuoteReadinessService evaluates submission completeness using a strategy pattern — each carrier (Southwind, Bulldog, Commodore) can define custom checks on top of the common checks.

Scoring formula:

Score = max(0, 100 − (blockerCount × 20) − (warningCount × 5))
Ready = blockerCount === 0

Common checks:

CheckSeverityTab
Company name missingBlockerInsured
Policy state missingBlockerInsured
No vehiclesBlockerCommercial Auto
No contactsWarningInsured
No prior carriers (established business)WarningLoss History
Knockout “yes” answers (referral triggers)WarningOther Coverages

External data checks (when available):

  • FMCSA inactive status → Blocker
  • Unsatisfactory safety rating → Blocker
  • High vehicle out-of-service rate (>30%) → Warning
  • Invalid VINs → Warning
  • Missing ADAS safety tech → Info

Per-tab completion badges are derived from the readiness items and displayed in the tab navigation bar (green check / yellow warning / red X).

Submission exceeds auto-bind threshold
MGA Underwriter reviews
┌─────┴──────┐
│ │
Decline Refer to carrier
│ │
DENY Carrier TU reviews
┌────┴─────┐
│ │
Approve Request info
│ │
BIND Producer responds
BIND or DENY

The @openinsure/underwriting package evaluates a set of configurable rules against every submission before and after rating. Rules either auto-bind (no UW touch), auto-refer, auto-decline, or flag specific conditions for manual review.

interface UWRule {
id: string;
name: string;
programId: string;
lineOfBusiness: LineOfBusiness;
condition: RuleCondition; // When this rule fires
action: RuleAction; // What happens when it fires
priority: number; // Lower number = evaluated first
}
type RuleCondition =
| { field: 'annualRevenue'; op: '>' | '<' | '>=' | '<='; value: number }
| { field: 'lossRatio'; op: '>' | '<'; value: number }
| { field: 'state'; op: 'in' | 'not_in'; values: string[] }
| { field: 'naicsCode'; op: 'startsWith'; value: string }
| { field: 'yearsInBusiness'; op: '<'; value: number }
| { field: 'openClaimsCount'; op: '>='; value: number }
| { field: 'experienceMod'; op: '>'; value: number }
| { and: RuleCondition[] }
| { or: RuleCondition[] };
type RuleAction =
| { type: 'AUTO_BIND' }
| { type: 'REFER'; reason: string; requiresInfo?: string[] }
| { type: 'DECLINE'; reason: string }
| { type: 'FLAG'; message: string; severity: 'INFO' | 'WARNING' | 'CRITICAL' };
[
{
"name": "High Revenue — Refer",
"condition": { "field": "annualRevenue", "op": ">", "value": 5000000 },
"action": { "type": "REFER", "reason": "Revenue exceeds $5M — senior UW review required" }
},
{
"name": "Poor Loss History",
"condition": {
"and": [
{ "field": "lossRatio", "op": ">", "value": 0.75 },
{ "field": "yearsInBusiness", "op": ">=", "value": 3 }
]
},
"action": { "type": "FLAG", "message": "5-year loss ratio > 75%", "severity": "CRITICAL" }
},
{
"name": "Excluded States",
"condition": { "field": "state", "op": "in", "values": ["NY", "CA", "FL"] },
"action": { "type": "DECLINE", "reason": "State not eligible for this program" }
},
{
"name": "New Venture",
"condition": { "field": "yearsInBusiness", "op": "<", "value": 2 },
"action": {
"type": "REFER",
"reason": "New venture — requires business plan and financials",
"requiresInfo": ["business_plan", "financial_statements"]
}
}
]
import { evaluateRules } from '@openinsure/rules';
const result = evaluateRules({
submission,
ratingAudit,
programRules: await fetchProgramRules(programId),
});
// result.decision: 'AUTO_BIND' | 'REFER' | 'DECLINE'
// result.flags: UWFlag[]
// result.requiredInfo: string[] (if REFER with requiresInfo)
// result.triggeredRules: UWRule[] (which rules fired)

Rules are managed via the Admin Portal → Programs → Underwriting Rules, or via API:

Terminal window
GET /v1/rules?programId=&lineOfBusiness=
POST /v1/rules
PUT /v1/rules/:id
DELETE /v1/rules/:id

After the rating waterfall completes, the UnderwritingAgent optionally applies an AI risk adjustment of ±5% based on factors the rate tables don’t capture:

  • Revenue trend (growing vs. contracting business)
  • Geographic risk concentration
  • Prior carrier’s reason for non-renewal
  • Industry-specific news (WorldIntelligenceAgent feed)
  • Management quality signals from the application

The adjustment is:

  1. Calculated by claude-sonnet-4-6 with the full submission context
  2. Bounded to ±5% (hard limit enforced in code — not adjustable by prompting)
  3. Logged to rating_audit as step 11 (“ai_risk_adjustment”)
  4. Flagged as an adverse action if the adjustment increases premium — the adverse action notice lists each contributing factor
{
"step": 11,
"name": "ai_risk_adjustment",
"factor": 1.03,
"rationale": "3% debit: risk concentration in high-CAT territory; prior carrier non-renewed for losses",
"adverseAction": true,
"adverseFactors": ["geographic_concentration", "prior_carrier_nonrenewal"]
}

The rating engine integrates with the DA module to enforce binding authority at quote time, not just at bind time. If a risk would exceed the DA limits, the system flags it during rating so underwriters know upfront.

See Delegated Authority for full configuration details.