NSLSolver
Guides

Node.js

Native fetch and Axios examples with proper timeouts, semaphore-based concurrency control, and retry logic.

Setup

Node.js 18+ has a global fetch — nothing to install for the basic path. For ergonomics, axios is fine too.

npm install axios   # optional
config.mjs
export const API_KEY = process.env.NSL_API_KEY;
export const BASE_URL = "https://api.nslsolver.com";
export const SOLVE_TIMEOUT_MS = 120_000;  // 180_000 for Kasada

Solve with fetch

solver.mjs
import { API_KEY, BASE_URL, SOLVE_TIMEOUT_MS } from "./config.mjs";

export async function solve(payload) {
  const r = await fetch(`${BASE_URL}/solve`, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      "X-API-Key": API_KEY,
    },
    body: JSON.stringify(payload),
    signal: AbortSignal.timeout(SOLVE_TIMEOUT_MS),
  });

  const data = await r.json();
  if (!data.success) {
    const err = new Error(data.error);
    err.status = r.status;
    throw err;
  }
  return data;
}

// Turnstile
const result = await solve({
  type: "turnstile",
  site_key: "0x4AAAAAAAB...",
  url: "https://example.com",
});
console.log(result.token, result.cost);

Axios alternative

solver-axios.mjs
import axios from "axios";
import { API_KEY, BASE_URL, SOLVE_TIMEOUT_MS } from "./config.mjs";

const client = axios.create({
  baseURL: BASE_URL,
  timeout: SOLVE_TIMEOUT_MS,
  headers: { "Content-Type": "application/json", "X-API-Key": API_KEY },
  validateStatus: () => true,  // we'll branch on status ourselves
});

export async function solve(payload) {
  const { status, data } = await client.post("/solve", payload);
  if (!data.success) {
    const err = new Error(data.error);
    err.status = status;
    throw err;
  }
  return data;
}

Concurrency control

The server's CPM bucket caps you at max_cpm solves per minute. Don't fight it — match your client-side concurrency to it.

semaphore.mjs
export class Semaphore {
  constructor(n) {
    this.permits = n;
    this.queue = [];
  }
  async acquire() {
    if (this.permits > 0) {
      this.permits--;
      return;
    }
    await new Promise((res) => this.queue.push(res));
    this.permits--;
  }
  release() {
    this.permits++;
    const next = this.queue.shift();
    if (next) next();
  }
}
pool.mjs
import { Semaphore } from "./semaphore.mjs";
import { solve } from "./solver.mjs";

// At startup
const balance = await fetch(`${BASE_URL}/balance`, {
  headers: { "X-API-Key": process.env.NSL_API_KEY },
}).then((r) => r.json());

const avgSolveSec = 1.5;
const limit = Math.max(1, Math.floor((balance.cpm_limit || 600) * avgSolveSec / 60));
const sem = new Semaphore(limit);

export async function solveBounded(payload) {
  await sem.acquire();
  try {
    return await solve(payload);
  } finally {
    sem.release();
  }
}

Don't spin up unbounded Promise.all

Promise.all(items.map(solve)) opens items.length parallel sockets at once. With a 600 CPM key and a 1000-item batch, you'll burn through your bucket in a single second and hit 429s. Wrap each call in solveBounded.

Retry with backoff

retry.mjs
const RETRY = new Set([429, 503]);
const FATAL = new Set([400, 401, 402, 403]);

const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

export async function solveWithRetry(payload, { attempts = 4 } = {}) {
  let lastErr;
  for (let i = 0; i < attempts; i++) {
    let r;
    try {
      r = await fetch(`${BASE_URL}/solve`, {
        method: "POST",
        headers: { "Content-Type": "application/json", "X-API-Key": API_KEY },
        body: JSON.stringify(payload),
        signal: AbortSignal.timeout(SOLVE_TIMEOUT_MS),
      });
    } catch (e) {
      lastErr = e;
      await sleep(Math.min(2 ** i * 1000, 8000));
      continue;
    }

    const data = await r.json();
    if (r.status === 200) return data;
    if (FATAL.has(r.status)) throw new Error(`non-retryable ${r.status}: ${data.error}`);
    if (RETRY.has(r.status)) {
      lastErr = data.error;
      await sleep(Math.min(2 ** i * 1000, 8000));
      continue;
    }
    throw new Error(`unexpected ${r.status}: ${data.error}`);
  }
  throw new Error(`attempts exhausted; last error: ${lastErr}`);
}

On this page