@btx-tools SDK — BTX service-challenge admission control
    Preparing search index...

    Module @btx-tools/challenges-sdk

    @btx-tools/challenges-sdk

    License npm

    Put a proof-of-work checkpoint in front of any endpoint — no CAPTCHA, no login, no API keys, no third-party service.

    Your server asks a caller to burn a few seconds of verifiable compute before you do something expensive or abusable. The work is a domain-bound MatMul proof defined and checked by the BTX chain — so there's no centralized issuer to trust, and a proof can't be replayed.

    📖 API Reference · 🟢 Stable 1.0.0 (API frozen under SemVer). RPC + pure-JS solver cross-validated byte-equal against btxd's own pinned test vectors; opt-in retry/backoff (onRetry), per-method timeouts, AbortSignal plumbing. All audit findings closed. CHANGELOG.

    Slowing down bots / scraping / spam usually means a CAPTCHA (annoys real users, increasingly bot-solved), accounts / API keys (signup friction + a user database), or a hosted anti-bot service (a third party, a bill, a privacy trade-off). A BTX service challenge instead makes the caller prove they spent a little real compute — cheap to verify, costly to spam at scale, anchored to a public chain, and fully self-hosted.

    Client ── POST /expensive ─────────────────▶  Server
    no proof yetissue a challenge
    Client ◀── 402 Payment Required ───────────────┤ (challenge rides in the X-BTX-Challenge header)
    solve the matmul work-proof
    │ (server-side via a nearby NON-mining btxd RPC → ~14 s; see Performance)

    Client ── POST /expensive + proof headers ──▶ Server
    redeem: verify + consume (anti-replay)
    Client ◀── 200 OKyour handler runs ─────────┘

    The middleware packages run this whole handshake for you, with no server-side challenge store (the challenge echoes back in a header on retry).

    • 🤖 Gate AI / inference APIs without a CAPTCHA or login wall
    • 🛡️ Per-tool-call proof-of-work for MCP / agent gateways (see @btx-tools/mcp-gateway)
    • 📝 Anonymous form / submission rate-limiting without accounts
    • 🚦 Replace hCaptcha / reCAPTCHA with self-hosted, chain-anchored proof — on the server side

    This SDK talks to a BTX full node (btxd) over JSON-RPC. It does not bundle or call any hosted service, and there is no public/shared endpoint — a project with zero BTX infrastructure can't just npm install and start gating traffic. You (or someone) must run a btxd you can reach:

    • The gate (your server) calls issue / verify / redeem against btxd — lightweight, fast RPCs.
    • Solving a challenge fast (~1–4 s) uses btxd's solvematmulservicechallenge on a node that is NOT mining — on a mining node it queues behind block work and can take 15+ minutes. A ~$5/mo VPS running btxd with gen=0 is enough.
    • Pure-JS solving (the browser-compatible Solver) needs no node, but is minutes-to-hours at production difficulty — practical only for low/calibrated difficulty or non-interactive (cron/batch) flows. See Performance.

    Minimal setup from zero: run a btxd full node (sync the chain, set rpcauth, expose RPC over TLS or bind to 127.0.0.1), then point rpcUrl / rpcAuth at it. There's deliberately no centralized solver service — that would defeat the proof's attacker-vs-defender asymmetry.

    npm install @btx-tools/challenges-sdk
    # or
    pnpm add @btx-tools/challenges-sdk
    import { BtxChallengeClient } from '@btx-tools/challenges-sdk';

    const client = new BtxChallengeClient({
    rpcUrl: 'http://127.0.0.1:19332',
    rpcAuth: { user: 'rpcuser', pass: 'rpcpass' },
    });

    // Server: issue a challenge bound to the requested resource
    const challenge = await client.issue({
    purpose: 'ai_inference_gate',
    resource: 'model:gpt-x|route:/v1/generate',
    subject: 'tenant:abc123',
    target_solve_time_s: 2,
    expires_in_s: 60,
    });

    // ... ship challenge to client; client solves locally and returns (nonce, digest) ...

    // Server: verify-and-consume atomically (anti-replay admission)
    const result = await client.redeem(challenge, nonce64_hex, digest_hex);

    if (result.valid && result.reason === 'ok') {
    // Run the expensive action
    }

    Basic-auth credentials are sent on every RPC call. Use HTTPS (or a localhost-only deployment) when btxd's RPC port is exposed beyond 127.0.0.1.

    Recommended terminations:

    • stunnel, nginx, or Caddy in front of btxd
    • Cloudflare Tunnel for remote operator access
    • Never expose btxd's RPC port (default 19332) directly to the public internet

    The SDK does NOT enforce HTTPS — that's a deployment concern. If you set rpcUrl: 'http://example.com:19332' from a production service, the SDK will happily transmit your credentials in plaintext.

    import {
    BtxError, // base class — all SDK errors extend this
    BtxRpcError, // btxd returned a JSON-RPC error envelope
    BtxHttpError, // non-2xx HTTP status
    BtxParseError, // 2xx but body wasn't valid JSON
    BtxTimeoutError, // request exceeded timeoutMs
    BtxNetworkError, // DNS/TCP/TLS-level failure
    } from '@btx-tools/challenges-sdk';

    try {
    await client.redeem(challenge, nonce, digest);
    } catch (err) {
    if (err instanceof BtxRpcError && err.code === -8) {
    // btxd rejected the request shape
    } else if (err instanceof BtxTimeoutError) {
    // user took too long to solve
    } else if (err instanceof BtxError) {
    // any other SDK-originated error
    }
    }

    Error response bodies are scanned and Authorization: Basic <token> patterns are redacted before storage — safe to log.

    Method RPC Description
    issue(params) getmatmulservicechallenge Issue a fresh challenge bound to (purpose, resource, subject).
    verify(...) verifymatmulserviceproof Stateless verify. Does NOT consume the challenge.
    redeem(...) redeemmatmulserviceproof Atomic verify + consume. Use for admission control.
    verifyBatch(entries) verifymatmulserviceproofs Batch (1–256) verify. No consumption.
    redeemBatch(entries) redeemmatmulserviceproofs Batch verify + consume, sequential.
    solve(challenge) solvematmulservicechallenge Server-side solver (fixtures + tests).
    call(method, params) (any) Low-level escape hatch.

    Four modes:

    • 'rpc' — delegates to btxd's solvematmulservicechallenge RPC. Server-side / Node only. Fast (sub-second to a few seconds) on a dedicated non-mining node — see the deployment note below.
    • 'wasm' — solves locally with the optional @btx-tools/matmul-wasm kernel: a byte-exact Rust→WASM port of the matmul PoW, ~24× faster than 'pure-js' (byte-identical proof). No node required. Throws a clear error if the package isn't installed. The published build targets browsers/bundlers (Vite, Next, Workers); in plain Node, build the package's nodejs target from source or use 'rpc'.
    • 'pure-js' — solves locally in pure TypeScript with @noble/hashes SHA-256. Browser-compatible, no optional package. Slowest at production difficulty (see the performance section).
    • 'auto' (default) — picks 'rpc' if opts.rpcClient is provided, else 'wasm' if @btx-tools/matmul-wasm is installed, else 'pure-js'.
    import { BtxChallengeClient, Solver } from '@btx-tools/challenges-sdk';

    // Server-side (RPC mode): delegates the solve to btxd
    const client = new BtxChallengeClient({ rpcUrl: '...', rpcAuth: { ... } });
    const proof = await Solver.solve(challenge, { mode: 'rpc', rpcClient: client });

    // No-node, fast (WASM mode): install `@btx-tools/matmul-wasm`, ~24× pure-JS
    const proof = await Solver.solve(challenge, { mode: 'wasm' });

    // Browser / no-RPC (pure-JS mode): solves locally, no optional package
    const proof = await Solver.solve(challenge, {
    mode: 'pure-js',
    pureJs: { maxTries: 5_000 }, // cap on attempts before giving up
    });

    // 'auto' (default) — rpc if a client is passed, else wasm (if installed), else pure-js
    const proof = await Solver.solve(challenge, { rpcClient: client });

    The pure-JS solver is a direct port of the canonical CPU path from btxd v0.29.7 src/matmul/. We cross-validate against 5 pinned golden vectors lifted from btxd's own test suite — see tests/unit/matmul/btxd-vectors.test.ts. Match is byte-equal for:

    • fromSeedRect(zero, 8)matrix_from_seed_deterministic
    • deriveNoiseSeed(TAG_EL, zero_sigma)noise_derived_seed_pinned_EL
    • noise.generate(zero_sigma, 4, 2) E_L + E_R — noise_EL_pinned_elements / noise_ER_pinned_elements
    • canonicalMatMul(n=8, b=4) transcript_hash — canonical_matmul_n8_b4_pinned_transcript
    • Live deriveSigma (2 nonces) — verifymatmulserviceproof.proof.sigma from a real btxd

    Plus 170+ internal unit tests covering field arithmetic, matrix ops, header serialization, retry/timeout/abort behavior, and solver dispatch.

    btxd's service-challenge solver shares the matmul backend with block-template mining. On a node that's actively mining, solvematmulservicechallenge queues behind block work and can take 15+ minutes per call — measured 2026-05-20 on a production mining rental, where the solve RPC didn't return even after btx-cli's own 15-minute transient-error timeout fired.

    For RPC mode at advertised latency (~1–4 seconds), point it at a dedicated btxd that is NOT mining (e.g., a $5/mo DO droplet with gen=0 in btx.conf). The SDK itself works fine — the bottleneck is the upstream solver service-sharing.

    Pure-JS solver bench at production matmul shape (n=512, b=16, r=8) on M-series Mac arm64 (2026-05-22, 5-sample mean):

    Engine Mean / attempt vs Node 22
    Node 22.20 / V8 4.6 s 1.0× (baseline)
    Deno 2.7 / V8 4.2 s 0.92× (slightly faster, within noise)
    Bun 1.3 / JavaScriptCore 9.8 s 2.1× slower
    Firefox SpiderMonkey untested
    Safari JavaScriptCore untested

    mul and the dot accumulator use bigint because the worst-case M31 product ((2^31-1)^2 ≈ 2^62) exceeds Number's 2^53 precision. The bigint-bounded inner loop is the dominant cost. Bun's JavaScriptCore engine is ~2× slower than V8 for bigint-heavy workloads — if Bun is your runtime, factor that into your target_solve_time_s calibration.

    Expected end-to-end solve time depends on challenge difficulty. At btxd's lowest service-challenge difficulty (target_solve_time_s = min_solve_time_s = 0.001), per-attempt success ≈ 1.3·10⁻³, so expected ≈ 770 attempts:

    Engine Expected solve at floor difficulty
    Node 22 / V8 ~59 min
    Deno 2.7 / V8 ~54 min
    Bun 1.3 / JSC ~2.1 hr

    Default difficulty is too slow for online browser use. Workable today for:

    • Server-side gating where you control difficulty (calibrate via target_solve_time_s for your target user wait)
    • Backend cron / batch jobs
    • Examples + demos with manually-issued low-difficulty challenges

    The WASM kernel (@btx-tools/matmul-wasm) makes no-node solving ~24× faster — but it is still not a casual browser captcha. It's a byte-exact Rust→WASM port of the matmul hot loop (~24× over pure-JS BigInt; byte-identical proof). On an 8-worker browser pool a floor-difficulty solve is ~16 s, and difficulty calibrates to the chain's fast native solver — so a "1–4 s" challenge is multiple seconds-to-minutes in a browser at the live n=512. SIMD's 2–4× doesn't close that gap. The matmul proof is shaped for GPU-fast native mining. Use mode: 'wasm' for fast no-node solving (server/edge, CLI, high-friction gates) and mode: 'rpc' against a nearby non-mining btxd (~1–4 s) for production server-side gating. A casual sub-second browser captcha needs an upstream browser-friendly proof primitive. See USE-CASES.md.

    Reproduce the bench:

    npx tsx packages/core/tests/perf/solver-bench.ts 10                              # Node
    deno run --allow-all --unstable-sloppy-imports tests/perf/solver-bench.ts 10 # Deno
    bun tests/perf/solver-bench.ts 10 # Bun

    For Express apps, install the companion package:

    npm install @btx-tools/middleware-express
    
    import express from 'express';
    import { BtxChallengeClient } from '@btx-tools/challenges-sdk';
    import { btxAdmission } from '@btx-tools/middleware-express';

    const client = new BtxChallengeClient({ rpcUrl: '...', rpcAuth: { ... } });
    const app = express();

    app.post(
    '/v1/generate',
    btxAdmission({
    client,
    purpose: 'ai_inference_gate',
    resource: (req) => `model:${req.body.model}|route:${req.path}`,
    subject: (req) => `tenant:${req.body.tenant_id}`,
    }),
    (req, res) => res.json({ ok: true, generated: '...' }),
    );

    That's it — one line, your route is gated by a BTX service challenge. Full docs at @btx-tools/middleware-express or in the package README.

    Also available: @btx-tools/middleware-fastify (Fastify plugin) and @btx-tools/middleware-hono (Hono — Node + edge: Cloudflare Workers, Deno, Bun). Same btxAdmission shape across all three.

    Shipped & stable at 1.0.0 — RPC client + Solver (rpc + pure-JS), pure-JS matmul port cross-validated against btxd goldens, retry/backoff + per-method timeouts + AbortSignal, Express/Fastify/Hono adapters, the @btx-tools/mcp-gateway companion, runnable examples, and a published API reference. Two deep audits closed every finding.

    Post-1.0 candidates (additive, non-breaking) — Cloudflare Worker template, WordPress plugin, Python SDK, LangChain bindings — are listed in the monorepo README.

    pnpm test                # all tests
    pnpm test:unit # msw-mocked HTTP only (fast)
    pnpm test:integration # live btxd via SSH (requires fleet access)

    The integration test target is btx-iowa by default — change SSH_TARGET in tests/integration/smoke.test.ts to retarget any healthy at-tip BTX node.

    MIT

    Classes

    BtxChallengeClient
    BtxError
    BtxHttpError
    BtxNetworkError
    BtxParseError
    BtxRpcError
    BtxTimeoutError
    Solver

    Interfaces

    BatchEntry
    BatchResult
    BtxClientOpts
    Challenge
    ChallengeBinding
    ChallengeHeaderContext
    ChallengeMatmul
    ChallengePayload
    ChallengeProofPolicy
    IssueParams
    RetryOptions
    RpcCallOpts
    SolveJsOptions
    SolverOptions
    SolverOutput
    VerifyResult
    WasmSolveOptions

    Type Aliases

    ChallengePurpose
    DifficultyPolicy
    SolverMode
    VerifyReason