Skip to main content

Error Handling Guide

A comprehensive guide to handling errors, edge cases, and partial failures when using the Kixago API.


Overview

The Kixago API uses standard HTTP status codes and JSON error responses. Understanding how to handle errors properly is critical for building robust applications.

Error Philosophy

  • Partial failures are non-fatal - One protocol failing doesn't break the entire response
  • Always return valid JSON - Even error responses are well-structured
  • Descriptive error messages - Clear explanations of what went wrong
  • Retry-friendly - Transient errors (429, 500) can be retried

HTTP Status Codes

Success Codes

CodeStatusDescription
200OKRequest successful, profile returned

Client Error Codes (4xx)

CodeStatusDescriptionRetry?
400Bad RequestInvalid wallet address or malformed request❌ No
401UnauthorizedMissing or invalid API key❌ No
404Not FoundEndpoint doesn't exist❌ No
429Too Many RequestsRate limit exceeded✅ Yes (after delay)

Server Error Codes (5xx)

CodeStatusDescriptionRetry?
500Internal Server ErrorUnexpected server error✅ Yes (with backoff)
502Bad GatewayUpstream service error✅ Yes (with backoff)
503Service UnavailableTemporary outage✅ Yes (with backoff)
504Gateway TimeoutRequest took too long✅ Yes (once)

Error Response Format

All error responses follow this structure:

{
"error": "Human-readable error message"
}

Example:

{
"error": "invalid address format - use hex address or ENS name"
}

Common Error Scenarios

Error 1: Invalid API Key (401)

Request:

curl "https://api.kixago.com/v1/risk-profile/0xf0bb..."
# (no API key header)

Response:

{
"error": "missing or invalid API key"
}

Causes:

  • ❌ No X-API-Key header
  • ❌ Wrong API key
  • ❌ API key revoked
  • ❌ API key expired (if expiration set)

Solution:

// ✅ Correct
const response = await fetch(url, {
headers: {
'X-API-Key': process.env.KIXAGO_API_KEY
}
});

// ❌ Wrong - missing header
const response = await fetch(url);

Error 2: Invalid Wallet Address (400)

Request:

curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/invalid"

Response:

{
"error": "invalid address format - use hex address or ENS name"
}

Causes:

  • ❌ Address too short/long
  • ❌ Missing 0x prefix
  • ❌ Invalid characters
  • ❌ Not a valid ENS name

Valid formats:

✅ 0xf0bb20865277aBd641a307eCe5Ee04E79073416C  (42 chars, 0x prefix)
✅ vitalik.eth (valid ENS name)
❌ f0bb20865277aBd641a307eCe5Ee04E79073416C (missing 0x)
❌ 0xf0bb (too short)
❌ random-string (invalid)

Validation example:

function isValidAddress(address: string): boolean {
// Hex address: 0x + 40 hex characters
if (/^0x[a-fA-F0-9]{40}$/.test(address)) {
return true;
}

// ENS name: ends with .eth
if (address.endsWith('.eth')) {
return true;
}

return false;
}

if (!isValidAddress(walletAddress)) {
throw new Error('Invalid wallet address format');
}

Error 3: ENS Resolution Failure (400)

Request:

curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/nonexistent.eth"

Response:

{
"error": "could not resolve ENS name: nonexistent.eth"
}

Causes:

  • ❌ ENS name doesn't exist
  • ❌ ENS name expired
  • ❌ ENS resolver error

Solution:

try {
const profile = await getRiskProfile('nonexistent.eth');
} catch (err) {
if (err.message.includes('could not resolve ENS')) {
// Handle ENS resolution failure
console.error('ENS name not found');
}
}

Error 4: Rate Limit Exceeded (429)

Request:

# (after exceeding rate limit)
curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/0xf0bb..."

Response:

{
"error": "rate limit exceeded",
"retry_after": 5
}

Headers:

HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705334405
Retry-After: 5

Rate limits by plan:

PlanRequests/MonthRate LimitBurst
Developer (Free)10,00010 req/sec20 req/sec
Startup100,00050 req/sec100 req/sec
Institution1,000,000200 req/sec400 req/sec

Handling rate limits:

async function fetchWithRateLimit(url: string) {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey }
});

if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('Retry-After') || '5');

console.log(`Rate limited - waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);

// Retry once
return fetch(url, {
headers: { 'X-API-Key': apiKey }
});
}

return response;
}

Error 5: Internal Server Error (500)

Request:

curl -H "X-API-Key: key" \
"https://api.kixago.com/v1/risk-profile/0xf0bb..."

Response:

{
"error": "Error generating portfolio risk"
}

Causes:

  • ❌ Unexpected server error
  • ❌ Database error
  • ❌ Upstream RPC node failure

Solution:

async function fetchWithRetry(url: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey }
});

if (response.status === 500 && i < maxRetries - 1) {
// Retry on 500 errors
const delay = 1000 * Math.pow(2, i); // Exponential backoff: 1s, 2s, 4s
console.log(`Server error - retrying in ${delay}ms...`);
await sleep(delay);
continue;
}

if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}

return await response.json();

} catch (err) {
if (i === maxRetries - 1) throw err;
}
}
}

Partial Failures (Non-Fatal)

Understanding aggregation_errors

The Kixago API queries multiple protocols across multiple chains. If one protocol fails, the API doesn't fail the entire request - it returns data from successful protocols.

Example response with partial failure:

{
"wallet_address": "0xf0bb20865277aBd641a307eCe5Ee04E79073416C",
"total_collateral_usd": 2139957718.47,
"lending_positions": [
{
"protocol": "Aave",
"chain": "Ethereum",
"collateral_usd": 2077178086.09
},
{
"protocol": "Aave",
"chain": "Base",
"collateral_usd": 62779632.38
}
],
"aggregation_errors": {
"CompoundV3:Ethereum": "could not get numAssets from Compound V3 market"
}
}

What happened:

  • ✅ Aave V3 on Ethereum: Success
  • ✅ Aave V3 on Base: Success
  • ❌ Compound V3 on Ethereum: Failed (but didn't break the request)

Common Aggregation Errors

Protocol KeyError MessageCauseImpact
CompoundV3:Ethereum"could not get numAssets from Compound V3 market"RPC error or contract issueMissing Compound positions
Aave:Ethereum"max retries exceeded: 429 Too Many Requests"Alchemy rate limitMissing Aave positions
MakerDAO:Ethereum"timeout waiting for response"RPC timeoutMissing MakerDAO vaults

Handling Aggregation Errors

const profile = await getRiskProfile(address);

if (profile.aggregation_errors && Object.keys(profile.aggregation_errors).length > 0) {
// Log warnings but continue
console.warn('⚠️ Some protocols failed:', profile.aggregation_errors);

// Optional: Send to error tracking (Sentry, etc.)
Sentry.captureMessage('Partial DeFi aggregation failure', {
extra: { errors: profile.aggregation_errors }
});
}

// Use the data you have
console.log(`Found ${profile.lending_positions.length} positions`);

Strategy 2: Retry Failed Protocols

const profile = await getRiskProfile(address);

if (profile.aggregation_errors && Object.keys(profile.aggregation_errors).length > 0) {
console.warn('Some protocols failed - retrying in 5s...');

// Wait and retry
await sleep(5000);
const retried = await getRiskProfile(address);

if (!retried.aggregation_errors || Object.keys(retried.aggregation_errors).length === 0) {
console.log('✅ Retry successful - all protocols now working');
return retried;
}
}

return profile; // Use partial data

Strategy 3: Fail if Critical Protocols Missing

const profile = await getRiskProfile(address);

// Define critical protocols for your use case
const criticalProtocols = ['Aave:Ethereum', 'Compound:Ethereum'];

if (profile.aggregation_errors) {
const failedCritical = criticalProtocols.some(protocol =>
Object.keys(profile.aggregation_errors).some(key => key.includes(protocol))
);

if (failedCritical) {
throw new Error('Critical protocols failed - cannot proceed with underwriting');
}
}

// Safe to proceed
return profile;

Strategy 4: Display Warning to Users

function DisplayPortfolio({ profile }) {
const hasPartialFailure = profile.aggregation_errors &&
Object.keys(profile.aggregation_errors).length > 0;

return (
<div>
{hasPartialFailure && (
<Alert severity="warning">
⚠️ Some DeFi protocols are temporarily unavailable.
The data shown may be incomplete.
<details>
<summary>Details</summary>
<pre>{JSON.stringify(profile.aggregation_errors, null, 2)}</pre>
</details>
</Alert>
)}

<PortfolioSummary data={profile} />
</div>
);
}

Network Errors

Timeout Errors

Symptom: Request takes longer than client timeout

// Set appropriate timeout (API can take 1-5 seconds)
const response = await fetch(url, {
headers: { 'X-API-Key': apiKey },
signal: AbortSignal.timeout(30000) // 30 second timeout
});

Recommended timeouts:

  • Client timeout: 30 seconds (to account for slow RPC nodes)
  • Server timeout: API has internal 30s timeout
  • Cache hit: < 100ms

Connection Errors

Symptom: Network unreachable, DNS failure, etc.

try {
const profile = await getRiskProfile(address);
} catch (err) {
if (err.name === 'TypeError' && err.message.includes('fetch')) {
// Network error
console.error('Network error - check your connection');
} else if (err.code === 'ECONNREFUSED') {
// Connection refused
console.error('Cannot connect to API - service may be down');
} else {
throw err;
}
}

Edge Cases

Case 1: Wallet with No DeFi Positions

Response:

{
"wallet_address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"total_collateral_usd": 0,
"total_borrowed_usd": 0,
"global_health_factor": 0,
"lending_positions": [],
"defi_score": null
}

Key indicator: defi_score is null

Handling:

const profile = await getRiskProfile(address);

if (!profile.defi_score) {
return {
message: 'This wallet has no DeFi lending positions',
recommendation: 'No credit history available'
};
}

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

Case 2: Null Token Details

Sometimes collateral_details or borrowed_details may be null:

{
"protocol": "Aave",
"chain": "Ethereum",
"collateral_usd": 2077178086.09,
"collateral_details": null,
"borrowed_details": [...]
}

Handling:

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

// Or with explicit check
if (position.collateral_details) {
for (const detail of position.collateral_details) {
console.log(detail.token);
}
}

Case 3: Extreme Values

Some wallets have extremely large positions (billions of dollars):

{
"total_collateral_usd": 2139957718.47
}

Handling in JavaScript:

// ❌ Wrong - precision loss
const collateral = 2139957718.47;
console.log(collateral); // May lose precision

// ✅ Correct - use BigInt for large integers or format for display
const formatted = new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD',
minimumFractionDigits: 0,
maximumFractionDigits: 0
}).format(collateral);

console.log(formatted); // "$2,139,957,718"

Complete Error Handling Example

TypeScript with Full Error Handling

// error-handling.ts
import fetch from 'node-fetch';

class KixagoAPIError extends Error {
constructor(
message: string,
public statusCode?: number,
public retryable: boolean = false
) {
super(message);
this.name = 'KixagoAPIError';
}
}

async function getRiskProfileSafe(
walletAddress: string,
maxRetries = 3
): Promise<RiskProfileResponse> {
let lastError: Error | null = null;

for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
const response = await fetch(
`https://api.kixago.com/v1/risk-profile/${walletAddress}`,
{
headers: {
'X-API-Key': process.env.KIXAGO_API_KEY!
},
signal: AbortSignal.timeout(30000)
}
);

// Handle specific HTTP errors
if (!response.ok) {
const errorData = await response.json().catch(() => ({}));
const errorMessage = errorData.error || `HTTP ${response.status}`;

switch (response.status) {
case 400:
throw new KixagoAPIError(
`Invalid request: ${errorMessage}`,
400,
false // Don't retry
);

case 401:
throw new KixagoAPIError(
'Invalid API key',
401,
false // Don't retry
);

case 429:
const retryAfter = parseInt(response.headers.get('Retry-After') || '5');

if (attempt < maxRetries - 1) {
console.log(`Rate limited - waiting ${retryAfter}s...`);
await sleep(retryAfter * 1000);
continue; // Retry
}

throw new KixagoAPIError(
'Rate limit exceeded',
429,
true // Retryable
);

case 500:
case 502:
case 503:
if (attempt < maxRetries - 1) {
const delay = 1000 * Math.pow(2, attempt); // Exponential backoff
console.log(`Server error - retrying in ${delay}ms...`);
await sleep(delay);
continue; // Retry
}

throw new KixagoAPIError(
'Server error',
response.status,
true // Retryable
);

default:
throw new KixagoAPIError(
errorMessage,
response.status,
false
);
}
}

// Parse successful response
const data = await response.json();

// Warn about partial failures
if (data.aggregation_errors && Object.keys(data.aggregation_errors).length > 0) {
console.warn('⚠️ Partial failure:', data.aggregation_errors);
}

return data;

} catch (err) {
lastError = err as Error;

// Don't retry non-retryable errors
if (err instanceof KixagoAPIError && !err.retryable) {
throw err;
}

// Network errors - retry
if (err instanceof TypeError) {
if (attempt < maxRetries - 1) {
const delay = 1000 * Math.pow(2, attempt);
console.log(`Network error - retrying in ${delay}ms...`);
await sleep(delay);
continue;
}
}

// Last attempt - throw
if (attempt === maxRetries - 1) {
throw lastError;
}
}
}

throw lastError || new Error('Max retries exceeded');
}

function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}

// Usage
try {
const profile = await getRiskProfileSafe('0xf0bb20865277aBd641a307eCe5Ee04E79073416C');

if (!profile.defi_score) {
console.log('No DeFi positions found');
} else {
console.log(`DeFi Score: ${profile.defi_score.defi_score}`);
}

} catch (err) {
if (err instanceof KixagoAPIError) {
console.error(`API Error (${err.statusCode}): ${err.message}`);

if (err.retryable) {
console.log('You can retry this request');
}
} else {
console.error('Unexpected error:', err);
}
}

Best Practices Checklist

✅ DO

  • Validate inputs before sending requests
  • Set appropriate timeouts (30 seconds recommended)
  • Implement retry logic for 429, 500, 502, 503 errors
  • Use exponential backoff when retrying
  • Check aggregation_errors for partial failures
  • Log errors for debugging and monitoring
  • Handle defi_score: null for wallets with no positions
  • Use optional chaining for nullable fields (?.)
  • Cache responses to reduce API calls
  • Monitor rate limit headers to avoid 429 errors

❌ DON'T

  • Don't retry 400, 401, 404 errors - they won't succeed
  • Don't retry infinitely - use max retry limit (3-5)
  • Don't ignore partial failures - log aggregation_errors
  • Don't fail on partial data - use what you have
  • Don't hardcode delays - use Retry-After header
  • Don't expose API keys in errors - sanitize error messages
  • Don't assume all fields exist - validate before accessing
  • Don't spam the API - respect rate limits

Monitoring and Alerting

What to Monitor

// Track error rates
function trackError(error: Error, context: any) {
// Send to monitoring service (Sentry, Datadog, etc.)
Sentry.captureException(error, {
tags: {
api: 'kixago',
endpoint: 'risk-profile'
},
extra: context
});

// Track metrics
metrics.increment('kixago.errors', {
type: error.name,
statusCode: (error as any).statusCode
});
}

// Track partial failures
function trackPartialFailure(errors: Record<string, string>) {
metrics.increment('kixago.partial_failures', {
protocols: Object.keys(errors).join(',')
});
}

// Track success rate
function trackSuccess(duration: number, cached: boolean) {
metrics.timing('kixago.response_time', duration);
metrics.increment('kixago.success', { cached });
}

Alerting Thresholds

Set up alerts for:

  • Error rate > 5% over 5 minutes
  • Partial failure rate > 20% over 5 minutes
  • 429 errors > 10 per minute (approaching rate limit)
  • Average response time > 10 seconds
  • No successful requests for 5 minutes

Next Steps


Need Help?


---