FEBUX-CV: Transparent Febuxostat Cardiovascular Risk-Context Stratification Before or During Urate-Lowering Therapy
FEBUX-CV: Transparent Febuxostat Cardiovascular Risk-Context Stratification Before or During Urate-Lowering Therapy
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
Abstract
Febuxostat is an important urate-lowering option when allopurinol is not tolerated, contraindicated, or ineffective, but cardiovascular safety remains a real bedside concern in patients with gout and high cardiac comorbidity. We present FEBUX-CV, a transparent executable skill for cardiovascular risk-context stratification before or during febuxostat exposure. The tool integrates established atherosclerotic cardiovascular disease (ASCVD), symptomatic heart failure, prior stroke or transient ischemic attack, recent acute coronary syndrome, renal function, diabetes, age, smoking status, whether a true allopurinol barrier exists, and whether a safer urate-lowering alternative is available. Each domain contributes a visible weighted component, and the Python implementation includes Monte Carlo uncertainty estimation. Demonstration scenarios show separation between a low-risk patient without major cardiovascular disease, a higher-risk older CKD patient with prior myocardial infarction and diabetes, and a very high-risk profile with recent acute coronary syndrome and symptomatic heart failure. FEBUX-CV is intended to support shared decision-making, documentation of therapeutic necessity, and safer escalation of urate-lowering therapy in cardiometabolically complex gout. It does not replace cardiology assessment, guideline interpretation, or urgent management of active ischemic syndromes.
Keywords: febuxostat, gout, cardiovascular safety, heart failure, ASCVD, hyperuricemia, clinical decision support, DeSci
1. Clinical problem
The practical question around febuxostat is no longer whether it lowers urate. It does. The harder clinical question is when its use is acceptable in a patient with meaningful cardiovascular disease. Since CARES raised concern and FAST complicated the interpretation, clinicians are left with an uncomfortable middle ground: febuxostat is neither universally unsafe nor automatically benign. The real decision depends on cardiovascular context and therapeutic necessity.
FEBUX-CV was built for that decision point.
2. Methodology
2.1 Design principles
The score follows five principles:
- Established cardiac disease should dominate, especially ASCVD, heart failure, stroke history, and recent acute coronary syndromes.
- Therapeutic necessity matters, because documented inability to use allopurinol changes the threshold for acceptable febuxostat exposure.
- Competing alternatives matter, because available safer options should lower tolerance for cardiovascular ambiguity.
- The output must stay transparent, so every domain can be reviewed clinically.
- The score should express context, not pretend causality, because the evidence base remains mixed.
2.2 Domains and weights
| Domain | Weight | Rationale |
|---|---|---|
| Established ASCVD | 0.23 | Major baseline cardiovascular substrate |
| Symptomatic heart failure | 0.20 | High-risk clinical state and major caution context |
| Prior stroke/TIA | 0.10 | Signals diffuse vascular vulnerability |
| Recent ACS (<12 months) | 0.12 | Time-sensitive unstable cardiovascular context |
| Renal function | 0.10 | CKD clusters with higher vascular risk and treatment complexity |
| Diabetes | 0.06 | Major cardiometabolic amplifier |
| Age | 0.05 | Older age often narrows safety margin |
| Smoking | 0.04 | Persistent modifiable vascular stressor |
| Allopurinol intolerance/contraindication | 0.05 | Lowers risk-context score because febuxostat need is stronger |
| Safer alternative available | 0.05 | Raises concern because acceptable ambiguity decreases |
The final composite score is clamped to 0-100.
2.3 Uncertainty estimation
The implementation performs 5,000 Monte Carlo simulations with small perturbations to eGFR, age, and a few workflow assumptions. The goal is to keep the output honest about uncertainty rather than implying false exactness.
3. Executable skill
3.1 Python code
The full standalone implementation is stored in skills/febux-cv/febux_cv.py and runs with only the Python standard library.
#!/usr/bin/env python3
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class FebuxPatient:
established_ascvd: bool = False
symptomatic_heart_failure: bool = False
prior_stroke_tia: bool = False
recent_acs_within_12_months: bool = False
egfr: int = 90
diabetes: bool = False
age: int = 55
current_smoker: bool = False
allopurinol_intolerant_or_contraindicated: bool = False
safer_urate_lowering_alternative_available: bool = True
@dataclass
class DomainScore:
name: str
score: float
weight: float
weighted: float
detail: str
@dataclass
class FebuxCvResult:
composite_score: float
risk_category: str
recommendation: str
safety_alert: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
WEIGHTS = {
"ascvd": 0.23,
"heart_failure": 0.20,
"stroke": 0.10,
"recent_acs": 0.12,
"renal": 0.10,
"diabetes": 0.06,
"age": 0.05,
"smoking": 0.04,
"allopurinol_barrier": 0.05,
"alternative": 0.05,
}3.2 Demo output
=== Scenario 1 — Low-risk gout patient without major cardiovascular disease ===
Composite score: 0.0/100
Risk category: LOW
=== Scenario 2 — Older CKD patient with prior MI and diabetes ===
Composite score: 22.7/100
Risk category: INTERMEDIATE
=== Scenario 3 — Very high-risk patient with recent ACS and symptomatic heart failure ===
Composite score: 57.7/100
Risk category: VERY HIGH4. Interpretation
- LOW (<10): febuxostat is usually acceptable if clinically indicated and therapeutic necessity is documented.
- INTERMEDIATE (10-24.9): review cardiovascular history carefully and make the reason for febuxostat explicit.
- HIGH (25-39.9): only proceed after deliberate risk-benefit review and tighter follow-up.
- VERY HIGH (≥40): avoid casual initiation or continuation; revisit alternatives and involve specialist review.
5. Clinical limitations
- This is a transparent context score, not a causal prediction model.
- It does not prove febuxostat will trigger a cardiovascular event in any individual patient.
- It does not replace CARES, FAST, or guideline interpretation.
- Active chest pain, decompensated heart failure, or acute neurologic symptoms require urgent evaluation regardless of score.
- Local practice patterns and regulatory warnings differ across health systems.
6. Why this solves a real problem
In gout care, cardiovascular context is often buried in old problem lists while the urate-lowering plan moves forward automatically. FEBUX-CV forces the prescriber to surface the cardiac substrate, assess therapeutic necessity, and document why febuxostat remains acceptable—or not.
References
- White WB, Saag KG, Becker MA, et al. Cardiovascular Safety of Febuxostat or Allopurinol in Patients with Gout. N Engl J Med. 2018;378(13):1200-1210. DOI: 10.1056/NEJMoa1710895
- Mackenzie IS, Ford I, Nuki G, et al. Febuxostat versus Allopurinol for Cardiovascular Outcomes in Patients with Gout (FAST): a multicentre, prospective, randomised, open-label, non-inferiority trial. Lancet. 2020;396(10264):1745-1757. DOI: 10.1016/S0140-6736(20)32234-0
- FitzGerald JD, Dalbeth N, Mikuls T, et al. 2020 American College of Rheumatology Guideline for the Management of Gout. Arthritis Care Res (Hoboken). 2020;72(6):744-760. DOI: 10.1002/acr.24180
Full executable Python skill
#!/usr/bin/env python3
"""
FEBUX-CV: febuxostat cardiovascular risk-context stratification before or
during urate-lowering therapy.
Authors: Dr. Erick Zamora-Tehozol, DNAI, RheumaAI
ORCID: 0000-0002-7888-3961
License: MIT
Clinical purpose:
Estimate cardiovascular concern when febuxostat is being considered or
continued in gout/hyperuricemia patients with meaningful cardiac comorbidity.
The goal is to make established ASCVD, heart failure, recent ischemic events,
CKD, and availability of alternatives explicit at the bedside.
This score does NOT replace cardiology evaluation, formal guideline review, or
urgent management of active coronary/cerebrovascular syndromes.
Key references:
- White WB, Saag KG, Becker MA, et al. N Engl J Med. 2018.
DOI: 10.1056/NEJMoa1710895
- Mackenzie IS, Ford I, Nuki G, et al. Lancet. 2020.
DOI: 10.1016/S0140-6736(20)32234-0
- FitzGerald JD, Dalbeth N, Mikuls T, et al. Arthritis Care Res (Hoboken). 2020.
DOI: 10.1002/acr.24180
"""
from __future__ import annotations
import random
from dataclasses import dataclass, field
from typing import List
@dataclass
class FebuxPatient:
established_ascvd: bool = False
symptomatic_heart_failure: bool = False
prior_stroke_tia: bool = False
recent_acs_within_12_months: bool = False
egfr: int = 90
diabetes: bool = False
age: int = 55
current_smoker: bool = False
allopurinol_intolerant_or_contraindicated: bool = False
safer_urate_lowering_alternative_available: bool = True
@dataclass
class DomainScore:
name: str
score: float
weight: float
weighted: float
detail: str
@dataclass
class FebuxCvResult:
composite_score: float
risk_category: str
recommendation: str
safety_alert: str
ci_lower: float
ci_upper: float
domains: List[dict]
notes: List[str] = field(default_factory=list)
WEIGHTS = {
"ascvd": 0.23,
"heart_failure": 0.20,
"stroke": 0.10,
"recent_acs": 0.12,
"renal": 0.10,
"diabetes": 0.06,
"age": 0.05,
"smoking": 0.04,
"allopurinol_barrier": 0.05,
"alternative": 0.05,
}
def score_binary(flag: bool, if_true: int, yes: str, no: str):
return (if_true, yes) if flag else (0, no)
def score_renal(egfr: int):
if egfr >= 90:
return 0, f"eGFR {egfr} mL/min/1.73m²"
if egfr >= 60:
return 10, f"eGFR {egfr} mL/min/1.73m²"
if egfr >= 45:
return 22, f"eGFR {egfr} mL/min/1.73m²"
if egfr >= 30:
return 34, f"eGFR {egfr} mL/min/1.73m²"
return 46, f"eGFR {egfr} mL/min/1.73m²"
def score_age(age: int):
if age < 50:
return 0, f"Age {age} years"
if age < 65:
return 10, f"Age {age} years"
if age < 75:
return 18, f"Age {age} years"
return 28, f"Age {age} years"
def score_allopurinol_barrier(flag: bool):
if flag:
return -22, "Allopurinol intolerance/contraindication documented, so febuxostat need is stronger"
return 10, "No strong allopurinol barrier documented"
def score_alternative(flag: bool):
if flag:
return 16, "A safer urate-lowering alternative remains available"
return -8, "No clearly safer urate-lowering alternative available"
def compute_febux_cv(patient: FebuxPatient, n_simulations: int = 5000, seed: int = 42) -> FebuxCvResult:
items = [
("ascvd", score_binary(patient.established_ascvd, 68, "Established ASCVD present", "No established ASCVD documented")),
("heart_failure", score_binary(patient.symptomatic_heart_failure, 82, "Symptomatic heart failure present", "No symptomatic heart failure documented")),
("stroke", score_binary(patient.prior_stroke_tia, 48, "Prior stroke/TIA documented", "No prior stroke/TIA documented")),
("recent_acs", score_binary(patient.recent_acs_within_12_months, 92, "Recent ACS within 12 months", "No recent ACS within 12 months")),
("renal", score_renal(patient.egfr)),
("diabetes", score_binary(patient.diabetes, 24, "Diabetes present", "No diabetes documented")),
("age", score_age(patient.age)),
("smoking", score_binary(patient.current_smoker, 28, "Current smoker", "Not a current smoker")),
("allopurinol_barrier", score_allopurinol_barrier(patient.allopurinol_intolerant_or_contraindicated)),
("alternative", score_alternative(patient.safer_urate_lowering_alternative_available)),
]
domains: List[DomainScore] = []
composite = 0.0
for name, (raw, detail) in items:
weight = WEIGHTS[name]
weighted = raw * weight
composite += weighted
domains.append(DomainScore(name, round(raw, 1), weight, round(weighted, 1), detail))
composite = round(max(0.0, min(composite, 100.0)), 1)
rng = random.Random(seed)
sims: List[float] = []
for _ in range(n_simulations):
noisy = FebuxPatient(
established_ascvd=patient.established_ascvd,
symptomatic_heart_failure=patient.symptomatic_heart_failure,
prior_stroke_tia=patient.prior_stroke_tia,
recent_acs_within_12_months=patient.recent_acs_within_12_months,
egfr=max(10, int(round(patient.egfr + rng.gauss(0, 4)))),
diabetes=patient.diabetes,
age=max(18, int(round(patient.age + rng.gauss(0, 2)))),
current_smoker=patient.current_smoker if rng.random() > 0.01 else not patient.current_smoker,
allopurinol_intolerant_or_contraindicated=patient.allopurinol_intolerant_or_contraindicated if rng.random() > 0.01 else not patient.allopurinol_intolerant_or_contraindicated,
safer_urate_lowering_alternative_available=patient.safer_urate_lowering_alternative_available if rng.random() > 0.01 else not patient.safer_urate_lowering_alternative_available,
)
noisy_items = [
("ascvd", score_binary(noisy.established_ascvd, 68, "", "")),
("heart_failure", score_binary(noisy.symptomatic_heart_failure, 82, "", "")),
("stroke", score_binary(noisy.prior_stroke_tia, 48, "", "")),
("recent_acs", score_binary(noisy.recent_acs_within_12_months, 92, "", "")),
("renal", score_renal(noisy.egfr)),
("diabetes", score_binary(noisy.diabetes, 24, "", "")),
("age", score_age(noisy.age)),
("smoking", score_binary(noisy.current_smoker, 28, "", "")),
("allopurinol_barrier", score_allopurinol_barrier(noisy.allopurinol_intolerant_or_contraindicated)),
("alternative", score_alternative(noisy.safer_urate_lowering_alternative_available)),
]
total = sum(score * WEIGHTS[name] for name, (score, _) in noisy_items)
sims.append(max(0.0, min(total, 100.0)))
sims.sort()
ci_lower = round(sims[int(0.025 * n_simulations)], 1)
ci_upper = round(sims[int(0.975 * n_simulations)], 1)
if composite < 10:
category = "LOW"
recommendation = "Febuxostat can usually be considered if clinically indicated, while still documenting baseline cardiovascular history and shared decision-making."
alert = "Low score does not prove cardiovascular neutrality; it only indicates absence of major bedside red flags in this framework."
elif composite < 25:
category = "INTERMEDIATE"
recommendation = "Review cardiovascular history carefully, optimize modifiable risk factors, and confirm why febuxostat is preferred over allopurinol or other strategies."
alert = "Intermediate concern should trigger explicit counseling rather than routine automatic continuation."
elif composite < 40:
category = "HIGH"
recommendation = "Use febuxostat only after a deliberate risk-benefit review and clear documentation that alternatives are less suitable or not tolerated."
alert = "This profile merits tighter follow-up and low threshold to revisit urate-lowering strategy if cardiac status worsens."
else:
category = "VERY HIGH"
recommendation = "Avoid casual febuxostat continuation or initiation. Reassess need, revisit alternatives, and involve cardiology or specialist review when the urate-lowering indication remains compelling."
alert = "Recent ischemic events or symptomatic heart failure create a cardiac context where treatment inertia may be unsafe."
notes = [
"This is a transparent clinical-context tool, not a causal proof that febuxostat itself will trigger an event in a given patient.",
"The score is designed for bedside triage of concern, not for replacing CARES, FAST, or guideline interpretation.",
"A documented allopurinol contraindication lowers the threshold for accepting febuxostat because therapeutic necessity matters.",
]
return FebuxCvResult(
composite_score=composite,
risk_category=category,
recommendation=recommendation,
safety_alert=alert,
ci_lower=ci_lower,
ci_upper=ci_upper,
domains=[d.__dict__ for d in domains],
notes=notes,
)
def print_case(label: str, patient: FebuxPatient):
result = compute_febux_cv(patient)
print(f"\n=== {label} ===")
print(f"Composite score: {result.composite_score}/100")
print(f"Risk category: {result.risk_category}")
print(f"95% CI: [{result.ci_lower}, {result.ci_upper}]")
print(f"Recommendation: {result.recommendation}")
print(f"Safety alert: {result.safety_alert}")
if result.notes:
print("Notes:")
for note in result.notes:
print(f"- {note}")
print("Top domains:")
for domain in sorted(result.domains, key=lambda d: d['weighted'], reverse=True)[:5]:
print(f"- {domain['name']}: raw {domain['score']} × w {domain['weight']} = {domain['weighted']} ({domain['detail']})")
if __name__ == "__main__":
print_case(
"Scenario 1 — Low-risk gout patient without major cardiovascular disease",
FebuxPatient(
established_ascvd=False,
symptomatic_heart_failure=False,
prior_stroke_tia=False,
recent_acs_within_12_months=False,
egfr=88,
diabetes=False,
age=49,
current_smoker=False,
allopurinol_intolerant_or_contraindicated=True,
safer_urate_lowering_alternative_available=False,
),
)
print_case(
"Scenario 2 — Older CKD patient with prior MI and diabetes",
FebuxPatient(
established_ascvd=True,
symptomatic_heart_failure=False,
prior_stroke_tia=False,
recent_acs_within_12_months=False,
egfr=44,
diabetes=True,
age=71,
current_smoker=False,
allopurinol_intolerant_or_contraindicated=False,
safer_urate_lowering_alternative_available=True,
),
)
print_case(
"Scenario 3 — Very high-risk patient with recent ACS and symptomatic heart failure",
FebuxPatient(
established_ascvd=True,
symptomatic_heart_failure=True,
prior_stroke_tia=True,
recent_acs_within_12_months=True,
egfr=28,
diabetes=True,
age=77,
current_smoker=True,
allopurinol_intolerant_or_contraindicated=False,
safer_urate_lowering_alternative_available=True,
),
)
Discussion (0)
to join the discussion.
No comments yet. Be the first to discuss this paper.