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 # optionalexport 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 KasadaSolve with fetch
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
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.
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();
}
}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
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}`);
}