> ## Documentation Index
> Fetch the complete documentation index at: https://docs.kaleidoswap.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Node SDK Troubleshooting

> Diagnose common Node SDK issues such as connection failures, version mismatches, and swap errors, with step-by-step solutions plus the full error hierarchy and retry patterns

## Installation Issues

> **npm install fails (TypeScript)**
>
> **Symptoms:** `npm install kaleido-sdk` fails with errors
>
> **Solutions:**
>
> 1. Ensure Node.js 18+ is installed: `node --version`
> 2. Clear npm cache: `npm cache clean --force`
> 3. Delete `node_modules` and `package-lock.json`, then reinstall
> 4. Try with a different package manager: `pnpm add kaleido-sdk`

> **pip install fails (Python)**
>
> **Symptoms:** `pip install kaleido-sdk` fails
>
> **Solutions:**
>
> 1. Ensure Python 3.11+ is installed: `python --version`
> 2. Use a virtual environment: `python -m venv .venv && source .venv/bin/activate`
> 3. Upgrade pip: `pip install --upgrade pip`
> 4. Try: `pip install kaleido-sdk --no-cache-dir`

> **Module not found, or ERR\_REQUIRE\_ESM**
>
> **Symptoms:** `Cannot find module 'kaleido-sdk'`, `ERR_REQUIRE_ESM`, import syntax errors, or `ModuleNotFoundError`
>
> **Cause (TypeScript):** The SDK is ESM-only, and the legacy `"moduleResolution": "node"` setting ignores the package's `exports` field.
>
> **Solutions:**
>
> * **TypeScript:** set `"moduleResolution"` to `"bundler"`, `"node16"`, or `"nodenext"` in `tsconfig.json`, and make the consuming project ESM — `"type": "module"` in `package.json`, or the `.mts` file extension. If you must stay on CommonJS, use a dynamic import: `const sdk = await import('kaleido-sdk')`
> * **Python:** Verify you are in the correct virtual environment
> * Verify the package is installed: `npm list kaleido-sdk` or `pip show kaleido-sdk`

## Runtime Errors

> **NetworkError: Connection refused**
>
> **Symptoms:** `NetworkError` when making API calls
>
> **Causes:**
>
> * API server unreachable
> * Incorrect `baseUrl`
> * Firewall blocking requests
>
> **Solutions:**
>
> 1. Verify the `baseUrl` is correct and accessible
> 2. Check internet connectivity
> 3. Try accessing the API URL in a browser: `https://api.signet.kaleidoswap.com/api/v1/market/assets`
> 4. If behind a firewall, ensure outbound HTTPS is allowed

> **Node not configured**
>
> **Symptoms:** `ConfigError` ("Node API not configured...") in TypeScript, or `NodeNotConfiguredError` in Python, when calling `client.rln.*` methods
>
> **Cause:** No `nodeUrl` / `node_url` was provided when creating the client.
>
> **Solution:**
>
> ```typescript TypeScript theme={null}
> const client = KaleidoClient.create({
>   baseUrl: 'https://api.signet.kaleidoswap.com',
>   nodeUrl: 'http://localhost:3001',  // Add this
> });
> ```
>
> ```python Python theme={null}
> client = KaleidoClient.create(
>     base_url="https://api.signet.kaleidoswap.com",
>     node_url="http://localhost:3001",  # Add this
> )
> ```
>
> Always check `client.hasNode()` / `client.has_node()` before calling RLN methods.

> **QuoteExpiredError**
>
> **Symptoms:** `QuoteExpiredError` when calling `initSwap` / `init_swap` with an `rfq_id`
>
> **Cause:** The quote's `expires_at` time has passed.
>
> **Solutions:**
>
> 1. Get a fresh quote immediately before calling `initSwap` — do not reuse an `rfq_id` across user think-time
> 2. Use WebSocket streaming for always-current quotes
> 3. Whitelist and execute promptly; the whole init → whitelist → execute sequence runs against one quote

> **ValidationError: Invalid amount**
>
> **Symptoms:** `ValidationError` with amount-related message
>
> **Causes:**
>
> * Amount below minimum or above maximum
> * Wrong precision (sending display units instead of raw)
> * Negative or zero amount
>
> **Solutions:**
>
> 1. Check min/max limits from the `listPairs` response
> 2. Ensure you are sending raw amounts, not display amounts — convert with `parseRawAmount` / `parse_raw_amount` from [Utilities](https://docs.kaleidoswap.com/cli/node-sdk/utilities.md)
> 3. Validate amounts before sending

> **APIError: 401 Unauthorized**
>
> **Symptoms:** `APIError` with status 401
>
> **Cause:** Invalid or missing API key.
>
> **Solutions:**
>
> 1. Check that `apiKey` / `api_key` is set correctly
> 2. Verify the key is valid and not expired
> 3. Some endpoints may not require an API key — check the [API Reference](https://docs.kaleidoswap.com/cli/node-sdk/api-reference.md)

> **TimeoutError**
>
> **Symptoms:** `TimeoutError` on API calls
>
> **Causes:**
>
> * Slow network connection
> * Server under heavy load
> * Timeout too short
>
> **Solutions:**
>
> 1. Increase the timeout: `timeout: 60` (seconds)
> 2. Check network connectivity
> 3. Implement retry logic using `error.isRetryable()` — see [Retry Patterns](#retry-patterns)

> **RateLimitError: 429 Too Many Requests**
>
> **Symptoms:** `RateLimitError` on repeated calls, typically while polling quotes
>
> **Cause:** Too many requests in the rate-limit window.
>
> **Solutions:**
>
> 1. Back off and retry, honouring any `retry_after` the response carries (in Python, `is_retryable()` returns `False` for rate limits, so back off manually)
> 2. Stream quotes over WebSocket instead of polling `getQuote` in a loop
> 3. Cache `listAssets` / `listPairs` rather than refetching them per operation

## Swap Issues

> **SwapError on execute: swapstring not whitelisted**
>
> **Symptoms:** `initSwap` succeeds, but `executeSwap` / `execute_swap` fails with `SwapError`
>
> **Cause:** The `swapstring` returned by `initSwap` was never whitelisted on your own node, so the taker side cannot honour the HTLC.
>
> **Solution:** The three steps must run in order, and the whitelist step is on `client.rln`, not `client.maker`:
>
> 1. `client.maker.initSwap(...)` → returns `swapstring`
> 2. `client.rln.whitelistSwap(swapstring)` → your node accepts the swap
> 3. `client.maker.executeSwap(...)` → the maker settles it
>
> See [How to Swap](https://docs.kaleidoswap.com/cli/node-sdk/how-to-swap.md) for the full sequence.

> **InsufficientBalanceError**
>
> **Symptoms:** `InsufficientBalanceError` on init or execute
>
> **Causes:**
>
> * Not enough outbound capacity on the channel for the leg you are sending
> * The dust reserve is not available on top of the swap amount
> * Balance is on-chain rather than in a channel
>
> **Solutions:**
>
> 1. Check `client.rln.listChannels()` for outbound capacity on the right asset, not just total balance
> 2. Confirm the amount is within the pair's min/max from `listPairs`
> 3. If capacity is short, order more inbound or outbound liquidity via LSPS1

> **Swap stuck in a pending state**
>
> **Symptoms:** `executeSwap` returned, but the assets have not arrived
>
> **Solutions:**
>
> 1. Poll `client.maker.getAtomicSwapStatus(...)` / `get_atomic_swap_status(...)` rather than assuming execute is terminal
> 2. Call `client.rln.refreshTransfers()` / `refresh_transfers()` to advance pending RGB transfers
> 3. Check `client.rln.listSwaps()` for the node's own view of the swap

> **Version mismatch: unexpected response shape**
>
> **Symptoms:** A call fails on parsing (Pydantic `ValidationError`, or a TypeScript field that is unexpectedly `undefined`) rather than returning a clean SDK error
>
> **Cause:** The SDK and the API it is talking to were generated against different spec versions. This is most common on the node side, where the RLN version is yours to control.
>
> **Solutions:**
>
> 1. Compare your node's version against the one your SDK release targets — see [RLN API Compatibility](https://docs.kaleidoswap.com/cli/node-sdk/rln-api-compatibility.md)
> 2. Upgrade the SDK: `npm install kaleido-sdk@latest` or `pip install --upgrade kaleido-sdk`
> 3. Check the [Changelog](https://docs.kaleidoswap.com/cli/node-sdk/changelog.md) for breaking changes between your version and the current one — several releases added now-required request fields

## WebSocket Issues

> **WebSocket not connecting**
>
> **Symptoms:** `connected` event never fires, or `WebSocketError`
>
> **Solutions:**
>
> 1. Verify the WebSocket URL is correct (should start with `wss://`)
> 2. Ensure `enableWebSocket` / `enable_websocket` was called before streaming
> 3. Check that WebSocket connections are not blocked by firewall or proxy
> 4. Try with a different client ID in the URL

> **No quotes received**
>
> **Symptoms:** `quoteResponse` / `quote_response` event never fires
>
> **Solutions:**
>
> 1. Verify the asset pair is valid and has available routes
> 2. Check that the amount is within min/max limits
> 3. Listen for `error` events on the WSClient
> 4. Verify the connection is established (check `connected` event)

> **Frequent disconnections**
>
> **Symptoms:** WebSocket disconnects and reconnects frequently
>
> **Solutions:**
>
> 1. Check internet stability
> 2. The WSClient auto-reconnects with exponential backoff
> 3. Monitor `reconnecting` events to track attempts
> 4. If `maxReconnectExceeded` fires, manually reconnect:
>
> ```typescript TypeScript theme={null}
> ws.on('maxReconnectExceeded', async () => {
>   console.log('Reconnecting manually...');
>   await ws.connect();
> });
> ```
>
> ```python Python theme={null}
> async def manual_reconnect():
>     print("Reconnecting manually...")
>     await ws.connect()
>
> ws.on("max_reconnect_exceeded", manual_reconnect)
> ```

## Language-Specific Issues

> **Type errors with OpenAPI types (TypeScript)**
>
> **Symptoms:** TypeScript compiler errors about incompatible types
>
> **Solutions:**
>
> 1. Ensure you are importing types from `kaleido-sdk`:
>    ```typescript theme={null}
>    import type { AssetResponseModel, TradingPairResponseModel } from 'kaleido-sdk';
>    ```
> 2. Check your TypeScript version is 5.0+
> 3. If using strict mode, you may need to handle `undefined` explicitly

> **Pydantic validation errors (Python)**
>
> **Symptoms:** `ValidationError` from Pydantic when parsing API responses
>
> **Solutions:**
>
> 1. Ensure `pydantic>=2.0` is installed
> 2. Check that you are using the correct request format
> 3. The API may have been updated — try updating the SDK: `pip install --upgrade kaleido-sdk`

> **httpx connection errors (Python)**
>
> **Symptoms:** `httpx.ConnectError` or similar
>
> **Solutions:**
>
> 1. Check that the API URL is reachable
> 2. If using a proxy, configure it via environment variables: `HTTP_PROXY`, `HTTPS_PROXY`
> 3. Increase timeout if the connection is slow

## Error Reference

Both SDKs share a nearly identical error class hierarchy. All errors extend from `KaleidoError`, so a single `catch` on it never lets an SDK failure escape unhandled:

```
KaleidoError (base)
├── APIError (HTTP 400-599)
│   └── RateLimitError (HTTP 429, TypeScript only)
├── RateLimitError (HTTP 429, Python — extends KaleidoError directly)
├── NetworkError (connectivity issues)
├── ValidationError (HTTP 400, 422)
├── TimeoutError (HTTP 408, 504)
├── WebSocketError (WebSocket issues)
├── NotFoundError (HTTP 404)
├── ConfigError (configuration issues)
├── SwapError (swap operation failures)
├── NodeNotConfiguredError (node not set up)
├── QuoteExpiredError (quote expired)
└── InsufficientBalanceError (not enough funds)
```

Two divergences between the SDKs are worth knowing before you write a handler:

* **`RateLimitError`** extends `APIError` in TypeScript but `KaleidoError` directly in Python — a Python `except APIError` handler will **not** catch rate-limit errors.
* **A missing node URL** raises `NodeNotConfiguredError` in Python, but `ConfigError` ("Node API not configured. Provide "nodeUrl" when creating the client.") in TypeScript.

### KaleidoError Properties

Every error carries these, whichever class it is:

| Property | Type | Description |
| - | - | - |
| `code` | `string` | Error code for programmatic handling |
| `message` | `string` | Human-readable error message |
| `statusCode` / `status_code` | `number?` | HTTP status code (if applicable) |
| `details` | `string?` | Additional error details |
| `isRetryable()` / `is_retryable()` | `boolean` | Whether the error is safe to retry |

### Error Classes

| Class | Raised when | Additional properties |
| - | - | - |
| `APIError` | HTTP 400-599 | TypeScript: `details`. Python: `response_body` |
| `RateLimitError` | HTTP 429 | `retryAfter` / `retry_after` — seconds to wait |
| `NetworkError` | DNS failure, connection refused, and other connectivity faults | — |
| `ValidationError` | HTTP 400, 422 — includes parsed FastAPI validation detail | — |
| `TimeoutError` | HTTP 408, 504 | — |
| `WebSocketError` | WebSocket connection or communication failure | — |
| `NotFoundError` | HTTP 404 | — |
| `ConfigError` | Invalid SDK configuration, such as a malformed URL. In TypeScript, also a `client.rln.*` call made without `nodeUrl` | — |
| `SwapError` | A swap operation failed | `swapId` / `swap_id` |
| `NodeNotConfiguredError` | Python only — a `client.rln.*` call made without `node_url` | — |
| `QuoteExpiredError` | An `rfq_id` is used after its `expires_at` | — |
| `InsufficientBalanceError` | The balance cannot cover the operation | `requiredAmount` / `required_amount`, `availableAmount` / `available_amount`, `asset` |

Which of these are safe to retry is decided per status, not per class — see [HTTP Error Mapping](#http-error-mapping) below.

### HTTP Error Mapping

The SDK automatically maps HTTP errors to typed exceptions using `mapHttpError` / `map_http_error`:

| HTTP Status | Error Class | Retryable |
| - | - | - |
| 400, 422 | `ValidationError` | No — the request itself is wrong |
| 404 | `NotFoundError` | No |
| 408, 504 | `TimeoutError` | Yes, always |
| 429 | `RateLimitError` | TypeScript: yes, after `retryAfter`. Python: `is_retryable()` returns `False` — back off using `retry_after` yourself |
| 500, 502, 503 | `APIError` | Yes — a server-side fault may clear |
| Other 4xx | `APIError` | No |
| Network failure | `NetworkError` | Yes, always |

Rather than hardcoding this table in your own retry logic, call `isRetryable()` / `is_retryable()` — it encodes exactly these rules and stays correct as the mapping evolves.

### Comprehensive Error Handling

Branch from the most specific class to the most general, and end on `KaleidoError` so nothing escapes. Subclasses must be tested before their parents — in TypeScript `RateLimitError` is an `APIError`, so an `APIError` branch placed first would swallow it.

```typescript TypeScript theme={null}
import { KaleidoError, RateLimitError, QuoteExpiredError } from 'kaleido-sdk';

try {
  const quote = await client.maker.getQuote({ /* ... */ });
  const swap = await client.maker.initSwap({ /* ... */ });
} catch (error) {
  if (error instanceof QuoteExpiredError) {
    // Get a fresh quote and retry
  } else if (error instanceof RateLimitError) {
    await new Promise(r => setTimeout(r, (error.retryAfter ?? 5) * 1000));
  } else if (error instanceof KaleidoError && error.isRetryable()) {
    // Back off and retry — see Retry Patterns below
  } else if (error instanceof KaleidoError) {
    console.error(`SDK error [${error.code}]: ${error.message}`);
  }
}
```

```python Python theme={null}
import asyncio

from kaleido_sdk import (
    KaleidoError,
    PairQuoteRequest,
    QuoteExpiredError,
    RateLimitError,
    SwapRequest,
)

try:
    quote = await client.maker.get_quote(PairQuoteRequest(...))
    swap = await client.maker.init_swap(SwapRequest(...))
except QuoteExpiredError:
    # Get a fresh quote and retry
    pass
except RateLimitError as e:
    await asyncio.sleep(e.retry_after or 5)
except KaleidoError as e:
    if e.is_retryable():
        # Back off and retry — see Retry Patterns below
        pass
    else:
        print(f"SDK error [{e.code}]: {e}")
```

Add a branch for a specific class only where your handling genuinely differs — `InsufficientBalanceError` to show the shortfall, `ValidationError` to point at the offending field. Everything else is already covered by the `isRetryable()` check and the base case.

### Retry Patterns

Use `isRetryable()` to implement automatic retries:

```typescript TypeScript theme={null}
async function withRetry<T>(fn: () => Promise<T>, maxRetries = 3): Promise<T> {
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      if (error instanceof KaleidoError && error.isRetryable() && attempt < maxRetries - 1) {
        const delay = Math.pow(2, attempt) * 1000;
        await new Promise(r => setTimeout(r, delay));
        continue;
      }
      throw error;
    }
  }
  throw new Error('Max retries exceeded');
}

const pairs = await withRetry(() => client.maker.listPairs());
```

```python Python theme={null}
import asyncio

async def with_retry(fn, max_retries=3):
    for attempt in range(max_retries):
        try:
            return await fn()
        except KaleidoError as e:
            if e.is_retryable() and attempt < max_retries - 1:
                delay = 2 ** attempt
                await asyncio.sleep(delay)
                continue
            raise
    raise RuntimeError("Max retries exceeded")

pairs = await with_retry(lambda: client.maker.list_pairs())
```

## Debugging

### Enable Verbose Logging

Python exposes the underlying `httpx` and `websockets` loggers:

```python theme={null}
import logging

logging.basicConfig(level=logging.DEBUG)
logging.getLogger("httpx").setLevel(logging.DEBUG)
logging.getLogger("websockets").setLevel(logging.DEBUG)
```

TypeScript has no equivalent switch for the transport. Pass a lower `logLevel` when creating the client for the SDK's own output — see [Configuration](https://docs.kaleidoswap.com/cli/node-sdk/installation.md#configuration) — and inspect the wire with `NODE_DEBUG=http`, or the network panel in your browser's DevTools.

### Validate Configuration

When calls fail before you can pin down a specific error, check these four things in order:

**1. The API URL resolves**

Open `<baseUrl>/api/v1/market/assets` in a browser. The SDK appends `/api/v1` itself, so the config value must not already include it.

**2. The client sees a node**

`client.hasNode()` / `client.has_node()` returns `false` whenever `nodeUrl` / `node_url` was omitted, and every `client.rln.*` call will fail on that alone.

**3. The node answers**

`client.rln.getNodeInfo()` / `get_node_info()` returning a pubkey proves the node is both reachable and unlocked.

**4. Both point at the same network**

A Signet `baseUrl` paired with a mainnet node yields confusing empty results rather than a clean error.

## Get Help

Check the [FAQ](https://docs.kaleidoswap.com/cli/node-sdk/faq.md) for questions rather than errors, and [Additional Resources](https://docs.kaleidoswap.com/cli/node-sdk/additional-resources.md) for upstream documentation and links.

For anything else, report a problem through your preferred channel from the options below, including:

1. SDK version (`getVersion()` / `get_version()`)
2. Language and runtime version (Node.js / Python)
3. Error message and stack trace
4. Minimal code to reproduce
5. Environment (Signet / Mainnet)

> **[Telegram Community](https://t.me/kaleidoswap)**
>
> Ask the community.

> **[GitHub Issues](https://github.com/kaleidoswap/kaleido-sdk/issues)**
>
> Report a bug on the relevant repository.

> **[Email Support](mailto:support@kaleidoswap.com)**
>
> Direct support for urgent issues.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.