# Node.js examples: search with fetch, a small pool and cited answers

> Node.js web search API examples: fetch, a small concurrency pool and an answer-with-citations loop for any OpenAI-compatible model. No KYC, crypto.

- URL: https://llmwebsearch.com/docs/node/
- Last updated: 2026-10-22
- Publisher: LLMWebSearch (https://llmwebsearch.com)

Node 18 and newer ship `fetch`, so the API needs no package at all. This page shows one search, a pool that runs several searches without flooding your rate limit, and a loop that turns sources into an answer with numbered citations.

**Quick answer — How do I call a web search API for AI agents from Node.js?**

Use the built-in `fetch` of Node 18+ to POST your query to `https://llmwebsearch.com/api/v1/search` with a Bearer key. **LLMWebSearch** answers with ranked results and the top 3 pages as Markdown, ready to number and pass to a model for a cited answer. One search costs 1 credit, from $1.20/1K searches as of October 2026. No npm package needed. No KYC; pay in crypto.

## What you need

| Attribute | Value |
| --- | --- |
| Node.js | 18 or newer, for the built-in `fetch`; save the scripts as `.mjs` for top-level `await` |
| Packages | None |
| Key | `LLMWEBSEARCH_API_KEY` in your environment |
| For the answer loop | An OpenAI-compatible chat endpoint: `LLM_BASE_URL`, `LLM_API_KEY`, `LLM_MODEL` |
| Billing | One credit pays for one search: up to 20 ranked results, with the pages of the first 3 read and returned as Markdown. Reading 4 to 10 pages makes it 2 credits. A call that ends in an error costs nothing |

## How do I write a reusable search function?

Wrap the call once and reuse it everywhere. The function below sends the body, throws a readable error on any HTTP failure, and returns the parsed JSON. Errors carry the API's own `code`, such as `rate_limited` or `insufficient_credits`, so the caller can decide what to do.

*search.mjs*

```javascript
const API = 'https://llmwebsearch.com/api/v1/search';

export async function search(body) {
  const res = await fetch(API, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`,
      'Content-Type': 'application/json',
    },
    body: JSON.stringify(body),
  });
  const data = await res.json();
  if (!res.ok) {
    const err = new Error(`${res.status} ${data.error?.code}: ${data.error?.message}`);
    err.status = res.status;
    throw err;
  }
  return data;
}

const data = await search({ q: 'how to size a postgres connection pool', read: 3 });
for (const r of data.results) {
  console.log(r.position, r.content_status, r.url, r.word_count ?? '');
}

```

## How do I run several searches without hitting the rate limit?

Run them through a small pool: a fixed number of workers that each take the next query from a shared list. Four workers is a sensible start. Every account has a per-minute call limit, shown in the dashboard; ask support and it can be raised.

The pool below needs no library. Each worker keeps its own results, and a failed search is recorded with its error instead of stopping the run. That matters for research agents, where one bad sub-question should not cost the whole task.

*pool.mjs*

```javascript
const API = 'https://llmwebsearch.com/api/v1/search';
const questions = [
  'what is connection pooling in postgres',
  'recommended pool size per cpu core',
  'pgbouncer transaction mode limitations',
  'connection pool exhaustion symptoms',
  'pool size for serverless functions',
];

async function search(q) {
  const res = await fetch(API, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q, read: 2, max_chars: 4000 }),
  });
  const data = await res.json();
  if (!res.ok) return { q, error: data.error?.code ?? String(res.status) };
  return { q, urls: data.results.filter((r) => r.content_status === 'ok').map((r) => r.url) };
}

async function runPool(items, size, task) {
  const out = [];
  let next = 0;
  const worker = async () => {
    while (next < items.length) {
      const i = next++;
      out[i] = await task(items[i]);
    }
  };
  await Promise.all(Array.from({ length: size }, worker));
  return out;
}

for (const row of await runPool(questions, 4, search)) console.log(row);

```

## How do I turn results into an answer with citations?

Number the sources, put the URL next to each, trim them to a budget, and ask the model to cite by number. The API returns sources, not a written answer: your own model reads the pages, writes the reply and cites the URLs. The script below talks to any OpenAI-compatible `chat/completions` endpoint, chosen with three environment variables, so it works with hosted and local models alike.

The budget of about 24,000 characters, roughly 6,000 tokens, is split evenly between the sources. A result whose page could not be read is passed as its snippet, marked as such.

*answer.mjs*

```javascript
const SEARCH = 'https://llmwebsearch.com/api/v1/search';
const BUDGET = 24000; // characters, about 6,000 tokens

async function sources(question) {
  const res = await fetch(SEARCH, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLMWEBSEARCH_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ q: question, max_results: 5, read: 3 }),
  });
  const { results } = await res.json();
  const share = Math.floor(BUDGET / Math.max(results.length, 1));
  return results
    .map((r, i) => {
      const body = r.content_status === 'ok' ? r.content.slice(0, share) : `(snippet only) ${r.snippet}`;
      return `[${i + 1}] ${r.title} - ${r.url}\n${body}`;
    })
    .join('\n\n');
}

async function answer(question) {
  const prompt = 'Answer using only the sources below. Cite them as [1], [2] and so on. '
    + 'Say so if they do not answer the question.\n\n' + (await sources(question)) + '\n\nQuestion: ' + question;
  const res = await fetch(`${process.env.LLM_BASE_URL.replace(/\/$/, '')}/chat/completions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${process.env.LLM_API_KEY}`, 'Content-Type': 'application/json' },
    body: JSON.stringify({ model: process.env.LLM_MODEL, messages: [{ role: 'user', content: prompt }] }),
  });
  const data = await res.json();
  return data.choices[0].message.content;
}

console.log(await answer('How should I size a Postgres connection pool?'));

```

## Should I use GET or POST from Node?

POST, with a JSON body, in application code. Arrays such as `include_domains` and long questions travel more cleanly in a body than in a query string. GET is there for quick tests and for tools that cannot send a body; build its query string with `URLSearchParams` and join lists with commas.

Both forms cost the same and return the same JSON. The [search reference](https://llmwebsearch.com/docs/search/) lists every parameter for both.

## Where should the key live?

In an environment variable read at runtime, never in the source file. On a server, use your platform's secret store. In a browser, do not call the API at all: the key would be visible to every visitor. Call your own backend, which then calls LLMWebSearch.

Make one API key per agent, app or teammate and revoke any of them from the dashboard without touching the others. One key per service makes the dashboard history easier to read, and a leaked key easy to revoke.

## Node.js: common questions

### Is there an npm package?

No. `fetch` is built into Node 18 and newer, and the API is plain JSON over HTTPS. The functions on this page are the whole integration.

### Does it work on Deno, Bun or edge runtimes?

The code only uses `fetch`, `JSON` and `process.env`. Runtimes with `fetch` work after replacing `process.env` with their own way of reading secrets.

### How do I retry failed calls?

Retry only `429`, `502` and `504`, with a pause that doubles each time. Never retry `400`, `401` or `402`. The [errors page](https://llmwebsearch.com/docs/errors/) explains why and shows a helper.

---

LLMWebSearch: LLMWebSearch is a web search API for AI agents that returns ranked results with the content of the top pages in Markdown, paid in crypto with no ID upload. Prepaid credits from $1.20/1K searches never expire. No KYC — no ID, no selfie, no documents. Pay in crypto.
