# API Best Practices

Practical guidance for working with the API in production

***

### 1. Parsing the Response Correctly <a href="#id-1-parsing-the-response-correctly" id="id-1-parsing-the-response-correctly"></a>

This is the most critical thing to get right. The API response is **not** a simple array of job objects.

#### The Problem <a href="#the-problem" id="the-problem"></a>

The API returns a top-level JSON array that contains **mixed types** — typically two strings followed by the actual jobs array:

```json
["string_value", "another_string", [{ "title": "...", ... }, { "title": "...", ... }]]
```

If you naively treat the response as `Job[]`, you'll get strings instead of job objects.

#### The Solution <a href="#the-solution" id="the-solution"></a>

Search for the nested array within the root array:

```javascript
const response = await fetch(url);
const data = await response.json();

if (!Array.isArray(data)) {
  throw new Error('Unexpected response format');
}

// Find the nested array containing job objects
let jobs = data.find(item => Array.isArray(item));

// Fallback: if every element is an object, the root array IS the jobs array
if (!jobs && data.length > 0 && typeof data[0] === 'object' && data[0] !== null) {
  jobs = data;
}

if (!jobs || !Array.isArray(jobs)) {
  throw new Error('Could not locate jobs array in response');
}
```

#### Python Equivalent <a href="#python-equivalent" id="python-equivalent"></a>

```python
import requests

resp = requests.get(url, params=params)
data = resp.json()

if not isinstance(data, list):
    raise ValueError("Unexpected response format")

jobs = next((item for item in data if isinstance(item, list)), None)

if jobs is None and len(data) > 0 and isinstance(data[0], dict):
    jobs = data

if not jobs:
    raise ValueError("Could not locate jobs array in response")
```

> **Do not assume the response structure is stable.** The mixed-type root array is the current behaviour, but defensive parsing that searches for the jobs array will survive format changes.

***

### 2. Handling HTML in Descriptions <a href="#id-2-handling-html-in-descriptions" id="id-2-handling-html-in-descriptions"></a>

Job descriptions frequently contain raw HTML markup:

```html
<p>We are looking for a <strong>Senior Solidity Developer</strong> to join our team.</p>
<ul>
  <li>5+ years of experience</li>
  <li>Knowledge of ERC-20 and ERC-721 standards</li>
</ul>
```

#### Strip HTML for Plain Text <a href="#strip-html-for-plain-text" id="strip-html-for-plain-text"></a>

```javascript
function stripHtml(html) {
  return html
    .replace(/<[^>]+>/g, ' ')  // Replace tags with spaces
    .replace(/\s+/g, ' ')       // Collapse whitespace
    .trim();
}
```

#### Truncate Long Descriptions <a href="#truncate-long-descriptions" id="truncate-long-descriptions"></a>

Descriptions can be very long. If you're displaying summaries or working within token limits (e.g., AI/LLM contexts), truncate after stripping:

```javascript
function cleanDescription(html, maxLength = 500) {
  const text = stripHtml(html);
  if (text.length > maxLength) {
    return text.substring(0, maxLength) + '...';
  }
  return text;
}
```

#### When to Disable Descriptions <a href="#when-to-disable-descriptions" id="when-to-disable-descriptions"></a>

Use `show_description=false` when:

* Building a job listing/browse view (titles + companies are enough)
* Doing an initial scan before fetching details
* Working within bandwidth or payload size constraints
* The consumer doesn't need description text (e.g., job count analytics)

***

### 3. Caching <a href="#id-3-caching" id="id-3-caching"></a>

The API returns recent job listings that don't change by the second. Caching responses for **5 minutes** is a sensible default — it reduces API calls significantly while keeping data fresh enough for most use cases.

#### Simple In-Memory Cache <a href="#simple-in-memory-cache" id="simple-in-memory-cache"></a>

```javascript
class Cache {
  constructor(ttlMs = 5 * 60 * 1000) {
    this.store = new Map();
    this.ttl = ttlMs;
  }

  get(key) {
    const entry = this.store.get(key);
    if (!entry) return undefined;
    if (Date.now() > entry.expiry) {
      this.store.delete(key);
      return undefined;
    }
    return entry.value;
  }

  set(key, value) {
    this.store.set(key, { value, expiry: Date.now() + this.ttl });
  }
}
```

#### Cache Key Strategy <a href="#cache-key-strategy" id="cache-key-strategy"></a>

Use the full set of filter parameters as the cache key. Serialise them deterministically:

```javascript
const cacheKey = JSON.stringify({ tag, country, remote, limit, show_description });
```

This ensures that `?tag=solidity&limit=10` and `?tag=solidity&limit=20` are cached separately.

#### When to Cache More Aggressively <a href="#when-to-cache-more-aggressively" id="when-to-cache-more-aggressively"></a>

* **Static reference data** (like the list of available tags) can be cached indefinitely or for hours
* **Broad, unfiltered queries** are expensive and change slowly — cache for 10+ minutes
* **Highly specific queries** (tag + country + remote) return smaller datasets that change more often — 5 minutes is appropriate

***

### 4. Rate Limiting and Retry Logic <a href="#id-4-rate-limiting-and-retry-logic" id="id-4-rate-limiting-and-retry-logic"></a>

The API enforces rate limits and returns `429 Too Many Requests` when exceeded.

#### Exponential Backoff with Jitter <a href="#exponential-backoff-with-jitter" id="exponential-backoff-with-jitter"></a>

```javascript
async function fetchWithRetry(url, params, maxRetries = 3) {
  for (let attempt = 0; attempt <= maxRetries; attempt++) {
    try {
      const response = await fetch(url + '?' + new URLSearchParams(params));
      if (response.ok) return response;

      if (response.status === 429 || response.status >= 500) {
        if (attempt === maxRetries) throw new Error(`Failed after ${maxRetries} retries`);

        const baseDelay = Math.min(1000 * Math.pow(2, attempt), 10000);
        const jitter = baseDelay * 0.2 * (Math.random() * 2 - 1);  // +/- 20%
        await new Promise(r => setTimeout(r, baseDelay + jitter));
        continue;
      }

      throw new Error(`API error: ${response.status}`);
    } catch (err) {
      if (attempt === maxRetries) throw err;
      // Retry on network errors too
      const delay = Math.min(1000 * Math.pow(2, attempt), 10000);
      await new Promise(r => setTimeout(r, delay));
    }
  }
}
```

#### What to Retry <a href="#what-to-retry" id="what-to-retry"></a>

| Error                         | Retry?  | Why                               |
| ----------------------------- | ------- | --------------------------------- |
| `429 Too Many Requests`       | **Yes** | Temporary rate limit — will clear |
| `5xx Server Error`            | **Yes** | Transient server issues           |
| Network timeout / no response | **Yes** | Connectivity blip                 |
| `401 Unauthorized`            | **No**  | Bad token — retrying won't help   |
| `403 Forbidden`               | **No**  | Permissions issue                 |
| `4xx` (other)                 | **No**  | Client error — fix the request    |

#### Recommended Retry Config <a href="#recommended-retry-config" id="recommended-retry-config"></a>

| Setting       | Value      | Rationale                                                         |
| ------------- | ---------- | ----------------------------------------------------------------- |
| Max retries   | 3          | Enough to ride out transient issues without hammering the API     |
| Initial delay | 1 second   | Gives the rate limiter time to reset                              |
| Max delay     | 10 seconds | Caps wait time to keep UX responsive                              |
| Jitter        | +/- 20%    | Prevents thundering herd if multiple clients retry simultaneously |

***

### 5. Input Validation <a href="#id-5-input-validation" id="id-5-input-validation"></a>

The API silently returns empty results for invalid filter values. Validate on the client side to catch mistakes early.

#### Validate Tags <a href="#validate-tags" id="validate-tags"></a>

```javascript
const VALID_TAGS = [
  "ai", "analyst", "backend", "bitcoin", "blockchain", "community-manager",
  "crypto", "cryptography", "cto", "customer-support", "dao", "data-science",
  "defi", "design", "developer-relations", "devops", "discord", "economy-designer",
  "entry-level", "erc", "erc-20", "evm", "front-end", "full-stack", "gaming",
  "ganache", "golang", "hardhat", "intern", "java", "javascript", "layer-2",
  "marketing", "mobile", "moderator", "nft", "node", "non-tech", "open-source",
  "openzeppelin", "pay-in-crypto", "product-manager", "project-manager",
  "react", "refi", "research", "ruby", "rust", "sales", "smart-contract",
  "solana", "solidity", "truffle", "web3-py", "web3js", "zero-knowledge"
];

function isValidTag(tag) {
  return VALID_TAGS.includes(tag.toLowerCase());
}
```

#### Validate Country Slugs <a href="#validate-country-slugs" id="validate-country-slugs"></a>

```javascript
function isValidCountrySlug(slug) {
  // Must be lowercase, may contain hyphens, no spaces or special chars
  return /^[a-z]+(-[a-z]+)*$/.test(slug);
}
```

#### Common Mistakes to Catch <a href="#common-mistakes-to-catch" id="common-mistakes-to-catch"></a>

| User Input          | Problem           | Correct Value      |
| ------------------- | ----------------- | ------------------ |
| `"Solidity"`        | Capitalised       | `"solidity"`       |
| `"react developer"` | Multi-word phrase | `"react"`          |
| `"web3 marketing"`  | Multi-word phrase | `"marketing"`      |
| `"USA"`             | Not a slug        | `"united-states"`  |
| `"UK"`              | Not a slug        | `"united-kingdom"` |
| `"solidty"`         | Typo              | `"solidity"`       |

***

### 6. Handling Optional Fields <a href="#id-6-handling-optional-fields" id="id-6-handling-optional-fields"></a>

Job objects have inconsistent field presence. Not every job has every field. Always code defensively:

```javascript
// Defensive field access
const title = job.title || 'Untitled Position';
const salary = job.salary || 'Not disclosed';
const tags = Array.isArray(job.tags) ? job.tags : [];
const isRemote = job.remote === true;
```

#### Extra Fields <a href="#extra-fields" id="extra-fields"></a>

Job objects may contain fields beyond the documented schema. The API can return additional metadata. Use a pass-through pattern to preserve these:

```javascript
// Preserve all fields, even unexpected ones
const processed = {
  id: job.id,
  title: job.title,
  company: job.company,
  // ... known fields ...
};

// Copy any extra fields
Object.keys(job).forEach(key => {
  if (!(key in processed)) {
    processed[key] = job[key];
  }
});
```

***

### 7. Efficient Multi-Tag Searching <a href="#id-7-efficient-multi-tag-searching" id="id-7-efficient-multi-tag-searching"></a>

Since the API only supports one tag per request, searching across multiple tags requires parallel requests.

#### Parallel Fetching with Deduplication <a href="#parallel-fetching-with-deduplication" id="parallel-fetching-with-deduplication"></a>

```javascript
async function searchMultipleTags(tags, baseFilters = {}) {
  const results = await Promise.all(
    tags.map(tag => fetchJobs({ ...baseFilters, tag }))
  );

  // Deduplicate by URL (most reliable unique identifier)
  const seen = new Set();
  const unique = [];

  for (const jobs of results) {
    for (const job of jobs) {
      const key = job.url || job.id || JSON.stringify(job);
      if (!seen.has(key)) {
        seen.add(key);
        unique.push(job);
      }
    }
  }

  return unique;
}

// Usage: find any engineering job
const engineeringJobs = await searchMultipleTags(
  ['backend', 'front-end', 'full-stack', 'smart-contract'],
  { remote: true, limit: 30 }
);
```

> **Be mindful of rate limits** when making parallel requests. If you're searching across many tags, consider batching with delays between groups.

***

### 8. Optimising Payload Size <a href="#id-8-optimising-payload-size" id="id-8-optimising-payload-size"></a>

Full responses with descriptions can be large. Optimise based on your use case:

| Scenario             | `show_description` | `limit` | Why                                                |
| -------------------- | ------------------ | ------- | -------------------------------------------------- |
| Job listing page     | `false`            | 50-100  | Titles + companies are enough for browsing         |
| Job detail view      | `true`             | 1-5     | Only fetch descriptions for jobs the user selected |
| AI agent summary     | `true`             | 10-20   | AI needs descriptions for context, but limit count |
| Analytics / counting | `false`            | 100     | Maximise data, minimise bandwidth                  |
| Alert / notification | `false`            | 10      | Quick check for new postings                       |

***

### 9. Error Handling Checklist <a href="#id-9-error-handling-checklist" id="id-9-error-handling-checklist"></a>

A robust integration should handle all of these:

* \[ ] **401** — Token is wrong or expired. Surface a clear message pointing to the token page.
* \[ ] **403** — Token permissions issue. Direct user to check their account.
* \[ ] **429** — Rate limit. Implement backoff + retry. Surface a "please wait" message if retry fails.
* \[ ] **5xx** — Server down. Retry with backoff. Show cached data if available.
* \[ ] **Network error** — No response received. Retry, then surface connectivity message.
* \[ ] **Empty results** — Not an error, but possibly an invalid filter. Suggest checking tag/country spelling.
* \[ ] **Unexpected response format** — The response shape changed. Log the raw response for debugging.
* \[ ] **Missing fields on jobs** — Gracefully handle `undefined` for any field.
* \[ ] **HTML in descriptions** — Strip or render depending on context.

***

### 10. Security Considerations <a href="#id-10-security-considerations" id="id-10-security-considerations"></a>

* **Never commit your API token** to version control. Use environment variables.
* **Never log the full request URL** in production — it contains your token as a query parameter.
* **Add `.env` and `.env.local` to `.gitignore`** if storing tokens in env files.
* **Rotate tokens** if you suspect they've been exposed (check web3.career account settings).
* If building a multi-user service, **never share tokens between users** — each user should authenticate with their own token.


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.bondex.app/api-reference/web3-career-jobs-api/api-best-practices.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
