Guides
Go
Idiomatic net/http client with timeouts, semaphore-bounded concurrency, and a retry helper.
Types
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
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
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
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.