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 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).
@btx-tools/mcp-gateway)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:
issue / verify / redeem against btxd — lightweight, fast RPCs.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.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:
19332) directly to the public internetThe 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.
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. |
SolverFour 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_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_transcriptderiveSigma (2 nonces) — verifymatmulserviceproof.proof.sigma from a real btxdPlus 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:
target_solve_time_s for your target user wait)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 # 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