mtok-relay 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/mtok-relay.mjs +183 -85
  2. package/package.json +1 -1
@@ -2157,7 +2157,7 @@ var secp256k1 = createCurve({
2157
2157
  }, sha256);
2158
2158
 
2159
2159
  // node_modules/viem/_esm/errors/version.js
2160
- var version = "2.56.0";
2160
+ var version = "2.56.8";
2161
2161
 
2162
2162
  // node_modules/viem/_esm/errors/base.js
2163
2163
  var errorConfig = {
@@ -2423,7 +2423,7 @@ function hexToBigInt(hex, opts = {}) {
2423
2423
  const value = BigInt(hex);
2424
2424
  if (!signed)
2425
2425
  return value;
2426
- const size2 = (hex.length - 2) / 2;
2426
+ const size2 = Math.ceil((hex.length - 2) / 2);
2427
2427
  const max = (1n << BigInt(size2) * 8n - 1n) - 1n;
2428
2428
  if (value <= max)
2429
2429
  return value;
@@ -4662,6 +4662,7 @@ function privateKeyToAccount(privateKey, options = {}) {
4662
4662
  }
4663
4663
 
4664
4664
  // src/config.mjs
4665
+ import { isIP } from "node:net";
4665
4666
  var flag = (args, name) => {
4666
4667
  const i = args.indexOf(name);
4667
4668
  return i !== -1 ? args[i + 1] : null;
@@ -4687,6 +4688,15 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
4687
4688
  if (maxOutputTokens != null && (!Number.isFinite(maxOutputTokens) || maxOutputTokens < 1)) {
4688
4689
  throw new Error("--max-output-tokens must be a positive integer");
4689
4690
  }
4691
+ const trustedProxies = String(env.RELAY_TRUSTED_PROXIES ?? "").split(",").map((value) => value.trim()).filter(Boolean);
4692
+ if (trustedProxies.some((value) => !isIP(value) || value.includes("%"))) throw new Error("RELAY_TRUSTED_PROXIES must contain exact IP addresses");
4693
+ const clientIpHeader = String(env.RELAY_CLIENT_IP_HEADER ?? "cf-connecting-ip").toLowerCase();
4694
+ if (!/^[a-z0-9-]+$/.test(clientIpHeader)) throw new Error("RELAY_CLIENT_IP_HEADER must be a header name");
4695
+ const maxConcurrentRequests = Number(env.RELAY_MAX_CONCURRENT_REQUESTS ?? 64);
4696
+ const upstreamTimeoutMs = Number(env.RELAY_UPSTREAM_TIMEOUT_MS ?? 12e4);
4697
+ for (const [name, value] of Object.entries({ RELAY_MAX_CONCURRENT_REQUESTS: maxConcurrentRequests, RELAY_UPSTREAM_TIMEOUT_MS: upstreamTimeoutMs })) {
4698
+ if (!Number.isSafeInteger(value) || value < 1 || value > 2147483647) throw new Error(`${name} must be a positive finite integer`);
4699
+ }
4690
4700
  if (!offerId) throw new Error("--offer <id> is required");
4691
4701
  if (!model) throw new Error("--model <id> is required (the offer model you serve)");
4692
4702
  if (!upstream) throw new Error("--upstream <url> is required");
@@ -4726,23 +4736,72 @@ function readRelayConfig({ argv = process.argv.slice(2), env = process.env } = {
4726
4736
  upstreamKey,
4727
4737
  settlementAddr,
4728
4738
  payerDenylist,
4729
- maxOutputTokens
4739
+ maxOutputTokens,
4740
+ trustedProxies,
4741
+ clientIpHeader,
4742
+ maxConcurrentRequests,
4743
+ upstreamTimeoutMs
4730
4744
  };
4731
4745
  }
4732
4746
 
4733
4747
  // src/http.mjs
4734
4748
  import http from "node:http";
4749
+ import { isIP as isIP2 } from "node:net";
4750
+
4751
+ // core/errors.js
4752
+ function apiError(status, code, message, details) {
4753
+ const err = new Error(message);
4754
+ err.status = status;
4755
+ err.code = code;
4756
+ if (details !== void 0) err.details = details;
4757
+ return err;
4758
+ }
4759
+
4760
+ // core/limiter.js
4761
+ function createLimiter({ windowMs = 6e4, max = 120, maxKeys = 1e4, now = () => Date.now() } = {}) {
4762
+ const windows = /* @__PURE__ */ new Map();
4763
+ return {
4764
+ check(key, scope = "api") {
4765
+ const id = `${scope}:${key}`;
4766
+ const t = now();
4767
+ let w = windows.get(id);
4768
+ if (!w || t - w.start >= windowMs) {
4769
+ w = { start: t, count: 0 };
4770
+ windows.delete(id);
4771
+ if (windows.size >= maxKeys) windows.delete(windows.keys().next().value);
4772
+ windows.set(id, w);
4773
+ }
4774
+ w.count += 1;
4775
+ if (w.count > max) {
4776
+ const retryInSeconds = Math.ceil((w.start + windowMs - t) / 1e3);
4777
+ throw apiError(429, "rate_limited", `Rate limit exceeded (${max}/${windowMs / 1e3}s). Retry in ~${retryInSeconds}s.`, { retryAfterSeconds: retryInSeconds });
4778
+ }
4779
+ },
4780
+ get size() {
4781
+ return windows.size;
4782
+ }
4783
+ };
4784
+ }
4785
+
4786
+ // src/http.mjs
4735
4787
  var MAX_BODY_BYTES = 256e3;
4736
- function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
4788
+ function readBody(req, { maxBytes = MAX_BODY_BYTES, timeoutMs = 1e4 } = {}) {
4737
4789
  return new Promise((resolve, reject) => {
4738
4790
  const chunks = [];
4739
4791
  let bytes = 0;
4740
4792
  let settled = false;
4793
+ const fail = (error) => {
4794
+ if (settled) return;
4795
+ settled = true;
4796
+ clearTimeout(timer);
4797
+ chunks.length = 0;
4798
+ reject(error);
4799
+ };
4800
+ const timer = setTimeout(() => fail(Object.assign(new Error("body_timeout"), { code: "body_timeout" })), timeoutMs);
4741
4801
  req.on("data", (d) => {
4742
4802
  bytes += d.length;
4743
4803
  if (bytes > maxBytes && !settled) {
4744
- settled = true;
4745
- reject(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
4804
+ fail(Object.assign(new Error("body_too_large"), { code: "body_too_large" }));
4746
4805
  req.resume();
4747
4806
  return;
4748
4807
  }
@@ -4752,6 +4811,7 @@ function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
4752
4811
  req.on("end", () => {
4753
4812
  if (settled) return;
4754
4813
  settled = true;
4814
+ clearTimeout(timer);
4755
4815
  const raw = Buffer.concat(chunks).toString("utf8");
4756
4816
  try {
4757
4817
  resolve(JSON.parse(raw || "{}"));
@@ -4759,54 +4819,82 @@ function readBody(req, { maxBytes = MAX_BODY_BYTES } = {}) {
4759
4819
  resolve({});
4760
4820
  }
4761
4821
  });
4762
- req.on("error", (e) => {
4763
- if (settled) return;
4764
- settled = true;
4765
- reject(e);
4766
- });
4822
+ req.on("error", fail);
4823
+ req.on("aborted", () => fail(new Error("body_aborted")));
4767
4824
  });
4768
4825
  }
4769
4826
  function send(res, status, body) {
4827
+ if (res.destroyed || res.writableEnded) return;
4770
4828
  const payload = JSON.stringify(body);
4771
4829
  res.writeHead(status, { "content-type": "application/json", "content-length": Buffer.byteLength(payload) });
4772
4830
  res.end(payload);
4773
4831
  }
4832
+ function canonicalIp(value) {
4833
+ if (typeof value !== "string" || value.includes("%") || !isIP2(value)) return null;
4834
+ if (isIP2(value) === 4) return value;
4835
+ const canonical = new URL(`http://[${value}]/`).hostname.slice(1, -1);
4836
+ const mapped = /^::ffff:([\da-f]+):([\da-f]+)$/.exec(canonical);
4837
+ if (mapped) return mapped.slice(1).flatMap((word) => [parseInt(word, 16) >> 8, parseInt(word, 16) & 255]).join(".");
4838
+ return canonical;
4839
+ }
4840
+ function clientRateKey(req, trustedProxies = /* @__PURE__ */ new Set(), header = "cf-connecting-ip") {
4841
+ const peer = canonicalIp(req.socket.remoteAddress) ?? "unknown";
4842
+ const forwarded = trustedProxies.has(peer) ? canonicalIp(req.headers[header]) : null;
4843
+ const address = forwarded ?? peer;
4844
+ if (!address.includes(":")) return address;
4845
+ const [left, right] = address.split("::");
4846
+ const start = left ? left.split(":") : [];
4847
+ const end = right ? right.split(":") : [];
4848
+ const words = right === void 0 ? start : [...start, ...Array(8 - start.length - end.length).fill("0"), ...end];
4849
+ return words.slice(0, 4).map((word) => word.padStart(4, "0")).join(":") + "::/64";
4850
+ }
4774
4851
  function startRelayServer({ config, handleDraw }) {
4775
- const windows = /* @__PURE__ */ new Map();
4776
- const windowMs = 6e4;
4777
- const maxPerMinute = Number(config.maxRequestsPerMinute ?? 120);
4778
- const checkRate = (req) => {
4779
- if (!Number.isFinite(maxPerMinute)) return true;
4780
- const key = req.socket.remoteAddress || "unknown";
4781
- const now = Date.now();
4782
- let w = windows.get(key);
4783
- if (!w || now - w.start >= windowMs) {
4784
- w = { start: now, count: 0 };
4785
- windows.set(key, w);
4786
- if (windows.size > 1e4) {
4787
- for (const [k, v] of windows) if (now - v.start >= windowMs) windows.delete(k);
4788
- }
4789
- }
4790
- w.count += 1;
4791
- return w.count <= maxPerMinute;
4792
- };
4793
- const server = http.createServer(async (req, res) => {
4852
+ const callers = createLimiter({ max: config.maxRequestsPerMinute ?? 120 });
4853
+ const aggregate = createLimiter({ max: config.maxTotalRequestsPerMinute ?? 1200 });
4854
+ const trustedProxies = new Set((config.trustedProxies ?? []).map(canonicalIp).filter(Boolean));
4855
+ const maxConcurrent = config.maxConcurrentRequests ?? 64;
4856
+ let active = 0;
4857
+ const server = http.createServer({ headersTimeout: 1e4, requestTimeout: 15e3, connectionsCheckingInterval: 1e3 }, async (req, res) => {
4858
+ const refuse = (status, error) => {
4859
+ if (res.headersSent || res.destroyed) return res.destroy();
4860
+ res.setHeader("connection", "close");
4861
+ res.once("finish", () => req.destroy());
4862
+ send(res, status, { error });
4863
+ };
4794
4864
  if (req.method !== "POST" || req.url !== "/chunk") {
4795
- return send(res, 404, { error: "not found" });
4865
+ return refuse(404, "not found");
4796
4866
  }
4797
- if (!checkRate(req)) return send(res, 429, { error: "rate_limited" });
4798
- let body;
4799
4867
  try {
4800
- body = await readBody(req);
4801
- } catch (e) {
4802
- if (e?.code === "body_too_large") return send(res, 413, { error: "body_too_large" });
4803
- return send(res, 400, { error: "bad body" });
4868
+ callers.check(clientRateKey(req, trustedProxies, config.clientIpHeader ?? "cf-connecting-ip"));
4869
+ aggregate.check("all");
4870
+ } catch {
4871
+ res.setHeader("retry-after", "60");
4872
+ return refuse(429, "rate_limited");
4804
4873
  }
4805
- if (body.request == null) {
4806
- return send(res, 400, { error: "bad_request", detail: "need a DRAW (request); the legacy FUND lane is retired, pay per draw on-chain" });
4874
+ if (active >= maxConcurrent) return refuse(503, "relay_busy");
4875
+ active++;
4876
+ try {
4877
+ let body;
4878
+ try {
4879
+ body = await readBody(req, { timeoutMs: config.bodyTimeoutMs ?? 1e4 });
4880
+ } catch (e) {
4881
+ if (e?.code === "body_too_large") return refuse(413, "body_too_large");
4882
+ if (e?.code === "body_timeout") return refuse(408, "relay_timeout");
4883
+ return refuse(400, "bad body");
4884
+ }
4885
+ if (body?.request == null) {
4886
+ return send(res, 400, { error: "bad_request", detail: "need a DRAW (request); the legacy FUND lane is retired, pay per draw on-chain" });
4887
+ }
4888
+ await handleDraw(body, res);
4889
+ } catch (error) {
4890
+ (config.log ?? console).error?.(`mtok relay: request failed (${error.message})`);
4891
+ refuse(503, "relay_unavailable");
4892
+ } finally {
4893
+ active--;
4807
4894
  }
4808
- return handleDraw(body, res);
4809
4895
  });
4896
+ server.maxConnections = maxConcurrent * 2;
4897
+ server.maxRequestsPerSocket = 100;
4810
4898
  server.listen(config.port, () => {
4811
4899
  console.log(`mtok-relay: listening on port ${config.port} offer=${config.offerId} model=${config.model} upstream=${config.upstream} api=${config.apiBase} settlement=${config.settlementAddr}`);
4812
4900
  });
@@ -4977,15 +5065,6 @@ function createRedemptionStore({ file = null, retentionMs = DEFAULT_RETENTION_MS
4977
5065
  };
4978
5066
  }
4979
5067
 
4980
- // core/errors.js
4981
- function apiError(status, code, message, details) {
4982
- const err = new Error(message);
4983
- err.status = status;
4984
- err.code = code;
4985
- if (details !== void 0) err.details = details;
4986
- return err;
4987
- }
4988
-
4989
5068
  // core/onchain.js
4990
5069
  var TRANSFER_TOPIC = "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
4991
5070
  var DRAW_PAID_TOPIC = "0xb0243f80521d0dccd159389597aba96047e60ba5d7a9df12b67e5cb75230ac41";
@@ -5056,8 +5135,9 @@ function createOnchainVerifier({
5056
5135
  receiptRetries = 3,
5057
5136
  receiptRetryMs = 700,
5058
5137
  sleepImpl = (ms) => new Promise((r) => setTimeout(r, ms)),
5059
- nowMs = () => Date.now()
5138
+ nowMs = () => Date.now(),
5060
5139
  // injectable clock for the optional draw-age guard (verifyDrawPaid maxPaidAgeMs).
5140
+ rpcTimeoutMs = 5e3
5061
5141
  } = {}) {
5062
5142
  const urls = (rpcUrls?.length ? rpcUrls : String(rpcUrl || "").split(",")).map((s) => String(s).trim()).filter(Boolean);
5063
5143
  const configured = Boolean(urls.length && usdcAddress);
@@ -5066,7 +5146,8 @@ function createOnchainVerifier({
5066
5146
  const res = await fetchImpl(url, {
5067
5147
  method: "POST",
5068
5148
  headers: { "content-type": "application/json" },
5069
- body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
5149
+ body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }),
5150
+ signal: AbortSignal.timeout(rpcTimeoutMs)
5070
5151
  });
5071
5152
  if (!res.ok) throw apiError(502, "rpc_error", `chain RPC returned ${res.status}`);
5072
5153
  const body = await res.json();
@@ -5091,13 +5172,16 @@ function createOnchainVerifier({
5091
5172
  async function assertChain() {
5092
5173
  if (!chainPinned) return true;
5093
5174
  if (chainOkUrls.size) return true;
5175
+ let timeout;
5094
5176
  for (const url of urls) {
5095
5177
  try {
5096
5178
  const hex = await rpcOn(url, "eth_chainId", []);
5097
5179
  if (typeof hex === "string" && parseInt(hex, 16) === expChain) chainOkUrls.add(url);
5098
- } catch {
5180
+ } catch (error) {
5181
+ if (error.name === "TimeoutError") timeout = error;
5099
5182
  }
5100
5183
  }
5184
+ if (!chainOkUrls.size && timeout) throw timeout;
5101
5185
  return chainOkUrls.size > 0;
5102
5186
  }
5103
5187
  async function fetchReceipt(txHash) {
@@ -5212,23 +5296,27 @@ function createOnchainVerifier({
5212
5296
  if (BigInt(event.sellerUsdAtomic) < BigInt(minSellerAtomic)) return { ok: false, reason: "amount_too_low" };
5213
5297
  const maxAge = Number(maxPaidAgeMs);
5214
5298
  if (Number.isFinite(maxAge) && maxAge > 0) {
5215
- const bn = matchedLog?.blockNumber;
5299
+ const bn = matchedLog?.blockNumber ?? got.receipt.blockNumber;
5216
5300
  let paidAtMs = null;
5217
5301
  if (bn != null) {
5218
- const blockTag = typeof bn === "string" && bn.startsWith("0x") ? bn : "0x" + BigInt(bn).toString(16);
5219
5302
  for (let i = 0; i <= receiptRetries; i++) {
5220
5303
  try {
5304
+ const blockTag = "0x" + BigInt(bn).toString(16);
5221
5305
  const block = await rpc("eth_getBlockByNumber", [blockTag, false]);
5222
5306
  if (block?.timestamp != null) {
5223
- paidAtMs = Number(BigInt(block.timestamp)) * 1e3;
5224
- break;
5307
+ const timestamp = Number(BigInt(block.timestamp)) * 1e3;
5308
+ if (Number.isSafeInteger(timestamp) && timestamp >= 0 && timestamp <= nowMs() + 6e4) {
5309
+ paidAtMs = timestamp;
5310
+ break;
5311
+ }
5225
5312
  }
5226
5313
  } catch {
5227
5314
  }
5228
5315
  if (i < receiptRetries) await sleepImpl(receiptRetryMs);
5229
5316
  }
5230
5317
  }
5231
- if (paidAtMs != null && nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
5318
+ if (paidAtMs == null) return { ok: false, reason: "payment_age_unavailable" };
5319
+ if (nowMs() - paidAtMs > maxAge) return { ok: false, reason: "payment_too_old" };
5232
5320
  }
5233
5321
  const consumed = /* @__PURE__ */ new Set();
5234
5322
  let sellerTransfer = null;
@@ -5423,6 +5511,17 @@ function createServeCore({
5423
5511
  return { status: 402, body: { error: "contract_mode_required", detail: "this relay only serves contract-mode draws; the platform is not reporting a dripContractAddress" } };
5424
5512
  }
5425
5513
  if (!drawPaidTxHash) return { status: 402, body: { error: "draw_payment_required", detail: "contract mode requires drawPaidTxHash before upstream delivery" } };
5514
+ let storedKey = cacheKey;
5515
+ let redemptionState = await redemption.state(storedKey);
5516
+ if (!redemptionState && oldLegacyKey) {
5517
+ const oldLegacyState = await redemption.state(oldLegacyKey);
5518
+ if (oldLegacyState) {
5519
+ storedKey = oldLegacyKey;
5520
+ redemptionState = oldLegacyState;
5521
+ } else {
5522
+ redemptionState = await redemption.state(cacheKey);
5523
+ }
5524
+ }
5426
5525
  let paid;
5427
5526
  try {
5428
5527
  paid = await verifier.verifyDrawPaid(drawPaidTxHash, {
@@ -5437,16 +5536,15 @@ function createServeCore({
5437
5536
  requestHash,
5438
5537
  sellerWallet,
5439
5538
  feeRecipient: currentFeeRecipient,
5440
- // #580: refuse a payment older than the redemption window. The JSONL
5441
- // payload cache AND the claim markers are both aged out at boot (#600),
5442
- // so past retention this age bound is the sole replay defense (its
5443
- // skip-on-unreadable-block residual is named in redemption.mjs). An
5444
- // honest retry is seconds-to-minutes old, never days.
5445
- maxPaidAgeMs: redemption.retentionMs
5539
+ // A known completion spends no new inference. New claims need a verified
5540
+ // age because both payload and claim markers expire after retention.
5541
+ maxPaidAgeMs: redemptionState === "complete" ? void 0 : redemption.retentionMs
5446
5542
  });
5447
5543
  } catch (e) {
5544
+ if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
5448
5545
  return { status: 402, body: { error: "payment_unverified", detail: e.message } };
5449
5546
  }
5547
+ if (paid?.reason === "payment_age_unavailable") return { status: 503, body: { error: "payment_age_unavailable", detail: "payment age could not be verified; retry this same paid draw", _bookingId: bookingId } };
5450
5548
  if (!paid?.ok) return { status: 402, body: { error: "payment_unverified", detail: paid?.reason || "unknown" } };
5451
5549
  const expectedFee = configuredFeeAtomic({
5452
5550
  sellerUsdAtomic: paid.event.sellerUsdAtomic,
@@ -5465,17 +5563,6 @@ function createServeCore({
5465
5563
  return { status: 403, body: { error: "payer_denied", detail: "payer screening failed: " + e.message } };
5466
5564
  }
5467
5565
  }
5468
- let storedKey = cacheKey;
5469
- let redemptionState = await redemption.state(storedKey);
5470
- if (!redemptionState && oldLegacyKey) {
5471
- const oldLegacyState = await redemption.state(oldLegacyKey);
5472
- if (oldLegacyState) {
5473
- storedKey = oldLegacyKey;
5474
- redemptionState = oldLegacyState;
5475
- } else {
5476
- redemptionState = await redemption.state(cacheKey);
5477
- }
5478
- }
5479
5566
  if (redemptionState === "complete") return { status: 200, body: await redemption.get(storedKey) };
5480
5567
  if (redemptionState === "pending") {
5481
5568
  return { status: 409, body: { error: "draw_pending", detail: "this paid draw was already claimed; refusing to run upstream again", _bookingId: bookingId } };
@@ -5506,6 +5593,7 @@ function createServeCore({
5506
5593
  try {
5507
5594
  completion = await upstream(safeRequest);
5508
5595
  } catch (e) {
5596
+ if (e.name === "TimeoutError") return { status: 503, body: { error: "relay_timeout", _bookingId: bookingId } };
5509
5597
  return { status: 502, body: { error: "upstream_error", detail: e.message } };
5510
5598
  }
5511
5599
  try {
@@ -5530,7 +5618,7 @@ function createServeCore({
5530
5618
  }
5531
5619
 
5532
5620
  // bridge/bridge.mjs
5533
- function httpUpstream({ baseUrl, key }) {
5621
+ function httpUpstream({ baseUrl, key, timeoutMs }) {
5534
5622
  const url = String(baseUrl || "").replace(/\/$/, "") + "/chat/completions";
5535
5623
  return async (payload) => {
5536
5624
  const res = await fetch(url, {
@@ -5539,7 +5627,8 @@ function httpUpstream({ baseUrl, key }) {
5539
5627
  "content-type": "application/json",
5540
5628
  ...key ? { authorization: `Bearer ${key}` } : {}
5541
5629
  },
5542
- body: JSON.stringify(payload)
5630
+ body: JSON.stringify(payload),
5631
+ ...timeoutMs == null ? {} : { signal: AbortSignal.timeout(timeoutMs) }
5543
5632
  });
5544
5633
  const text = await res.text();
5545
5634
  let json;
@@ -5593,7 +5682,7 @@ async function createRelayRuntime(config) {
5593
5682
  if (!verifier.configured) throw new Error("onchain verifier not configured (missing usdcAddress in /api/config)");
5594
5683
  const payerDenylist = new Set((config.payerDenylist ?? []).map((a) => String(a).trim().toLowerCase()).filter(Boolean));
5595
5684
  const screenPayer = typeof config.screenPayer === "function" ? config.screenPayer : null;
5596
- const upstream = httpUpstream({ baseUrl: config.upstream + "/v1", key: config.upstreamKey });
5685
+ const upstream = httpUpstream({ baseUrl: config.upstream + "/v1", key: config.upstreamKey, timeoutMs: config.upstreamTimeoutMs ?? 12e4 });
5597
5686
  const payerDenied = async (payer) => {
5598
5687
  if (!payer) return false;
5599
5688
  if (payerDenylist.has(payer)) return true;
@@ -5639,18 +5728,27 @@ async function createRelayRuntime(config) {
5639
5728
  if (drawLocks.get(bookingId) === tail) drawLocks.delete(bookingId);
5640
5729
  }
5641
5730
  };
5642
- const handleDraw = (body, res) => withBookingLock(String(body?.bookingId ?? ""), async () => {
5643
- await refreshPlatformFeeIfStale();
5644
- let out = await core.serve(body);
5645
- if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
5646
- out = await core.serve(body);
5731
+ let active = 0;
5732
+ const handleDraw = async (body, res) => {
5733
+ if (active >= (config.maxConcurrentRequests ?? 64)) return send(res, 503, { error: "relay_busy" });
5734
+ active++;
5735
+ try {
5736
+ return await withBookingLock(String(body?.bookingId ?? ""), async () => {
5737
+ await refreshPlatformFeeIfStale();
5738
+ let out = await core.serve(body);
5739
+ if (out.status === 402 && out.body?.detail === "fee_amount_too_low" && await refreshPlatformFee()) {
5740
+ out = await core.serve(body);
5741
+ }
5742
+ return send(res, out.status, out.body);
5743
+ });
5744
+ } finally {
5745
+ active--;
5647
5746
  }
5648
- return send(res, out.status, out.body);
5649
- });
5747
+ };
5650
5748
  return { handleDraw };
5651
5749
  }
5652
5750
  async function fetchPlatformConfig(config) {
5653
- const r = await fetch(config.apiBase + "/api/config");
5751
+ const r = await fetch(config.apiBase + "/api/config", { signal: AbortSignal.timeout(5e3) });
5654
5752
  if (!r.ok) throw new Error("config fetch failed: " + r.status);
5655
5753
  const body = await r.json();
5656
5754
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mtok-relay",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
4
4
  "description": "Reference seller relay for mtok.market — accepts on-chain-prepaid chunk draws and serves inference from an upstream you control.",
5
5
  "type": "module",
6
6
  "bin": {