A joint life annuity priced from a scanned health form, a prescription and two pension statements, with a referral for an unmatched medicine.
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.
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.
| Field | Label | Type | Default | Allowed values |
|---|---|---|---|---|
quote_date | Quote date | date | 2026-11-10 | - |
main_dob | Applicant date of birth | date | 1961-06-01 | - |
spouse_dob | Spouse date of birth | string | - | |
basis | Single or joint life | string | single | single, joint |
survivor_pct | Survivor's income | decimal | 66.67 | 50, 66.67, 100 |
escalation | Escalation | string | level | level, rpi, fixed3 |
guarantee_years | Guarantee period | integer | 0 | 0, 5, 10 |
frequency | Payment frequency | string | monthly | monthly, quarterly, annual |
timing | Payment timing | string | advance | advance, arrears |
purchase_price | Purchase price | decimal | 100000 | - |
postcode | Postcode | string | EX1 1AA | - |
smoker_status | Smoking | string | never | never, former, current |
cigs_per_day | Cigarettes a day | integer | 0 | - |
years_quit | Years since stopping | integer | 0 | - |
bmi | Body mass index | decimal | 25 | - |
diabetes_type2 | Type 2 diabetes | boolean | false | - |
hypertension | High blood pressure | boolean | false | - |
medicines_unmatched | Medicine with no declared condition | boolean | false | - |
Steps run in order. Each one can use the inputs and the outputs of every step before it.
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": ""
}
}
}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
}
}
}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}}"
}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)"
}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
}
}
}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
}
]
}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
}
]
}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
}
}
}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
}
}
}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
}
}
}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
}
}
}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
}
}
}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)"
}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)"
}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)"
}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)"
}This quote was priced by the Ptally rating engine. The trace below follows it through every step to the final result.
{
"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
}{{main_dob.age(YY, quote_date)}} → 67{{spouse_dob.age(YY, quote_date)}} → 69{{postcode.postcode(area)}} → "TA"true === true ? 2 : 0 → 2true === true ? 1 : 0 → 1"former" === 'current' ? 3 : "former" === 'former' && 11 < 10 ? 1 : 0 → 030.9 > 30 ? 1 : 0 → 12 + 1 + 0 + 1 = 4min(67 + 4, 100) = 71"joint" === 'joint' ? 66.67 : 0 → 66.67"joint" === 'joint' && 69 > 0 ? 69 - 67 : 0 → 2Purchase price under 10,000: not firedApplicant age outside 55 to 85: not firedJoint life without spouse date of birth: not firedMedicine with no declared condition: firedHealth rating over 6 years: not firedPurchase price over 1,000,000: not firedenhanced_base_rate = 874.54 · standard_base_rate = 794.8postcode_factor = 1.01enhanced_guarantee_factor = 0.99 · standard_guarantee_factor = 0.997payment_factor = 1joint_factor = 0.92round(184,912.5 / 10000 * 874.54 * 1.01 * 0.99 * 1 * 0.92, 2) = 14,876.14round(184,912.5 / 10000 * 794.8 * 1.01 * 0.997 * 1 * 0.92, 2) = 13,615.34round(14,876.14 / 12, 2) = 1,239.68round(14,876.14 - 13,615.34, 2) = 1,260.8The adviser uploaded 5 files. The AI client extracted 41 fields, each linked to the page it came from, then built the quote input above.
| Field | Value | Source |
|---|---|---|
applicant_name | Judith Halloran | Application form p1 |
applicant_dob | 1959-03-14 | Application form p1 |
applicant_ni_number | QQ 12 34 56 C | Application form p1 |
applicant_postcode | TA19 0ZZ | Application form p1 |
applicant_phone | 07700 900418 | Application form p1 |
applicant_email | judith.halloran@example.com | Application form p1 |
spouse_name | Peter Halloran | Application form p2 |
spouse_dob | 1957-08-02 | Application form p2 |
spouse_ni_number | QQ 65 43 21 A | Application form p2 |
height_m | 1.63 | Questionnaire p2 |
weight_kg | 82 | Questionnaire p2 |
bmi | 30.9 | Calculated from questionnaire p2 |
smoker_status | former | Questionnaire p2 |
cigs_per_day | 20 | Questionnaire p2 |
years_smoked | 30 | Questionnaire p2 |
year_stopped | 2015 | Questionnaire p2 |
alcohol_units_per_week | 6 | Questionnaire p2 |
diabetes_type2 | true | Questionnaire p3 |
diabetes_diagnosed | 2019 | Questionnaire p3 |
diabetes_medicine | Metformin 500 mg twice daily | Questionnaire p3, prescription |
hypertension | true | Questionnaire p3 |
hypertension_medicine | Ramipril 5 mg daily | Questionnaire p3, prescription |
other_conditions_declared | none | Questionnaire p3 |
prescription_item_1 | Metformin 500 mg | Prescription |
prescription_item_2 | Ramipril 5 mg | Prescription |
prescription_item_3 | Atorvastatin 20 mg (spouse) | Prescription |
prescription_date | 2026-10-21 | Prescription |
medicines_unmatched | true | Prescription against questionnaire p3 |
pp_provider | Demo Personal Pension | Pension statement PP p1 |
pp_policy_number | DPP-0048213 | Pension statement PP p1 |
pp_fund_value | 182400 | Pension statement PP p1 |
pp_statement_date | 2026-10-31 | Pension statement PP p1 |
pp_transfer | full | Pension statement PP p1 |
pp_protected_tax_free_cash | false | Pension statement PP p2 |
gpp_provider | Demo Workplace Pension Trust | Workplace pension valuation, Summary sheet |
gpp_policy_number | DWPT-771905 | Workplace pension valuation, Summary sheet |
gpp_fund_value | 64150 | Workplace pension valuation, Summary sheet |
gpp_statement_date | 2026-10-28 | Workplace pension valuation, Summary sheet |
gpp_transfer | full | Workplace pension valuation, Summary sheet |
gpp_protected_tax_free_cash | false | Workplace pension valuation, Options sheet |
guaranteed_annuity_rate | false | Pension statement PP p2, valuation Options sheet |
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.
{
"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
}
}{
"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"]
}| Event | Time | Actor | Detail |
|---|---|---|---|
| Documents received | 09:02 | Adviser | 5 files |
| Fields extracted | 09:03 | Claude · MCP | 41 fields, each linked to a page |
| Checks run | 09:03 | Ptally | 1 referral: spouse medicine |
| Quoted and saved | 09:04 | Claude · MCP | £14,876.14 a year · demo-basis-2026-11-03 |
| Options compared | 09:11 | Adviser | RPI £10,939.97 · no guarantee £15,026.41 |
| Referral cleared | 09:20 | Underwriter | High cholesterol confirmed, no change |
| Pack generated | 09:26 | Ptally | Data hash 32e276ab |
| Sent for signature | 09:31 | Adviser | Applicant, spouse, adviser |
Send a rate table or a sample basis. We'll build it in Ptally and show a synthetic case priced end to end in your AI client.