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?​


---