@btx-tools/challenges-sdk
v1.2.0
Published
TypeScript SDK for BTX service challenges — chain-anchored proof-of-work admission control for APIs, agent gateways, and form submissions
Maintainers
Readme
@btx-tools/challenges-sdk
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.
What is this?
Why use it
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.
How it works — issue → solve → redeem
Client ── POST /expensive ─────────────────▶ Server
│ no proof yet → issue 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 → ~1–4 s; see Performance)
▼
Client ── POST /expensive + proof headers ──▶ Server
│ redeem: verify + consume (anti-replay)
Client ◀── 200 OK — your 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).
Use cases
- 🤖 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
Prerequisites: you need a BTX node
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/redeemagainstbtxd— lightweight, fast RPCs. - Solving a challenge fast (~1–4 s) uses
btxd'ssolvematmulservicechallengeon a node that is NOT mining — on a mining node it queues behind block work and can take 15+ minutes. A ~$5/mo VPS runningbtxdwithgen=0is 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.
Install
npm install @btx-tools/challenges-sdk
# or
pnpm add @btx-tools/challenges-sdkQuickstart
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
}Security
HTTPS / TLS
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.
Error handling
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.
API
BtxChallengeClient
| 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. |
Solver
Four modes:
'rpc'— delegates to btxd'ssolvematmulservicechallengeRPC. 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-wasmkernel: 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'snodejstarget from source or use'rpc'.'pure-js'— solves locally in pure TypeScript with@noble/hashesSHA-256. Browser-compatible, no optional package. Slowest at production difficulty (see the performance section).'auto'(default) — picks'rpc'ifopts.rpcClientis provided, else'wasm'if@btx-tools/matmul-wasmis 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 });Algorithm correctness
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_deterministicderiveNoiseSeed(TAG_EL, zero_sigma)—noise_derived_seed_pinned_ELnoise.generate(zero_sigma, 4, 2)E_L + E_R —noise_EL_pinned_elements/noise_ER_pinned_elementscanonicalMatMul(n=8, b=4)transcript_hash —canonical_matmul_n8_b4_pinned_transcript- Live
deriveSigma(2 nonces) —verifymatmulserviceproof.proof.sigmafrom a real btxd
Plus 170+ internal unit tests covering field arithmetic, matrix ops, header serialization, retry/timeout/abort behavior, and solver dispatch.
⚠️ Deployment note — RPC mode against a mining btxd
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.
Performance
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_sfor 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 liven=512. SIMD's 2–4× doesn't close that gap. The matmul proof is shaped for GPU-fast native mining. Usemode: 'wasm'for fast no-node solving (server/edge, CLI, high-friction gates) andmode: '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. SeeUSE-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 # BunDrop-in middleware
For Express apps, install the companion package:
npm install @btx-tools/middleware-expressimport 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.
Status & roadmap
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.
Testing
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.
Links
License
MIT
