NSLSolver
Guides

Go

Idiomatic net/http client with timeouts, semaphore-bounded concurrency, and a retry helper.

Types

types.go
package nsl

type SolveReq struct {
    Type      string `json:"type"`
    SiteKey   string `json:"site_key,omitempty"`
    URL       string `json:"url"`
    Action    string `json:"action,omitempty"`
    CData     string `json:"cdata,omitempty"`
    Proxy     string `json:"proxy,omitempty"`
    UserAgent string `json:"user_agent,omitempty"`

    // Kasada
    UAVersion    int          `json:"ua_version,omitempty"`
    KasadaConfig *KasadaCfg   `json:"kasada_config,omitempty"`
}

type KasadaCfg struct {
    PJSPath    string `json:"p_js_path"`
    FPHost     string `json:"fp_host"`
    TLHost     string `json:"tl_host"`
    CDConstant string `json:"cd_constant,omitempty"`
}

type SolveResp struct {
    Success   bool              `json:"success"`
    Type      string            `json:"type"`
    Token     string            `json:"token,omitempty"`
    Cookies   map[string]string `json:"cookies,omitempty"`
    UserAgent string            `json:"user_agent,omitempty"`
    Headers   map[string]any    `json:"headers,omitempty"`
    Cost      float64           `json:"cost,omitempty"`
    Error     string            `json:"error,omitempty"`
}

type BalanceResp struct {
    Success            bool     `json:"success"`
    Balance            float64  `json:"balance"`
    Unlimited          bool     `json:"unlimited"`
    AllowedTypes       []string `json:"allowed_types"`
    MaxCPM             int      `json:"max_cpm"`
    CurrentCPM         int      `json:"current_cpm"`
    CPMLimit           int      `json:"cpm_limit"`
    UnlimitedExpiresAt string   `json:"unlimited_expires_at,omitempty"`
    Error              string   `json:"error,omitempty"`
}

Client

client.go
package nsl

import (
    "bytes"
    "context"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "time"
)

const baseURL = "https://api.nslsolver.com"

var ErrNonRetryable = errors.New("non-retryable error")

type Client struct {
    APIKey string
    HTTP   *http.Client
}

func New() *Client {
    return &Client{
        APIKey: os.Getenv("NSL_API_KEY"),
        HTTP:   &http.Client{Timeout: 180 * time.Second},
    }
}

func (c *Client) Solve(ctx context.Context, req SolveReq) (*SolveResp, int, error) {
    body, _ := json.Marshal(req)
    httpReq, _ := http.NewRequestWithContext(ctx, "POST", baseURL+"/solve", bytes.NewReader(body))
    httpReq.Header.Set("Content-Type", "application/json")
    httpReq.Header.Set("X-API-Key", c.APIKey)

    resp, err := c.HTTP.Do(httpReq)
    if err != nil {
        return nil, 0, err
    }
    defer resp.Body.Close()

    buf, _ := io.ReadAll(resp.Body)
    var out SolveResp
    if jerr := json.Unmarshal(buf, &out); jerr != nil {
        return nil, resp.StatusCode, fmt.Errorf("decode: %w (body: %s)", jerr, string(buf))
    }
    return &out, resp.StatusCode, nil
}

func (c *Client) Balance(ctx context.Context) (*BalanceResp, error) {
    req, _ := http.NewRequestWithContext(ctx, "GET", baseURL+"/balance", nil)
    req.Header.Set("X-API-Key", c.APIKey)

    resp, err := c.HTTP.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var out BalanceResp
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return nil, err
    }
    return &out, nil
}

Retry with backoff

retry.go
package nsl

import (
    "context"
    "fmt"
    "math/rand"
    "time"
)

func isFatal(status int) bool {
    return status == 400 || status == 401 || status == 402 || status == 403
}

func isRetryable(status int) bool {
    return status == 429 || status == 503
}

func (c *Client) SolveWithRetry(ctx context.Context, req SolveReq, attempts int) (*SolveResp, error) {
    var lastErr error
    for i := 0; i < attempts; i++ {
        resp, status, err := c.Solve(ctx, req)
        if err != nil {
            lastErr = err
        } else if status == 200 {
            return resp, nil
        } else if isFatal(status) {
            return nil, fmt.Errorf("%w (%d): %s", ErrNonRetryable, status, resp.Error)
        } else if isRetryable(status) {
            lastErr = fmt.Errorf("%d: %s", status, resp.Error)
        } else {
            return nil, fmt.Errorf("unexpected %d: %s", status, resp.Error)
        }

        // exponential backoff with jitter, capped at 8s
        wait := time.Duration(1<<i) * time.Second
        if wait > 8*time.Second {
            wait = 8 * time.Second
        }
        wait += time.Duration(rand.Intn(250)) * time.Millisecond
        select {
        case <-ctx.Done():
            return nil, ctx.Err()
        case <-time.After(wait):
        }
    }
    return nil, fmt.Errorf("attempts exhausted: %w", lastErr)
}

Bounded worker pool

pool.go
package main

import (
    "context"
    "fmt"
    "sync"
    "time"

    "yourmod/nsl"
)

func main() {
    ctx := context.Background()
    c := nsl.New()

    bal, err := c.Balance(ctx)
    if err != nil {
        panic(err)
    }

    avgSolve := 1.5 // seconds
    workers := int(float64(bal.CPMLimit) * avgSolve / 60.0)
    if workers < 1 {
        workers = 1
    }

    jobs := make(chan nsl.SolveReq, 1000)
    var wg sync.WaitGroup

    for w := 0; w < workers; w++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for req := range jobs {
                rctx, cancel := context.WithTimeout(ctx, 120*time.Second)
                resp, err := c.SolveWithRetry(rctx, req, 3)
                cancel()
                if err != nil {
                    fmt.Println("solve error:", err)
                    continue
                }
                fmt.Println("token:", resp.Token[:32], "cost:", resp.Cost)
            }
        }()
    }

    // feed jobs ...
    close(jobs)
    wg.Wait()
}

HTTP timeout

Set the client timeout to 120s for Turnstile/Challenge, 180s for Kasada. Lower values just throw away in-progress solves you'd never be billed for anyway.

On this page