Enhanced annuity from a health questionnaire

A joint life annuity priced from a scanned health form, a prescription and two pension statements, with a referral for an unmatched medicine.

A demo basis, not a default model. The rates are illustrative and the case is synthetic: fictional people, specimen NI numbers and example.com addresses. See how it fits into an adviser's journey on Retirement income.
The line of business

Enhanced annuity from a health questionnaire

An annuity turns a pension fund into an income for life. In the UK, people with health conditions or a history of smoking can get a higher, enhanced income. The price depends on health and lifestyle answers, prescriptions and pension statements. These arrive as scans and PDFs, so most of the work is reading them.

Methodology

How the model rates

This is a rate table model on a demo basis, not a mortality projection. A base rate per 10,000 of purchase price is looked up by age and escalation.

The enhanced price looks the rate up at a rated age: the applicant's age plus years for declared health. The standard price uses the actual age.

Postcode area, guarantee period, payment frequency and timing, and the survivor's pension then adjust the income. Each factor is a lookup table.

Purchases under 10,000 and applicants outside 55 to 85 are declined. An unmatched medicine, a rating over 6 years or a purchase over 1,000,000 is referred.

18inputs
16steps
5lookup tables
8tests

Inputs

18 fields
FieldLabelTypeDefaultAllowed values
quote_dateQuote datedate2026-11-10-
main_dobApplicant date of birthdate1961-06-01-
spouse_dobSpouse date of birthstring-
basisSingle or joint lifestringsinglesingle, joint
survivor_pctSurvivor's incomedecimal66.6750, 66.67, 100
escalationEscalationstringlevellevel, rpi, fixed3
guarantee_yearsGuarantee periodinteger00, 5, 10
frequencyPayment frequencystringmonthlymonthly, quarterly, annual
timingPayment timingstringadvanceadvance, arrears
purchase_pricePurchase pricedecimal100000-
postcodePostcodestringEX1 1AA-
smoker_statusSmokingstringnevernever, former, current
cigs_per_dayCigarettes a dayinteger0-
years_quitYears since stoppinginteger0-
bmiBody mass indexdecimal25-
diabetes_type2Type 2 diabetesbooleanfalse-
hypertensionHigh blood pressurebooleanfalse-
medicines_unmatchedMedicine with no declared conditionbooleanfalse-
The model

Step by step

Steps run in order. Each one can use the inputs and the outputs of every step before it.

01Transform
Ages and postcode area

Works out both ages at the quote date, and the postcode area from the postcode.

{
  "id": "case_details",
  "key": "case_details",
  "step": "transform",
  "name": "Ages and postcode area",
  "retain": true,
  "format": {
    "main_age": {
      "key": "main_age",
      "label": "Applicant age",
      "exp": "{{main_dob.age(YY, quote_date)}}",
      "type": "integer",
      "def": 0
    },
    "spouse_age": {
      "key": "spouse_age",
      "label": "Spouse age",
      "exp": "{{spouse_dob.age(YY, quote_date)}}",
      "type": "integer",
      "def": 0
    },
    "postcode_area": {
      "key": "postcode_area",
      "label": "Postcode area",
      "exp": "{{postcode.postcode(area)}}",
      "type": "string",
      "def": ""
    }
  }
}
02Transform
Health and lifestyle loads

Years added to the applicant's age for each declared condition. Type 2 diabetes 2, high blood pressure 1, current smoker 3, stopped under 10 years ago 1, BMI over 30 1.

{
  "id": "health_loads",
  "key": "health_loads",
  "step": "transform",
  "name": "Health and lifestyle loads",
  "retain": true,
  "format": {
    "diabetes_load": {
      "key": "diabetes_load",
      "label": "Type 2 diabetes load",
      "exp": "{{diabetes_type2}} === true ? 2 : 0",
      "type": "integer",
      "def": 0
    },
    "blood_pressure_load": {
      "key": "blood_pressure_load",
      "label": "High blood pressure load",
      "exp": "{{hypertension}} === true ? 1 : 0",
      "type": "integer",
      "def": 0
    },
    "smoking_load": {
      "key": "smoking_load",
      "label": "Smoking load",
      "exp": "{{smoker_status}} === 'current' ? 3 : {{smoker_status}} === 'former' && {{years_quit}} < 10 ? 1 : 0",
      "type": "integer",
      "def": 0
    },
    "bmi_load": {
      "key": "bmi_load",
      "label": "BMI load",
      "exp": "{{bmi}} > 30 ? 1 : 0",
      "type": "integer",
      "def": 0
    }
  }
}
03Calculation
Health rating

The health rating in years: the sum of the loads.

{
  "id": "rating_years",
  "key": "rating_years",
  "step": "calculation",
  "name": "Health rating",
  "def": 0,
  "formula": "{{diabetes_load}} + {{blood_pressure_load}} + {{smoking_load}} + {{bmi_load}}"
}
04Calculation
Rated age

The applicant's age plus the health rating, capped at 100. The enhanced price uses the rated age; the standard price uses the actual age.

{
  "id": "rated_age",
  "key": "rated_age",
  "step": "calculation",
  "name": "Rated age",
  "def": 0,
  "formula": "min({{main_age}} + {{rating_years}}, 100)"
}
05Transform
Joint life terms

For joint life, the survivor percentage and the spouse's age less the applicant's. Both are zero for single life.

{
  "id": "joint_terms",
  "key": "joint_terms",
  "step": "transform",
  "name": "Joint life terms",
  "retain": true,
  "format": {
    "survivor_key": {
      "key": "survivor_key",
      "label": "Survivor percentage used",
      "exp": "{{basis}} === 'joint' ? {{survivor_pct}} : 0",
      "type": "decimal",
      "def": 0
    },
    "age_gap": {
      "key": "age_gap",
      "label": "Spouse age minus applicant age",
      "exp": "{{basis}} === 'joint' && {{spouse_age}} > 0 ? {{spouse_age}} - {{main_age}} : 0",
      "type": "integer",
      "def": 0
    }
  }
}
06Decline rules
Decline rules

Cases the demo basis will not quote. A purchase price under 10,000, an applicant under 55 or over 85, or joint life with no spouse date of birth.

{
  "id": "annuity_declines",
  "key": "annuity_declines",
  "step": "exclusion",
  "name": "Decline rules",
  "def": false,
  "items": [
    {
      "id": "min_purchase",
      "name": "Purchase price under 10,000",
      "exp": "{{purchase_price}} < 10000",
      "def": false
    },
    {
      "id": "age_range",
      "name": "Applicant age outside 55 to 85",
      "exp": "{{main_age}} < 55 || {{main_age}} > 85",
      "def": false
    },
    {
      "id": "joint_no_spouse",
      "name": "Joint life without spouse date of birth",
      "exp": "{{basis}} === 'joint' && !{{spouse_age}}",
      "def": false
    }
  ]
}
07Refer rules
Refer rules

Cases priced but sent to an underwriter. A medicine with no declared condition, a rating over 6 years, or a purchase price over 1,000,000.

{
  "id": "annuity_referrals",
  "key": "annuity_referrals",
  "step": "refer",
  "name": "Refer rules",
  "def": false,
  "items": [
    {
      "id": "unmatched_medicine",
      "name": "Medicine with no declared condition",
      "exp": "{{medicines_unmatched}} === true",
      "def": false
    },
    {
      "id": "high_rating",
      "name": "Health rating over 6 years",
      "exp": "{{rating_years}} > 6",
      "def": false
    },
    {
      "id": "large_purchase",
      "name": "Purchase price over 1,000,000",
      "exp": "{{purchase_price}} > 1000000",
      "def": false
    }
  ]
}
08Lookup table
Base annuity rates

Annual income per 10,000 of purchase price by age and escalation, single life, paid monthly in advance with no guarantee. Looked up at the rated age and at the actual age.

Showing the first 10 of 153 rows.

{
  "id": "base_rates",
  "key": "base_rates",
  "step": "collection",
  "name": "Base annuity rates",
  "retain": true,
  "collection": [
    ["rate_age", "rate_escalation", "rate_per_10k"],
    [50, "level", 560],
    [50, "rpi", 336],
    [50, "fixed3", 408.8],
    [51, "level", 571.52],
    [51, "rpi", 345.17],
    [51, "fixed3", 418.34],
    [52, "level", 583.27],
    [52, "rpi", 354.58],
    [52, "fixed3", 428.11],
    [53, "level", 595.26]
  ],
  "format": {
    "enhanced_base_rate": {
      "key": "enhanced_base_rate",
      "label": "Base rate at rated age",
      "exp": "collection.filter(rate_age=={{rated_age}}).filter(rate_escalation=={{escalation}}).map(rate_per_10k).first()",
      "type": "decimal",
      "def": 0
    },
    "standard_base_rate": {
      "key": "standard_base_rate",
      "label": "Base rate at actual age",
      "exp": "collection.filter(rate_age=={{main_age}}).filter(rate_escalation=={{escalation}}).map(rate_per_10k).first()",
      "type": "decimal",
      "def": 0
    }
  }
}
09Lookup table
Postcode area factor

Adjusts income by postcode area. Areas not listed take 1.00.

Showing the first 10 of 30 rows.

{
  "id": "postcode_factors",
  "key": "postcode_factors",
  "step": "collection",
  "name": "Postcode area factor",
  "retain": true,
  "collection": [
    ["pc_area", "pc_factor"],
    ["G", 1.05],
    ["ML", 1.04],
    ["PA", 1.04],
    ["DD", 1.03],
    ["L", 1.03],
    ["NE", 1.03],
    ["SR", 1.03],
    ["BD", 1.03],
    ["M", 1.02],
    ["S", 1.02]
  ],
  "format": {
    "postcode_factor": {
      "key": "postcode_factor",
      "label": "Postcode area factor",
      "exp": "collection.filter(pc_area=={{postcode_area}}).map(pc_factor).first()",
      "type": "decimal",
      "def": 1
    }
  }
}
10Lookup table
Guarantee factor

Reduces income for a five or ten year guarantee. The cost rises with age, so it is looked up at the rated age and at the actual age.

{
  "id": "guarantee_factors",
  "key": "guarantee_factors",
  "step": "collection",
  "name": "Guarantee factor",
  "retain": true,
  "collection": [
    ["gtee_years", "gtee_age_min", "gtee_age_max", "gtee_factor"],
    [0, 0, 120, 1],
    [5, 0, 69, 0.997],
    [5, 70, 79, 0.99],
    [5, 80, 120, 0.975],
    [10, 0, 69, 0.985],
    [10, 70, 79, 0.96],
    [10, 80, 120, 0.91]
  ],
  "format": {
    "enhanced_guarantee_factor": {
      "key": "enhanced_guarantee_factor",
      "label": "Guarantee factor at rated age",
      "exp": "collection.filter(gtee_years=={{guarantee_years}}).filter(gtee_age_min<={{rated_age}}).filter(gtee_age_max>={{rated_age}}).map(gtee_factor).first()",
      "type": "decimal",
      "def": 1
    },
    "standard_guarantee_factor": {
      "key": "standard_guarantee_factor",
      "label": "Guarantee factor at actual age",
      "exp": "collection.filter(gtee_years=={{guarantee_years}}).filter(gtee_age_min<={{main_age}}).filter(gtee_age_max>={{main_age}}).map(gtee_factor).first()",
      "type": "decimal",
      "def": 1
    }
  }
}
11Lookup table
Frequency and timing factor

Adjusts income for how often and when it is paid. Monthly in advance is 1.00.

{
  "id": "payment_factors",
  "key": "payment_factors",
  "step": "collection",
  "name": "Frequency and timing factor",
  "retain": true,
  "collection": [
    ["pay_frequency", "pay_timing", "pay_factor"],
    ["monthly", "advance", 1],
    ["monthly", "arrears", 1.004],
    ["quarterly", "advance", 0.995],
    ["quarterly", "arrears", 1.007],
    ["annual", "advance", 0.975],
    ["annual", "arrears", 1.02]
  ],
  "format": {
    "payment_factor": {
      "key": "payment_factor",
      "label": "Frequency and timing factor",
      "exp": "collection.filter(pay_frequency=={{frequency}}).filter(pay_timing=={{timing}}).map(pay_factor).first()",
      "type": "decimal",
      "def": 1
    }
  }
}
12Lookup table
Joint life factor

Reduces income to pay for the survivor's pension, by survivor percentage and the spouse's age less the applicant's. Single life takes 1.00.

Showing the first 10 of 13 rows.

{
  "id": "joint_factors",
  "key": "joint_factors",
  "step": "collection",
  "name": "Joint life factor",
  "retain": true,
  "collection": [
    ["jl_survivor", "jl_gap_min", "jl_gap_max", "jl_factor"],
    [0, -99, 99, 1],
    [50, -99, -5, 0.94],
    [50, -4, -1, 0.95],
    [50, 0, 4, 0.96],
    [50, 5, 99, 0.97],
    [66.67, -99, -5, 0.89],
    [66.67, -4, -1, 0.905],
    [66.67, 0, 4, 0.92],
    [66.67, 5, 99, 0.935],
    [100, -99, -5, 0.81]
  ],
  "format": {
    "joint_factor": {
      "key": "joint_factor",
      "label": "Joint life factor",
      "exp": "collection.filter(jl_survivor=={{survivor_key}}).filter(jl_gap_min<={{age_gap}}).filter(jl_gap_max>={{age_gap}}).map(jl_factor).first()",
      "type": "decimal",
      "def": 1
    }
  }
}
13Calculation
Enhanced annual income

Purchase price divided by 10,000, times the base rate at the rated age and every factor.

{
  "id": "enhanced_annual_income",
  "key": "enhanced_annual_income",
  "step": "calculation",
  "name": "Enhanced annual income",
  "def": 0,
  "formula": "round({{purchase_price}} / 10000 * {{enhanced_base_rate}} * {{postcode_factor}} * {{enhanced_guarantee_factor}} * {{payment_factor}} * {{joint_factor}}, 2)"
}
14Calculation
Standard annual income

The same calculation at the applicant's actual age, with no health rating.

{
  "id": "standard_annual_income",
  "key": "standard_annual_income",
  "step": "calculation",
  "name": "Standard annual income",
  "def": 0,
  "formula": "round({{purchase_price}} / 10000 * {{standard_base_rate}} * {{postcode_factor}} * {{standard_guarantee_factor}} * {{payment_factor}} * {{joint_factor}}, 2)"
}
15Calculation
Monthly income

Enhanced annual income divided by 12.

{
  "id": "monthly_income",
  "key": "monthly_income",
  "step": "calculation",
  "name": "Monthly income",
  "def": 0,
  "formula": "round({{enhanced_annual_income}} / 12, 2)"
}
16Calculation
Enhancement

Enhanced annual income less standard annual income.

{
  "id": "enhancement",
  "key": "enhancement",
  "step": "calculation",
  "name": "Enhancement",
  "def": 0,
  "formula": "round({{enhanced_annual_income}} - {{standard_annual_income}}, 2)"
}
Worked example

Demo case: Judith Halloran, joint life

This quote was priced by the Ptally rating engine. The trace below follows it through every step to the final result.

Quote input
{
  "quote_date": "2026-11-10",
  "main_dob": "1959-03-14",
  "spouse_dob": "1957-08-02",
  "basis": "joint",
  "survivor_pct": 66.67,
  "escalation": "level",
  "guarantee_years": 5,
  "frequency": "monthly",
  "timing": "advance",
  "purchase_price": 184912.5,
  "postcode": "TA19 0ZZ",
  "smoker_status": "former",
  "cigs_per_day": 20,
  "years_quit": 11,
  "bmi": 30.9,
  "diabetes_type2": true,
  "hypertension": true,
  "medicines_unmatched": true
}
Result14,876.14VALIDREFER
Calculation trace
  1. Ages and postcode areaTransform
    {{main_dob.age(YY, quote_date)}} → 67{{spouse_dob.age(YY, quote_date)}} → 69{{postcode.postcode(area)}} → "TA"
  2. Health and lifestyle loadsTransform
    true === true ? 2 : 0 → 2true === true ? 1 : 0 → 1"former" === 'current' ? 3 : "former" === 'former' && 11 < 10 ? 1 : 0 → 030.9 > 30 ? 1 : 0 → 1
  3. Health ratingCalculation
    2 + 1 + 0 + 1 = 4
  4. Rated ageCalculation
    min(67 + 4, 100) = 71
  5. Joint life termsTransform
    "joint" === 'joint' ? 66.67 : 0 → 66.67"joint" === 'joint' && 69 > 0 ? 69 - 67 : 0 → 2
  6. Decline rulesDecline rules
    Purchase price under 10,000: not firedApplicant age outside 55 to 85: not firedJoint life without spouse date of birth: not fired
  7. Refer rulesRefer rules
    Medicine with no declared condition: firedHealth rating over 6 years: not firedPurchase price over 1,000,000: not fired
  8. Base annuity ratesLookup table
    enhanced_base_rate = 874.54 · standard_base_rate = 794.8
  9. Postcode area factorLookup table
    postcode_factor = 1.01
  10. Guarantee factorLookup table
    enhanced_guarantee_factor = 0.99 · standard_guarantee_factor = 0.997
  11. Frequency and timing factorLookup table
    payment_factor = 1
  12. Joint life factorLookup table
    joint_factor = 0.92
  13. Enhanced annual incomeCalculation
    round(184,912.5 / 10000 * 874.54 * 1.01 * 0.99 * 1 * 0.92, 2) = 14,876.14
  14. Standard annual incomeCalculation
    round(184,912.5 / 10000 * 794.8 * 1.01 * 0.997 * 1 * 0.92, 2) = 13,615.34
  15. Monthly incomeCalculation
    round(14,876.14 / 12, 2) = 1,239.68
  16. EnhancementCalculation
    round(14,876.14 - 13,615.34, 2) = 1,260.8
From the documents

What the AI client read

The adviser uploaded 5 files. The AI client extracted 41 fields, each linked to the page it came from, then built the quote input above.

FieldValueSource
applicant_nameJudith HalloranApplication form p1
applicant_dob1959-03-14Application form p1
applicant_ni_numberQQ 12 34 56 CApplication form p1
applicant_postcodeTA19 0ZZApplication form p1
applicant_phone07700 900418Application form p1
applicant_emailjudith.halloran@example.comApplication form p1
spouse_namePeter HalloranApplication form p2
spouse_dob1957-08-02Application form p2
spouse_ni_numberQQ 65 43 21 AApplication form p2
height_m1.63Questionnaire p2
weight_kg82Questionnaire p2
bmi30.9Calculated from questionnaire p2
smoker_statusformerQuestionnaire p2
cigs_per_day20Questionnaire p2
years_smoked30Questionnaire p2
year_stopped2015Questionnaire p2
alcohol_units_per_week6Questionnaire p2
diabetes_type2trueQuestionnaire p3
diabetes_diagnosed2019Questionnaire p3
diabetes_medicineMetformin 500 mg twice dailyQuestionnaire p3, prescription
hypertensiontrueQuestionnaire p3
hypertension_medicineRamipril 5 mg dailyQuestionnaire p3, prescription
other_conditions_declarednoneQuestionnaire p3
prescription_item_1Metformin 500 mgPrescription
prescription_item_2Ramipril 5 mgPrescription
prescription_item_3Atorvastatin 20 mg (spouse)Prescription
prescription_date2026-10-21Prescription
medicines_unmatchedtruePrescription against questionnaire p3
pp_providerDemo Personal PensionPension statement PP p1
pp_policy_numberDPP-0048213Pension statement PP p1
pp_fund_value182400Pension statement PP p1
pp_statement_date2026-10-31Pension statement PP p1
pp_transferfullPension statement PP p1
pp_protected_tax_free_cashfalsePension statement PP p2
gpp_providerDemo Workplace Pension TrustWorkplace pension valuation, Summary sheet
gpp_policy_numberDWPT-771905Workplace pension valuation, Summary sheet
gpp_fund_value64150Workplace pension valuation, Summary sheet
gpp_statement_date2026-10-28Workplace pension valuation, Summary sheet
gpp_transferfullWorkplace pension valuation, Summary sheet
gpp_protected_tax_free_cashfalseWorkplace pension valuation, Options sheet
guaranteed_annuity_ratefalsePension statement PP p2, valuation Options sheet
Over MCP

The run_quote call

The AI client prices the case with one tool call and saves it. A referral doesn't stop the price; it flags the case for an underwriter.

Request
{
  "project_reference": "<your Individual Annuity project>",
  "save": true,
  "input": {
    "quote_date": "2026-11-10",
    "main_dob": "1959-03-14",
    "spouse_dob": "1957-08-02",
    "basis": "joint",
    "survivor_pct": 66.67,
    "escalation": "level",
    "guarantee_years": 5,
    "frequency": "monthly",
    "timing": "advance",
    "purchase_price": 184912.5,
    "postcode": "TA19 0ZZ",
    "smoker_status": "former",
    "cigs_per_day": 20,
    "years_quit": 11,
    "bmi": 30.9,
    "diabetes_type2": true,
    "hypertension": true,
    "medicines_unmatched": true
  }
}
Response (abridged)
{
  "dry_run": false,
  "quote_reference": "PTQ-DEMO-24117",
  "valid": true,
  "result": 14876.14,
  "output": {
    "annual_income": 14876.14,
    "monthly_income": 1239.68,
    "standard_annual_income": 13615.34,
    "enhancement": 1260.8,
    "rating_years": 4,
    "basis_version": "demo-basis-2026-11-03",
    "refer": true
  },
  "refers": ["Medicine with no declared condition"]
}

Case history

Data hash 32e276ab
EventTimeActorDetail
Documents received09:02Adviser5 files
Fields extracted09:03Claude · MCP41 fields, each linked to a page
Checks run09:03Ptally1 referral: spouse medicine
Quoted and saved09:04Claude · MCP£14,876.14 a year · demo-basis-2026-11-03
Options compared09:11AdviserRPI £10,939.97 · no guarantee £15,026.41
Referral cleared09:20UnderwriterHigh cholesterol confirmed, no change
Pack generated09:26PtallyData hash 32e276ab
Sent for signature09:31AdviserApplicant, spouse, adviser