Skip to main content

Response Structure Overview

A complete guide to understanding and using the /v1/risk-profile API response.


Response Overview

Every successful API call returns a comprehensive JSON object with seven main sections:

{
// 1. Portfolio Summary (top-level fields)
wallet_address: string;
total_collateral_usd: number;
total_borrowed_usd: number;
global_health_factor: number;
global_ltv: number;
positions_at_risk_count: number;
last_updated: string;
aggregation_duration: string;

// 2. DeFi Credit Score (300-850)
defi_score: DeFiScore | null;

// 3. Individual Lending Positions
lending_positions: LendingPosition[];

// 4. Protocol Errors (optional)
aggregation_errors?: Record<string, string>;
}

Response size: Typically 20-50 KB (varies by position count)


Section 1: Portfolio Summary

The top-level fields provide a quick snapshot of the entire portfolio.

Example

{
"wallet_address": "0xf0bb20865277aBd641a307eCe5Ee04E79073416C",
"total_collateral_usd": 2139957718.47,
"total_borrowed_usd": 1905081695.88,
"global_health_factor": 1.067,
"global_ltv": 89.02,
"positions_at_risk_count": 2,
"last_updated": "2025-10-21T04:19:25.986916933Z",
"aggregation_duration": "4.682898286s"
}

Field Reference

FieldTypeDescriptionExample Value
wallet_addressstringEthereum address (checksummed)"0xf0bb..."
total_collateral_usdnumberSum of all collateral across all protocols/chains2139957718.47
total_borrowed_usdnumberSum of all debt across all protocols/chains1905081695.88
global_health_factornumberPortfolio-wide liquidation risk (< 1.0 = liquidated)1.067
global_ltvnumberLoan-to-value ratio (%) across entire portfolio89.02
positions_at_risk_countnumberCount of positions with health_factor < 1.22
last_updatedstringISO 8601 timestamp when data was last fetched"2025-10-21T04:19:25Z"
aggregation_durationstringTime taken to fetch all data"4.682s"

Understanding Health Factor

Health factor is the most critical metric in DeFi lending:

health_factor = (collateral × liquidation_threshold) / debt

Interpretation:

Health FactorStatusRisk LevelAction
> 2.0🟢 Very SafeVery Low RiskNo action needed
1.5 - 2.0🔵 SafeLow RiskMonitor occasionally
1.3 - 1.5🟡 ModerateMedium RiskWatch closely
1.1 - 1.3🟠 RiskyHigh RiskReduce leverage soon
1.0 - 1.1🔴 Danger ZoneVery High RiskTake action NOW
< 1.0⚫ LiquidatedLiquidatedPosition is being liquidated

Example calculation:

If health_factor = 1.067, the position can be liquidated if collateral value drops:

(1.067 - 1.0) / 1.067 = 6.28% price drop = liquidation

This wallet is 6.3% away from liquidation - extremely risky!


Understanding LTV (Loan-to-Value)

LTV measures how much debt you have relative to collateral:

ltv = (total_borrowed / total_collateral) × 100

Interpretation:

LTV RangeRisk LevelTypical Use Case
0-30%Very LowConservative long-term holding
31-50%LowNormal borrowing
51-70%MediumModerate leverage
71-85%HighAggressive leverage
86-95%Very HighExtreme leverage (risky)
> 95%CriticalDanger zone

Example:

{
"total_collateral_usd": 2139957718.47,
"total_borrowed_usd": 1905081695.88,
"global_ltv": 89.02
}
LTV = (1,905,081,695.88 / 2,139,957,718.47) × 100 = 89.02%

This is extremely high leverage - very vulnerable to price swings.


Positions at Risk Count

Counts how many individual positions have health_factor < 1.2:

{
"positions_at_risk_count": 2,
"lending_positions": [
{
"protocol": "Aave",
"chain": "Ethereum",
"health_factor": 1.065,
"is_at_risk": true // ← health_factor < 1.2
},
{
"protocol": "Aave",
"chain": "Base",
"health_factor": 1.160,
"is_at_risk": true // ← health_factor < 1.2
}
]
}

Use this to:

  • Quickly identify if any positions need attention
  • Filter positions to show only at-risk ones
  • Trigger alerts when count > 0

Data Freshness

The last_updated timestamp shows when the data was fetched:

{
"last_updated": "2025-10-21T04:19:25.986916933Z"
}

Important notes:

  • ✅ Data is real-time on-chain (not indexed/delayed)
  • ✅ Cached for 30 seconds after first fetch
  • ✅ Each position has its own last_updated timestamp
  • ✅ Check X-Cache-Status header to see if response was cached

Example:

const profile = await getRiskProfile(address);
const dataAge = Date.now() - new Date(profile.last_updated).getTime();

if (dataAge > 60000) { // More than 1 minute old
console.log('Data may be stale, consider refetching');
}

Aggregation Performance

The aggregation_duration shows how long it took to fetch all data:

{
"aggregation_duration": "4.682898286s"
}

Typical durations:

  • 0.5-2 seconds: Single chain (e.g., Ethereum only)
  • 2-5 seconds: Multi-chain with many positions
  • <100ms: Cached response (subsequent requests within 30s)

What affects duration:

  • Number of chains queried
  • Number of protocols with positions
  • RPC node response time
  • Network conditions

Section 2: DeFi Score Object

The defi_score object contains the complete credit analysis:

{
"defi_score": {
"defi_score": 474,
"risk_level": "High Risk",
"risk_category": "HIGH_RISK",
"color": "orange",
"score_breakdown": { /* ... */ },
"risk_factors": [ /* ... */ ],
"recommendations": { /* ... */ },
"liquidation_simulation": { /* ... */ },
"calculated_at": "2025-10-21T04:19:25.987006056Z"
}
}

This section is so important, it has its own detailed guide:

👉 Complete DeFi Score Explanation →

Quick Reference

Top-Level Score Fields:

FieldTypeDescription
defi_scorenumberFinal credit score (300-850)
risk_levelstringHuman-readable risk level
risk_categoryenumProgrammatic risk category
colorstringSuggested UI color
calculated_atstringWhen score was calculated

Score Categories:

Score Rangerisk_categoryrisk_levelcolor
750-850VERY_LOW_RISKVery Low Riskgreen
650-749LOW_RISKLow Riskblue
550-649MODERATE_RISKMedium Riskyellow
450-549HIGH_RISKHigh Riskorange
300-449URGENT_ACTION_REQUIREDVery High Riskred

Usage example:

const { defi_score, risk_category } = profile.defi_score;

// Quick decision logic
switch (risk_category) {
case 'VERY_LOW_RISK':
case 'LOW_RISK':
return 'APPROVED';

case 'MODERATE_RISK':
return 'MANUAL_REVIEW';

case 'HIGH_RISK':
case 'URGENT_ACTION_REQUIRED':
return 'DECLINED';
}

Null Score

If the wallet has no DeFi positions, defi_score will be null:

{
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"total_collateral_usd": 0,
"total_borrowed_usd": 0,
"lending_positions": [],
"defi_score": null // ← No positions = no score
}

Handle this in your code:

const profile = await getRiskProfile(address);

if (!profile.defi_score) {
return {
message: 'No DeFi lending positions found',
score: null
};
}

// Safe to access score now
const score = profile.defi_score.defi_score;

Section 3: Lending Positions

The lending_positions array contains individual positions across all protocols and chains.

Example

{
"lending_positions": [
{
"protocol": "Aave",
"protocol_version": "V3",
"chain": "Ethereum",
"user_address": "0xf0bb20865277aBd641a307eCe5Ee04E79073416C",
"collateral_usd": 2077178086.09,
"borrowed_usd": 1853661183.91,
"health_factor": 1.065,
"ltv_current": 89.24,
"is_at_risk": true,
"collateral_details": [
{
"token": "weETH",
"amount": 496800.01,
"usd_value": 2077178086.09,
"token_address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee"
}
],
"borrowed_details": [
{
"token": "WETH",
"amount": 478632.98,
"usd_value": 1853661183.91,
"token_address": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2"
}
],
"last_updated": "2025-10-21T04:19:25.986860777Z"
},
{
"protocol": "Aave",
"protocol_version": "V3",
"chain": "Base",
"collateral_usd": 62779632.38,
"borrowed_usd": 51420511.97,
"health_factor": 1.160,
"ltv_current": 81.91,
"is_at_risk": true,
"collateral_details": [ /* ... */ ],
"borrowed_details": [ /* ... */ ],
"last_updated": "2025-10-21T04:19:24.172942614Z"
}
]
}

Position Fields

FieldTypeDescription
protocolstringProtocol name (Aave, Compound, MakerDAO)
protocol_versionstringVersion (V3, V2, 1)
chainstringBlockchain (Ethereum, Base, Arbitrum, Polygon)
user_addressstringWallet address
collateral_usdnumberTotal collateral for this position
borrowed_usdnumberTotal debt for this position
health_factornumberPosition-specific health factor
ltv_currentnumberPosition-specific LTV (%)
is_at_riskbooleantrue if health_factor < 1.2
collateral_detailsarrayArray of collateral tokens
borrowed_detailsarrayArray of borrowed tokens
last_updatedstringWhen this position was fetched

Working with Positions

Filter by Chain

const ethereumPositions = profile.lending_positions.filter(
pos => pos.chain === 'Ethereum'
);

console.log(`Found ${ethereumPositions.length} Ethereum positions`);

Filter by Protocol

const aavePositions = profile.lending_positions.filter(
pos => pos.protocol === 'Aave'
);

Find At-Risk Positions

const atRiskPositions = profile.lending_positions.filter(
pos => pos.is_at_risk
);

if (atRiskPositions.length > 0) {
console.log('⚠️ WARNING: Positions at risk of liquidation:');
atRiskPositions.forEach(pos => {
console.log(` - ${pos.protocol} on ${pos.chain}: HF ${pos.health_factor.toFixed(3)}`);
});
}

Sum by Protocol

const protocolTotals = profile.lending_positions.reduce((acc, pos) => {
const key = pos.protocol;
if (!acc[key]) {
acc[key] = { collateral: 0, debt: 0 };
}
acc[key].collateral += pos.collateral_usd;
acc[key].debt += pos.borrowed_usd;
return acc;
}, {});

console.log(protocolTotals);
// {
// "Aave": { collateral: 2139957718.47, debt: 1905081695.88 }
// }

Token Details

Each position includes token-level breakdown in collateral_details and borrowed_details:

{
token: string; // Symbol (e.g., "WETH", "USDC")
amount: number; // Token amount in native decimals
usd_value: number; // Current USD value
token_address: string; // Contract address
}

Example:

{
"collateral_details": [
{
"token": "weETH",
"amount": 496800.01,
"usd_value": 2077178086.09,
"token_address": "0xCd5fE23C85820F7B72D0926FC9b05b43E359b7ee"
}
]
}

Usage:

// Get all unique collateral tokens
const collateralTokens = new Set();
profile.lending_positions.forEach(pos => {
pos.collateral_details?.forEach(detail => {
collateralTokens.add(detail.token);
});
});

console.log('Collateral tokens:', Array.from(collateralTokens));
// ["weETH", "WETH", "USDC"]
// Calculate total exposure to a specific token
function getTokenExposure(profile, tokenSymbol) {
let totalCollateral = 0;
let totalDebt = 0;

profile.lending_positions.forEach(pos => {
pos.collateral_details?.forEach(detail => {
if (detail.token === tokenSymbol) {
totalCollateral += detail.usd_value;
}
});

pos.borrowed_details?.forEach(detail => {
if (detail.token === tokenSymbol) {
totalDebt += detail.usd_value;
}
});
});

return { collateral: totalCollateral, debt: totalDebt };
}

const wethExposure = getTokenExposure(profile, 'WETH');
console.log(`WETH - Collateral: $${wethExposure.collateral}, Debt: $${wethExposure.debt}`);

Null Token Details

Sometimes collateral_details or borrowed_details may be null:

{
"protocol": "Aave",
"chain": "Ethereum",
"collateral_usd": 2077178086.09,
"collateral_details": null, // ← Can happen
"borrowed_details": [ /* ... */ ]
}

Why this happens:

  • RPC call to fetch token details failed
  • Protocol-specific issue (rare)
  • Token metadata unavailable

You still get the USD totals (collateral_usd, borrowed_usd), just not the token-level breakdown.

Handle in code:

position.collateral_details?.forEach(detail => {
// Safe - only runs if collateral_details is not null
console.log(detail.token);
});

Section 4: Aggregation Errors

The optional aggregation_errors object contains non-fatal errors from individual protocols:

{
"aggregation_errors": {
"CompoundV3:Ethereum": "could not get numAssets from Compound V3 market"
}
}

Key format: "Protocol:Chain""Error message"

Why Errors are Non-Fatal

Philosophy: One protocol failing should not break the entire request.

Example scenario:

  • ✅ Aave V3 on Ethereum: Success
  • ✅ Aave V3 on Base: Success
  • ❌ Compound V3 on Ethereum: Failed (RPC rate limit)

Result: You still get Aave data, plus an error entry for Compound.

{
"lending_positions": [
{ "protocol": "Aave", "chain": "Ethereum", /* ... */ },
{ "protocol": "Aave", "chain": "Base", /* ... */ }
],
"aggregation_errors": {
"CompoundV3:Ethereum": "max retries exceeded: 429 Too Many Requests"
}
}

Common Error Messages

Error MessageCauseSolution
"could not get numAssets from Compound V3 market"Compound V3 contract issueWait and retry (usually temporary)
"max retries exceeded: 429 Too Many Requests"RPC rate limit (Alchemy)Use own RPC node or upgrade Alchemy tier
"call to decimals for TOKEN returned empty response"Token contract issueNon-critical - we fall back to external pricing API
"Aave: could not get asset price"Chainlink oracle issueNon-critical - price fetched from CoinDesk API

Handling Errors in Code

const profile = await getRiskProfile(address);

// Check for partial failures
if (profile.aggregation_errors && Object.keys(profile.aggregation_errors).length > 0) {
console.warn('⚠️ Some protocols failed:');

Object.entries(profile.aggregation_errors).forEach(([protocolKey, error]) => {
console.warn(` - ${protocolKey}: ${error}`);
});

// Still use the data you have
console.log(`✅ Successfully fetched ${profile.lending_positions.length} positions`);
} else {
console.log('✅ Complete data - all protocols successful');
}

Response Navigation Patterns

Pattern 1: Quick Risk Assessment

Get the most important metrics in one line:

const { global_health_factor, global_ltv, defi_score } = profile;
const score = defi_score?.defi_score ?? null;

console.log(`Score: ${score}, HF: ${global_health_factor.toFixed(2)}, LTV: ${global_ltv.toFixed(1)}%`);
// Output: Score: 474, HF: 1.07, LTV: 89.0%

Pattern 2: Position Summary Table

console.table(
profile.lending_positions.map(pos => ({
Protocol: `${pos.protocol} ${pos.protocol_version}`,
Chain: pos.chain,
Collateral: `$${pos.collateral_usd.toLocaleString()}`,
Debt: `$${pos.borrowed_usd.toLocaleString()}`,
'Health Factor': pos.health_factor.toFixed(3),
'At Risk': pos.is_at_risk ? '⚠️' : '✅'
}))
);

Output:

┌─────────┬──────────┬──────────┬──────────────┬─────────────┬────────────────┬─────────┐
│ (index) │ Protocol │ Chain │ Collateral │ Debt │ Health Factor │ At Risk │
├─────────┼──────────┼──────────┼──────────────┼─────────────┼────────────────┼─────────┤
│ 0 │ 'Aave V3'│'Ethereum'│ '$2,077M' │ '$1,853M' │ '1.065' │ '⚠️' │
│ 1 │ 'Aave V3'│ 'Base' │ '$62.8M' │ '$51.4M' │ '1.160' │ '⚠️' │
└─────────┴──────────┴──────────┴──────────────┴─────────────┴────────────────┴─────────┘

Pattern 3: Immediate Actions Check

const immediateActions = profile.defi_score?.recommendations?.immediate ?? [];

if (immediateActions.length > 0) {
console.log('🚨 URGENT ACTIONS REQUIRED:');
immediateActions.forEach(action => console.log(` - ${action}`));
} else {
console.log('✅ No urgent actions needed');
}

Pattern 4: Multi-Chain Breakdown

const chainSummary = profile.lending_positions.reduce((acc, pos) => {
if (!acc[pos.chain]) {
acc[pos.chain] = {
positions: 0,
collateral: 0,
debt: 0
};
}
acc[pos.chain].positions++;
acc[pos.chain].collateral += pos.collateral_usd;
acc[pos.chain].debt += pos.borrowed_usd;
return acc;
}, {});

console.log('Chain Summary:');
Object.entries(chainSummary).forEach(([chain, data]) => {
console.log(` ${chain}: ${data.positions} positions, $${data.collateral.toLocaleString()} collateral`);
});

Output:

Chain Summary:
Ethereum: 1 positions, $2,077,178,086 collateral
Base: 1 positions, $62,779,632 collateral

Pattern 5: Liquidation Risk Alert

const { buffer_percentage, scenarios } = profile.defi_score.liquidation_simulation;

const liquidatedScenarios = scenarios.filter(s => s.status === 'LIQUIDATED');

if (liquidatedScenarios.length > 0) {
console.log(`⚠️ LIQUIDATION RISK: ${buffer_percentage.toFixed(1)}% buffer remaining`);
console.log(` Liquidated if collateral drops ${liquidatedScenarios[0].event}`);
} else {
console.log('✅ No liquidation risk in stress test scenarios');
}

Complete Response Reference

Minimal Response (No Positions)

{
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"total_collateral_usd": 0,
"total_borrowed_usd": 0,
"global_health_factor": 0,
"global_ltv": 0,
"positions_at_risk_count": 0,
"last_updated": "2025-10-21T04:19:25Z",
"aggregation_duration": "0.652s",
"lending_positions": [],
"defi_score": null
}

Full Response (With Positions)

See the complete example in the endpoint documentation →


Best Practices

✅ DO

  • Check defi_score for null before accessing properties
  • Use risk_category for logic (not numeric score ranges)
  • Handle aggregation_errors gracefully (log warnings, don't fail)
  • Use is_at_risk flag for quick filtering
  • Cache responses client-side for at least 30 seconds
  • Check collateral_details for null before iterating

❌ DON'T

  • Don't assume positions exist - always check lending_positions.length
  • Don't ignore aggregation_errors - you might have incomplete data
  • Don't compare health factors across protocols without understanding their liquidation thresholds
  • Don't refetch more than once per 30 seconds (you'll get cached data anyway)

Next Steps


Need Help?


---