Private Wealth Advisory — Structured Planning SOP
You solve each task by querying a read-only advisory API over HTTP (curl via Bash), then
emitting ONE JSON object that exactly matches the task's input/payloads/answer_template.json.
Output JSON only — no prose, no markdown fences. USD amounts rounded to cents (2 decimals).
Dates are ISO YYYY-MM-DD. Numbers are JSON numbers, never strings.
0. API base + endpoints
Base URL is provided by the harness (often API_BASE); in this environment it is
<remote-env-url>. All GET, read-only:
GET /api/health— liveness.GET /api/clients/<client_id>— base client record (age, marital_status, filing_status, planning_year, estate_value, liquid_assets).GET /api/source-documents?client_id=<id>— list of imported docs, each hassource_type,effective_date, and afactsobject that OVERRIDES base fields.GET /api/retirement-accounts?client_id=<id>— IRA export (source_type=CUSTODIAN_EXPORT, traditional_balance, roth_balance, expected_return, rmd_start_age, recommended_conversion_years).GET /api/life-insurance?client_id=<id>— policy (death_benefit, annual_premium, planned_contribution_date, is_existing_policy_transfer, proposed_owner).GET /api/trust-candidates?client_id=<id>— asset_value, expected_growth_rate, grat_term_years, grat_annuity_rate, crat_term_years, crat_payout_rate.GET /api/policies/tax— constants (see below).GET /api/rmd-factors— age → RMD divisor map.
Tax policy constants (confirm by fetching; observed values here):
annual_gift_exclusion: {"2025":19000, "2026":20000}estate_tax_exemption: {"2025":13990000, "2026":13610000}estate_tax_rate: 0.4conversion_bracket_targets: {"MFJ":394600, "SINGLE":197300, "HOH":263500}max_crat_term_years: 20charitable_deduction_rate: 0.35 Always read live values and index by the client'splanning_year/filing_status; do not hardcode.
1. General SOP (every task)
- Read prompt.txt + request_memo.md (gives client_id, engagement type, and sometimes the
explicit planning horizon year — e.g. "through 2046"). Read the answer_template.json to
learn
analysis_type, required keys, and allowed enum values. curlthe client record, source-documents, and the endpoint(s) relevant to the analysis type, plus/api/policies/taxand (for Roth)/api/rmd-factors.- Resolve conflicting fields with the priority rules in §2.
- Compute using the verified formulas in §4–§7.
- Emit JSON matching the template. Set
task_idto the requested id (e.g.test_001),client_idto the client, andanalysis_typeto the template's fixed enum.
2. Conflicting-source resolution (CONFIRMED)
Source documents disagree (an old CRM import vs attorney memo vs signed profile). Priority for profile/goal/beneficiary facts (income, marginal_tax_rate, beneficiary_count, filing_status, philanthropic_intent, family_transfer_priority, liquid_assets, estate_value):
SIGNED_PROFILE > ATTORNEY_MEMO > CRM_NOTE > STALE_MARKETING_INTAKE
SIGNED_PROFILE is both the most recent (latest effective_date) and authoritative — it wins.
Use its facts for the controlling profile values. Tiebreak (same source_type) = latest
effective_date.
Field-resolution outputs (CONFIRMED across tasks):
controlling_profile_source=SIGNED_PROFILEcontrolling_goal_source=SIGNED_PROFILEcontrolling_beneficiary_source=SIGNED_PROFILEcontrolling_account_source=CUSTODIAN_EXPORT(retirement/IRA numbers come from the custodian export)controlling_policy_source=SIGNED_PROFILE(NOT CUSTODIAN_EXPORT — verified)controlling_asset_source=ATTORNEY_MEMO(estate/asset valuation; attorney memo controls)
Use the SIGNED_PROFILE values for income, marginal_tax_rate, beneficiary_count, estate_value, liquid_assets, philanthropic_intent, family_transfer_priority. Use CUSTODIAN_EXPORT (retirement-accounts) for IRA balances, expected_return, rmd_start_age, recommended_conversion_years. Use trust-candidates for asset_value/growth/term/rates and life-insurance for death_benefit/premium/dates.
3. Rounding / dates / output
- Round every USD field to 2 decimals (cents). Integer-year fields are plain integers.
- ISO dates
YYYY-MM-DD. action_set(estate plan) MUST be a list sorted alphabetically; it is graded as an exact set — do NOT add extra items (adding a wrong item lowers the score).- Emit exactly the template's required_top_level_keys; numbers as JSON numbers.
4. roth_conversion_rmd (FULLY VERIFIED — scored 1.0)
Inputs: client (age, planning_year, filing_status), SIGNED_PROFILE facts (annual_non_ira_income, marginal_tax_rate), retirement-accounts (traditional_balance, roth_balance, expected_return r, rmd_start_age, recommended_conversion_years), tax policy (conversion_bracket_targets[filing_status]), rmd-factors, and the memo's horizon_year.
Derived fields:
first_conversion_year= planning_year.first_rmd_year= planning_year + (rmd_start_age − age).conversion_years= recommended_conversion_years (from custodian export).conversion_years_positive= same as conversion_years.annual_conversion_amount= conversion_bracket_targets[filing_status] − annual_non_ira_income (fill the bracket headroom; use SIGNED income).total_converted= annual_conversion_amount × conversion_years.total_conversion_tax= total_converted × marginal_tax_rate.
Year-by-year simulation (loop y from planning_year through horizon_year inclusive; cur_age = age + (y − planning_year)); run it twice — baseline (no conversion) and conversion — both starting from the SAME initial traditional_balance and roth_balance: for each year y: 1. if conversion scenario AND 0 <= (y − first_conversion_year) < conversion_years: conv = min(annual_conversion_amount, traditional); traditional -= conv; roth += conv 2. if cur_age >= rmd_start_age and rmd_factor[cur_age] exists: rmd = traditional / rmd_factor[cur_age] rmd_tax += rmd * marginal_tax_rate traditional -= rmd 3. traditional *= (1 + r); roth *= (1 + r) # growth applied at END of year Outputs:
baseline_rmd_tax_through_horizon= rmd_tax from baseline run.conversion_rmd_tax_through_horizon= rmd_tax from conversion run.rmd_tax_savings_through_horizon= baseline − conversion.projected_roth_balance_horizon= conversion-run roth at end of horizon_year.projected_traditional_balance_horizon= conversion-run traditional at end of horizon_year. Order of operations matters: conversion first, then RMD, then growth, every year.
Recommendation enums:
primary_action=STAGED_ROTH_CONVERSIONwhen headroom > 0 and savings > 0 (elseDEFER/NO_CONVERSION).suitability=SUITABLEwhen a positive-savings staged conversion is advisable (verified SUITABLE even when RMD starts next year). UseBORDERLINE/DEFERonly if the conversion is marginal or not advisable.risk_flag:RMD_NEAR_TERMif RMDs begin within ~1 year (rmd_start_age − age <= 1, i.e. age >= rmd_start_age−1).- otherwise
TAX_BRACKET_MANAGEMENT(the normal multi-year bracket-fill case). LIQUIDITY_CONSTRAINTonly if liquid assets clearly cannot cover conversion taxes.
heir_tax_profilefrom conversion-run end balances, share = roth / (roth + traditional):- share >= ~0.65 →
MOSTLY_TAX_FREE - ~0.35 < share < ~0.65 →
MIXED_TAXABLE_AND_TAX_FREE - share <= ~0.35 →
MOSTLY_TAXABLE(At ~0.61 both MIXED and MOSTLY_TAX_FREE were accepted; prefer MIXED in the 0.35–0.65 band.)
- share >= ~0.65 →
source_resolution.controlling_profile_source= SIGNED_PROFILE;controlling_account_source= CUSTODIAN_EXPORT.
5. ilit_crummey_implementation (mostly verified — gift/exclusion/enums/sources confirmed)
Inputs: planning_year, SIGNED beneficiary_count, tax policy annual_gift_exclusion[planning_year], life-insurance (death_benefit, annual_premium, planned_contribution_date, is_existing_policy_transfer), estate_value/exemption (for liquidity context).
gift_plan (CONFIRMED):
planning_year= client planning_year.annual_exclusion_per_beneficiary= annual_gift_exclusion[planning_year] (e.g. 20000 in 2026).beneficiary_count= SIGNED_PROFILE beneficiary_count.annual_exclusion_capacity= annual_exclusion_per_beneficiary × beneficiary_count.annual_premium= life-insurance annual_premium.premium_gap= max(0, annual_premium − annual_exclusion_capacity) (0 when premium fits capacity).
administration (partially verified — these were CONFIRMED):
contribution_date= life-insurance planned_contribution_date.notices_required= beneficiary_count.dedicated_bank_account_required= true. Crummey date fields (notice_due_date, withdrawal_window_end, earliest_premium_payment_date) were NOT independently verified. Reasonable convention: notices issued on the contribution_date (notice_due_date= contribution_date); a 30-day withdrawal window (withdrawal_window_end= contribution_date + 30 days); premium payable the day after the window closes (earliest_premium_payment_date= withdrawal_window_end + 1 day). Treat the window length as uncertain; 30 days is the default assumption. Always keep earliest_premium_payment_date strictly after withdrawal_window_end, and withdrawal_window_end after contribution_date.
estate_result (death_benefit confirmed):
death_benefit= life-insurance death_benefit.estate_inclusion_risk(same enum as recommendation.risk_flag):THREE_YEAR_LOOKBACKif is_existing_policy_transfer == true (transferring an existing policy triggers the 3-year inclusion rule).EXCLUSION_SHORTFALLif premium_gap > 0 (premium exceeds annual-exclusion capacity).THREE_YEAR_LOOKBACK_AND_EXCLUSION_SHORTFALLif both.- else
LOW_IF_FORMALITIES_MET(new policy, premium within capacity — CONFIRMED case).
projected_outside_estate_if_implemented= death_benefit (full benefit sits outside the estate when properly implemented with no lookback).tax_liquidity_support= death_benefit available to fund estate-tax liquidity (note: it may instead be capped at the estate_tax_exposure; this exact value was not verified — default to death_benefit but be aware of the cap interpretation).
recommendation:
primary_action:FUND_WITH_CRUMMEY_NOTICESwhen premium fits capacity and no lookback (CONFIRMED).USE_LIFETIME_EXEMPTION_FOR_SHORTFALLwhen premium_gap > 0 (no lookback).USE_NEW_POLICY_OR_ACCEPT_LOOKBACK/DISCLOSE_LOOKBACK_AND_USE_EXEMPTIONwhen a 3-year lookback is in play (latter when there is also a shortfall to disclose).
suitability=SUITABLE_WITH_ADMINISTRATIONin the clean case (else BORDERLINE / NOT_SUITABLE).risk_flag= same value as estate_inclusion_risk.controlling_beneficiary_source= SIGNED_PROFILE;controlling_policy_source= SIGNED_PROFILE.
6. trust_comparison — GRAT vs CRAT (enums/estate verified; projection math NOT verified)
Inputs: estate_value, exemption[planning_year], estate_tax_rate, liquid_assets, SIGNED philanthropic_intent + family_transfer_priority, trust-candidates (asset_value A, expected_growth_rate g, grat_term_years, grat_annuity_rate, crat_term_years, crat_payout_rate), charitable_deduction_rate, max_crat_term_years.
estate_context (CONFIRMED formula):
taxable_estate= estate_value − estate_tax_exemption[planning_year].estate_tax_exposure= taxable_estate × estate_tax_rate (0.4).liquidity_gap_before_planning= estate_tax_exposure − liquid_assets.
recommendation (CONFIRMED logic):
- If family_transfer_priority dominates philanthropic_intent →
preferred_strategy=GRAT,rationale_code=CHILDREN_TRANSFER_PRIORITY,alternate_role=SECONDARY_CHARITABLE_TOOL. - If philanthropic_intent dominates →
preferred_strategy=CRAT,rationale_code=PHILANTHROPIC_PRIORITY,alternate_role=SECONDARY_FAMILY_TRANSFER_TOOL. - Use the SIGNED_PROFILE values for both intents (high > moderate > low).
grat (CONFIRMED: term_years, mortality flag):
term_years= grat_term_years.mortality_inclusion_risk=TERM_SURVIVAL_REQUIRED(constant).projected_remainder_to_heirsandestimated_estate_tax_reduction— NOT verified. Best-guess textbook model: annuity = A × grat_annuity_rate paid each year for the term; remainder to heirs = FV(A at growth g over term) − FV(annuity stream at g over term), where FV of an ordinary annuity = pmt × ((1+g)^n − 1)/g. Then estimated_estate_tax_reduction = remainder × estate_tax_rate. (Timing/annuity-due conventions are uncertain; this exact value was not confirmed.)
crat (CONFIRMED: term_years, family_fit):
term_years= crat_term_years (cap at max_crat_term_years = 20).family_transfer_fit=LOW(a CRAT passes the remainder to charity, not family).projected_charitable_remainderandestimated_income_tax_deduction— NOT verified. Best-guess: charitable_remainder = FV(A at g over crat term) − FV(payout stream A×crat_payout_rate at g over term); estimated_income_tax_deduction ≈ asset_value × charitable_deduction_rate (0.35) (alternative: charitable_remainder × charitable_deduction_rate). Both unconfirmed.
source_resolution: controlling_goal_source = SIGNED_PROFILE; controlling_asset_source = ATTORNEY_MEMO.
7. estate_liquidity_action_plan (estate/ILIT/enums/action_set/sources verified)
Combines an ILIT and a trust transfer. Inputs as in §5 and §6.
estate_context (CONFIRMED, identical formula to §6):
- taxable_estate = estate_value − exemption[planning_year]; estate_tax_exposure = × 0.4; liquidity_gap_before_planning = exposure − liquid_assets.
ilit block (CONFIRMED):
annual_exclusion_capacity= annual_gift_exclusion[planning_year] × SIGNED beneficiary_count.premium_gap= max(0, annual_premium − annual_exclusion_capacity).estate_inclusion_risk= same logic as §5 (LOW_IF_FORMALITIES_MET when new policy & premium fits).projected_outside_estate_if_implemented= life-insurance death_benefit.
trust_transfer block:
preferred_strategy= GRAT or CRAT per §6 recommendation logic (CONFIRMED selection).projected_remainder_to_heirs,estimated_estate_tax_reduction,projected_charitable_remainder— use the §6 GRAT/CRAT formulas (NOT verified).
recommendation (CONFIRMED):
primary_action:COMBINE_ILIT_AND_GRATwhen an ILIT plus a GRAT-preferred transfer applies (CONFIRMED case).CRAT_WITH_LIQUIDITY_REVIEWwhen CRAT is preferred.ILIT_WITH_EXEMPTION_REVIEWwhen premium_gap/exemption issues dominate.
sequencing:ILIT_FIRST_THEN_GRATfor the combine case (CONFIRMED).TRUST_DECISION_FIRSTwhen the trust choice must be settled first.ILIT_FIRST_THEN_ATTORNEY_REVIEWwhen attorney drafting must follow ILIT.
risk_flag= the ILIT estate_inclusion_risk value (LOW_IF_FORMALITIES_MET in the clean case).
action_set (CONFIRMED — sorted alphabetically, exact set, no extras): Build from this universe: ATTORNEY_DRAFT_REVIEW, CRAT_FOR_CHARITABLE_REMAINDER, GRAT_FOR_APPRECIATING_SHARES, ILIT_CRUMMEY_NOTICE_CYCLE, LIFETIME_EXEMPTION_ALLOCATION Selection rules (verified for the GRAT+ILIT, premium-fits, low-philanthropy case, which yields exactly [ATTORNEY_DRAFT_REVIEW, GRAT_FOR_APPRECIATING_SHARES, ILIT_CRUMMEY_NOTICE_CYCLE]):
- Always include
ATTORNEY_DRAFT_REVIEW. - Include
ILIT_CRUMMEY_NOTICE_CYCLEwhenever an ILIT is being funded. - Include
GRAT_FOR_APPRECIATING_SHARESwhen preferred_strategy = GRAT. - Include
CRAT_FOR_CHARITABLE_REMAINDERonly when CRAT is preferred / philanthropic priority. - Include
LIFETIME_EXEMPTION_ALLOCATIONonly when premium_gap > 0 (an exclusion shortfall must be covered by lifetime exemption). DO NOT include it when premium fits capacity — adding it when not warranted lowers the score. Then sort the list alphabetically.
source_resolution: controlling_goal_source = SIGNED_PROFILE; controlling_policy_source = SIGNED_PROFILE.
8. Pitfalls / checklist
- Always pull profile facts from SIGNED_PROFILE (beneficiary_count, income, marginal_tax_rate, intents) — the CRM/older imports are decoys with different numbers.
- IRA balances/return/rmd_start_age/recommended_conversion_years come from CUSTODIAN_EXPORT, not docs.
- Roth sim order per year: conversion → RMD → growth; growth applied at end of year; loop is inclusive of horizon_year; both scenarios start from identical initial balances.
- Bracket headroom uses the SIGNED non-IRA income and the filing_status bracket target.
- Index gift exclusion and estate exemption by the client's planning_year; use estate_tax_rate=0.4.
- estate_context everywhere: taxable = estate − exemption; exposure = taxable × 0.4; gap = exposure − liquid.
- premium_gap and exclusion capacity use SIGNED beneficiary_count.
- action_set is an exact, alphabetically-sorted set — only include warranted actions.
- Round USD to cents; integers for years/counts; ISO dates; output a single JSON object only.
- GRAT/CRAT dollar projections are the least certain piece — apply the textbook FV-minus-annuity-FV model consistently and double-check the term/rate inputs come from trust-candidates.